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 Part3 Server - v1.24.0 support #5401

Merged
merged 4 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion instrumentation/net/http/otelhttp/internal/semconv/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package semconv // import "go.opentelemetry.io/contrib/instrumentation/net/http/
import (
"fmt"
"net/http"
"os"
"strings"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
Expand Down Expand Up @@ -52,7 +54,13 @@ type HTTPServer interface {
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{}
env := strings.ToLower(os.Getenv("OTEL_HTTP_CLIENT_COMPATIBILITY_MODE"))
switch env {
case "http":
return newHTTPServer{}
default:
return oldHTTPServer{}
}
}

// ServerStatus returns a span status code and message for an HTTP status code
Expand Down
42 changes: 42 additions & 0 deletions instrumentation/net/http/otelhttp/internal/semconv/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@

import (
"net"
"net/http"
"strconv"
"strings"

"go.opentelemetry.io/otel/attribute"
semconvNew "go.opentelemetry.io/otel/semconv/v1.24.0"
)

// splitHostPort splits a network address hostport of the form "host",
Expand Down Expand Up @@ -47,3 +51,41 @@
}
return host, int(p)
}

func requiredHTTPPort(https bool, port int) int { // nolint:revive
if https {
if port > 0 && port != 443 {
return port

Check warning on line 58 in instrumentation/net/http/otelhttp/internal/semconv/util.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/net/http/otelhttp/internal/semconv/util.go#L57-L58

Added lines #L57 - L58 were not covered by tests
}
} else {
if port > 0 && port != 80 {
return port
}
}
return -1

Check warning on line 65 in instrumentation/net/http/otelhttp/internal/semconv/util.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/net/http/otelhttp/internal/semconv/util.go#L65

Added line #L65 was not covered by tests
}

func serverClientIP(xForwardedFor string) string {
if idx := strings.Index(xForwardedFor, ","); idx >= 0 {
xForwardedFor = xForwardedFor[:idx]

Check warning on line 70 in instrumentation/net/http/otelhttp/internal/semconv/util.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/net/http/otelhttp/internal/semconv/util.go#L70

Added line #L70 was not covered by tests
}
return xForwardedFor
}

func netProtocol(proto string) (name string, version string) {
name, version, _ = strings.Cut(proto, "/")
name = strings.ToLower(name)
return name, version
}

var methodLookup = map[string]attribute.KeyValue{
http.MethodConnect: semconvNew.HTTPRequestMethodConnect,
http.MethodDelete: semconvNew.HTTPRequestMethodDelete,
http.MethodGet: semconvNew.HTTPRequestMethodGet,
http.MethodHead: semconvNew.HTTPRequestMethodHead,
http.MethodOptions: semconvNew.HTTPRequestMethodOptions,
http.MethodPatch: semconvNew.HTTPRequestMethodPatch,
http.MethodPost: semconvNew.HTTPRequestMethodPost,
http.MethodPut: semconvNew.HTTPRequestMethodPut,
http.MethodTrace: semconvNew.HTTPRequestMethodTrace,
}
156 changes: 156 additions & 0 deletions instrumentation/net/http/otelhttp/internal/semconv/v1.24.0.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// 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/http"
"slices"
"strings"

"go.opentelemetry.io/otel/attribute"
semconvNew "go.opentelemetry.io/otel/semconv/v1.24.0"
)

type newHTTPServer struct{}

var _ HTTPServer = newHTTPServer{}

// TraceRequest 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.
func (n newHTTPServer) RequestTraceAttrs(server string, req *http.Request) []attribute.KeyValue {
const MaxAttributes = 11
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
attrs := make([]attribute.KeyValue, MaxAttributes)
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
var host string
var p int
if server == "" {
host, p = splitHostPort(req.Host)
} else {
// Prioritize the primary server name.
host, p = splitHostPort(server)
if p < 0 {
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
_, p = splitHostPort(req.Host)

Check warning on line 46 in instrumentation/net/http/otelhttp/internal/semconv/v1.24.0.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/net/http/otelhttp/internal/semconv/v1.24.0.go#L44-L46

Added lines #L44 - L46 were not covered by tests
}
}

attrs[0] = semconvNew.ServerAddress(host)
i := 1
if hostPort := requiredHTTPPort(req.TLS != nil, p); hostPort > 0 {
attrs[i] = semconvNew.ServerPort(hostPort)
i++
}
i += n.method(req.Method, attrs[i:]) // Max 2
i += n.scheme(req.TLS != nil, attrs[i:]) // Max 1

if peer, peerPort := splitHostPort(req.RemoteAddr); peer != "" {
// The Go HTTP server sets RemoteAddr to "IP:port", this will not be a
// file-path that would be interpreted with a sock family.
attrs[i] = semconvNew.NetworkPeerAddress(peer)
i++
if peerPort > 0 {
attrs[i] = semconvNew.NetworkPeerPort(peerPort)
i++
}
}

if useragent := req.UserAgent(); useragent != "" {
// This is the same between v1.20, and v1.24
attrs[i] = semconvNew.UserAgentOriginal(useragent)
i++
}

if clientIP := serverClientIP(req.Header.Get("X-Forwarded-For")); clientIP != "" {
attrs[i] = semconvNew.ClientAddress(clientIP)
i++
}

if req.URL != nil && req.URL.Path != "" {
attrs[i] = semconvNew.URLPath(req.URL.Path)
i++
}

protoName, protoVersion := netProtocol(req.Proto)
if protoName != "" && protoName != "http" {
attrs[i] = semconvNew.NetworkProtocolName(protoName)
i++

Check warning on line 89 in instrumentation/net/http/otelhttp/internal/semconv/v1.24.0.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/net/http/otelhttp/internal/semconv/v1.24.0.go#L88-L89

Added lines #L88 - L89 were not covered by tests
}
if protoVersion != "" {
attrs[i] = semconvNew.NetworkProtocolVersion(protoVersion)
i++
}

return slices.Clip(attrs[:i])
}

