Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP Semconv migration Part1 Server - v1.20.0 support #5333

Merged
merged 26 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f52ee4d
added interface around semconvutil
MadVikingGod Feb 2, 2024
d76db42
Added duplicate semconv
MadVikingGod Feb 2, 2024
7235735
WIP
MadVikingGod Feb 8, 2024
5803ab9
Finish http/dup and add tests.
MadVikingGod Feb 13, 2024
40fbe92
Add License
MadVikingGod Feb 13, 2024
c9a2845
More tests, added v1.24.0, lint
MadVikingGod Feb 14, 2024
cd00b34
Merge remote-tracking branch 'upstream/main' into mvg/semconv-update2
MadVikingGod Feb 14, 2024
7ac81a1
Add documentation about migration
MadVikingGod Feb 16, 2024
a119fb4
Use Sync availabe in 1.20, fix lint
MadVikingGod Feb 19, 2024
c6a8f18
Fix lint and PR comments
MadVikingGod Feb 29, 2024
53dbd7c
Change which versions removed the extra attribute.
MadVikingGod Feb 29, 2024
f035765
Update comment
MadVikingGod Feb 29, 2024
c5dfc91
Merge remote-tracking branch 'upstream/main' into mvg/semconv-update2
MadVikingGod Feb 29, 2024
d0f8cc4
Address PR feedback
MadVikingGod Mar 12, 2024
30adb7e
Merge branch 'main' into mvg/semconv-update2
MadVikingGod Mar 12, 2024
fa04b4e
Remove dup and new semconvs
MadVikingGod Mar 29, 2024
24c48fc
update changelog
MadVikingGod Mar 29, 2024
3ae77dc
Merge remote-tracking branch 'upstream/main' into mvg/semconv/server/old
MadVikingGod Mar 29, 2024
e44c5a3
Address PR feedback
MadVikingGod Mar 29, 2024
d1cf39d
Addressed PR feedback
MadVikingGod Apr 1, 2024
a171ccb
Address more PR feedback
MadVikingGod Apr 4, 2024
333b890
fix lint
MadVikingGod Apr 4, 2024
5515eb2
Merge branch 'main' into mvg/semconv/server/old
MadVikingGod Apr 4, 2024
aa0518a
Merge branch 'main' into mvg/semconv/server/old
MadVikingGod Apr 11, 2024
1bf2dca
Merge branch 'main' into mvg/semconv/server/old
MadVikingGod Apr 16, 2024
324cb8d
Merge branch 'main' into mvg/semconv/server/old
MadVikingGod Apr 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
51 changes: 15 additions & 36 deletions instrumentation/net/http/otelhttp/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,16 @@
package otelhttp // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"

import (
"io"
"net/http"
"time"

"github.com/felixge/httpsnoop"

"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconvutil"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/propagation"
semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
"go.opentelemetry.io/otel/trace"
)

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

