Skip to content

Commit bdd9b1e

Browse files
authored
Fix tests, readme, add interface for custom metrics recording (#5)
* Fix tests, readme, add interface for custom metrics recording * fix CI
1 parent 50871d5 commit bdd9b1e

6 files changed

Lines changed: 424 additions & 114 deletions

File tree

.github/workflows/checks.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ jobs:
4141
4242
- name: Run gosec
4343
run: |
44-
go install github.com/securego/gosec/v2/cmd/gosec@latest
44+
# 2026-02-16: gosec is broken for multi-module repos, so we use v2.23.0 instead of the latest version.
45+
# go install github.com/securego/gosec/v2/cmd/gosec@latest
46+
go install github.com/securego/gosec/v2/cmd/gosec@v2.23.0
4547
gosec -exclude-dir=.cache ./...
4648

4749
- name: Run govulncheck

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ go get github.com/labstack/echo-opentelemetry
2828
Use as an import statement
2929

3030
```go
31-
import "github.com/labstack/echo-opentelemetry"
31+
import echootel "github.com/labstack/echo-opentelemetry"
3232
```
3333

3434
Add middleware in simplified form, by providing only the server name
@@ -41,7 +41,6 @@ Add middleware with configuration options
4141

4242
```go
4343
e.Use(echootel.NewMiddlewareWithConfig(echootel.Config{
44-
ServerName: "my-server",
4544
TracerProvider: tp,
4645
}))
4746
```

extrator.go

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -64,28 +64,30 @@ func NewMetrics(meter metric.Meter) (*Metrics, error) {
6464
}, nil
6565
}
6666

