otelroundtripper.New   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 22
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 19
nop 1
dl 0
loc 22
rs 9.45
c 0
b 0
f 0
1
package otelroundtripper
2
3
import (
4
	"context"
5
	"errors"
6
	"go.opentelemetry.io/otel/attribute"
7
	api "go.opentelemetry.io/otel/metric"
8
	semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
9
	"net"
10
	"net/http"
11
	"strings"
12
	"time"
13
)
14
15
type otelHTTPMetrics struct {
16
	attemptsCounter         api.Int64Counter
17
	noRequestCounter        api.Int64Counter
18
	errorsCounter           api.Int64Counter
19
	successesCounter        api.Int64Counter
20
	failureCounter          api.Int64Counter
21
	redirectCounter         api.Int64Counter
22
	timeoutsCounter         api.Int64Counter
23
	canceledCounter         api.Int64Counter
24
	deadlineExceededCounter api.Int64Counter
25
	totalDurationCounter    api.Int64Histogram
26
	inFlightCounter         api.Int64UpDownCounter
27
}
28
29
// otelRoundTripper is the http.RoundTripper which emits open telemetry metrics
30
type otelRoundTripper struct {
31
	parent     http.RoundTripper
32
	attributes []attribute.KeyValue
33
	metrics    otelHTTPMetrics
34
}
35
36
// New creates a new instance of the http.RoundTripper
37
func New(options ...Option) http.RoundTripper {
38
	cfg := defaultConfig()
39
40
	for _, option := range options {
41
		option.apply(cfg)
42
	}
43
44
	return &otelRoundTripper{
45
		parent:     cfg.parent,
46
		attributes: cfg.attributes,
47
		metrics: otelHTTPMetrics{
48
			noRequestCounter:        mustCounter(cfg.meter.Int64Counter(cfg.name + ".no_request")),
49
			errorsCounter:           mustCounter(cfg.meter.Int64Counter(cfg.name + ".errors")),
50
			successesCounter:        mustCounter(cfg.meter.Int64Counter(cfg.name + ".success")),
51
			timeoutsCounter:         mustCounter(cfg.meter.Int64Counter(cfg.name + ".timeouts")),
52
			canceledCounter:         mustCounter(cfg.meter.Int64Counter(cfg.name + ".cancelled")),
53
			deadlineExceededCounter: mustCounter(cfg.meter.Int64Counter(cfg.name + ".deadline_exceeded")),
54
			totalDurationCounter:    mustHistogram(cfg.meter.Int64Histogram(cfg.name + ".total_duration")),
55
			inFlightCounter:         mustUpDownCounter(cfg.meter.Int64UpDownCounter(cfg.name + ".in_flight")),
56
			attemptsCounter:         mustCounter(cfg.meter.Int64Counter(cfg.name + ".attempts")),
57
			failureCounter:          mustCounter(cfg.meter.Int64Counter(cfg.name + ".failures")),
58
			redirectCounter:         mustCounter(cfg.meter.Int64Counter(cfg.name + ".redirects")),
59
		},
60
	}
61
}
62
63
func mustCounter(counter api.Int64Counter, err error) api.Int64Counter {
64
	if err != nil {
65
		panic(err)
66
	}
67
	return counter
68
}
69
70
func mustUpDownCounter(counter api.Int64UpDownCounter, err error) api.Int64UpDownCounter {
71
	if err != nil {
72
		panic(err)
73
	}
74
	return counter
75
}
76
77
func mustHistogram(histogram api.Int64Histogram, err error) api.Int64Histogram {
78
	if err != nil {
79
		panic(err)
80
	}
81
	return histogram
82
}
83
84
// RoundTrip executes a single HTTP transaction, returning a Response for the provided Request.
85
func (roundTripper *otelRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
86
	ctx := roundTripper.extractCtx(request)
87
	attributes := roundTripper.requestAttributes(request)
88
89
	roundTripper.beforeHook(ctx, attributes, request)
90
91
	start := time.Now()
92
	response, err := roundTripper.parent.RoundTrip(request)
93
	duration := time.Since(start).Milliseconds()
94
95
	if err != nil {
96
		roundTripper.errorHook(ctx, err, attributes)
97
		return response, err
98
	}
99
100
	attributes = roundTripper.responseAttributes(attributes, response)
101
	roundTripper.afterHook(ctx, duration, attributes)
102
103
	if roundTripper.isRedirection(response) {
104
		roundTripper.redirectHook(ctx, attributes)
105
		return response, err
106
	}
107
108
	if roundTripper.isFailure(response) {
109
		roundTripper.failureHook(ctx, attributes)
110
		return response, err
111
	}
112
113
	roundTripper.successHook(ctx, attributes)
114
	return response, err
115
}
116
117
func (roundTripper *otelRoundTripper) isFailure(response *http.Response) bool {
118
	if response == nil {
119
		return false
120
	}
121
	return response.StatusCode >= http.StatusBadRequest
122
}
123
124
func (roundTripper *otelRoundTripper) isRedirection(response *http.Response) bool {
125
	if response == nil {
126
		return false
127
	}
128
	return response.StatusCode >= http.StatusMultipleChoices && response.StatusCode < http.StatusBadRequest
129
}
130
131
func (roundTripper *otelRoundTripper) failureHook(
132
	ctx context.Context,
133
	attributes []attribute.KeyValue,
134
) {
135
	roundTripper.metrics.inFlightCounter.Add(ctx, -1, api.WithAttributes(attributes...))
136
	roundTripper.metrics.failureCounter.Add(ctx, 1, api.WithAttributes(attributes...))
137
}
138
139
func (roundTripper *otelRoundTripper) redirectHook(
140
	ctx context.Context,
141
	attributes []attribute.KeyValue,
142
) {
143
	roundTripper.metrics.inFlightCounter.Add(ctx, -1, api.WithAttributes(attributes...))
144
	roundTripper.metrics.redirectCounter.Add(ctx, 1, api.WithAttributes(attributes...))
145
}
146
147
func (roundTripper *otelRoundTripper) successHook(
148
	ctx context.Context,
149
	attributes []attribute.KeyValue,
150
) {
151
	roundTripper.metrics.inFlightCounter.Add(ctx, -1, api.WithAttributes(attributes...))
152
	roundTripper.metrics.successesCounter.Add(ctx, 1, api.WithAttributes(attributes...))
153
}
154
155
func (roundTripper *otelRoundTripper) beforeHook(
156
	ctx context.Context,
157
	attributes []attribute.KeyValue,
158
	request *http.Request,
159
) {
160
	roundTripper.metrics.inFlightCounter.Add(ctx, 1, api.WithAttributes(attributes...))
161
	roundTripper.metrics.attemptsCounter.Add(ctx, 1, api.WithAttributes(attributes...))
162
	if request == nil {
163
		roundTripper.metrics.noRequestCounter.Add(ctx, 1, api.WithAttributes(attributes...))
164
	}
165
}
166
167
func (roundTripper *otelRoundTripper) afterHook(
168
	ctx context.Context,
169
	duration int64,
170
	attributes []attribute.KeyValue,
171
) {
172
	roundTripper.metrics.totalDurationCounter.Record(ctx, duration, api.WithAttributes(attributes...))
173
}
174
175
func (roundTripper *otelRoundTripper) responseAttributes(
176
	attributes []attribute.KeyValue,
177
	response *http.Response,
178
) []attribute.KeyValue {
179
	return append(
180
		append([]attribute.KeyValue{}, attributes...),
181
		roundTripper.extractResponseAttributes(response)...,
182
	)
183
}
184
185
func (roundTripper *otelRoundTripper) requestAttributes(request *http.Request) []attribute.KeyValue {
186
	return append(
187
		append(
188
			[]attribute.KeyValue{},
189
			roundTripper.attributes...,
190
		),
191
		roundTripper.extractRequestAttributes(request)...,
192
	)
193
}
194
195
func (roundTripper *otelRoundTripper) errorHook(ctx context.Context, err error, attributes []attribute.KeyValue) {
196
	roundTripper.metrics.inFlightCounter.Add(ctx, -1, api.WithAttributes(attributes...))
197
	roundTripper.metrics.errorsCounter.Add(ctx, 1, api.WithAttributes(attributes...))
198
199
	var timeoutErr net.Error
200
	if errors.As(err, &timeoutErr) && timeoutErr.Timeout() {
201
		roundTripper.metrics.timeoutsCounter.Add(ctx, 1, api.WithAttributes(attributes...))
202
	}
203
204
	if strings.HasSuffix(err.Error(), context.Canceled.Error()) {
205
		roundTripper.metrics.canceledCounter.Add(ctx, 1, api.WithAttributes(attributes...))
206
	}
207
}
208
209
func (roundTripper *otelRoundTripper) extractResponseAttributes(response *http.Response) []attribute.KeyValue {
210
	if response != nil {
211
		return []attribute.KeyValue{
212
			semconv.HTTPStatusCodeKey.Int(response.StatusCode),
213
			semconv.HTTPFlavorKey.String(response.Proto),
214
		}
215
	}
216
	return nil
217
}
218
219
func (roundTripper *otelRoundTripper) extractRequestAttributes(request *http.Request) []attribute.KeyValue {
220
	if request != nil {
221
		return []attribute.KeyValue{
222
			semconv.HTTPURLKey.String(request.URL.String()),
223
			semconv.HTTPMethodKey.String(request.Method),
224
		}
225
	}
226
	return nil
227
}
228
229
func (roundTripper *otelRoundTripper) extractCtx(request *http.Request) context.Context {
230
	if request != nil && request.Context() != nil {
231
		return request.Context()
232
	}
233
	return context.Background()
234
}
235