Skip to content

Commit ce79eff

Browse files
flc1125dmathieu
andauthored
feat(otelhttp): generate New Server Metrics in otelhttp (#6411)
Co-authored-by: Damien Mathieu <[email protected]>
1 parent a3180db commit ce79eff

File tree

10 files changed

+452
-101
lines changed

10 files changed

+452
-101
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
88

99
## [Unreleased]
1010

11+
### Added
12+
13+
- Generate server metrics with semantic conventions v1.26 in `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` when `OTEL_SEMCONV_STABILITY_OPT_IN` is set to `http/dup`. (#6411)
14+
1115
## [1.33.0/0.58.0/0.27.0/0.13.0/0.8.0/0.6.0/0.5.0] - 2024-12-12
1216

1317
### Added

instrumentation/net/http/otelhttp/internal/semconv/env.go

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import (
1616
"go.opentelemetry.io/otel/metric"
1717
)
1818

19+
// OTelSemConvStabilityOptIn is an environment variable.
20+
// That can be set to "old" or "http/dup" to opt into the new HTTP semantic conventions.
21+
const OTelSemConvStabilityOptIn = "OTEL_SEMCONV_STABILITY_OPT_IN"
22+
1923
type ResponseTelemetry struct {
2024
StatusCode int
2125
ReadBytes int64
@@ -31,6 +35,11 @@ type HTTPServer struct {
3135
requestBytesCounter metric.Int64Counter
3236
responseBytesCounter metric.Int64Counter
3337
serverLatencyMeasure metric.Float64Histogram
38+
39+
// New metrics
40+
requestBodySizeHistogram metric.Int64Histogram
41+
responseBodySizeHistogram metric.Int64Histogram
42+
requestDurationHistogram metric.Float64Histogram
3443
}
3544

3645
// RequestTraceAttrs returns trace attributes for an HTTP request received by a
@@ -103,38 +112,56 @@ type MetricData struct {
103112
ElapsedTime float64
104113
}
105114

106-
var metricAddOptionPool = &sync.Pool{
107-
New: func() interface{} {
108-
return &[]metric.AddOption{}
109-
},
110-
}
115+
var (
116+
metricAddOptionPool = &sync.Pool{
117+
New: func() interface{} {
118+
return &[]metric.AddOption{}
119+
},
120+
}
111121

112-
func (s HTTPServer) RecordMetrics(ctx context.Context, md ServerMetricData) {
113-
if s.requestBytesCounter == nil || s.responseBytesCounter == nil || s.serverLatencyMeasure == nil {
114-
// This will happen if an HTTPServer{} is used instead of NewHTTPServer.
115-
return
122+
metricRecordOptionPool = &sync.Pool{
123+
New: func() interface{} {
124+
return &[]metric.RecordOption{}
125+
},
116126
}
127+
)
117128

118-
attributes := OldHTTPServer{}.MetricAttributes(md.ServerName, md.Req, md.StatusCode, md.AdditionalAttributes)
119-
o := metric.WithAttributeSet(attribute.NewSet(attributes...))
120-
addOpts := metricAddOptionPool.Get().(*[]metric.AddOption)
121-
*addOpts = append(*addOpts, o)
122-
s.requestBytesCounter.Add(ctx, md.RequestSize, *addOpts...)
123-
s.responseBytesCounter.Add(ctx, md.ResponseSize, *addOpts...)
124-
s.serverLatencyMeasure.Record(ctx, md.ElapsedTime, o)
125-
*addOpts = (*addOpts)[:0]
126-
metricAddOptionPool.Put(addOpts)
129+
func (s HTTPServer) RecordMetrics(ctx context.Context, md ServerMetricData) {
130+
if s.requestBytesCounter != nil && s.responseBytesCounter != nil && s.serverLatencyMeasure != nil {
131+
attributes := OldHTTPServer{}.MetricAttributes(md.ServerName, md.Req, md.StatusCode, md.AdditionalAttributes)
132+
o := metric.WithAttributeSet(attribute.NewSet(attributes...))
133+
addOpts := metricAddOptionPool.Get().(*[]metric.AddOption)
134+
*addOpts = append(*addOpts, o)
135+
s.requestBytesCounter.Add(ctx, md.RequestSize, *addOpts...)
136+
s.responseBytesCounter.Add(ctx, md.ResponseSize, *addOpts...)
137+
s.serverLatencyMeasure.Record(ctx, md.ElapsedTime, o)
138+
*addOpts = (*addOpts)[:0]
139+
metricAddOptionPool.Put(addOpts)
140+
}
127141

128-
// TODO: Duplicate Metrics
142+
if s.duplicate && s.requestDurationHistogram != nil && s.requestBodySizeHistogram != nil && s.responseBodySizeHistogram != nil {
143+
attributes := CurrentHTTPServer{}.MetricAttributes(md.ServerName, md.Req, md.StatusCode, md.AdditionalAttributes)
144+
o := metric.WithAttributeSet(attribute.NewSet(attributes...))
145+
recordOpts := metricRecordOptionPool.Get().(*[]metric.RecordOption)
146+
*recordOpts = append(*recordOpts, o)
147+
s.requestBodySizeHistogram.Record(ctx, md.RequestSize, *recordOpts...)
148+
s.responseBodySizeHistogram.Record(ctx, md.ResponseSize, *recordOpts...)
149+
s.requestDurationHistogram.Record(ctx, md.ElapsedTime, o)
150+
*recordOpts = (*recordOpts)[:0]
151+
metricRecordOptionPool.Put(recordOpts)
152+
}
129153
}
130154

131155
func NewHTTPServer(meter metric.Meter) HTTPServer {
132-
env := strings.ToLower(os.Getenv("OTEL_SEMCONV_STABILITY_OPT_IN"))
156+
env := strings.ToLower(os.Getenv(OTelSemConvStabilityOptIn))
133157
duplicate := env == "http/dup"
134158
server := HTTPServer{
135159
duplicate: duplicate,
136160
}
137161
server.requestBytesCounter, server.responseBytesCounter, server.serverLatencyMeasure = OldHTTPServer{}.createMeasures(meter)
162+
if duplicate {
163+
server.requestBodySizeHistogram, server.responseBodySizeHistogram, server.requestDurationHistogram = CurrentHTTPServer{}.createMeasures(meter)
164+
}
138165
return server
139166
}
140167

@@ -148,7 +175,7 @@ type HTTPClient struct {
148175
}
149176

150177
func NewHTTPClient(meter metric.Meter) HTTPClient {
151-
env := strings.ToLower(os.Getenv("OTEL_SEMCONV_STABILITY_OPT_IN"))
178+
env := strings.ToLower(os.Getenv(OTelSemConvStabilityOptIn))
152179
client := HTTPClient{
153180
duplicate: env == "http/dup",
154181
}

instrumentation/net/http/otelhttp/internal/semconv/httpconv.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ import (
77
"fmt"
88
"net/http"
99
"reflect"
10+
"slices"
1011
"strconv"
1112
"strings"
1213

1314
"go.opentelemetry.io/otel/attribute"
15+
"go.opentelemetry.io/otel/metric"
16+
"go.opentelemetry.io/otel/metric/noop"
1417
semconvNew "go.opentelemetry.io/otel/semconv/v1.26.0"
1518
)
1619

@@ -199,6 +202,86 @@ func (n CurrentHTTPServer) Route(route string) attribute.KeyValue {
199202
return semconvNew.HTTPRoute(route)
200203
}
201204

205+
func (n CurrentHTTPServer) createMeasures(meter metric.Meter) (metric.Int64Histogram, metric.Int64Histogram, metric.Float64Histogram) {
206+
if meter == nil {
207+
return noop.Int64Histogram{}, noop.Int64Histogram{}, noop.Float64Histogram{}
208+
}
209+
210+
var err error
211+
requestBodySizeHistogram, err := meter.Int64Histogram(
212+
semconvNew.HTTPServerRequestBodySizeName,
213+
metric.WithUnit(semconvNew.HTTPServerRequestBodySizeUnit),
214+
metric.WithDescription(semconvNew.HTTPServerRequestBodySizeDescription),
215+
)
216+
handleErr(err)
217+
218+
responseBodySizeHistogram, err := meter.Int64Histogram(
219+
semconvNew.HTTPServerResponseBodySizeName,
220+
metric.WithUnit(semconvNew.HTTPServerResponseBodySizeUnit),
221+
metric.WithDescription(semconvNew.HTTPServerResponseBodySizeDescription),
222+
)
223+
handleErr(err)
224+
requestDurationHistogram, err := meter.Float64Histogram(
225+
semconvNew.HTTPServerRequestDurationName,
226+
metric.WithUnit(semconvNew.HTTPServerRequestDurationUnit),
227+
metric.WithDescription(semconvNew.HTTPServerRequestDurationDescription),
228+
)
229+
handleErr(err)
230+
231+
return requestBodySizeHistogram, responseBodySizeHistogram, requestDurationHistogram
232+
}
233+
234+
func (n CurrentHTTPServer) MetricAttributes(server string, req *http.Request, statusCode int, additionalAttributes []attribute.KeyValue) []attribute.KeyValue {
235+
num := len(additionalAttributes) + 3
236+
var host string
237+
var p int
238+
if server == "" {
239+
host, p = SplitHostPort(req.Host)
240+
} else {
241+
// Prioritize the primary server name.
242+
host, p = SplitHostPort(server)
243+
if p < 0 {
244+
_, p = SplitHostPort(req.Host)
245+
}
246+
}
247+
hostPort := requiredHTTPPort(req.TLS != nil, p)
248+
if hostPort > 0 {
249+
num++
250+
}
251+
protoName, protoVersion := netProtocol(req.Proto)
252+
if protoName != "" {
253+
num++
254+
}
255+
if protoVersion != "" {
256+
num++
257+
}
258+
259+
if statusCode > 0 {
260+
num++
261+
}
262+
263+
attributes := slices.Grow(additionalAttributes, num)
264+
attributes = append(attributes,
265+
semconvNew.HTTPRequestMethodKey.String(standardizeHTTPMethod(req.Method)),
266+
n.scheme(req.TLS != nil),
267+
semconvNew.ServerAddress(host))
268+
269+
if hostPort > 0 {
270+
attributes = append(attributes, semconvNew.ServerPort(hostPort))
271+
}
272+
if protoName != "" {
273+
attributes = append(attributes, semconvNew.NetworkProtocolName(protoName))
274+
}
275+
if protoVersion != "" {
276+
attributes = append(attributes, semconvNew.NetworkProtocolVersion(protoVersion))
277+
}
278+
279+
if statusCode > 0 {
280+
attributes = append(attributes, semconvNew.HTTPResponseStatusCode(statusCode))
281+
}
282+
return attributes
283+
}
284+
202285
type CurrentHTTPClient struct{}
203286

204287
// RequestTraceAttrs returns trace attributes for an HTTP request made by a client.

instrumentation/net/http/otelhttp/internal/semconv/httpconv_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,72 @@ import (
99
"testing"
1010

1111
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
1213

1314
"go.opentelemetry.io/otel/attribute"
1415
"go.opentelemetry.io/otel/codes"
1516
)
1617

18+
func TestCurrentHttpServer_MetricAttributes(t *testing.T) {
19+
defaultRequest, err := http.NewRequest("GET", "http://example.com/path?query=test", nil)
20+
require.NoError(t, err)
21+
22+
tests := []struct {
23+
name string
24+
server string
25+
req *http.Request
26+
statusCode int
27+
additionalAttributes []attribute.KeyValue
28+
wantFunc func(t *testing.T, attrs []attribute.KeyValue)
29+
}{
30+
{
31+
name: "routine testing",
32+
server: "",
33+
req: defaultRequest,
34+
statusCode: 200,
35+
additionalAttributes: []attribute.KeyValue{attribute.String("test", "test")},
36+
wantFunc: func(t *testing.T, attrs []attribute.KeyValue) {
37+
require.Len(t, attrs, 7)
38+
assert.ElementsMatch(t, []attribute.KeyValue{
39+
attribute.String("http.request.method", "GET"),
40+
attribute.String("url.scheme", "http"),
41+
attribute.String("server.address", "example.com"),
42+
attribute.String("network.protocol.name", "http"),
43+
attribute.String("network.protocol.version", "1.1"),
44+
attribute.Int64("http.response.status_code", 200),
45+
attribute.String("test", "test"),
46+
}, attrs)
47+
},
48+
},
49+
{
50+
name: "use server address",
51+
server: "example.com:9999",
52+
req: defaultRequest,
53+
statusCode: 200,
54+
additionalAttributes: nil,
55+
wantFunc: func(t *testing.T, attrs []attribute.KeyValue) {
56+
require.Len(t, attrs, 7)
57+
assert.ElementsMatch(t, []attribute.KeyValue{
58+
attribute.String("http.request.method", "GET"),
59+
attribute.String("url.scheme", "http"),
60+
attribute.String("server.address", "example.com"),
61+
attribute.Int("server.port", 9999),
62+
attribute.String("network.protocol.name", "http"),
63+
attribute.String("network.protocol.version", "1.1"),
64+
attribute.Int64("http.response.status_code", 200),
65+
}, attrs)
66+
},
67+
},
68+
}
69+
70+
for _, tt := range tests {
71+
t.Run(tt.name, func(t *testing.T) {
72+
got := CurrentHTTPServer{}.MetricAttributes(tt.server, tt.req, tt.statusCode, tt.additionalAttributes)
73+
tt.wantFunc(t, got)
74+
})
75+
}
76+
}
77+
1778
func TestNewMethod(t *testing.T) {
1879
testCases := []struct {
1980
method string

instrumentation/net/http/otelhttp/internal/semconv/test/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/stretchr/testify v1.10.0
77
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0
88
go.opentelemetry.io/otel v1.33.0
9+
go.opentelemetry.io/otel/metric v1.33.0
910
go.opentelemetry.io/otel/sdk v1.33.0
1011
go.opentelemetry.io/otel/sdk/metric v1.33.0
1112
)
@@ -17,7 +18,6 @@ require (
1718
github.com/google/uuid v1.6.0 // indirect
1819
github.com/pmezard/go-difflib v1.0.0 // indirect
1920
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
20-
go.opentelemetry.io/otel/metric v1.33.0 // indirect
2121
go.opentelemetry.io/otel/trace v1.33.0 // indirect
2222
golang.org/x/sys v0.28.0 // indirect
2323
gopkg.in/yaml.v3 v3.0.1 // indirect

0 commit comments

Comments
 (0)