67-
// IncrementValues represents the values to be used for Increment method.
68-
type IncrementValues struct {
67+
// RecordValues represents the values to Record metrics.
68+
type RecordValues struct {
6969
// RequestDuration is the duration of request processing. Will be used with `http.server.request.duration` metric.
7070
RequestDuration time.Duration
7171

72-
// RequestSize is the size of the request body in bytes. Will be used with `http.server.request.body.size` metric.
73-
RequestSize int64
72+
// ExtractedValues are values extracted from HTTP request and response before and after processing the next middleware/handler.
73+
ExtractedValues Values
7474

75-
// ResponseSize is the size of the response body in bytes. Will be used with `http.server.response.body.size` metric.
76-
ResponseSize int64
77-
78-
// Attributes are additional attributes to be used for incremented metrics.
75+
// Attributes are attributes to be used for recording metrics.
76+
// If left empty, the Metrics.Record method will use default attributes by calling Values.MetricAttributes().
7977
Attributes []attribute.KeyValue
8078
}
8179

82-
// Increment records the given IncrementValues to the Metrics instance.
83-
func (m *Metrics) Increment(ctx context.Context, v IncrementValues) {
84-
o := metric.WithAttributeSet(attribute.NewSet(v.Attributes...))
80+
// Record records the given RecordValues to the Metrics instance.
81+
func (m *Metrics) Record(ctx context.Context, v RecordValues) {
82+
attrs := v.Attributes
83+
if len(attrs) == 0 {
84+
attrs = v.ExtractedValues.MetricAttributes()
85+
}
86+
o := metric.WithAttributeSet(attribute.NewSet(attrs...))
8587

8688
m.requestDurationHistogram.Inst().Record(ctx, v.RequestDuration.Seconds(), o)
87-
m.requestBodySizeHistogram.Inst().Record(ctx, v.RequestSize, o)
88-
m.responseBodySizeHistogram.Inst().Record(ctx, v.ResponseSize, o)
89+
m.requestBodySizeHistogram.Inst().Record(ctx, v.ExtractedValues.HTTPRequestBodySize, o)
90+
m.responseBodySizeHistogram.Inst().Record(ctx, v.ExtractedValues.HTTPResponseBodySize, o)
8991
}
9092

9193
// Values represent extracted values from HTTP request and response to be used for Span and Metrics attributes.
@@ -248,6 +250,18 @@ type Values struct {
248250
// * metric - Conditionally Required If and only if it's available
249251
HTTPRoute string // metric, span
250252

253+
// HTTPRequestBodySize (`http.request.body.size`) is the size of the request payload body in bytes. This is the number
254+
// of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length)
255+
// header. For requests using transport encoding, this should be the compressed size.
256+
// Spec: https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpclientrequestbodysize
257+
//
258+
// Go: This value is taken from `Request.ContentLength` can be negative (-1) if the size is unknown.
259+
//
260+
// Requirement Level:
261+
// * span - opt-in attribute
262+
// * metric - optional, is actual Histogram metric (`http.client.request.body.size`) and NOT attribute to metric.
263+
HTTPRequestBodySize int64 // metric
264+
251265
// HTTPResponseStatusCode (`http.response.status_code`) is HTTP response status code.
252266
// See also RFC: https://datatracker.ietf.org/doc/html/rfc7231#section-6
253267
// Spec: https://opentelemetry.io/docs/specs/semconv/registry/attributes/http/
@@ -267,18 +281,6 @@ type Values struct {
267281
// * span - opt-in attribute
268282
// * metric - optional, is actual Histogram metric (`http.server.response.body.size`) and NOT attribute to metric.
269283
HTTPResponseBodySize int64 // metric
270-
271-
// HTTPRequestBodySize (`http.request.body.size`) is the size of the request payload body in bytes. This is the number
272-
// of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length)
273-
// header. For requests using transport encoding, this should be the compressed size.
274-
// Spec: https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpclientrequestbodysize
275-
//
276-
// Go: This value is taken from `Request.ContentLength` can be negative (-1) if the size is unknown.
277-
//
278-
// Requirement Level:
279-
// * span - opt-in attribute
280-
// * metric - optional, is actual Histogram metric (`http.client.request.body.size`) and NOT attribute to metric.
281-
HTTPRequestBodySize int64 // metric
282284
}
283285

284286
// ExtractRequest extracts values from the given HTTP request and populates the Values struct.

extrator_test.go

Lines changed: 206 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package echootel
22

33
import (
4+
"context"
45
"net/http"
56
"testing"
67

@@ -38,9 +39,42 @@ func TestValues_ExtractRequest(t *testing.T) {
3839
NetworkProtocolName: "http",
3940
NetworkProtocolVersion: "1.1",
4041
HTTPRoute: "",
42+
HTTPRequestBodySize: 0,
43+
HTTPResponseStatusCode: 0,
44+
HTTPResponseBodySize: 0,
45+
},
46+
},
47+
{
48+
name: "GET request, user agent, pattern",
49+
whenRequest: func() *http.Request {
50+
r := defaultRequest.Clone(context.Background())
51+
r.Method = "gEt"
52+
r.Host = "example.com:8433"
53+
r.ContentLength = -1
54+
55+
r.RemoteAddr = "127.0.0.1:8080"
56+
r.Pattern = "/path"
57+
r.Header.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36")
58+
return r
59+
}(),
60+
expect: Values{
61+
HTTPMethod: "GET",
62+
HTTPMethodOriginal: "gEt",
63+
ServerAddress: "example.com",
64+
ServerPort: 8433,
65+
NetworkPeerAddress: "127.0.0.1",
66+
NetworkPeerPort: 8080,
67+
ClientAddress: "127.0.0.1",
68+
URLScheme: "http",
69+
URLPath: "/path",
70+
UserAgentOriginal: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36",
71+
NetworkProtocolName: "http",
72+
NetworkProtocolVersion: "1.1",
73+
HTTPRoute: "/path",
74+
HTTPRequestBodySize: -1,
75+
// these are filled later
4176
HTTPResponseStatusCode: 0,
4277
HTTPResponseBodySize: 0,
43-
HTTPRequestBodySize: 0,
4478
},
4579
},
4680
}
@@ -62,6 +96,93 @@ func TestValues_ExtractRequest(t *testing.T) {
6296
}
6397
}
6498

99+
func TestValues_SpanStartAttributes(t *testing.T) {
100+
var testCases = []struct {
101+
name string
102+
given Values
103+
expectAttributes []attribute.KeyValue
104+
}{
105+
{
106+
name: "common + NetworkPeerAddress",
107+
given: Values{
108+
ServerAddress: "test.com",
109+
URLScheme: "http",
110+
NetworkPeerAddress: "127.0.0.1",
111+
},
112+
expectAttributes: []attribute.KeyValue{
113+
attribute.String("http.request.method", "_OTHER"),
114+
attribute.String("server.address", "test.com"),
115+
attribute.String("url.scheme", "http"),
116+
attribute.String("network.peer.address", "127.0.0.1"),
117+
},
118+
},
119+
{
120+
name: "common + NetworkPeerPort",
121+
given: Values{
122+
ServerAddress: "test.com",
123+
URLScheme: "http",
124+
NetworkPeerPort: 8080,
125+
},
126+
expectAttributes: []attribute.KeyValue{
127+
attribute.String("http.request.method", "_OTHER"),
128+
attribute.String("server.address", "test.com"),
129+
attribute.String("url.scheme", "http"),
130+
attribute.Int("network.peer.port", 8080),
131+
},
132+
},
133+
{
134+
name: "common + ClientAddress",
135+
given: Values{
136+
ServerAddress: "test.com",
137+
URLScheme: "http",
138+
ClientAddress: "127.0.0.1",
139+
},
140+
expectAttributes: []attribute.KeyValue{
141+
attribute.String("http.request.method", "_OTHER"),
142+
attribute.String("server.address", "test.com"),
143+
attribute.String("url.scheme", "http"),
144+
attribute.String("client.address", "127.0.0.1"),
145+
},
146+
},
147+
{
148+
name: "common + UserAgentOriginal",
149+
given: Values{
150+
ServerAddress: "test.com",
151+
URLScheme: "http",
152+
UserAgentOriginal: "Firefox/91.0.2",
153+
},
154+
expectAttributes: []attribute.KeyValue{
155+
attribute.String("http.request.method", "_OTHER"),
156+
attribute.String("server.address", "test.com"),
157+
attribute.String("url.scheme", "http"),
158+
attribute.String("user_agent.original", "Firefox/91.0.2"),
159+
},
160+
},
161+
{
162+
name: "common + URLPath",
163+
given: Values{
164+
ServerAddress: "test.com",
165+
URLScheme: "http",
166+
URLPath: "/test/path",
167+
},
168+
expectAttributes: []attribute.KeyValue{
169+
attribute.String("http.request.method", "_OTHER"),
170+
attribute.String("server.address", "test.com"),
171+
attribute.String("url.scheme", "http"),
172+
attribute.String("url.path", "/test/path"),
173+
},
174+
},
175+
}
176+
for _, tc := range testCases {
177+
t.Run(tc.name, func(t *testing.T) {
178+
attr := tc.given.SpanStartAttributes()
179+
180+
assert.Len(t, attr, len(tc.expectAttributes))
181+
assert.ElementsMatch(t, tc.expectAttributes, attr)
182+
})
183+
}
184+
}
185+
65186
func TestValues_SpanEndAttributes(t *testing.T) {
66187
var testCases = []struct {
67188
name string
@@ -478,3 +599,87 @@ func TestSpanNameFormatter(t *testing.T) {
478599
})
479600
}
480601
}
602+
603+
func TestSplitProto(t *testing.T) {
604+
var testCases = []struct {
605+
name string
606+
whenProto string
607+
expectName string
608+
expectVersion string
609+
}{
610+
{
611+
name: "empty",
612+
whenProto: "",
613+
expectName: "",
614+
expectVersion: "",
615+
},
616+
{
617+
name: "http uppercase",
618+
whenProto: "HTTP/1.1",
619+
expectName: "http",
620+
expectVersion: "1.1",
621+
},
622+
{
623+
name: "quic uppercase",
624+
whenProto: "QUIC/2",
625+
expectName: "quic",
626+
expectVersion: "2",
627+
},
628+
{
629+
name: "spdy uppercase",
630+
whenProto: "SPDY/3.1",
631+
expectName: "spdy",
632+
expectVersion: "3.1",
633+
},
634+
{
635+
name: "already lowercase",
636+
whenProto: "http/2",
637+
expectName: "http",
638+
expectVersion: "2",
639+
},
640+
{
641+
name: "mixed case default branch",
642+
whenProto: "HtTp/1.0",
643+
expectName: "http",
644+
expectVersion: "1.0",
645+
},
646+
{
647+
name: "unknown protocol",
648+
whenProto: "FTP/1.0",
649+
expectName: "ftp",
650+
expectVersion: "1.0",
651+
},
652+
{
653+
name: "no version segment",
654+
whenProto: "HTTP",
655+
expectName: "http",
656+
expectVersion: "",
657+
},
658+
{
659+
name: "no slash unknown protocol",
660+
whenProto: "CustomProto",
661+
expectName: "customproto",
662+
expectVersion: "",
663+
},
664+
{
665+
name: "extra slash only splits first",
666+
whenProto: "HTTP/1.1/extra",
667+
expectName: "http",
668+
expectVersion: "1.1/extra",
669+
},
670+
{
671+
name: "leading slash",
672+
whenProto: "/1.0",
673+
expectName: "",
674+
expectVersion: "1.0",
675+
},
676+
}
677+
for _, tc := range testCases {
678+
t.Run(tc.name, func(t *testing.T) {
679+
name, version := splitProto(tc.whenProto)
680+
681+
assert.Equal(t, tc.expectName, name)
682+
assert.Equal(t, tc.expectVersion, version)
683+
})
684+
}
685+
}

0 commit comments

Comments
 (0)