traceSemconv semconv.HTTPServer
requestBytesCounter metric.Int64Counter
responseBytesCounter metric.Int64Counter
serverLatencyMeasure metric.Float64Histogram
Expand All @@ -56,6 +55,8 @@ func NewHandler(handler http.Handler, operation string, opts ...Option) http.Han
func NewMiddleware(operation string, opts ...Option) func(http.Handler) http.Handler {
h := middleware{
operation: operation,

traceSemconv: semconv.NewHTTPServer(),
}

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

ctx := h.propagators.Extract(r.Context(), propagation.HeaderCarrier(r.Header))
opts := []trace.SpanStartOption{
trace.WithAttributes(semconvutil.HTTPServerRequest(h.server, r)...),
}
if h.server != "" {
hostAttr := semconv.NetHostName(h.server)
opts = append(opts, trace.WithAttributes(hostAttr))
trace.WithAttributes(h.traceSemconv.RequestTraceAttrs(h.server, r)...),
}

opts = append(opts, h.spanStartOptions...)
if h.publicEndpoint || (h.publicEndpointFn != nil && h.publicEndpointFn(r.WithContext(ctx))) {
opts = append(opts, trace.WithNewRoot())
Expand Down Expand Up @@ -213,7 +211,14 @@ func (h *middleware) serveHTTP(w http.ResponseWriter, r *http.Request, next http

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

setAfterServeAttributes(span, bw.read.Load(), rww.written, rww.statusCode, bw.err, rww.err)
span.SetStatus(semconv.ServerStatus(rww.statusCode))
span.SetAttributes(h.traceSemconv.ResponseTraceAttrs(semconv.ResponseTelemetry{
StatusCode: rww.statusCode,
ReadBytes: bw.read.Load(),
ReadError: bw.err,
WriteBytes: rww.written,
WriteError: rww.err,
})...)

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

func setAfterServeAttributes(span trace.Span, read, wrote int64, statusCode int, rerr, werr error) {
attributes := []attribute.KeyValue{}

// TODO: Consider adding an event after each read and write, possibly as an
// option (defaulting to off), so as to not create needlessly verbose spans.
if read > 0 {
attributes = append(attributes, ReadBytesKey.Int64(read))
}
if rerr != nil && rerr != io.EOF {
attributes = append(attributes, ReadErrorKey.String(rerr.Error()))
}
if wrote > 0 {
attributes = append(attributes, WroteBytesKey.Int64(wrote))
}
if statusCode > 0 {
attributes = append(attributes, semconv.HTTPStatusCode(statusCode))
}
span.SetStatus(semconvutil.HTTPServerStatus(statusCode))

if werr != nil && werr != io.EOF {
attributes = append(attributes, WriteErrorKey.String(werr.Error()))
}
span.SetAttributes(attributes...)
}

// WithRouteTag annotates spans and metrics with the provided route name
// with HTTP route attribute.
func WithRouteTag(route string, h http.Handler) http.Handler {
attr := semconv.NewHTTPServer().Route(route)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
attr := semconv.HTTPRouteKey.String(route)

span := trace.SpanFromContext(r.Context())
span.SetAttributes(attr)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package semconv

import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"go.opentelemetry.io/otel/attribute"
)

type testServerReq struct {
hostname string
serverPort int
peerAddr string
peerPort int
clientIP string
}

func testTraceRequest(t *testing.T, serv HTTPServer, want func(testServerReq) []attribute.KeyValue) {
t.Helper()

got := make(chan *http.Request, 1)
handler := func(w http.ResponseWriter, r *http.Request) {
got <- r
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
close(got)
w.WriteHeader(http.StatusOK)
}

srv := httptest.NewServer(http.HandlerFunc(handler))
defer srv.Close()

srvURL, err := url.Parse(srv.URL)
require.NoError(t, err)
srvPort, err := strconv.ParseInt(srvURL.Port(), 10, 32)
require.NoError(t, err)

resp, err := srv.Client().Get(srv.URL)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())

req := <-got
peer, peerPort := splitHostPort(req.RemoteAddr)

const user = "alice"
req.SetBasicAuth(user, "pswrd")

const clientIP = "127.0.0.5"
req.Header.Add("X-Forwarded-For", clientIP)

srvReq := testServerReq{
hostname: srvURL.Hostname(),
serverPort: int(srvPort),
peerAddr: peer,
peerPort: peerPort,
clientIP: clientIP,
}

assert.ElementsMatch(t, want(srvReq), serv.RequestTraceAttrs("", req))
}

func testTraceResponse(t *testing.T, serv HTTPServer, want []attribute.KeyValue) {
t.Helper()
emptyResp := ResponseTelemetry{}
assert.Len(t, serv.ResponseTraceAttrs(emptyResp), 0)

resp := ResponseTelemetry{
StatusCode: 200,
ReadBytes: 701,
ReadError: fmt.Errorf("read error"),
WriteBytes: 802,
WriteError: fmt.Errorf("write error"),
}
assert.ElementsMatch(t, want, serv.ResponseTraceAttrs(resp))
}
69 changes: 69 additions & 0 deletions instrumentation/net/http/otelhttp/internal/semconv/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package semconv // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv"

import (
"fmt"
"net/http"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
)

type ResponseTelemetry struct {
StatusCode int
ReadBytes int64
ReadError error
WriteBytes int64
WriteError error
}

type HTTPServer interface {
// RequestTraceAttrs returns trace attributes for an HTTP request received by a
// server.
//
// The server must be the primary server name if it is known. For example this
// would be the ServerName directive
// (https://httpd.apache.org/docs/2.4/mod/core.html#servername) for an Apache
// server, and the server_name directive
// (http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name) for an
// nginx server. More generically, the primary server name would be the host
// header value that matches the default virtual host of an HTTP server. It
// should include the host identifier and if a port is used to route to the
// server that port identifier should be included as an appropriate port
// suffix.
//
// If the primary server name is not known, server should be an empty string.
// The req Host will be used to determine the server instead.
RequestTraceAttrs(server string, req *http.Request) []attribute.KeyValue

// ResponseTraceAttrs returns trace attributes for telemetry from an HTTP response.
//
// If any of the fields in the ResponseTelemetry are not set the attribute will be omitted.
ResponseTraceAttrs(ResponseTelemetry) []attribute.KeyValue

// Route returns the attribute for the route.
Route(string) attribute.KeyValue
}

// var warnOnce = sync.Once{}

func NewHTTPServer() HTTPServer {
// TODO (#5331): Detect version based on environment variable OTEL_HTTP_CLIENT_COMPATIBILITY_MODE.
// TODO (#5331): Add warning of use of a deprecated version of Semantic Versions.
return oldHTTPServer{}
}

// ServerStatus returns a span status code and message for an HTTP status code
// value returned by a server. Status codes in the 400-499 range are not
// returned as errors.
func ServerStatus(code int) (codes.Code, string) {
if code < 100 || code >= 600 {
return codes.Error, fmt.Sprintf("Invalid HTTP status code %d", code)

Check warning on line 63 in instrumentation/net/http/otelhttp/internal/semconv/env.go

View check run for this annotation

Codecov / codecov/patch

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

Added line #L63 was not covered by tests
}
if code >= 500 {
return codes.Error, ""
}
return codes.Unset, ""
}
MrAlias marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package semconv

import (
"net/http"
"net/url"
"testing"

"go.opentelemetry.io/otel/attribute"
)

var benchHTTPServerRequestResults []attribute.KeyValue

// BenchmarkHTTPServerRequest allows comparison between different version of the HTTP server.
// To use an alternative start this test with OTEL_HTTP_CLIENT_COMPATIBILITY_MODE set to the
// version under test.
MrAlias marked this conversation as resolved.
Show resolved Hide resolved
func BenchmarkHTTPServerRequest(b *testing.B) {
// Request was generated from TestHTTPServerRequest request.
req := &http.Request{
Method: http.MethodGet,
URL: &url.URL{
Path: "/",
},
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: http.Header{
"User-Agent": []string{"Go-http-client/1.1"},
"Accept-Encoding": []string{"gzip"},
},
Body: http.NoBody,
Host: "127.0.0.1:39093",
RemoteAddr: "127.0.0.1:38738",
RequestURI: "/",
}
serv := NewHTTPServer()

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
benchHTTPServerRequestResults = serv.RequestTraceAttrs("", req)
}
}
49 changes: 49 additions & 0 deletions instrumentation/net/http/otelhttp/internal/semconv/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package semconv // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv"

import (
"net"
"strconv"
"strings"
)

// splitHostPort splits a network address hostport of the form "host",
// "host%zone", "[host]", "[host%zone], "host:port", "host%zone:port",
// "[host]:port", "[host%zone]:port", or ":port" into host or host%zone and
// port.
//
// An empty host is returned if it is not provided or unparsable. A negative
// port is returned if it is not provided or unparsable.
func splitHostPort(hostport string) (host string, port int) {
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
port = -1

if strings.HasPrefix(hostport, "[") {
addrEnd := strings.LastIndex(hostport, "]")
if addrEnd < 0 {
// Invalid hostport.
return
}
if i := strings.LastIndex(hostport[addrEnd:], ":"); i < 0 {
host = hostport[1:addrEnd]
return
}
} else {
if i := strings.LastIndex(hostport, ":"); i < 0 {
host = hostport
return
}
}

host, pStr, err := net.SplitHostPort(hostport)
if err != nil {
return
}

p, err := strconv.ParseUint(pStr, 10, 16)
if err != nil {
return
}
return host, int(p)
}
41 changes: 41 additions & 0 deletions instrumentation/net/http/otelhttp/internal/semconv/util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package semconv

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestSplitHostPort(t *testing.T) {
tests := []struct {
hostport string
host string
port int
}{
{"", "", -1},
{":8080", "", 8080},
{"127.0.0.1", "127.0.0.1", -1},
{"www.example.com", "www.example.com", -1},
{"127.0.0.1%25en0", "127.0.0.1%25en0", -1},
{"[]", "", -1}, // Ensure this doesn't panic.
{"[fe80::1", "", -1},
{"[fe80::1]", "fe80::1", -1},
{"[fe80::1%25en0]", "fe80::1%25en0", -1},
{"[fe80::1]:8080", "fe80::1", 8080},
{"[fe80::1]::", "", -1}, // Too many colons.
{"127.0.0.1:", "127.0.0.1", -1},
{"127.0.0.1:port", "127.0.0.1", -1},
{"127.0.0.1:8080", "127.0.0.1", 8080},
{"www.example.com:8080", "www.example.com", 8080},
{"127.0.0.1%25en0:8080", "127.0.0.1%25en0", 8080},
}

for _, test := range tests {
h, p := splitHostPort(test.hostport)
assert.Equal(t, test.host, h, test.hostport)
assert.Equal(t, test.port, p, test.hostport)
}
}