func (n newHTTPServer) method(method string, attrs []attribute.KeyValue) int {
if method == "" {
attrs[0] = semconvNew.HTTPRequestMethodGet
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
return 1

Check warning on line 102 in instrumentation/net/http/otelhttp/internal/semconv/v1.24.0.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/net/http/otelhttp/internal/semconv/v1.24.0.go#L101-L102

Added lines #L101 - L102 were not covered by tests
}
if attr, ok := methodLookup[method]; ok {
attrs[0] = attr
return 1
}

if attr, ok := methodLookup[strings.ToUpper(method)]; ok {
attrs[0] = attr
} else {
// If the Original methos is not a standard HTTP method fallback to GET
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
attrs[0] = semconvNew.HTTPRequestMethodGet
}
attrs[1] = semconvNew.HTTPRequestMethodOriginal(method)
return 2
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
}

func (n newHTTPServer) scheme(https bool, attrs []attribute.KeyValue) int { // nolint:revive
if https {
attrs[0] = semconvNew.URLScheme("https")
return 1

Check warning on line 122 in instrumentation/net/http/otelhttp/internal/semconv/v1.24.0.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/net/http/otelhttp/internal/semconv/v1.24.0.go#L121-L122

Added lines #L121 - L122 were not covered by tests
}
attrs[0] = semconvNew.URLScheme("http")
return 1
}

// TraceResponse 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.
func (n newHTTPServer) ResponseTraceAttrs(resp ResponseTelemetry) []attribute.KeyValue {
attributes := []attribute.KeyValue{}
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved

if resp.ReadBytes > 0 {
attributes = append(attributes,
semconvNew.HTTPRequestBodySize(int(resp.ReadBytes)),
)
}
if resp.WriteBytes > 0 {
attributes = append(attributes,
semconvNew.HTTPResponseBodySize(int(resp.WriteBytes)),
)
}
if resp.StatusCode > 0 {
attributes = append(attributes,
semconvNew.HTTPResponseStatusCode(resp.StatusCode),
)
}

return attributes
}

// Route returns the attribute for the route.
func (n newHTTPServer) Route(route string) attribute.KeyValue {
return semconvNew.HTTPRoute(route)

Check warning on line 155 in instrumentation/net/http/otelhttp/internal/semconv/v1.24.0.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/net/http/otelhttp/internal/semconv/v1.24.0.go#L154-L155

Added lines #L154 - L155 were not covered by tests
}
125 changes: 125 additions & 0 deletions instrumentation/net/http/otelhttp/internal/semconv/v1.24.0_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package semconv

import (
"fmt"
"net/http"
"testing"

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

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

func TestNewTraceRequest(t *testing.T) {
t.Setenv("OTEL_HTTP_CLIENT_COMPATIBILITY_MODE", "http")
serv := NewHTTPServer()
want := func(req testServerReq) []attribute.KeyValue {
return []attribute.KeyValue{
attribute.String("http.request.method", "GET"),
attribute.String("url.scheme", "http"),
attribute.String("server.address", req.hostname),
attribute.Int("server.port", req.serverPort),
attribute.String("network.peer.address", req.peerAddr),
attribute.Int("network.peer.port", req.peerPort),
attribute.String("user_agent.original", "Go-http-client/1.1"),
attribute.String("client.address", req.clientIP),
attribute.String("network.protocol.version", "1.1"),
attribute.String("url.path", "/"),
}
}
testTraceRequest(t, serv, want)
}

func TestNewTraceResponse(t *testing.T) {
testCases := []struct {
name string
resp ResponseTelemetry
want []attribute.KeyValue
}{
{
name: "empty",
resp: ResponseTelemetry{},
want: nil,
},
{
name: "no errors",
resp: ResponseTelemetry{
StatusCode: 200,
ReadBytes: 701,
WriteBytes: 802,
},
want: []attribute.KeyValue{
attribute.Int("http.request.body.size", 701),
attribute.Int("http.response.body.size", 802),
attribute.Int("http.response.status_code", 200),
},
},
{
name: "with errors",
resp: ResponseTelemetry{
StatusCode: 200,
ReadBytes: 701,
ReadError: fmt.Errorf("read error"),
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
WriteBytes: 802,
WriteError: fmt.Errorf("write error"),
},
want: []attribute.KeyValue{
attribute.Int("http.request.body.size", 701),
attribute.Int("http.response.body.size", 802),
attribute.Int("http.response.status_code", 200),
},
},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
got := newHTTPServer{}.ResponseTraceAttrs(tt.resp)
assert.ElementsMatch(t, tt.want, got)
})
}
}

func TestNewMethod(t *testing.T) {
testCases := []struct {
method string
n int
want []attribute.KeyValue
}{
{
method: http.MethodPost,
n: 1,
want: []attribute.KeyValue{
attribute.String("http.request.method", "POST"),
},
},
{
method: "Put",
n: 2,
want: []attribute.KeyValue{
attribute.String("http.request.method", "PUT"),
attribute.String("http.request.method_original", "Put"),
},
},
{
method: "Unknown",
n: 2,
want: []attribute.KeyValue{
attribute.String("http.request.method", "GET"),
attribute.String("http.request.method_original", "Unknown"),
},
},
}

for _, tt := range testCases {
t.Run(tt.method, func(t *testing.T) {
attrs := make([]attribute.KeyValue, 5)
n := newHTTPServer{}.method(tt.method, attrs[1:])
require.Equal(t, tt.n, n, "Length doesn't match")
require.ElementsMatch(t, tt.want, attrs[1:n+1])
})
}
}
Loading