feat: add OpenTracing middleware

This commit is contained in:
Daniel G. Taylor 2020-08-31 23:32:00 -07:00
parent f976f54c6c
commit 69d47ad239
No known key found for this signature in database
GPG key ID: 7BD6DC99C9A87E22
6 changed files with 77 additions and 0 deletions

View file

@ -26,6 +26,7 @@ Features include:
- Automatic recovery from panics with traceback & request logging
- Structured logging middleware using [Zap](https://github.com/uber-go/zap)
- Automatic handling of `Prefer: return=minimal` from [RFC 7240](https://tools.ietf.org/html/rfc7240#section-4.2)
- [OpenTracing](https://opentracing.io/) for requests and errors
- Per-operation request size limits & timeouts with sane defaults
- [Content negotiation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation) between server and client
- Support for gzip ([RFC 1952](https://tools.ietf.org/html/rfc1952)) & Brotli ([RFC 7932](https://tools.ietf.org/html/rfc7932)) content encoding via the `Accept-Encoding` header.
@ -480,6 +481,30 @@ app.Middleware(func(next http.Handler) http.Handler {
When using the `cli.NewRouter` convenience method, a set of default middleware is added for you. See `middleware.DefaultChain` for more info.
### Enabling OpenTracing
[OpenTracing](https://opentracing.io/) support is built-in, but you have to tell the global tracer where to send the information, otherwise it acts as a no-op. For example, if you use [DataDog APM](https://www.datadoghq.com/blog/opentracing-datadog-cncf/) and have the agent configured wherever you deploy your service:
```go
import (
"github.com/opentracing/opentracing-go"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/opentracer"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
)
func main() {
t := opentracer.New(tracer.WithAgentAddr("host:port"))
defer tracer.Stop()
// Set it as a Global Tracer
opentracing.SetGlobalTracer(t)
app := cli.NewRouter("My Cool Service", "1.0.0")
// register routes here
app.Run()
}
```
### Timeouts, Deadlines, Cancellation & Limits
Huma provides utilities to prevent long-running handlers and issues with huge request bodies and slow clients with sane defaults out of the box.

1
go.mod
View file

@ -13,6 +13,7 @@ require (
github.com/magiconair/properties v1.8.2 // indirect
github.com/mattn/go-isatty v0.0.12
github.com/mitchellh/mapstructure v1.3.3 // indirect
github.com/opentracing/opentracing-go v1.2.0
github.com/pelletier/go-toml v1.8.0 // indirect
github.com/spf13/afero v1.3.4 // indirect
github.com/spf13/cast v1.3.1 // indirect

2
go.sum
View file

@ -171,6 +171,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=

View file

@ -23,6 +23,7 @@ func DefaultChain(next http.Handler) http.Handler {
// Note: logger goes before recovery so that recovery can use it. We don't
// expect the logger to cause panics.
return chi.Chain(
OpenTracing,
Logger,
Recovery,
ContentEncoding,

41
middleware/opentracing.go Normal file
View file

@ -0,0 +1,41 @@
package middleware
import (
"net/http"
"github.com/go-chi/chi"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
)
// OpenTracing provides a middleware for cross-service tracing support.
func OpenTracing(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tracer := opentracing.GlobalTracer()
// Get any incoming tracing context via HTTP headers & create the span.
ctx, _ := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header))
span := tracer.StartSpan("http.request", ext.RPCServerOption(ctx))
defer span.Finish()
// Set basic HTTP info
ext.HTTPMethod.Set(span, r.Method)
ext.HTTPUrl.Set(span, r.URL.String())
ext.Component.Set(span, "huma")
span.SetTag("span.type", "web")
// Update context & continue the middleware chain.
r = r.WithContext(opentracing.ContextWithSpan(r.Context(), span))
ws := statusRecorder{ResponseWriter: w}
next.ServeHTTP(&ws, r)
// If we have a Chi route template, save it
if chictx := chi.RouteContext(r.Context()); chictx != nil {
span.SetTag("resource.name", chictx.RoutePattern())
span.SetOperationName(r.Method + " " + chictx.RoutePattern())
}
// Save the status code
ext.HTTPStatusCode.Set(span, uint16(ws.status))
})
}

View file

@ -12,6 +12,8 @@ import (
"github.com/danielgtaylor/huma"
"github.com/go-chi/chi"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"go.uber.org/zap"
)
@ -110,6 +112,11 @@ func Recovery(next http.Handler) http.Handler {
fmt.Printf("Caught panic: %v\n%s\n\nFrom request:\n%s", err, debug.Stack(), string(request))
}
// If OpenTracing is enabled, augment the span with error info
if span := opentracing.SpanFromContext(r.Context()); span != nil {
span.SetTag(string(ext.Error), err)
}
ctx := huma.ContextFromRequest(w, r)
ctx.WriteError(http.StatusInternalServerError, "Unrecoverable internal server error")
}