Skip to content

Commit 30ed923

Browse files
authored
HTTP Semconv migration Part1 Server - v1.20.0 support (#5333)
* added interface around semconvutil --------- Signed-off-by: Aaron Clawson <[email protected]>
1 parent 0ebeecf commit 30ed923

File tree

8 files changed

+446
-36
lines changed

8 files changed

+446
-36
lines changed

instrumentation/net/http/otelhttp/handler.go

Lines changed: 15 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,16 @@
44
package otelhttp // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
55

66
import (
7-
"io"
87
"net/http"
98
"time"
109

1110
"github.com/felixge/httpsnoop"
1211

12+
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv"
1313
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconvutil"
1414
"go.opentelemetry.io/otel"
15-
"go.opentelemetry.io/otel/attribute"
1615
"go.opentelemetry.io/otel/metric"
1716
"go.opentelemetry.io/otel/propagation"
18-
semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
1917
"go.opentelemetry.io/otel/trace"
2018
)
2119

@@ -35,6 +33,7 @@ type middleware struct {
3533
publicEndpoint bool
3634
publicEndpointFn func(*http.Request) bool
3735

36+
traceSemconv semconv.HTTPServer
3837
requestBytesCounter metric.Int64Counter
3938
responseBytesCounter metric.Int64Counter
4039
serverLatencyMeasure metric.Float64Histogram
@@ -56,6 +55,8 @@ func NewHandler(handler http.Handler, operation string, opts ...Option) http.Han
5655
func NewMiddleware(operation string, opts ...Option) func(http.Handler) http.Handler {
5756
h := middleware{
5857
operation: operation,
58+
59+
traceSemconv: semconv.NewHTTPServer(),
5960
}
6061

6162
defaultOpts := []Option{
@@ -132,12 +133,9 @@ func (h *middleware) serveHTTP(w http.ResponseWriter, r *http.Request, next http
132133

133134
ctx := h.propagators.Extract(r.Context(), propagation.HeaderCarrier(r.Header))
134135
opts := []trace.SpanStartOption{
135-
trace.WithAttributes(semconvutil.HTTPServerRequest(h.server, r)...),
136-
}
137-
if h.server != "" {
138-
hostAttr := semconv.NetHostName(h.server)
139-
opts = append(opts, trace.WithAttributes(hostAttr))
136+
trace.WithAttributes(h.traceSemconv.RequestTraceAttrs(h.server, r)...),
140137
}
138+
141139
opts = append(opts, h.spanStartOptions...)
142140
if h.publicEndpoint || (h.publicEndpointFn != nil && h.publicEndpointFn(r.WithContext(ctx))) {
143141
opts = append(opts, trace.WithNewRoot())
@@ -213,7 +211,14 @@ func (h *middleware) serveHTTP(w http.ResponseWriter, r *http.Request, next http
213211

214212
next.ServeHTTP(w, r.WithContext(ctx))
215213

216-
setAfterServeAttributes(span, bw.read.Load(), rww.written, rww.statusCode, bw.err, rww.err)
214+
span.SetStatus(semconv.ServerStatus(rww.statusCode))
215+
span.SetAttributes(h.traceSemconv.ResponseTraceAttrs(semconv.ResponseTelemetry{
216+
StatusCode: rww.statusCode,
217+
ReadBytes: bw.read.Load(),
218+
ReadError: bw.err,
219+
WriteBytes: rww.written,
220+
WriteError: rww.err,
221+
})...)
217222

218223
// Add metrics
219224
attributes := append(labeler.Get(), semconvutil.HTTPServerRequestMetrics(h.server, r)...)
@@ -230,37 +235,11 @@ func (h *middleware) serveHTTP(w http.ResponseWriter, r *http.Request, next http
230235
h.serverLatencyMeasure.Record(ctx, elapsedTime, o)
231236
}
232237

233-
func setAfterServeAttributes(span trace.Span, read, wrote int64, statusCode int, rerr, werr error) {
234-
attributes := []attribute.KeyValue{}
235-
236-
// TODO: Consider adding an event after each read and write, possibly as an
237-
// option (defaulting to off), so as to not create needlessly verbose spans.
238-
if read > 0 {
239-
attributes = append(attributes, ReadBytesKey.Int64(read))
240-
}
241-
if rerr != nil && rerr != io.EOF {
242-
attributes = append(attributes, ReadErrorKey.String(rerr.Error()))
243-
}
244-
if wrote > 0 {
245-
attributes = append(attributes, WroteBytesKey.Int64(wrote))
246-
}
247-
if statusCode > 0 {
248-
attributes = append(attributes, semconv.HTTPStatusCode(statusCode))
249-
}
250-
span.SetStatus(semconvutil.HTTPServerStatus(statusCode))
251-
252-
if werr != nil && werr != io.EOF {
253-
attributes = append(attributes, WriteErrorKey.String(werr.Error()))
254-
}
255-
span.SetAttributes(attributes...)
256-
}
257-
258238
// WithRouteTag annotates spans and metrics with the provided route name
259239
// with HTTP route attribute.
260240
func WithRouteTag(route string, h http.Handler) http.Handler {
241+
attr := semconv.NewHTTPServer().Route(route)
261242
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
262-
attr := semconv.HTTPRouteKey.String(route)
263-
264243
span := trace.SpanFromContext(r.Context())
265244
span.SetAttributes(attr)
266245

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package semconv
5+
6+
import (
7+
"net/http"
8+
"net/url"
9+
"testing"
10+
11+
"go.opentelemetry.io/otel/attribute"
12+
)
13+
14+
var benchHTTPServerRequestResults []attribute.KeyValue
15+
16+
// BenchmarkHTTPServerRequest allows comparison between different version of the HTTP server.
17+
// To use an alternative start this test with OTEL_HTTP_CLIENT_COMPATIBILITY_MODE set to the
18+
// version under test.
19+
func BenchmarkHTTPServerRequest(b *testing.B) {
20+
// Request was generated from TestHTTPServerRequest request.
21+
req := &http.Request{
22+
Method: http.MethodGet,
23+
URL: &url.URL{
24+
Path: "/",
25+
},
26+
Proto: "HTTP/1.1",
27+
ProtoMajor: 1,
28+
ProtoMinor: 1,
29+
Header: http.Header{
30+
"User-Agent": []string{"Go-http-client/1.1"},
31+
"Accept-Encoding": []string{"gzip"},
32+
},
33+
Body: http.NoBody,
34+
Host: "127.0.0.1:39093",
35+
RemoteAddr: "127.0.0.1:38738",
36+
RequestURI: "/",
37+
}
38+
serv := NewHTTPServer()
39+
40+
b.ReportAllocs()
41+
b.ResetTimer()
42+
for i := 0; i < b.N; i++ {
43+
benchHTTPServerRequestResults = serv.RequestTraceAttrs("", req)
44+
}
45+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package semconv
5+
6+
import (
7+
"net/http"
8+
"net/http/httptest"
9+
"net/url"
10+
"strconv"
11+
"testing"
12+
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
16+
"go.opentelemetry.io/otel/attribute"
17+
)
18+
19+
type testServerReq struct {
20+
hostname string
21+
serverPort int
22+
peerAddr string
23+
peerPort int
24+
clientIP string
25+
}
26+
27+
func testTraceRequest(t *testing.T, serv HTTPServer, want func(testServerReq) []attribute.KeyValue) {
28+
t.Helper()
29+
30+
got := make(chan *http.Request, 1)
31+
handler := func(w http.ResponseWriter, r *http.Request) {
32+
got <- r
33+
close(got)
34+
w.WriteHeader(http.StatusOK)
35+
}
36+
37+
srv := httptest.NewServer(http.HandlerFunc(handler))
38+
defer srv.Close()
39+
40+
srvURL, err := url.Parse(srv.URL)
41+
require.NoError(t, err)
42+
srvPort, err := strconv.ParseInt(srvURL.Port(), 10, 32)
43+
require.NoError(t, err)
44+
45+
resp, err := srv.Client().Get(srv.URL)
46+
require.NoError(t, err)
47+
require.NoError(t, resp.Body.Close())
48+
49+
req := <-got
50+
peer, peerPort := splitHostPort(req.RemoteAddr)
51+
52+
const user = "alice"
53+
req.SetBasicAuth(user, "pswrd")
54+
55+
const clientIP = "127.0.0.5"
56+
req.Header.Add("X-Forwarded-For", clientIP)
57+
58+
srvReq := testServerReq{
59+
hostname: srvURL.Hostname(),
60+
serverPort: int(srvPort),
61+
peerAddr: peer,
62+
peerPort: peerPort,
63+
clientIP: clientIP,
64+
}
65+
66+
assert.ElementsMatch(t, want(srvReq), serv.RequestTraceAttrs("", req))
67+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package semconv // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv"
5+
6+
import (
7+
"fmt"
8+
"net/http"
9+
10+
"go.opentelemetry.io/otel/attribute"
11+
"go.opentelemetry.io/otel/codes"
12+
)
13+
14+
type ResponseTelemetry struct {
15+
StatusCode int
16+
ReadBytes int64
17+
ReadError error
18+
WriteBytes int64
19+
WriteError error
20+
}
21+
22+
type HTTPServer interface {
23+
// RequestTraceAttrs returns trace attributes for an HTTP request received by a
24+
// server.
25+
//
26+
// The server must be the primary server name if it is known. For example this
27+
// would be the ServerName directive
28+
// (https://httpd.apache.org/docs/2.4/mod/core.html#servername) for an Apache
29+
// server, and the server_name directive
30+
// (http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name) for an
31+
// nginx server. More generically, the primary server name would be the host
32+
// header value that matches the default virtual host of an HTTP server. It
33+
// should include the host identifier and if a port is used to route to the
34+
// server that port identifier should be included as an appropriate port
35+
// suffix.
36+
//
37+
// If the primary server name is not known, server should be an empty string.
38+
// The req Host will be used to determine the server instead.
39+
RequestTraceAttrs(server string, req *http.Request) []attribute.KeyValue
40+
41+
// ResponseTraceAttrs returns trace attributes for telemetry from an HTTP response.
42+
//
43+
// If any of the fields in the ResponseTelemetry are not set the attribute will be omitted.
44+
ResponseTraceAttrs(ResponseTelemetry) []attribute.KeyValue
45+
46+
// Route returns the attribute for the route.
47+
Route(string) attribute.KeyValue
48+
}
49+
50+
// var warnOnce = sync.Once{}
51+
52+
func NewHTTPServer() HTTPServer {
53+
// TODO (#5331): Detect version based on environment variable OTEL_HTTP_CLIENT_COMPATIBILITY_MODE.
54+
// TODO (#5331): Add warning of use of a deprecated version of Semantic Versions.
55+
return oldHTTPServer{}
56+
}
57+
58+
// ServerStatus returns a span status code and message for an HTTP status code
59+
// value returned by a server. Status codes in the 400-499 range are not
60+
// returned as errors.
61+
func ServerStatus(code int) (codes.Code, string) {
62+
if code < 100 || code >= 600 {
63+
return codes.Error, fmt.Sprintf("Invalid HTTP status code %d", code)
64+
}
65+
if code >= 500 {
66+
return codes.Error, ""
67+
}
68+
return codes.Unset, ""
69+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package semconv // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv"
5+
6+
import (
7+
"net"
8+
"strconv"
9+
"strings"
10+
)
11+
12+
// splitHostPort splits a network address hostport of the form "host",
13+
// "host%zone", "[host]", "[host%zone], "host:port", "host%zone:port",
14+
// "[host]:port", "[host%zone]:port", or ":port" into host or host%zone and
15+
// port.
16+
//
17+
// An empty host is returned if it is not provided or unparsable. A negative
18+
// port is returned if it is not provided or unparsable.
19+
func splitHostPort(hostport string) (host string, port int) {
20+
port = -1
21+
22+
if strings.HasPrefix(hostport, "[") {
23+
addrEnd := strings.LastIndex(hostport, "]")
24+
if addrEnd < 0 {
25+
// Invalid hostport.
26+
return
27+
}
28+
if i := strings.LastIndex(hostport[addrEnd:], ":"); i < 0 {
29+
host = hostport[1:addrEnd]
30+
return
31+
}
32+
} else {
33+
if i := strings.LastIndex(hostport, ":"); i < 0 {
34+
host = hostport
35+
return
36+
}
37+
}
38+
39+
host, pStr, err := net.SplitHostPort(hostport)
40+
if err != nil {
41+
return
42+
}
43+
44+
p, err := strconv.ParseUint(pStr, 10, 16)
45+
if err != nil {
46+
return
47+
}
48+
return host, int(p)
49+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package semconv
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestSplitHostPort(t *testing.T) {
13+
tests := []struct {
14+
hostport string
15+
host string
16+
port int
17+
}{
18+
{"", "", -1},
19+
{":8080", "", 8080},
20+
{"127.0.0.1", "127.0.0.1", -1},
21+
{"www.example.com", "www.example.com", -1},
22+
{"127.0.0.1%25en0", "127.0.0.1%25en0", -1},
23+
{"[]", "", -1}, // Ensure this doesn't panic.
24+
{"[fe80::1", "", -1},
25+
{"[fe80::1]", "fe80::1", -1},
26+
{"[fe80::1%25en0]", "fe80::1%25en0", -1},
27+
{"[fe80::1]:8080", "fe80::1", 8080},
28+
{"[fe80::1]::", "", -1}, // Too many colons.
29+
{"127.0.0.1:", "127.0.0.1", -1},
30+
{"127.0.0.1:port", "127.0.0.1", -1},
31+
{"127.0.0.1:8080", "127.0.0.1", 8080},
32+
{"www.example.com:8080", "www.example.com", 8080},
33+
{"127.0.0.1%25en0:8080", "127.0.0.1%25en0", 8080},
34+
}
35+
36+
for _, test := range tests {
37+
h, p := splitHostPort(test.hostport)
38+
assert.Equal(t, test.host, h, test.hostport)
39+
assert.Equal(t, test.port, p, test.hostport)
40+
}
41+
}

0 commit comments

Comments
 (0)