Passed
Push — main ( eb148d...532cfb )
by Acho
01:02 queued 11s
created

otelroundtripper.mustCounter   A

Complexity

Conditions 2

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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