|
| 1 | +package ldmiddleware |
| 2 | + |
| 3 | +import ( |
| 4 | + "net/http" |
| 5 | + "time" |
| 6 | + |
| 7 | + "github.com/felixge/httpsnoop" |
| 8 | + "github.com/google/uuid" |
| 9 | + |
| 10 | + "github.com/launchdarkly/go-sdk-common/v3/ldcontext" |
| 11 | + "github.com/launchdarkly/go-sdk-common/v3/ldvalue" |
| 12 | + ld "github.com/launchdarkly/go-server-sdk/v7" |
| 13 | +) |
| 14 | + |
| 15 | +// RequestKeyFunc allows callers to override the request context key for the LDContext. |
| 16 | +// Return (key, true) to use the provided key; return ("", false) to fall back to the default UUID key. |
| 17 | +type RequestKeyFunc func(r *http.Request) (string, bool) |
| 18 | + |
| 19 | +// AddScopedClientForRequest returns a net/http middleware that, for each incoming request, |
| 20 | +// creates an LDScopedClient seeded with a `request`-kind LDContext populated with useful |
| 21 | +// HTTP request attributes (e.g., method, path, host, userAgent), and stores it in the |
| 22 | +// request's Go context. Downstream handlers can retrieve it via ld.GetScopedClient. |
| 23 | +func AddScopedClientForRequest(client *ld.LDClient) func(next http.Handler) http.Handler { |
| 24 | + return AddScopedClientForRequestWithKeyFn(client, nil) |
| 25 | +} |
| 26 | + |
| 27 | +// AddScopedClientForRequestWithKeyFn is like AddScopedClientForRequest, but allows providing a function to override |
| 28 | +// the context key used for the `request`-kind LDContext. If the function returns ok=false or an empty key, |
| 29 | +// a random UUID will be used. |
| 30 | +func AddScopedClientForRequestWithKeyFn( |
| 31 | + client *ld.LDClient, keyFn RequestKeyFunc, |
| 32 | +) func(next http.Handler) http.Handler { |
| 33 | + return func(next http.Handler) http.Handler { |
| 34 | + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 35 | + // Determine request context key |
| 36 | + requestKey := "" |
| 37 | + if keyFn != nil { |
| 38 | + if k, ok := keyFn(r); ok && k != "" { |
| 39 | + requestKey = k |
| 40 | + } |
| 41 | + } |
| 42 | + if requestKey == "" { |
| 43 | + requestKey = uuid.New().String() |
| 44 | + } |
| 45 | + |
| 46 | + b := ldcontext.NewBuilder(requestKey).Kind("ld_request").Anonymous(true) |
| 47 | + b.SetString("method", r.Method) |
| 48 | + b.SetString("host", r.Host) |
| 49 | + b.SetString("userAgent", r.UserAgent()) |
| 50 | + if r.URL != nil { |
| 51 | + b.SetString("path", r.URL.Path) |
| 52 | + b.SetString("scheme", r.URL.Scheme) |
| 53 | + b.SetString("query", r.URL.RawQuery) |
| 54 | + } |
| 55 | + b.SetString("proto", r.Proto) |
| 56 | + b.SetString("remoteAddr", r.RemoteAddr) |
| 57 | + requestCtx := b.Build() |
| 58 | + |
| 59 | + scoped := ld.NewScopedClient(client, requestCtx) |
| 60 | + ctxWithScoped := ld.GoContextWithScopedClient(r.Context(), scoped) |
| 61 | + |
| 62 | + next.ServeHTTP(w, r.WithContext(ctxWithScoped)) |
| 63 | + }) |
| 64 | + } |
| 65 | +} |
| 66 | + |
| 67 | +// TrackTiming sends a LD event "http.request.duration_ms" with the duration of the request in milliseconds. |
| 68 | +// This middleware must be after AddScopedClientForRequest in the middleware chain, as it uses the scoped client |
| 69 | +// from the Go context. |
| 70 | +// |
| 71 | +// The timing event will include all LaunchDarkly contexts added to the scoped client. You may add more |
| 72 | +// contexts to the scoped client _during_ the request, and they will be included in the timing event sent |
| 73 | +// when the request completes. |
| 74 | +func TrackTiming(next http.Handler) http.Handler { |
| 75 | + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 76 | + startTime := time.Now() |
| 77 | + next.ServeHTTP(w, r) |
| 78 | + duration := time.Since(startTime) |
| 79 | + scoped, ok := ld.GetScopedClient(r.Context()) |
| 80 | + if !ok { |
| 81 | + return |
| 82 | + } |
| 83 | + _ = scoped.TrackMetric("http.request.duration_ms", float64(duration.Milliseconds()), ldvalue.Null()) |
| 84 | + }) |
| 85 | +} |
| 86 | + |
| 87 | +// TrackErrorResponses sends a LD event "http.response.4xx" or "http.response.5xx" if the response code is 4xx or 5xx. |
| 88 | +// This middleware must be after AddScopedClientForRequest in the middleware chain, as it uses the scoped client |
| 89 | +// from the Go context. |
| 90 | +// |
| 91 | +// The error event will include all LaunchDarkly contexts added to the scoped client. You may add more |
| 92 | +// contexts to the scoped client _during_ the request, and they will be included in the error event sent |
| 93 | +// when the request completes. |
| 94 | +func TrackErrorResponses(next http.Handler) http.Handler { |
| 95 | + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 96 | + metrics := httpsnoop.CaptureMetrics(next, w, r) |
| 97 | + if metrics.Code < 400 { |
| 98 | + return |
| 99 | + } |
| 100 | + scoped, ok := ld.GetScopedClient(r.Context()) |
| 101 | + if !ok { |
| 102 | + return |
| 103 | + } |
| 104 | + if metrics.Code < 500 { |
| 105 | + _ = scoped.TrackEvent("http.response.4xx") |
| 106 | + return |
| 107 | + } |
| 108 | + _ = scoped.TrackEvent("http.response.5xx") |
| 109 | + }) |
| 110 | +} |
0 commit comments