Skip to content

Commit 38c01c9

Browse files
Chief-Rishabravisuhag
authored andcommitted
feat: add open-telemetry metrics to http clients
1 parent 4cff3fb commit 38c01c9

File tree

25 files changed

+866
-522
lines changed

25 files changed

+866
-522
lines changed

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ require (
6969
gopkg.in/yaml.v3 v3.0.1
7070
)
7171

72+
require github.com/felixge/httpsnoop v1.0.3 // indirect
73+
7274
require (
7375
cloud.google.com/go v0.110.6 // indirect
7476
cloud.google.com/go/compute v1.22.0 // indirect
@@ -217,6 +219,7 @@ require (
217219
github.com/zeebo/xxh3 v1.0.2 // indirect
218220
gitlab.com/flimzy/testy v0.8.0 // indirect
219221
go.opencensus.io v0.24.0 // indirect
222+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0
220223
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect
221224
go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.39.0 // indirect
222225
go.opentelemetry.io/otel/trace v1.16.0 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
574574
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
575575
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
576576
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
577+
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
578+
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
577579
github.com/flimzy/diff v0.1.7 h1:DRbd+lN3lY1xVuQrfqvDNsqBwA6RMbClMs6tS5sqWWk=
578580
github.com/flimzy/diff v0.1.7/go.mod h1:lFJtC7SPsK0EroDmGTSrdtWKAxOk3rO+q+e04LL05Hs=
579581
github.com/flimzy/testy v0.1.17 h1:Y+TUugY6s4B/vrOEPo6SUKafc41W5aiX3qUWvhAPMdI=
@@ -1607,6 +1609,8 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.4
16071609
go.opentelemetry.io/contrib/instrumentation/host v0.42.0 h1:/GMlvboQJd4LWxNX/oGYLv06J5a/M/flauLruM/3U2g=
16081610
go.opentelemetry.io/contrib/instrumentation/host v0.42.0/go.mod h1:w6v1mVemRjTTdfejACjf+LgVA6zKtHOWmdAIf3icx7A=
16091611
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4=
1612+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 h1:pginetY7+onl4qN1vl0xW/V/v6OBZ0vVdH+esuJgvmM=
1613+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0/go.mod h1:XiYsayHc36K3EByOO6nbAXnAWbrUxdjUROCEeeROOH8=
16101614
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0 h1:EbmAUG9hEAMXyfWEasIt2kmh/WmXUznUksChApTgBGc=
16111615
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0/go.mod h1:rD9feqRYP24P14t5kmhNMqsqm1jvKmpx2H2rKVw52V8=
16121616
go.opentelemetry.io/contrib/samplers/probability/consistent v0.11.0 h1:K4HJhe01GFkIo9JZ8iX1hMeI5MrkEMggXQM0YJamtEQ=
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package otelhttpclient
2+
3+
import (
4+
"context"
5+
"net/http"
6+
7+
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
8+
"go.opentelemetry.io/otel/attribute"
9+
)
10+
11+
type labelerContextKeyType int
12+
13+
const lablelerContextKey labelerContextKeyType = 0
14+
15+
// AnnotateRequest adds telemetry related annotations to request context and returns.
16+
// The request context on the returned request should be retained.
17+
// Ensure `route` is a route template and not actual URL to prevent high cardinality
18+
// on the metrics.
19+
func AnnotateRequest(req *http.Request, route string) *http.Request {
20+
ctx := req.Context()
21+
22+
l := &otelhttp.Labeler{}
23+
l.Add(attribute.String(attributeHTTPRoute, route))
24+
25+
return req.WithContext(context.WithValue(ctx, lablelerContextKey, l))
26+
}
27+
28+
// LabelerFromContext returns the labeler annotation from the context if exists.
29+
func LabelerFromContext(ctx context.Context) (*otelhttp.Labeler, bool) {
30+
l, ok := ctx.Value(lablelerContextKey).(*otelhttp.Labeler)
31+
if !ok {
32+
l = &otelhttp.Labeler{}
33+
}
34+
return l, ok
35+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package otelhttpclient
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"net/http"
7+
"time"
8+
9+
"go.opentelemetry.io/otel"
10+
"go.opentelemetry.io/otel/attribute"
11+
"go.opentelemetry.io/otel/metric"
12+
)
13+
14+
// Refer OpenTelemetry Semantic Conventions for HTTP Client.
15+
// https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-metrics.md#http-client
16+
const (
17+
metricClientDuration = "http.client.duration"
18+
metricClientRequestSize = "http.client.request.size"
19+
metricClientResponseSize = "http.client.response.size"
20+
21+
attributeNetProtoName = "network.protocol.name"
22+
attributeNetProtoVersion = "network.protocol.version"
23+
24+
attributeServerPort = "server.port"
25+
attributeServerAddress = "server.address"
26+
attributeHTTPRoute = "http.route"
27+
attributeRequestMethod = "http.request.method"
28+
attributeResponseStatusCode = "http.response.status_code"
29+
)
30+
31+
type httpTransport struct {
32+
roundTripper http.RoundTripper
33+
34+
metricClientDuration metric.Float64Histogram
35+
metricClientRequestSize metric.Int64Counter
36+
metricClientResponseSize metric.Int64Counter
37+
}
38+
39+
func NewHTTPTransport(baseTransport http.RoundTripper) http.RoundTripper {
40+
if _, ok := baseTransport.(*httpTransport); ok {
41+
return baseTransport
42+
}
43+
44+
if baseTransport == nil {
45+
baseTransport = http.DefaultTransport
46+
}
47+
48+
icl := &httpTransport{roundTripper: baseTransport}
49+
icl.createMeasures(otel.Meter("github.com/goto/meteor/metrics/otehttpclient"))
50+
51+
return icl
52+
}
53+
54+
func (tr *httpTransport) RoundTrip(req *http.Request) (*http.Response, error) {
55+
ctx := req.Context()
56+
startAt := time.Now()
57+
labeler, _ := LabelerFromContext(req.Context())
58+
59+
var bw bodyWrapper
60+
if req.Body != nil && req.Body != http.NoBody {
61+
bw.ReadCloser = req.Body
62+
req.Body = &bw
63+
}
64+
65+
port := req.URL.Port()
66+
if port == "" {
67+
port = "80"
68+
if req.URL.Scheme == "https" {
69+
port = "443"
70+
}
71+
}
72+
73+
attribs := append(labeler.Get(),
74+
attribute.String(attributeNetProtoName, "http"),
75+
attribute.String(attributeRequestMethod, req.Method),
76+
attribute.String(attributeServerAddress, req.URL.Hostname()),
77+
attribute.String(attributeServerPort, port),
78+
)
79+
80+
resp, err := tr.roundTripper.RoundTrip(req)
81+
if err != nil {
82+
attribs = append(attribs,
83+
attribute.Int(attributeResponseStatusCode, 0),
84+
attribute.String(attributeNetProtoVersion, fmt.Sprintf("%d.%d", req.ProtoMajor, req.ProtoMinor)),
85+
)
86+
} else {
87+
attribs = append(attribs,
88+
attribute.Int(attributeResponseStatusCode, resp.StatusCode),
89+
attribute.String(attributeNetProtoVersion, fmt.Sprintf("%d.%d", resp.ProtoMajor, resp.ProtoMinor)),
90+
)
91+
}
92+
93+
elapsedTime := float64(time.Since(startAt)) / float64(time.Millisecond)
94+
withAttribs := metric.WithAttributes(attribs...)
95+
tr.metricClientDuration.Record(ctx, elapsedTime, withAttribs)
96+
tr.metricClientRequestSize.Add(ctx, int64(bw.read), withAttribs)
97+
if resp != nil {
98+
tr.metricClientResponseSize.Add(ctx, resp.ContentLength, withAttribs)
99+
}
100+
101+
return resp, err
102+
}
103+
104+
func (tr *httpTransport) createMeasures(meter metric.Meter) {
105+
var err error
106+
107+
tr.metricClientRequestSize, err = meter.Int64Counter(metricClientRequestSize)
108+
handleErr(err)
109+
110+
tr.metricClientResponseSize, err = meter.Int64Counter(metricClientResponseSize)
111+
handleErr(err)
112+
113+
tr.metricClientDuration, err = meter.Float64Histogram(metricClientDuration)
114+
handleErr(err)
115+
}
116+
117+
func handleErr(err error) {
118+
if err != nil {
119+
otel.Handle(err)
120+
}
121+
}
122+
123+
// bodyWrapper wraps a http.Request.Body (an io.ReadCloser) to track the number
124+
// of bytes read and the last error.
125+
type bodyWrapper struct {
126+
io.ReadCloser
127+
128+
read int
129+
err error
130+
}
131+
132+
func (w *bodyWrapper) Read(b []byte) (int, error) {
133+
n, err := w.ReadCloser.Read(b)
134+
w.read += n
135+
w.err = err
136+
return n, err
137+
}
138+
139+
func (w *bodyWrapper) Close() error {
140+
return w.ReadCloser.Close()
141+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package otelhttpclient_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/raystack/meteor/metrics/otelhttpclient"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestNewHTTPTransport(t *testing.T) {
11+
tr := otelhttpclient.NewHTTPTransport(nil)
12+
assert.NotNil(t, tr)
13+
}

0 commit comments

Comments
 (0)