├── README.md ├── go.mod ├── config.go ├── go.sum ├── _example └── main.go └── httptracer.go /README.md: -------------------------------------------------------------------------------- 1 | httptracer 2 | ========== 3 | 4 | ## License 5 | 6 | MIT 7 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-chi/httptracer 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/go-chi/chi/v5 v5.0.0 7 | github.com/opentracing/opentracing-go v1.2.0 8 | ) 9 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package httptracer 2 | 3 | import "net/http" 4 | 5 | type Config struct { 6 | // Service details to record 7 | ServiceName string 8 | ServiceVersion string 9 | 10 | // Operation name 11 | // 12 | // The span operation name record for the http request trace. 13 | // Default value if empty is set to "http.request" 14 | OperationName string 15 | 16 | // SampleRate sample rate, value between 0 to 1.0 17 | // 18 | // Only span a percentage of the spans. Default value is 19 | // set to 1.0 20 | SampleRate float64 21 | 22 | // Skip particular requests from the tracer 23 | SkipFunc func(r *http.Request) bool 24 | 25 | // SetTagFunc 26 | // 27 | // ... 28 | // SetTagFunc func(r *http.Request) map[string]interface{} 29 | 30 | // Tags 31 | // 32 | // Extra tags to include with a span 33 | Tags map[string]interface{} 34 | } 35 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-chi/chi/v5 v5.0.0 h1:DBPx88FjZJH3FsICfDAfIfnb7XxKIYVGG6lOPlhENAg= 4 | github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= 5 | github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 6 | github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 10 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 11 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 12 | -------------------------------------------------------------------------------- /_example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-chi/chi/v5" 7 | "github.com/go-chi/chi/v5/middleware" 8 | "github.com/go-chi/httptracer" 9 | "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/opentracer" 10 | "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" 11 | ) 12 | 13 | func main() { 14 | // Tracer setup 15 | 16 | // Start the regular tracer and return it as an opentracing.Tracer interface. You 17 | // may use the same set of options as you normally would with the Datadog tracer. 18 | tr := opentracer.New( 19 | tracer.WithService("htdemo"), 20 | tracer.WithAgentAddr("0.0.0.0:8126"), 21 | ) 22 | 23 | // Stop it using the regular Stop call for the tracer package. 24 | // defer tracer.Stop() 25 | 26 | // Set the global OpenTracing tracer. 27 | // opentracing.SetGlobalTracer(t) 28 | 29 | // HTTP service 30 | r := chi.NewRouter() 31 | r.Use(middleware.Logger) 32 | r.Use(middleware.Recoverer) 33 | 34 | r.Use(httptracer.Tracer(tr, httptracer.Config{ 35 | ServiceName: "htdemo", 36 | ServiceVersion: "v0.1.0", 37 | SampleRate: 1, 38 | SkipFunc: func(r *http.Request) bool { 39 | return r.URL.Path == "/health" 40 | }, 41 | Tags: map[string]interface{}{ 42 | "_dd.measured": 1, // datadog, turn on metrics for http.request stats 43 | // "_dd1.sr.eausr": 1, // datadog, event sample rate 44 | }, 45 | })) 46 | 47 | r.Get("/", func(w http.ResponseWriter, r *http.Request) { 48 | w.Write([]byte("index")) 49 | }) 50 | 51 | r.Get("/ping", func(w http.ResponseWriter, r *http.Request) { 52 | w.Write([]byte("pong")) 53 | }) 54 | 55 | r.Get("/health", func(w http.ResponseWriter, r *http.Request) { 56 | w.Write([]byte("good")) 57 | }) 58 | 59 | r.Get("/fail", func(w http.ResponseWriter, r *http.Request) { 60 | w.WriteHeader(500) 61 | w.Write([]byte("ah, internal server error")) 62 | }) 63 | 64 | r.Get("/panic", func(w http.ResponseWriter, r *http.Request) { 65 | panic("oh no") 66 | }) 67 | 68 | http.ListenAndServe(":3333", r) 69 | } 70 | -------------------------------------------------------------------------------- /httptracer.go: -------------------------------------------------------------------------------- 1 | package httptracer 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "net/http" 7 | "runtime/debug" 8 | "sync/atomic" 9 | 10 | "github.com/go-chi/chi/v5/middleware" 11 | "github.com/opentracing/opentracing-go" 12 | "github.com/opentracing/opentracing-go/ext" 13 | ) 14 | 15 | func Tracer(tr opentracing.Tracer, cfg Config) func(next http.Handler) http.Handler { 16 | if cfg.OperationName == "" { 17 | cfg.OperationName = "http.request" 18 | } 19 | if cfg.SampleRate == 0 || cfg.SampleRate > 1 { 20 | cfg.SampleRate = 1.0 21 | } 22 | 23 | count := int64(0) 24 | 25 | return func(next http.Handler) http.Handler { 26 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 27 | 28 | // Skip tracer 29 | if cfg.SkipFunc != nil && cfg.SkipFunc(r) { 30 | next.ServeHTTP(w, r) 31 | return 32 | } 33 | 34 | // Sample portion of requests, or 1.0 will sample all 35 | atomic.AddInt64(&count, 1) 36 | if math.Mod(float64(count)*cfg.SampleRate, 1.0) != 0 { 37 | next.ServeHTTP(w, r) 38 | return 39 | } 40 | atomic.StoreInt64(&count, 0) 41 | 42 | // Pass request through tracer 43 | serverSpanCtx, _ := tr.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header)) 44 | span, traceCtx := opentracing.StartSpanFromContextWithTracer(r.Context(), tr, cfg.OperationName, ext.RPCServerOption(serverSpanCtx)) 45 | defer span.Finish() 46 | 47 | defer func() { 48 | if err := recover(); err != nil { 49 | ext.HTTPStatusCode.Set(span, uint16(500)) 50 | ext.Error.Set(span, true) 51 | span.SetTag("error.type", "panic") 52 | span.LogKV( 53 | "event", "error", 54 | "error.kind", "panic", 55 | "message", err, 56 | "stack", string(debug.Stack()), 57 | ) 58 | span.Finish() 59 | 60 | panic(err) 61 | } 62 | }() 63 | 64 | for k, v := range cfg.Tags { 65 | span.SetTag(k, v) 66 | } 67 | 68 | span.SetTag("service.name", cfg.ServiceName) 69 | span.SetTag("version", cfg.ServiceVersion) 70 | ext.SpanKind.Set(span, ext.SpanKindRPCServerEnum) 71 | ext.HTTPMethod.Set(span, r.Method) 72 | ext.HTTPUrl.Set(span, r.URL.Path) 73 | 74 | resourceName := r.URL.Path 75 | resourceName = r.Method + " " + resourceName 76 | span.SetTag("resource.name", resourceName) 77 | 78 | // pass the span through the request context and serve the request to the next middleware 79 | ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) 80 | next.ServeHTTP(ww, r.WithContext(traceCtx)) 81 | 82 | // set the resource name as we get it only once the handler is executed 83 | // resourceName := chi.RouteContext(r.Context()).RoutePattern() 84 | // if resourceName == "" { 85 | // resourceName = r.URL.Path 86 | // } 87 | 88 | // set the status code 89 | status := ww.Status() 90 | ext.HTTPStatusCode.Set(span, uint16(status)) 91 | 92 | if status >= 500 && status < 600 { 93 | // mark 5xx server error 94 | ext.Error.Set(span, true) 95 | span.SetTag("error.type", fmt.Sprintf("%d: %s", status, http.StatusText(status))) 96 | span.LogKV( 97 | "event", "error", 98 | "message", fmt.Sprintf("%d: %s", status, http.StatusText(status)), 99 | ) 100 | } 101 | }) 102 | } 103 | } 104 | --------------------------------------------------------------------------------