diff --git a/cmd/influxd/launcher/launcher.go b/cmd/influxd/launcher/launcher.go index 385971c1aef..befb9c00326 100644 --- a/cmd/influxd/launcher/launcher.go +++ b/cmd/influxd/launcher/launcher.go @@ -835,27 +835,30 @@ func (m *Launcher) run(ctx context.Context) (err error) { pkgHTTPServer = http.NewHandlerPkg(pkgServerLogger, m.apibackend.HTTPErrorHandler, pkgSVC) } - // HTTP server - var platformHandler nethttp.Handler = http.NewPlatformHandler(m.apibackend, http.WithResourceHandler(pkgHTTPServer)) - m.reg.MustRegister(platformHandler.(*http.PlatformHandler).PrometheusCollectors()...) - httpLogger := m.log.With(zap.String("service", "http")) - if logconf.Level == zap.DebugLevel { - platformHandler = http.LoggingMW(httpLogger)(platformHandler) - } - - handler := http.NewHandlerFromRegistry(httpLogger, "platform", m.reg) - handler.Handler = platformHandler + { + platformHandler := http.NewPlatformHandler(m.apibackend, http.WithResourceHandler(pkgHTTPServer)) + + httpLogger := m.log.With(zap.String("service", "http")) + m.httpServer.Handler = http.NewHandlerFromRegistry( + "platform", + m.reg, + http.WithLog(httpLogger), + http.WithAPIHandler(platformHandler), + ) - m.httpServer.Handler = handler - // If we are in testing mode we allow all data to be flushed and removed. - if m.testing { - m.httpServer.Handler = http.DebugFlush(ctx, handler, flushers) + if logconf.Level == zap.DebugLevel { + m.httpServer.Handler = http.LoggingMW(httpLogger)(m.httpServer.Handler) + } + // If we are in testing mode we allow all data to be flushed and removed. + if m.testing { + m.httpServer.Handler = http.DebugFlush(ctx, m.httpServer.Handler, flushers) + } } ln, err := net.Listen("tcp", m.httpBindAddress) if err != nil { - httpLogger.Error("failed http listener", zap.Error(err)) - httpLogger.Info("Stopping") + m.log.Error("failed http listener", zap.Error(err)) + m.log.Info("Stopping") return err } @@ -867,8 +870,8 @@ func (m *Launcher) run(ctx context.Context) (err error) { cer, err = tls.LoadX509KeyPair(m.httpTLSCert, m.httpTLSKey) if err != nil { - httpLogger.Error("failed to load x509 key pair", zap.Error(err)) - httpLogger.Info("Stopping") + m.log.Error("failed to load x509 key pair", zap.Error(err)) + m.log.Info("Stopping") return err } transport = "https" @@ -895,7 +898,7 @@ func (m *Launcher) run(ctx context.Context) (err error) { } } log.Info("Stopping") - }(httpLogger) + }(m.log) return nil } diff --git a/http/handler.go b/http/handler.go index 20779de5251..e58b55a1d70 100644 --- a/http/handler.go +++ b/http/handler.go @@ -5,14 +5,11 @@ import ( "encoding/json" "net/http" _ "net/http/pprof" // used for debug pprof at the default path. - "strings" - "time" + "github.com/go-chi/chi" "github.com/influxdata/influxdb/kit/prom" - "github.com/influxdata/influxdb/kit/tracing" - "github.com/opentracing/opentracing-go" + kithttp "github.com/influxdata/influxdb/kit/transport/http" "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" "go.uber.org/zap" ) @@ -31,16 +28,7 @@ const ( // All other requests are passed down to the sub handler. type Handler struct { name string - // MetricsHandler handles metrics requests - MetricsHandler http.Handler - // ReadyHandler handles readiness checks - ReadyHandler http.Handler - // HealthHandler handles health requests - HealthHandler http.Handler - // DebugHandler handles debug requests - DebugHandler http.Handler - // Handler handles all other requests - Handler http.Handler + r chi.Router requests *prometheus.CounterVec requestDur *prometheus.HistogramVec @@ -49,104 +37,97 @@ type Handler struct { log *zap.Logger } -// NewHandler creates a new handler with the given name. -// The name is used to tag the metrics produced by this handler. -// -// The MetricsHandler is set to the default prometheus handler. -// It is the caller's responsibility to call prometheus.MustRegister(h.PrometheusCollectors()...). -// In most cases, you want to use NewHandlerFromRegistry instead. -func NewHandler(name string) *Handler { - h := &Handler{ - name: name, - MetricsHandler: promhttp.Handler(), - DebugHandler: http.DefaultServeMux, +type ( + handlerOpts struct { + log *zap.Logger + apiHandler http.Handler + debugHandler http.Handler + healthHandler http.Handler + metricsHandler http.Handler + readyHandler http.Handler + } + + HandlerOptFn func(opts *handlerOpts) +) + +func WithLog(l *zap.Logger) HandlerOptFn { + return func(opts *handlerOpts) { + opts.log = l + } +} + +func WithAPIHandler(h http.Handler) HandlerOptFn { + return func(opts *handlerOpts) { + opts.apiHandler = h + } +} + +func WithDebugHandler(h http.Handler) HandlerOptFn { + return func(opts *handlerOpts) { + opts.debugHandler = h + } +} + +func WithHealthHandler(h http.Handler) HandlerOptFn { + return func(opts *handlerOpts) { + opts.healthHandler = h + } +} + +func WithMetricsHandler(h http.Handler) HandlerOptFn { + return func(opts *handlerOpts) { + opts.metricsHandler = h + } +} + +func WithReadyHandler(h http.Handler) HandlerOptFn { + return func(opts *handlerOpts) { + opts.readyHandler = h } - h.initMetrics() - return h } // NewHandlerFromRegistry creates a new handler with the given name, // and sets the /metrics endpoint to use the metrics from the given registry, // after self-registering h's metrics. -func NewHandlerFromRegistry(log *zap.Logger, name string, reg *prom.Registry) *Handler { +func NewHandlerFromRegistry(name string, reg *prom.Registry, opts ...HandlerOptFn) *Handler { + opt := handlerOpts{ + log: zap.NewNop(), + debugHandler: http.DefaultServeMux, + healthHandler: http.HandlerFunc(HealthHandler), + metricsHandler: reg.HTTPHandler(), + readyHandler: ReadyHandler(), + } + for _, o := range opts { + o(&opt) + } + h := &Handler{ - name: name, - MetricsHandler: reg.HTTPHandler(), - ReadyHandler: http.HandlerFunc(ReadyHandler), - HealthHandler: http.HandlerFunc(HealthHandler), - DebugHandler: http.DefaultServeMux, - log: log, + name: name, + log: opt.log, } h.initMetrics() + + r := chi.NewRouter() + r.Use( + kithttp.Trace(name), + kithttp.Metrics(name, h.requests, h.requestDur), + ) + { + r.Mount(MetricsPath, opt.metricsHandler) + r.Mount(ReadyPath, opt.readyHandler) + r.Mount(HealthPath, opt.healthHandler) + r.Mount(DebugPath, opt.debugHandler) + r.Mount("/", opt.apiHandler) + } + h.r = r + reg.MustRegister(h.PrometheusCollectors()...) return h } // ServeHTTP delegates a request to the appropriate subhandler. func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - var span opentracing.Span - span, r = tracing.ExtractFromHTTPRequest(r, h.name) - - statusW := newStatusResponseWriter(w) - w = statusW - - // record prometheus metrics and finish traces - defer func(start time.Time) { - duration := time.Since(start) - statusClass := statusW.statusCodeClass() - ua := userAgent(r) - - h.requests.With(prometheus.Labels{ - "handler": h.name, - "method": r.Method, - "path": r.URL.Path, - "status": statusClass, - "user_agent": ua, - }).Inc() - h.requestDur.With(prometheus.Labels{ - "handler": h.name, - "method": r.Method, - "path": r.URL.Path, - "status": statusClass, - "user_agent": ua, - }).Observe(duration.Seconds()) - - span.LogKV("user_agent", ua) - for k, v := range r.Header { - if len(v) == 0 { - continue - } - - // yeah, we don't need these - if k == "Authorization" || k == "User-Agent" { - continue - } - - // If header has multiple values, only the first value will be logged on the trace. - span.LogKV(k, v[0]) - } - span.Finish() - }(time.Now()) - - switch { - case r.URL.Path == MetricsPath: - h.MetricsHandler.ServeHTTP(w, r) - case r.URL.Path == ReadyPath: - h.ReadyHandler.ServeHTTP(w, r) - case r.URL.Path == HealthPath: - h.HealthHandler.ServeHTTP(w, r) - case strings.HasPrefix(r.URL.Path, DebugPath): - h.DebugHandler.ServeHTTP(w, r) - default: - h.Handler.ServeHTTP(w, r) - } -} - -func encodeResponse(ctx context.Context, w http.ResponseWriter, code int, res interface{}) error { - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.WriteHeader(code) - - return json.NewEncoder(w).Encode(res) + h.r.ServeHTTP(w, r) } // PrometheusCollectors satisifies prom.PrometheusCollector. @@ -161,19 +142,27 @@ func (h *Handler) initMetrics() { const namespace = "http" const handlerSubsystem = "api" + labelNames := []string{"handler", "method", "path", "status", "user_agent"} h.requests = prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: namespace, Subsystem: handlerSubsystem, Name: "requests_total", Help: "Number of http requests received", - }, []string{"handler", "method", "path", "status", "user_agent"}) + }, labelNames) h.requestDur = prometheus.NewHistogramVec(prometheus.HistogramOpts{ Namespace: namespace, Subsystem: handlerSubsystem, Name: "request_duration_seconds", Help: "Time taken to respond to HTTP request", - }, []string{"handler", "method", "path", "status", "user_agent"}) + }, labelNames) +} + +func encodeResponse(ctx context.Context, w http.ResponseWriter, code int, res interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(code) + + return json.NewEncoder(w).Encode(res) } func logEncodingError(log *zap.Logger, r *http.Request, err error) { diff --git a/http/handler_test.go b/http/handler_test.go index 78bd1259688..76f0a448c98 100644 --- a/http/handler_test.go +++ b/http/handler_test.go @@ -42,14 +42,13 @@ func TestHandler_ServeHTTP(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := &Handler{ - name: tt.fields.name, - Handler: tt.fields.handler, - log: tt.fields.log, - } - h.initMetrics() reg := prom.NewRegistry(zaptest.NewLogger(t)) - reg.MustRegister(h.PrometheusCollectors()...) + h := NewHandlerFromRegistry( + tt.fields.name, + reg, + WithLog(tt.fields.log), + WithAPIHandler(tt.fields.handler), + ) tt.args.r.Header.Set("User-Agent", "ua1") h.ServeHTTP(tt.args.w, tt.args.r) diff --git a/http/middleware.go b/http/middleware.go index 10c6b0f2461..cfa0fdaa89c 100644 --- a/http/middleware.go +++ b/http/middleware.go @@ -9,39 +9,15 @@ import ( "strings" "time" - "github.com/influxdata/influxdb/kit/tracing" + kithttp "github.com/influxdata/influxdb/kit/transport/http" "go.uber.org/zap" ) -// Middleware constructor. -type Middleware func(http.Handler) http.Handler - -func traceMW(next http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - span, ctx := tracing.StartSpanFromContext(r.Context()) - defer span.Finish() - next.ServeHTTP(w, r.WithContext(ctx)) - } - return http.HandlerFunc(fn) -} - -func skipOptionsMW(next http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - if r.Method == "OPTIONS" { - return - } - next.ServeHTTP(w, r) - } - return http.HandlerFunc(fn) -} - // LoggingMW middleware for logging inflight http requests. -func LoggingMW(log *zap.Logger) Middleware { +func LoggingMW(log *zap.Logger) kithttp.Middleware { return func(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { - srw := &statusResponseWriter{ - ResponseWriter: w, - } + srw := kithttp.NewStatusResponseWriter(w) var buf bytes.Buffer r.Body = &bodyEchoer{ @@ -66,12 +42,12 @@ func LoggingMW(log *zap.Logger) Middleware { zap.String("path", r.URL.Path), zap.String("query", r.URL.Query().Encode()), zap.String("proto", r.Proto), - zap.Int("status_code", srw.code()), - zap.Int("response_size", srw.responseBytes), + zap.Int("status_code", srw.Code()), + zap.Int("response_size", srw.ResponseBytes()), zap.Int64("content_length", r.ContentLength), zap.String("referrer", r.Referer()), zap.String("remote", r.RemoteAddr), - zap.String("user_agent", r.UserAgent()), + zap.String("user_agent", kithttp.UserAgent(r)), zap.Duration("took", time.Since(start)), errField, errReferenceField, @@ -177,7 +153,7 @@ func (b *bodyEchoer) Close() error { return b.rc.Close() } -func applyMW(h http.Handler, m ...Middleware) http.Handler { +func applyMW(h http.Handler, m ...kithttp.Middleware) http.Handler { if len(m) < 1 { return h } diff --git a/http/middleware_test.go b/http/middleware_test.go index eb717f907e2..7701d7a9b06 100644 --- a/http/middleware_test.go +++ b/http/middleware_test.go @@ -108,6 +108,8 @@ func TestLoggingMW(t *testing.T) { testEndpoint := func(tt testRun) func(t *testing.T) { fn := func(t *testing.T) { + t.Helper() + log, buf := newDebugLogger(t) reqURL := urlWithQueries(tt.path, tt.queryPairs...) @@ -150,6 +152,7 @@ func TestLoggingMW(t *testing.T) { continue } fallthrough + case "user_agent": default: if expectedV := expected[k]; expectedV != v { t.Errorf("unexpected value(%q) for key(%q): expected=%q", v, k, expectedV) diff --git a/http/org_service.go b/http/org_service.go index 93269018db8..5ecacf79ba3 100644 --- a/http/org_service.go +++ b/http/org_service.go @@ -10,6 +10,7 @@ import ( "github.com/influxdata/httprouter" "github.com/influxdata/influxdb" "github.com/influxdata/influxdb/kit/tracing" + kithttp "github.com/influxdata/influxdb/kit/transport/http" "github.com/influxdata/influxdb/pkg/httpc" "go.uber.org/zap" ) @@ -72,7 +73,7 @@ const ( organizationsIDLabelsIDPath = "/api/v2/orgs/:id/labels/:lid" ) -func checkOrganziationExists(handler *OrgHandler) Middleware { +func checkOrganziationExists(handler *OrgHandler) kithttp.Middleware { fn := func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/http/pkger_http_server.go b/http/pkger_http_server.go index 359d91a270b..9b06c9ddb59 100644 --- a/http/pkger_http_server.go +++ b/http/pkger_http_server.go @@ -36,10 +36,11 @@ func NewHandlerPkg(log *zap.Logger, errHandler influxdb.HTTPErrorHandler, svc pk } r := chi.NewRouter() - r.Use(middleware.RequestID) - r.Use(middleware.RealIP) - r.Use(traceMW) - r.Use(middleware.Recoverer) + r.Use( + middleware.Recoverer, + middleware.RequestID, + middleware.RealIP, + ) { r.With(middleware.AllowContentType("text/yml", "application/x-yaml", "application/json")). diff --git a/http/platform_handler.go b/http/platform_handler.go index 3d2c0167b48..9c4ee1968ad 100644 --- a/http/platform_handler.go +++ b/http/platform_handler.go @@ -4,7 +4,7 @@ import ( "net/http" "strings" - "github.com/prometheus/client_golang/prometheus" + kithttp "github.com/influxdata/influxdb/kit/transport/http" ) // PlatformHandler is a collection of all the service handlers. @@ -14,19 +14,6 @@ type PlatformHandler struct { APIHandler http.Handler } -func setCORSResponseHeaders(next http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - if origin := r.Header.Get("Origin"); origin != "" { - w.Header().Set("Access-Control-Allow-Origin", origin) - w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") - w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, Authorization") - } - next.ServeHTTP(w, r) - } - - return http.HandlerFunc(fn) -} - // NewPlatformHandler returns a platform handler that serves the API and associated assets. func NewPlatformHandler(b *APIBackend, opts ...APIHandlerOptFn) *PlatformHandler { h := NewAuthenticationHandler(b.Logger, b.HTTPErrorHandler) @@ -46,8 +33,8 @@ func NewPlatformHandler(b *APIBackend, opts ...APIHandlerOptFn) *PlatformHandler assetHandler := NewAssetHandler() assetHandler.Path = b.AssetsPath - wrappedHandler := setCORSResponseHeaders(h) - wrappedHandler = skipOptionsMW(wrappedHandler) + wrappedHandler := kithttp.SetCORS(h) + wrappedHandler = kithttp.SkipOptions(wrappedHandler) return &PlatformHandler{ AssetHandler: assetHandler, @@ -74,9 +61,3 @@ func (h *PlatformHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.APIHandler.ServeHTTP(w, r) } - -// PrometheusCollectors satisfies the prom.PrometheusCollector interface. -func (h *PlatformHandler) PrometheusCollectors() []prometheus.Collector { - // TODO: collect and return relevant metrics. - return nil -} diff --git a/http/query_handler.go b/http/query_handler.go index 09fa3ffb478..f1a52c2a786 100644 --- a/http/query_handler.go +++ b/http/query_handler.go @@ -24,6 +24,7 @@ import ( "github.com/influxdata/influxdb/http/metric" "github.com/influxdata/influxdb/kit/check" "github.com/influxdata/influxdb/kit/tracing" + kithttp "github.com/influxdata/influxdb/kit/transport/http" "github.com/influxdata/influxdb/logger" "github.com/influxdata/influxdb/query" "github.com/pkg/errors" @@ -120,15 +121,15 @@ func (h *FluxHandler) handleQuery(w http.ResponseWriter, r *http.Request) { // Ideally this will be moved when we solve https://github.com/influxdata/influxdb/issues/13403 var orgID influxdb.ID var requestBytes int - sw := newStatusResponseWriter(w) + sw := kithttp.NewStatusResponseWriter(w) w = sw defer func() { h.EventRecorder.Record(ctx, metric.Event{ OrgID: orgID, Endpoint: r.URL.Path, // This should be sufficient for the time being as it should only be single endpoint. RequestBytes: requestBytes, - ResponseBytes: sw.responseBytes, - Status: sw.code(), + ResponseBytes: sw.ResponseBytes(), + Status: sw.Code(), }) }() diff --git a/http/ready.go b/http/ready.go index d212eb1f30d..12bf03c7f55 100644 --- a/http/ready.go +++ b/http/ready.go @@ -9,26 +9,28 @@ import ( "github.com/influxdata/influxdb/toml" ) -var up = time.Now() - // ReadyHandler is a default readiness handler. The default behaviour is always ready. -func ReadyHandler(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) +func ReadyHandler() http.Handler { + up := time.Now() + fn := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) - var status = struct { - Status string `json:"status"` - Start time.Time `json:"started"` - Up toml.Duration `json:"up"` - }{ - Status: "ready", - Start: up, - Up: toml.Duration(time.Since(up)), - } + var status = struct { + Status string `json:"status"` + Start time.Time `json:"started"` + // TODO(jsteenb2): learn why and leave comment for this being a toml.Duration + Up toml.Duration `json:"up"` + }{ + Status: "ready", + Start: up, + Up: toml.Duration(time.Since(up)), + } - enc := json.NewEncoder(w) - enc.SetIndent("", " ") - err := enc.Encode(status) - if err != nil { - fmt.Fprintf(w, "Error encoding status data: %v\n", err) + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + if err := enc.Encode(status); err != nil { + fmt.Fprintf(w, "Error encoding status data: %v\n", err) + } } + return http.HandlerFunc(fn) } diff --git a/http/router.go b/http/router.go index f17ca8ee36a..eae1aef9d9c 100644 --- a/http/router.go +++ b/http/router.go @@ -11,6 +11,7 @@ import ( "github.com/go-chi/chi/middleware" "github.com/influxdata/httprouter" platform "github.com/influxdata/influxdb" + kithttp "github.com/influxdata/influxdb/kit/transport/http" influxlogger "github.com/influxdata/influxdb/logger" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -32,9 +33,9 @@ func newBaseChiRouter(errorHandler platform.HTTPErrorHandler) chi.Router { bh := baseHandler{HTTPErrorHandler: errorHandler} router.NotFound(bh.notFound) router.MethodNotAllowed(bh.methodNotAllowed) - router.Use(skipOptionsMW) + router.Use(kithttp.SkipOptions) router.Use(middleware.StripSlashes) - router.Use(setCORSResponseHeaders) + router.Use(kithttp.SetCORS) return router } diff --git a/http/write_handler.go b/http/write_handler.go index 8229d5e7083..334f8c4a487 100644 --- a/http/write_handler.go +++ b/http/write_handler.go @@ -10,15 +10,15 @@ import ( "time" "github.com/influxdata/httprouter" - "github.com/influxdata/influxdb/http/metric" - "go.uber.org/zap" - "github.com/influxdata/influxdb" pcontext "github.com/influxdata/influxdb/context" + "github.com/influxdata/influxdb/http/metric" "github.com/influxdata/influxdb/kit/tracing" + kithttp "github.com/influxdata/influxdb/kit/transport/http" "github.com/influxdata/influxdb/models" "github.com/influxdata/influxdb/storage" "github.com/influxdata/influxdb/tsdb" + "go.uber.org/zap" ) // WriteBackend is all services and associated parameters required to construct @@ -99,15 +99,15 @@ func (h *WriteHandler) handleWrite(w http.ResponseWriter, r *http.Request) { // Ideally this will be moved when we solve https://github.com/influxdata/influxdb/issues/13403 var orgID influxdb.ID var requestBytes int - sw := newStatusResponseWriter(w) + sw := kithttp.NewStatusResponseWriter(w) w = sw defer func() { h.EventRecorder.Record(ctx, metric.Event{ OrgID: orgID, Endpoint: r.URL.Path, // This should be sufficient for the time being as it should only be single endpoint. RequestBytes: requestBytes, - ResponseBytes: sw.responseBytes, - Status: sw.code(), + ResponseBytes: sw.ResponseBytes(), + Status: sw.Code(), }) }() diff --git a/kit/transport/http/middleware.go b/kit/transport/http/middleware.go new file mode 100644 index 00000000000..ca14797866c --- /dev/null +++ b/kit/transport/http/middleware.go @@ -0,0 +1,123 @@ +package http + +import ( + "net/http" + "path" + "strings" + "time" + + "github.com/influxdata/influxdb" + "github.com/influxdata/influxdb/kit/tracing" + ua "github.com/mileusna/useragent" + "github.com/prometheus/client_golang/prometheus" +) + +// Middleware constructor. +type Middleware func(http.Handler) http.Handler + +func SetCORS(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + if origin := r.Header.Get("Origin"); origin != "" { + w.Header().Set("Access-Control-Allow-Origin", origin) + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") + w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, Authorization") + } + next.ServeHTTP(w, r) + } + + return http.HandlerFunc(fn) +} + +func Metrics(name string, reqMetric *prometheus.CounterVec, durMetric *prometheus.HistogramVec) Middleware { + return func(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + statusW := NewStatusResponseWriter(w) + + defer func(start time.Time) { + label := prometheus.Labels{ + "handler": name, + "method": r.Method, + "path": normalizePath(r.URL.Path), + "status": statusW.StatusCodeClass(), + "user_agent": UserAgent(r), + } + durMetric.With(label).Observe(time.Since(start).Seconds()) + reqMetric.With(label).Inc() + }(time.Now()) + + next.ServeHTTP(statusW, r) + } + return http.HandlerFunc(fn) + } +} + +func SkipOptions(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + if r.Method == "OPTIONS" { + return + } + next.ServeHTTP(w, r) + } + return http.HandlerFunc(fn) +} + +func Trace(name string) Middleware { + return func(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + span, r := tracing.ExtractFromHTTPRequest(r, name) + defer span.Finish() + + span.LogKV("user_agent", UserAgent(r)) + for k, v := range r.Header { + if len(v) == 0 { + continue + } + + if k == "Authorization" || k == "User-Agent" { + continue + } + + // If header has multiple values, only the first value will be logged on the trace. + span.LogKV(k, v[0]) + } + + next.ServeHTTP(w, r) + } + return http.HandlerFunc(fn) + } +} + +func UserAgent(r *http.Request) string { + header := r.Header.Get("User-Agent") + if header == "" { + return "unknown" + } + + return ua.Parse(header).Name +} + +func normalizePath(p string) string { + var parts []string + for head, tail := shiftPath(p); ; head, tail = shiftPath(tail) { + piece := head + if len(piece) == influxdb.IDLength { + if _, err := influxdb.IDFromString(head); err == nil { + piece = ":id" + } + } + parts = append(parts, piece) + if tail == "/" { + break + } + } + return "/" + path.Join(parts...) +} + +func shiftPath(p string) (head, tail string) { + p = path.Clean("/" + p) + i := strings.Index(p[1:], "/") + 1 + if i <= 0 { + return p[1:], "/" + } + return p[1:i], p[i:] +} diff --git a/kit/transport/http/middleware_test.go b/kit/transport/http/middleware_test.go new file mode 100644 index 00000000000..050c5dc383c --- /dev/null +++ b/kit/transport/http/middleware_test.go @@ -0,0 +1,45 @@ +package http + +import ( + "path" + "testing" + + "github.com/influxdata/influxdb" + "github.com/stretchr/testify/assert" +) + +func Test_normalizePath(t *testing.T) { + tests := []struct { + name string + path string + expected string + }{ + { + name: "1", + path: path.Join("/api/v2/organizations", influxdb.ID(2).String()), + expected: "/api/v2/organizations/:id", + }, + { + name: "2", + path: "/api/v2/organizations", + expected: "/api/v2/organizations", + }, + { + name: "3", + path: "/", + expected: "/", + }, + { + name: "4", + path: path.Join("/api/v2/organizations", influxdb.ID(2).String(), "users", influxdb.ID(3).String()), + expected: "/api/v2/organizations/:id/users/:id", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := normalizePath(tt.path) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/http/status.go b/kit/transport/http/status_response_writer.go similarity index 60% rename from http/status.go rename to kit/transport/http/status_response_writer.go index a81156fc9ea..68efba4828e 100644 --- a/http/status.go +++ b/kit/transport/http/status_response_writer.go @@ -2,31 +2,31 @@ package http import "net/http" -type statusResponseWriter struct { +type StatusResponseWriter struct { statusCode int responseBytes int http.ResponseWriter } -func newStatusResponseWriter(w http.ResponseWriter) *statusResponseWriter { - return &statusResponseWriter{ +func NewStatusResponseWriter(w http.ResponseWriter) *StatusResponseWriter { + return &StatusResponseWriter{ ResponseWriter: w, } } -func (w *statusResponseWriter) Write(b []byte) (int, error) { +func (w *StatusResponseWriter) Write(b []byte) (int, error) { n, err := w.ResponseWriter.Write(b) w.responseBytes += n return n, err } // WriteHeader writes the header and captures the status code. -func (w *statusResponseWriter) WriteHeader(statusCode int) { +func (w *StatusResponseWriter) WriteHeader(statusCode int) { w.statusCode = statusCode w.ResponseWriter.WriteHeader(statusCode) } -func (w *statusResponseWriter) code() int { +func (w *StatusResponseWriter) Code() int { code := w.statusCode if code == 0 { // When statusCode is 0 then WriteHeader was never called and we can assume that @@ -36,9 +36,13 @@ func (w *statusResponseWriter) code() int { return code } -func (w *statusResponseWriter) statusCodeClass() string { +func (w *StatusResponseWriter) ResponseBytes() int { + return w.responseBytes +} + +func (w *StatusResponseWriter) StatusCodeClass() string { class := "XXX" - switch w.code() / 100 { + switch w.Code() / 100 { case 1: class = "1XX" case 2: