Skip to content

Commit 034c09d

Browse files
authored
trace requests per route (#189)
* trace requests per route * minor tweak on the option * use pattern and method for tracing * change the naming pattern for the resources * add logger and debug flag for tracing * use a logrus to take LD from info to debug * don't log errors that are expected
1 parent 4b21cdc commit 034c09d

File tree

15 files changed

+265
-76
lines changed

15 files changed

+265
-76
lines changed

featureflag/featureflag.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,7 @@ func NewClient(cfg *Config, logger logrus.FieldLogger) (Client, error) {
3737
config.SendEvents = false
3838
}
3939

40-
if logger == nil {
41-
logger = noopLogger()
42-
}
40+
config.Loggers.SetBaseLogger(wrapLogger(logger))
4341

4442
if cfg.RelayHost != "" {
4543
config.BaseUri = cfg.RelayHost
@@ -99,8 +97,23 @@ func (c *ldClient) AllEnabledFlags(key string) []string {
9997
return flags
10098
}
10199

102-
func noopLogger() *logrus.Logger {
103-
l := logrus.New()
104-
l.SetOutput(ioutil.Discard)
105-
return l
100+
func wrapLogger(logger logrus.FieldLogger) infoToDebugLogger {
101+
if logger == nil {
102+
l := logrus.New()
103+
l.SetOutput(ioutil.Discard)
104+
logger = l
105+
}
106+
107+
return infoToDebugLogger{logger.WithField("component", "launch_darkly")}
108+
}
109+
110+
type infoToDebugLogger struct {
111+
log logrus.FieldLogger
112+
}
113+
114+
func (l infoToDebugLogger) Println(values ...interface{}) {
115+
l.log.Debugln(values...)
116+
}
117+
func (l infoToDebugLogger) Printf(format string, values ...interface{}) {
118+
l.log.Debugf(format, values...)
106119
}

nconf/args.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func (args *RootArgs) Setup(config interface{}, serviceName, version string) (lo
5353
}
5454

5555
// Handles the 'enabled' flag itself
56-
tracing.Configure(&rootConfig.Tracing, serviceName)
56+
tracing.Configure(&rootConfig.Tracing, log, serviceName)
5757

5858
if err := featureflag.Init(rootConfig.FeatureFlag, log); err != nil {
5959
return nil, errors.Wrap(err, "Failed to configure featureflags")

router/errors.go

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -86,40 +86,40 @@ func httpError(code int, fmtString string, args ...interface{}) *HTTPError {
8686
}
8787
}
8888

89-
// HandleError will handle an error
89+
// HandleError will handle any error. If it is of type *HTTPError then it will
90+
// log anything of a 50x or an InternalError. It will write the right error response
91+
// to the client. This way if you return a BadRequestError, it will simply write to the client.
92+
// Any non-HTTPError will be treated as unhandled and result in a 50x
9093
func HandleError(err error, w http.ResponseWriter, r *http.Request) {
94+
if err == nil || reflect.ValueOf(err).IsNil() {
95+
return
96+
}
97+
9198
log := tracing.GetLogger(r)
9299
errorID := tracing.GetRequestID(r)
93100

94101
var notifyBugsnag bool
95102

96103
switch e := err.(type) {
97-
case nil:
98-
return
99104
case *HTTPError:
100-
// assert to *HTTPError to check nil intrface
101-
httpErr := err.(*HTTPError)
102-
if httpErr == nil {
103-
return
104-
}
105-
105+
e.ErrorID = errorID
106106
if e.Code >= http.StatusInternalServerError {
107-
e.ErrorID = errorID
108-
// this will get us the stack trace too
109-
log.WithError(e.Cause()).Error(e.Error())
110107
notifyBugsnag = true
111-
} else {
112-
log.WithError(e.Cause()).Infof("unexpected error: %s", e.Error())
108+
elog := log.WithError(e)
109+
if e.InternalError != nil {
110+
elog = elog.WithField("internal_err", e.InternalError.Error())
111+
}
112+
113+
elog.Errorf("internal server error: %s", e.InternalMessage)
114+
} else if e.InternalError != nil {
115+
notifyBugsnag = true
116+
log.WithError(e).Infof("unexpected error: %s", e.InternalMessage)
113117
}
114118

115119
if jsonErr := SendJSON(w, e.Code, e); jsonErr != nil {
116120
log.WithError(jsonErr).Error("Failed to write the JSON error response")
117121
}
118122
default:
119-
// this is 5ns slower than using unsafe but a unhandled internal server error should not happen that often
120-
if reflect.ValueOf(err).IsNil() {
121-
return
122-
}
123123
notifyBugsnag = true
124124
metriks.Inc("unhandled_errors", 1)
125125
log.WithError(e).Errorf("Unhandled server error: %s", e.Error())
@@ -129,6 +129,7 @@ func HandleError(err error, w http.ResponseWriter, r *http.Request) {
129129
log.WithError(writeErr).Error("Error writing generic error message")
130130
}
131131
}
132+
132133
if notifyBugsnag {
133134
bugsnag.Notify(err, r, r.Context(), bugsnag.MetaData{
134135
"meta": map[string]interface{}{

router/errors_test.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func TestHandleError_ErrorIsNil(t *testing.T) {
2525
httptest.NewRequest(http.MethodGet, "/", nil),
2626
logger,
2727
"test",
28+
"test",
2829
)
2930

3031
HandleError(nil, w, r)
@@ -40,6 +41,7 @@ func TestHandleError_ErrorIsNilPointerToTypeHTTPError(t *testing.T) {
4041
httptest.NewRequest(http.MethodGet, "/", nil),
4142
logger,
4243
"test",
44+
"test",
4345
)
4446

4547
h := func(_ http.ResponseWriter, _ *http.Request) *HTTPError {
@@ -52,13 +54,34 @@ func TestHandleError_ErrorIsNilPointerToTypeHTTPError(t *testing.T) {
5254
assert.Empty(t, w.Header())
5355
}
5456

57+
func TestHandleError_ErrorIsNilInterface(t *testing.T) {
58+
logger, loggerOutput := test.NewNullLogger()
59+
w, r, _ := tracing.NewTracer(
60+
httptest.NewRecorder(),
61+
httptest.NewRequest(http.MethodGet, "/", nil),
62+
logger,
63+
"test",
64+
"test",
65+
)
66+
67+
h := func(_ http.ResponseWriter, _ *http.Request) error {
68+
return nil
69+
}
70+
71+
HandleError(h(w, r), w, r)
72+
73+
assert.Empty(t, loggerOutput.AllEntries())
74+
assert.Empty(t, w.Header())
75+
}
76+
5577
func TestHandleError_StandardError(t *testing.T) {
5678
logger, loggerOutput := test.NewNullLogger()
5779
w, r, _ := tracing.NewTracer(
5880
httptest.NewRecorder(),
5981
httptest.NewRequest(http.MethodGet, "/", nil),
6082
logger,
6183
"test",
84+
"test",
6285
)
6386

6487
HandleError(errors.New("random error"), w, r)
@@ -76,6 +99,7 @@ func TestHandleError_HTTPError(t *testing.T) {
7699
httptest.NewRequest(http.MethodGet, "/", nil),
77100
logger,
78101
"test",
102+
"test",
79103
)
80104

81105
httpErr := &HTTPError{
@@ -95,7 +119,33 @@ func TestHandleError_HTTPError(t *testing.T) {
95119
assert.Equal(t, expectedBody, string(b))
96120

97121
require.Len(t, loggerOutput.AllEntries(), 1)
98-
assert.Equal(t, httpErr.InternalMessage, loggerOutput.AllEntries()[0].Message)
122+
assert.Equal(t, "internal server error: "+httpErr.InternalMessage, loggerOutput.AllEntries()[0].Message)
123+
}
124+
125+
func TestHandleError_NoLogForNormalErrors(t *testing.T) {
126+
logger, loggerOutput := test.NewNullLogger()
127+
recorder := httptest.NewRecorder()
128+
w, r, _ := tracing.NewTracer(
129+
recorder,
130+
httptest.NewRequest(http.MethodGet, "/", nil),
131+
logger,
132+
"test",
133+
"test",
134+
)
135+
136+
httpErr := BadRequestError("not found yo.")
137+
138+
HandleError(httpErr, w, r)
139+
140+
resp := recorder.Result()
141+
b, err := ioutil.ReadAll(resp.Body)
142+
require.NoError(t, err)
143+
144+
expectedBody := fmt.Sprintf(`{"code":400,"msg":"not found yo.","error_id":"%s"}`, tracing.GetRequestID(r))
145+
assert.Equal(t, expectedBody, string(b))
146+
147+
// we shouldn't log anything, this is a normal error
148+
require.Len(t, loggerOutput.AllEntries(), 0)
99149
}
100150

101151
type OtherError struct {
@@ -113,6 +163,7 @@ func TestHandleError_ErrorIsNilPointerToTypeOtherError(t *testing.T) {
113163
httptest.NewRequest(http.MethodGet, "/", nil),
114164
logger,
115165
"test",
166+
"test",
116167
)
117168

118169
var oe *OtherError

router/middleware.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,11 @@ func BugSnag() Middleware {
146146
next.ServeHTTP(w, r.WithContext(ctx))
147147
})
148148
}
149+
150+
func TrackAllRequests(log logrus.FieldLogger, service string) Middleware {
151+
return MiddlewareFunc(func(w http.ResponseWriter, r *http.Request, next http.Handler) {
152+
// This is to maintain some legacy span work. It will cause the APM requests
153+
// to show up as the method on the top level
154+
tracing.TrackRequest(w, r, log, service, r.Method, next)
155+
})
156+
}

router/middleware_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ func TestRecoveryInternalTracer(t *testing.T) {
134134
httptest.NewRequest(http.MethodGet, "/", nil),
135135
logger,
136136
t.Name(),
137+
"some_resource",
137138
)
138139
mw := Recoverer(logrus.New())
139140
handler := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

router/options.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
package router
22

3-
import (
4-
"github.com/sirupsen/logrus"
5-
)
6-
73
type Option func(r *chiWrapper)
84

95
func OptEnableCORS(r *chiWrapper) {
@@ -27,7 +23,7 @@ func OptVersionHeader(svcName, version string) Option {
2723
}
2824
}
2925

30-
func OptTracingMiddleware(log logrus.FieldLogger, svcName string) Option {
26+
func OptEnableTracing(svcName string) Option {
3127
return func(r *chiWrapper) {
3228
r.svcName = svcName
3329
r.enableTracing = true

router/router.go

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package router
22

33
import (
44
"net/http"
5+
"strings"
56

67
"github.com/netlify/netlify-commons/tracing"
78
"github.com/rs/cors"
@@ -14,8 +15,10 @@ import (
1415
type chiWrapper struct {
1516
chi chi.Router
1617

17-
version string
18-
svcName string
18+
version string
19+
svcName string
20+
tracingPrefix string
21+
rootLogger logrus.FieldLogger
1922

2023
healthEndpoint string
2124
healthHandler APIHandler
@@ -51,11 +54,12 @@ type Router interface {
5154
ServeHTTP(http.ResponseWriter, *http.Request)
5255
}
5356

54-
// creates a router with sensible defaults (xff, request id, cors)
57+
// New creates a router with sensible defaults (xff, request id, cors)
5558
func New(log logrus.FieldLogger, options ...Option) Router {
5659
r := &chiWrapper{
57-
chi: chi.NewRouter(),
58-
version: "unknown",
60+
chi: chi.NewRouter(),
61+
version: "unknown",
62+
rootLogger: log,
5963
}
6064

6165
xffmw, _ := xff.Default()
@@ -81,43 +85,44 @@ func New(log logrus.FieldLogger, options ...Option) Router {
8185
if r.healthEndpoint != "" {
8286
r.Use(HealthCheck(r.healthEndpoint, r.healthHandler))
8387
}
84-
if r.enableTracing {
85-
r.Use(tracing.Middleware(log, r.svcName))
86-
}
8788

8889
return r
8990
}
9091

9192
// Route allows creating a generic route
9293
func (r *chiWrapper) Route(pattern string, fn func(Router)) {
9394
r.chi.Route(pattern, func(c chi.Router) {
94-
fn(&chiWrapper{chi: c})
95+
wrapper := new(chiWrapper)
96+
*wrapper = *r
97+
wrapper.chi = c
98+
wrapper.tracingPrefix = sanitizePattern(pattern)
99+
fn(wrapper)
95100
})
96101
}
97102

98103
// Method adds a routes for a `pattern` that matches the `method` HTTP method.
99104
func (r *chiWrapper) Method(method, pattern string, h APIHandler) {
100-
r.chi.Method(method, pattern, HandlerFunc(h))
105+
r.chi.Method(method, pattern, r.traceRequest(method, pattern, h))
101106
}
102107

103108
// Get adds a GET route
104109
func (r *chiWrapper) Get(pattern string, fn APIHandler) {
105-
r.chi.Get(pattern, HandlerFunc(fn))
110+
r.chi.Get(pattern, r.traceRequest(http.MethodGet, pattern, fn))
106111
}
107112

108113
// Post adds a POST route
109114
func (r *chiWrapper) Post(pattern string, fn APIHandler) {
110-
r.chi.Post(pattern, HandlerFunc(fn))
115+
r.chi.Post(pattern, r.traceRequest(http.MethodPost, pattern, fn))
111116
}
112117

113118
// Put adds a PUT route
114119
func (r *chiWrapper) Put(pattern string, fn APIHandler) {
115-
r.chi.Put(pattern, HandlerFunc(fn))
120+
r.chi.Put(pattern, r.traceRequest(http.MethodPut, pattern, fn))
116121
}
117122

118123
// Delete adds a DELETE route
119124
func (r *chiWrapper) Delete(pattern string, fn APIHandler) {
120-
r.chi.Delete(pattern, HandlerFunc(fn))
125+
r.chi.Delete(pattern, r.traceRequest(http.MethodDelete, pattern, fn))
121126
}
122127

123128
// WithBypass adds an inline chi middleware for an endpoint handler
@@ -138,6 +143,11 @@ func (r *chiWrapper) ServeHTTP(w http.ResponseWriter, req *http.Request) {
138143

139144
// Mount attaches another http.Handler along ./pattern/*
140145
func (r *chiWrapper) Mount(pattern string, h http.Handler) {
146+
if r.enableTracing {
147+
h = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
148+
tracing.TrackRequest(w, req, r.rootLogger, r.svcName, pattern, h)
149+
})
150+
}
141151
r.chi.Mount(pattern, h)
142152
}
143153

@@ -154,3 +164,32 @@ func HandlerFunc(fn APIHandler) http.HandlerFunc {
154164
}
155165
}
156166
}
167+
168+
func (r *chiWrapper) traceRequest(method, pattern string, fn APIHandler) http.HandlerFunc {
169+
f := HandlerFunc(fn)
170+
if r.enableTracing {
171+
pattern = sanitizePattern(pattern)
172+
if r.tracingPrefix != "" {
173+
pattern = r.tracingPrefix + "." + pattern
174+
}
175+
176+
resourceName := strings.ToUpper(method)
177+
if pattern != "" {
178+
resourceName += "::" + pattern
179+
}
180+
181+
return func(w http.ResponseWriter, req *http.Request) {
182+
tracing.TrackRequest(w, req, r.rootLogger, r.svcName, resourceName, f)
183+
}
184+
}
185+
return f
186+
}
187+
188+
func sanitizePattern(pattern string) string {
189+
pattern = strings.TrimPrefix(pattern, "/")
190+
pattern = strings.ReplaceAll(pattern, "{", "")
191+
pattern = strings.ReplaceAll(pattern, "}", "")
192+
pattern = strings.ReplaceAll(pattern, "/", ".")
193+
pattern = strings.TrimSuffix(pattern, ".")
194+
return pattern
195+
}

0 commit comments

Comments
 (0)