1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2024-12-14 10:13:10 +02:00
opentelemetry-go/instrumentation/othttp/handler.go
Andy Schweig 5438916f62
Expose optional ResponseWriter interfaces. (#979)
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.
2020-07-30 10:30:47 -07:00

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)
})
}