|
4 | 4 | package otelx |
5 | 5 |
|
6 | 6 | import ( |
| 7 | + "cmp" |
| 8 | + "context" |
7 | 9 | "net/http" |
8 | 10 | "strings" |
9 | 11 |
|
10 | 12 | "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" |
11 | 13 | ) |
12 | 14 |
|
13 | | -func isHealthFilter(r *http.Request) bool { |
14 | | - path := r.URL.Path |
15 | | - return !strings.HasPrefix(path, "/health/") |
16 | | -} |
| 15 | +var WithDefaultFilters otelhttp.Option = otelhttp.WithFilter(func(r *http.Request) bool { |
| 16 | + return !(strings.HasPrefix(r.URL.Path, "/health") || |
| 17 | + strings.HasPrefix(r.URL.Path, "/admin/health") || |
| 18 | + strings.HasPrefix(r.URL.Path, "/metrics") || |
| 19 | + strings.HasPrefix(r.URL.Path, "/admin/metrics")) |
| 20 | +}) |
17 | 21 |
|
18 | | -func isAdminHealthFilter(r *http.Request) bool { |
19 | | - path := r.URL.Path |
20 | | - return !strings.HasPrefix(path, "/admin/health/") |
21 | | -} |
| 22 | +type contextKey int |
22 | 23 |
|
23 | | -func filterOpts() []otelhttp.Option { |
24 | | - filters := []otelhttp.Filter{ |
25 | | - isHealthFilter, |
26 | | - isAdminHealthFilter, |
27 | | - } |
28 | | - opts := []otelhttp.Option{} |
29 | | - for _, f := range filters { |
30 | | - opts = append(opts, otelhttp.WithFilter(f)) |
31 | | - } |
32 | | - return opts |
| 24 | +const callbackContextKey contextKey = iota |
| 25 | + |
| 26 | +func SpanNameRecorderMiddleware(next http.Handler) http.Handler { |
| 27 | + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 28 | + defer func() { |
| 29 | + cb, _ := r.Context().Value(callbackContextKey).(func(string)) |
| 30 | + if cb == nil { |
| 31 | + return |
| 32 | + } |
| 33 | + if r.Pattern != "" { |
| 34 | + cb(r.Pattern) |
| 35 | + } |
| 36 | + }() |
| 37 | + next.ServeHTTP(w, r) |
| 38 | + }) |
33 | 39 | } |
34 | 40 |
|
35 | | -// NewHandler returns a wrapped otelhttp.NewHandler with our request filters. |
36 | | -func NewHandler(handler http.Handler, operation string, opts ...otelhttp.Option) http.Handler { |
37 | | - opts = append(filterOpts(), opts...) |
38 | | - return otelhttp.NewHandler(handler, operation, opts...) |
| 41 | +func SpanNameRecorderNegroniFunc(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { |
| 42 | + defer func() { |
| 43 | + cb, _ := r.Context().Value(callbackContextKey).(func(string)) |
| 44 | + if cb == nil { |
| 45 | + return |
| 46 | + } |
| 47 | + if r.Pattern != "" { |
| 48 | + cb(r.Pattern) |
| 49 | + } |
| 50 | + }() |
| 51 | + next(w, r) |
39 | 52 | } |
40 | 53 |
|
41 | | -// TraceHandler wraps otelx.NewHandler, passing the URL path as the span name. |
42 | | -func TraceHandler(h http.Handler, opts ...otelhttp.Option) http.Handler { |
43 | | - // Use a span formatter to set the span name to the URL path, rather than passing in the operation to NewHandler. |
44 | | - // This allows us to use the same handler for multiple routes. |
45 | | - middlewareOpts := []otelhttp.Option{ |
| 54 | +func NewMiddleware(next http.Handler, operation string, opts ...otelhttp.Option) http.Handler { |
| 55 | + myOpts := []otelhttp.Option{ |
| 56 | + WithDefaultFilters, |
46 | 57 | otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string { |
47 | | - return r.URL.Path |
| 58 | + return cmp.Or(r.Pattern, operation, r.Method+" "+r.URL.Path) |
48 | 59 | }), |
49 | 60 | } |
50 | | - return NewHandler(h, "", append(middlewareOpts, opts...)...) |
| 61 | + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 62 | + callback := func(s string) { |
| 63 | + r.Pattern = cmp.Or(r.Pattern, s) |
| 64 | + } |
| 65 | + ctx := context.WithValue(r.Context(), callbackContextKey, callback) |
| 66 | + r2 := r.WithContext(ctx) |
| 67 | + next.ServeHTTP(w, r2) |
| 68 | + r.Pattern = cmp.Or(r2.Pattern, r.Pattern) // best-effort in case callback never is called |
| 69 | + }) |
| 70 | + return otelhttp.NewHandler(handler, operation, append(myOpts, opts...)...) |
51 | 71 | } |
0 commit comments