mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2024-12-14 10:13:10 +02:00
5438916f62
http.ResponseWriters may implement additional interfaces (http.CloseNotifier, http.Flusher, http.Hijacker, http.Pusher, io.ReaderFrom) that get lost when the ResponseWriter is wrapped in another object. This change uses the httpsnoop package to wrap the ResponseWriter so that the resulting object implements any of the optional interfaces that the original ResponseWriter implements as well as using the replacement ResponseWriter methods that gather information for tracing.
222 lines
6.8 KiB
Go
222 lines
6.8 KiB
Go
// Copyright The OpenTelemetry Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package othttp
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/felixge/httpsnoop"
|
|
|
|
"go.opentelemetry.io/otel/api/global"
|
|
"go.opentelemetry.io/otel/api/kv"
|
|
"go.opentelemetry.io/otel/api/metric"
|
|
"go.opentelemetry.io/otel/api/propagation"
|
|
"go.opentelemetry.io/otel/api/standard"
|
|
"go.opentelemetry.io/otel/api/trace"
|
|
)
|
|
|
|
var _ http.Handler = &Handler{}
|
|
|
|
// Handler is http middleware that corresponds to the http.Handler interface and
|
|
// is designed to wrap a http.Mux (or equivalent), while individual routes on
|
|
// the mux are wrapped with WithRouteTag. A Handler will add various attributes
|
|
// to the span using the kv.Keys defined in this package.
|
|
type Handler struct {
|
|
operation string
|
|
handler http.Handler
|
|
|
|
tracer trace.Tracer
|
|
meter metric.Meter
|
|
propagators propagation.Propagators
|
|
spanStartOptions []trace.StartOption
|
|
readEvent bool
|
|
writeEvent bool
|
|
filters []Filter
|
|
spanNameFormatter func(string, *http.Request) string
|
|
counters map[string]metric.Int64Counter
|
|
valueRecorders map[string]metric.Int64ValueRecorder
|
|
}
|
|
|
|
func defaultHandlerFormatter(operation string, _ *http.Request) string {
|
|
return operation
|
|
}
|
|
|
|
// NewHandler wraps the passed handler, functioning like middleware, in a span
|
|
// named after the operation and with any provided Options.
|
|
func NewHandler(handler http.Handler, operation string, opts ...Option) http.Handler {
|
|
h := Handler{
|
|
handler: handler,
|
|
operation: operation,
|
|
}
|
|
|
|
const domain = "go.opentelemetry.io/otel/instrumentation/othttp"
|
|
|
|
defaultOpts := []Option{
|
|
WithTracer(global.Tracer(domain)),
|
|
WithMeter(global.Meter(domain)),
|
|
WithPropagators(global.Propagators()),
|
|
WithSpanOptions(trace.WithSpanKind(trace.SpanKindServer)),
|
|
WithSpanNameFormatter(defaultHandlerFormatter),
|
|
}
|
|
|
|
c := NewConfig(append(defaultOpts, opts...)...)
|
|
h.configure(c)
|
|
h.createMeasures()
|
|
|
|
return &h
|
|
}
|
|
|
|
func (h *Handler) configure(c *Config) {
|
|
h.tracer = c.Tracer
|
|
h.meter = c.Meter
|
|
h.propagators = c.Propagators
|
|
h.spanStartOptions = c.SpanStartOptions
|
|
h.readEvent = c.ReadEvent
|
|
h.writeEvent = c.WriteEvent
|
|
h.filters = c.Filters
|
|
h.spanNameFormatter = c.SpanNameFormatter
|
|
}
|
|
|
|
func handleErr(err error) {
|
|
if err != nil {
|
|
global.Handle(err)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) createMeasures() {
|
|
h.counters = make(map[string]metric.Int64Counter)
|
|
h.valueRecorders = make(map[string]metric.Int64ValueRecorder)
|
|
|
|
requestBytesCounter, err := h.meter.NewInt64Counter(RequestContentLength)
|
|
handleErr(err)
|
|
|
|
responseBytesCounter, err := h.meter.NewInt64Counter(ResponseContentLength)
|
|
handleErr(err)
|
|
|
|
serverLatencyMeasure, err := h.meter.NewInt64ValueRecorder(ServerLatency)
|
|
handleErr(err)
|
|
|
|
h.counters[RequestContentLength] = requestBytesCounter
|
|
h.counters[ResponseContentLength] = responseBytesCounter
|
|
h.valueRecorders[ServerLatency] = serverLatencyMeasure
|
|
}
|
|
|
|
// ServeHTTP serves HTTP requests (http.Handler)
|
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
requestStartTime := time.Now()
|
|
for _, f := range h.filters {
|
|
if !f(r) {
|
|
// Simply pass through to the handler if a filter rejects the request
|
|
h.handler.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
}
|
|
|
|
opts := append([]trace.StartOption{
|
|
trace.WithAttributes(standard.NetAttributesFromHTTPRequest("tcp", r)...),
|
|
trace.WithAttributes(standard.EndUserAttributesFromHTTPRequest(r)...),
|
|
trace.WithAttributes(standard.HTTPServerAttributesFromHTTPRequest(h.operation, "", r)...),
|
|
}, h.spanStartOptions...) // start with the configured options
|
|
|
|
ctx := propagation.ExtractHTTP(r.Context(), h.propagators, r.Header)
|
|
ctx, span := h.tracer.Start(ctx, h.spanNameFormatter(h.operation, r), opts...)
|
|
defer span.End()
|
|
|
|
readRecordFunc := func(int64) {}
|
|
if h.readEvent {
|
|
readRecordFunc = func(n int64) {
|
|
span.AddEvent(ctx, "read", ReadBytesKey.Int64(n))
|
|
}
|
|
}
|
|
bw := bodyWrapper{ReadCloser: r.Body, record: readRecordFunc}
|
|
r.Body = &bw
|
|
|
|
writeRecordFunc := func(int64) {}
|
|
if h.writeEvent {
|
|
writeRecordFunc = func(n int64) {
|
|
span.AddEvent(ctx, "write", WroteBytesKey.Int64(n))
|
|
}
|
|
}
|
|
|
|
rww := &respWriterWrapper{ResponseWriter: w, record: writeRecordFunc, ctx: ctx, props: h.propagators}
|
|
|
|
// Wrap w to use our ResponseWriter methods while also exposing
|
|
// other interfaces that w may implement (http.CloseNotifier,
|
|
// http.Flusher, http.Hijacker, http.Pusher, io.ReaderFrom).
|
|
|
|
w = httpsnoop.Wrap(w, httpsnoop.Hooks{
|
|
Header: func(httpsnoop.HeaderFunc) httpsnoop.HeaderFunc {
|
|
return rww.Header
|
|
},
|
|
Write: func(httpsnoop.WriteFunc) httpsnoop.WriteFunc {
|
|
return rww.Write
|
|
},
|
|
WriteHeader: func(httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc {
|
|
return rww.WriteHeader
|
|
},
|
|
})
|
|
|
|
h.handler.ServeHTTP(w, r.WithContext(ctx))
|
|
|
|
setAfterServeAttributes(span, bw.read, rww.written, rww.statusCode, bw.err, rww.err)
|
|
|
|
// Add request metrics
|
|
|
|
labels := standard.HTTPServerMetricAttributesFromHTTPRequest(h.operation, r)
|
|
|
|
h.counters[RequestContentLength].Add(ctx, bw.read, labels...)
|
|
h.counters[ResponseContentLength].Add(ctx, rww.written, labels...)
|
|
|
|
elapsedTime := time.Since(requestStartTime).Microseconds()
|
|
|
|
h.valueRecorders[ServerLatency].Record(ctx, elapsedTime, labels...)
|
|
}
|
|
|
|
func setAfterServeAttributes(span trace.Span, read, wrote int64, statusCode int, rerr, werr error) {
|
|
kv := []kv.KeyValue{}
|
|
|
|
// TODO: Consider adding an event after each read and write, possibly as an
|
|
// option (defaulting to off), so as to not create needlessly verbose spans.
|
|
if read > 0 {
|
|
kv = append(kv, ReadBytesKey.Int64(read))
|
|
}
|
|
if rerr != nil && rerr != io.EOF {
|
|
kv = append(kv, ReadErrorKey.String(rerr.Error()))
|
|
}
|
|
if wrote > 0 {
|
|
kv = append(kv, WroteBytesKey.Int64(wrote))
|
|
}
|
|
if statusCode > 0 {
|
|
kv = append(kv, standard.HTTPAttributesFromHTTPStatusCode(statusCode)...)
|
|
span.SetStatus(standard.SpanStatusFromHTTPStatusCode(statusCode))
|
|
}
|
|
if werr != nil && werr != io.EOF {
|
|
kv = append(kv, WriteErrorKey.String(werr.Error()))
|
|
}
|
|
span.SetAttributes(kv...)
|
|
}
|
|
|
|
// WithRouteTag annotates a span with the provided route name using the
|
|
// RouteKey Tag.
|
|
func WithRouteTag(route string, h http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
span := trace.SpanFromContext(r.Context())
|
|
span.SetAttributes(standard.HTTPRouteKey.String(route))
|
|
h.ServeHTTP(w, r)
|
|
})
|
|
}
|