Skip to content

Commit e8226c4

Browse files
AndrewGrachovjmacdMrAlias
authored
Add default metrics to othttp instrumentation Fixes #542 (#861)
* Add default metrics to othttp instrumentation * Changed metrics names, add tests, add standard labels to metrics * Initialization global error handling, remove requests count metric, tuneup test * Apply suggestions from code review Co-authored-by: Tyler Yahn <[email protected]> Co-authored-by: Joshua MacDonald <[email protected]> Co-authored-by: Tyler Yahn <[email protected]>
1 parent 85c32d5 commit e8226c4

File tree

6 files changed

+131
-7
lines changed

6 files changed

+131
-7
lines changed

api/standard/http.go

+19-4
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,15 @@ func HTTPClientAttributesFromHTTPRequest(request *http.Request) []kv.KeyValue {
148148

149149
func httpCommonAttributesFromHTTPRequest(request *http.Request) []kv.KeyValue {
150150
attrs := []kv.KeyValue{}
151+
if ua := request.UserAgent(); ua != "" {
152+
attrs = append(attrs, HTTPUserAgentKey.String(ua))
153+
}
154+
155+
return append(attrs, httpBasicAttributesFromHTTPRequest(request)...)
156+
}
157+
158+
func httpBasicAttributesFromHTTPRequest(request *http.Request) []kv.KeyValue {
159+
attrs := []kv.KeyValue{}
151160

152161
if request.TLS != nil {
153162
attrs = append(attrs, HTTPSchemeHTTPS)
@@ -159,10 +168,6 @@ func httpCommonAttributesFromHTTPRequest(request *http.Request) []kv.KeyValue {
159168
attrs = append(attrs, HTTPHostKey.String(request.Host))
160169
}
161170

162-
if ua := request.UserAgent(); ua != "" {
163-
attrs = append(attrs, HTTPUserAgentKey.String(ua))
164-
}
165-
166171
flavor := ""
167172
if request.ProtoMajor == 1 {
168173
flavor = fmt.Sprintf("1.%d", request.ProtoMinor)
@@ -176,6 +181,16 @@ func httpCommonAttributesFromHTTPRequest(request *http.Request) []kv.KeyValue {
176181
return attrs
177182
}
178183

184+
// HTTPServerMetricAttributesFromHTTPRequest generates low-cardinality attributes
185+
// to be used with server-side HTTP metrics.
186+
func HTTPServerMetricAttributesFromHTTPRequest(serverName string, request *http.Request) []kv.KeyValue {
187+
attrs := []kv.KeyValue{}
188+
if serverName != "" {
189+
attrs = append(attrs, HTTPServerNameKey.String(serverName))
190+
}
191+
return append(attrs, httpBasicAttributesFromHTTPRequest(request)...)
192+
}
193+
179194
// HTTPServerAttributesFromHTTPRequest generates attributes of the
180195
// http namespace as specified by the OpenTelemetry specification for
181196
// a span on the server side. Currently, only basic authentication is

instrumentation/othttp/common.go

+8
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ const (
2828
WriteErrorKey = kv.Key("http.write_error") // if an error occurred while writing a reply, the string of the error (io.EOF is not recorded)
2929
)
3030

31+
// Server HTTP metrics
32+
const (
33+
RequestCount = "http.server.request_count" // Incoming request count total
34+
RequestContentLength = "http.server.request_content_length" // Incoming request bytes total
35+
ResponseContentLength = "http.server.response_content_length" // Incoming response bytes total
36+
ServerLatency = "http.server.duration" // Incoming end to end duration, microseconds
37+
)
38+
3139
// Filter is a predicate used to determine whether a given http.request should
3240
// be traced. A Filter must return true if the request should be traced.
3341
type Filter func(*http.Request) bool

instrumentation/othttp/config.go

+10
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package othttp
1717
import (
1818
"net/http"
1919

20+
"go.opentelemetry.io/otel/api/metric"
2021
"go.opentelemetry.io/otel/api/propagation"
2122
"go.opentelemetry.io/otel/api/trace"
2223
)
@@ -25,6 +26,7 @@ import (
2526
// and othttp.Transport types.
2627
type Config struct {
2728
Tracer trace.Tracer
29+
Meter metric.Meter
2830
Propagators propagation.Propagators
2931
SpanStartOptions []trace.StartOption
3032
ReadEvent bool
@@ -63,6 +65,14 @@ func WithTracer(tracer trace.Tracer) Option {
6365
})
6466
}
6567

68+
// WithMeter configures a specific meter. If this option
69+
// isn't specified then the global meter is used.
70+
func WithMeter(meter metric.Meter) Option {
71+
return OptionFunc(func(c *Config) {
72+
c.Meter = meter
73+
})
74+
}
75+
6676
// WithPublicEndpoint configures the Handler to link the span with an incoming
6777
// span context. If this option is not provided, then the association is a child
6878
// association instead of a link.

instrumentation/othttp/handler.go

+47-1
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ package othttp
1717
import (
1818
"io"
1919
"net/http"
20+
"time"
2021

2122
"go.opentelemetry.io/otel/api/global"
2223
"go.opentelemetry.io/otel/api/kv"
24+
"go.opentelemetry.io/otel/api/metric"
2325
"go.opentelemetry.io/otel/api/propagation"
2426
"go.opentelemetry.io/otel/api/standard"
2527
"go.opentelemetry.io/otel/api/trace"
@@ -36,12 +38,15 @@ type Handler struct {
3638
handler http.Handler
3739

3840
tracer trace.Tracer
41+
meter metric.Meter
3942
propagators propagation.Propagators
4043
spanStartOptions []trace.StartOption
4144
readEvent bool
4245
writeEvent bool
4346
filters []Filter
4447
spanNameFormatter func(string, *http.Request) string
48+
counters map[string]metric.Int64Counter
49+
valueRecorders map[string]metric.Int64ValueRecorder
4550
}
4651

4752
func defaultHandlerFormatter(operation string, _ *http.Request) string {
@@ -56,21 +61,26 @@ func NewHandler(handler http.Handler, operation string, opts ...Option) http.Han
5661
operation: operation,
5762
}
5863

64+
const domain = "go.opentelemetry.io/otel/instrumentation/othttp"
65+
5966
defaultOpts := []Option{
60-
WithTracer(global.Tracer("go.opentelemetry.io/otel/instrumentation/othttp")),
67+
WithTracer(global.Tracer(domain)),
68+
WithMeter(global.Meter(domain)),
6169
WithPropagators(global.Propagators()),
6270
WithSpanOptions(trace.WithSpanKind(trace.SpanKindServer)),
6371
WithSpanNameFormatter(defaultHandlerFormatter),
6472
}
6573

6674
c := NewConfig(append(defaultOpts, opts...)...)
6775
h.configure(c)
76+
h.createMeasures()
6877

6978
return &h
7079
}
7180

7281
func (h *Handler) configure(c *Config) {
7382
h.tracer = c.Tracer
83+
h.meter = c.Meter
7484
h.propagators = c.Propagators
7585
h.spanStartOptions = c.SpanStartOptions
7686
h.readEvent = c.ReadEvent
@@ -79,8 +89,33 @@ func (h *Handler) configure(c *Config) {
7989
h.spanNameFormatter = c.SpanNameFormatter
8090
}
8191

92+
func handleErr(err error) {
93+
if err != nil {
94+
global.Handle(err)
95+
}
96+
}
97+
98+
func (h *Handler) createMeasures() {
99+
h.counters = make(map[string]metric.Int64Counter)
100+
h.valueRecorders = make(map[string]metric.Int64ValueRecorder)
101+
102+
requestBytesCounter, err := h.meter.NewInt64Counter(RequestContentLength)
103+
handleErr(err)
104+
105+
responseBytesCounter, err := h.meter.NewInt64Counter(ResponseContentLength)
106+
handleErr(err)
107+
108+
serverLatencyMeasure, err := h.meter.NewInt64ValueRecorder(ServerLatency)
109+
handleErr(err)
110+
111+
h.counters[RequestContentLength] = requestBytesCounter
112+
h.counters[ResponseContentLength] = responseBytesCounter
113+
h.valueRecorders[ServerLatency] = serverLatencyMeasure
114+
}
115+
82116
// ServeHTTP serves HTTP requests (http.Handler)
83117
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
118+
requestStartTime := time.Now()
84119
for _, f := range h.filters {
85120
if !f(r) {
86121
// Simply pass through to the handler if a filter rejects the request
@@ -121,6 +156,17 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
121156

122157
setAfterServeAttributes(span, bw.read, rww.written, rww.statusCode, bw.err, rww.err)
123158
span.SetStatus(standard.SpanStatusFromHTTPStatusCode(rww.statusCode))
159+
160+
// Add request metrics
161+
162+
labels := standard.HTTPServerMetricAttributesFromHTTPRequest(h.operation, r)
163+
164+
h.counters[RequestContentLength].Add(ctx, bw.read, labels...)
165+
h.counters[ResponseContentLength].Add(ctx, rww.written, labels...)
166+
167+
elapsedTime := time.Since(requestStartTime).Microseconds()
168+
169+
h.valueRecorders[ServerLatency].Record(ctx, elapsedTime, labels...)
124170
}
125171

126172
func setAfterServeAttributes(span trace.Span, read, wrote int64, statusCode int, rerr, werr error) {

instrumentation/othttp/handler_example_test.go

+13
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626

2727
"go.opentelemetry.io/otel/api/global"
2828
"go.opentelemetry.io/otel/api/trace"
29+
mstdout "go.opentelemetry.io/otel/exporters/metric/stdout"
2930
"go.opentelemetry.io/otel/exporters/trace/stdout"
3031
"go.opentelemetry.io/otel/instrumentation/othttp"
3132
sdktrace "go.opentelemetry.io/otel/sdk/trace"
@@ -57,7 +58,19 @@ func ExampleNewHandler() {
5758
if err != nil {
5859
log.Fatal(err)
5960
}
61+
62+
pusher, err := mstdout.NewExportPipeline(mstdout.Config{
63+
PrettyPrint: true,
64+
DoNotPrintTime: true, // This makes the output deterministic
65+
})
66+
67+
if err != nil {
68+
log.Fatal(err)
69+
}
70+
71+
meterProvider := pusher.Provider()
6072
global.SetTraceProvider(tp)
73+
global.SetMeterProvider(meterProvider)
6174

6275
figureOutName := func(ctx context.Context, s string) (string, error) {
6376
pp := strings.SplitN(s, "/", 2)

instrumentation/othttp/handler_test.go

+34-2
Original file line numberDiff line numberDiff line change
@@ -14,34 +14,66 @@
1414
package othttp
1515

1616
import (
17+
"fmt"
1718
"io"
1819
"io/ioutil"
1920
"net/http"
2021
"net/http/httptest"
2122
"testing"
2223

24+
"github.com/stretchr/testify/assert"
25+
26+
"go.opentelemetry.io/otel/api/kv"
27+
"go.opentelemetry.io/otel/api/standard"
28+
mockmeter "go.opentelemetry.io/otel/internal/metric"
2329
mocktrace "go.opentelemetry.io/otel/internal/trace"
2430
)
2531

32+
func assertMetricLabels(t *testing.T, expectedLabels []kv.KeyValue, measurementBatches []mockmeter.Batch) {
33+
for _, batch := range measurementBatches {
34+
assert.ElementsMatch(t, expectedLabels, batch.Labels)
35+
}
36+
}
37+
2638
func TestHandlerBasics(t *testing.T) {
2739
rr := httptest.NewRecorder()
2840

2941
var id uint64
3042
tracer := mocktrace.MockTracer{StartSpanID: &id}
43+
meterimpl, meter := mockmeter.NewMeter()
44+
45+
operation := "test_handler"
3146

3247
h := NewHandler(
3348
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
3449
if _, err := io.WriteString(w, "hello world"); err != nil {
3550
t.Fatal(err)
3651
}
37-
}), "test_handler",
38-
WithTracer(&tracer))
52+
}), operation,
53+
WithTracer(&tracer),
54+
WithMeter(meter),
55+
)
3956

4057
r, err := http.NewRequest(http.MethodGet, "http://localhost/", nil)
4158
if err != nil {
4259
t.Fatal(err)
4360
}
4461
h.ServeHTTP(rr, r)
62+
63+
if len(meterimpl.MeasurementBatches) == 0 {
64+
t.Fatalf("got 0 recorded measurements, expected 1 or more")
65+
}
66+
67+
labelsToVerify := []kv.KeyValue{
68+
standard.HTTPServerNameKey.String(operation),
69+
standard.HTTPSchemeHTTP,
70+
standard.HTTPHostKey.String(r.Host),
71+
standard.HTTPFlavorKey.String(fmt.Sprintf("1.%d", r.ProtoMinor)),
72+
}
73+
74+
standard.HTTPServerMetricAttributesFromHTTPRequest(operation, r)
75+
assertMetricLabels(t, labelsToVerify, meterimpl.MeasurementBatches)
76+
4577
if got, expected := rr.Result().StatusCode, http.StatusOK; got != expected {
4678
t.Fatalf("got %d, expected %d", got, expected)
4779
}

0 commit comments

Comments
 (0)