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 Metrics - v1.20.0 support #5818

Merged
7 changes: 0 additions & 7 deletions instrumentation/net/http/otelhttp/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,6 @@ const (
WriteErrorKey = attribute.Key("http.write_error") // if an error occurred while writing a reply, the string of the error (io.EOF is not recorded)
)

// Server HTTP metrics.
const (
serverRequestSize = "http.server.request.size" // Incoming request bytes total
serverResponseSize = "http.server.response.size" // Incoming response bytes total
serverDuration = "http.server.duration" // Incoming end to end duration, milliseconds
)

// Client HTTP metrics.
const (
clientRequestSize = "http.client.request.size" // Outgoing request bytes total
Expand Down
66 changes: 15 additions & 51 deletions instrumentation/net/http/otelhttp/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ import (

"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request"
"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"
"go.opentelemetry.io/otel/trace"
)
Expand All @@ -25,7 +22,6 @@ type middleware struct {
server string

tracer trace.Tracer
meter metric.Meter
propagators propagation.TextMapPropagator
spanStartOptions []trace.SpanStartOption
readEvent bool
Expand All @@ -35,10 +31,7 @@ type middleware struct {
publicEndpoint bool
publicEndpointFn func(*http.Request) bool

traceSemconv semconv.HTTPServer
requestBytesCounter metric.Int64Counter
responseBytesCounter metric.Int64Counter
serverLatencyMeasure metric.Float64Histogram
semconv semconv.HTTPServer
}

func defaultHandlerFormatter(operation string, _ *http.Request) string {
Expand All @@ -57,8 +50,6 @@ 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 All @@ -68,7 +59,6 @@ func NewMiddleware(operation string, opts ...Option) func(http.Handler) http.Han

c := newConfig(append(defaultOpts, opts...)...)
h.configure(c)
h.createMeasures()

return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -79,7 +69,6 @@ func NewMiddleware(operation string, opts ...Option) func(http.Handler) http.Han

func (h *middleware) configure(c *config) {
h.tracer = c.Tracer
h.meter = c.Meter
h.propagators = c.Propagators
h.spanStartOptions = c.SpanStartOptions
h.readEvent = c.ReadEvent
Expand All @@ -89,6 +78,7 @@ func (h *middleware) configure(c *config) {
h.publicEndpoint = c.PublicEndpoint
h.publicEndpointFn = c.PublicEndpointFn
h.server = c.ServerName
h.semconv = semconv.NewHTTPServer(c.Meter)
}

func handleErr(err error) {
Expand All @@ -97,30 +87,6 @@ func handleErr(err error) {
}
}

func (h *middleware) createMeasures() {
var err error
h.requestBytesCounter, err = h.meter.Int64Counter(
serverRequestSize,
metric.WithUnit("By"),
metric.WithDescription("Measures the size of HTTP request messages."),
)
handleErr(err)

h.responseBytesCounter, err = h.meter.Int64Counter(
serverResponseSize,
metric.WithUnit("By"),
metric.WithDescription("Measures the size of HTTP response messages."),
)
handleErr(err)

h.serverLatencyMeasure, err = h.meter.Float64Histogram(
serverDuration,
metric.WithUnit("ms"),
metric.WithDescription("Measures the duration of inbound HTTP requests."),
)
handleErr(err)
}

// serveHTTP sets up tracing and calls the given next http.Handler with the span
// context injected into the request context.
func (h *middleware) serveHTTP(w http.ResponseWriter, r *http.Request, next http.Handler) {
Expand All @@ -135,7 +101,7 @@ 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(h.traceSemconv.RequestTraceAttrs(h.server, r)...),
trace.WithAttributes(h.semconv.RequestTraceAttrs(h.server, r)...),
}

opts = append(opts, h.spanStartOptions...)
Expand Down Expand Up @@ -212,35 +178,33 @@ func (h *middleware) serveHTTP(w http.ResponseWriter, r *http.Request, next http

statusCode := rww.StatusCode()
bytesWritten := rww.BytesWritten()
span.SetStatus(semconv.ServerStatus(statusCode))
span.SetAttributes(h.traceSemconv.ResponseTraceAttrs(semconv.ResponseTelemetry{
span.SetStatus(h.semconv.Status(statusCode))
span.SetAttributes(h.semconv.ResponseTraceAttrs(semconv.ResponseTelemetry{
StatusCode: statusCode,
ReadBytes: bw.BytesRead(),
ReadError: bw.Error(),
WriteBytes: bytesWritten,
WriteError: rww.Error(),
})...)

// Add metrics
attributes := append(labeler.Get(), semconvutil.HTTPServerRequestMetrics(h.server, r)...)
if statusCode > 0 {
attributes = append(attributes, semconv.HTTPStatusCode(statusCode))
}
o := metric.WithAttributeSet(attribute.NewSet(attributes...))

h.requestBytesCounter.Add(ctx, bw.BytesRead(), o)
h.responseBytesCounter.Add(ctx, bytesWritten, o)

// Use floating point division here for higher precision (instead of Millisecond method).
elapsedTime := float64(time.Since(requestStartTime)) / float64(time.Millisecond)

h.serverLatencyMeasure.Record(ctx, elapsedTime, o)
h.semconv.RecordMetrics(ctx, semconv.MetricData{
ServerName: h.server,
Req: r,
StatusCode: statusCode,
AdditionalAttributes: labeler.Get(),
RequestSize: bw.BytesRead(),
ResponseSize: bytesWritten,
ElapsedTime: elapsedTime,
})
}

// 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)
attr := semconv.NewHTTPServer(nil).Route(route)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
span := trace.SpanFromContext(r.Context())
span.SetAttributes(attr)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func BenchmarkHTTPServerRequest(b *testing.B) {
RemoteAddr: "127.0.0.1:38738",
RequestURI: "/",
}
serv := NewHTTPServer()
serv := NewHTTPServer(nil)

b.ReportAllocs()
b.ResetTimer()
Expand Down
53 changes: 46 additions & 7 deletions instrumentation/net/http/otelhttp/internal/semconv/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
package semconv // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv"

import (
"context"
"fmt"
"net/http"
"os"
"strings"

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

type ResponseTelemetry struct {
Expand All @@ -23,6 +25,11 @@ type ResponseTelemetry struct {

type HTTPServer struct {
duplicate bool

// Old metrics
requestBytesCounter metric.Int64Counter
responseBytesCounter metric.Int64Counter
serverLatencyMeasure metric.Float64Histogram
}

// RequestTraceAttrs returns trace attributes for an HTTP request received by a
Expand Down Expand Up @@ -63,15 +70,10 @@ func (s HTTPServer) Route(route string) attribute.KeyValue {
return oldHTTPServer{}.Route(route)
}

func NewHTTPServer() HTTPServer {
env := strings.ToLower(os.Getenv("OTEL_HTTP_CLIENT_COMPATIBILITY_MODE"))
return HTTPServer{duplicate: env == "http/dup"}
}

// ServerStatus returns a span status code and message for an HTTP status code
// Status 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) {
func (s HTTPServer) Status(code int) (codes.Code, string) {
if code < 100 || code >= 600 {
return codes.Error, fmt.Sprintf("Invalid HTTP status code %d", code)
}
Expand All @@ -81,6 +83,43 @@ func ServerStatus(code int) (codes.Code, string) {
return codes.Unset, ""
}

type MetricData struct {
ServerName string
Req *http.Request
StatusCode int
AdditionalAttributes []attribute.KeyValue

RequestSize int64
ResponseSize int64
ElapsedTime float64
}

func (s HTTPServer) RecordMetrics(ctx context.Context, md MetricData) {
if s.requestBytesCounter == nil || s.responseBytesCounter == nil || s.serverLatencyMeasure == nil {
// This will happen if an HTTPServer{} is used insted of NewHTTPServer.
return
}

attributes := oldHTTPServer{}.MetricAttributes(md.ServerName, md.Req, md.StatusCode, md.AdditionalAttributes)
o := metric.WithAttributeSet(attribute.NewSet(attributes...))
addOpts := []metric.AddOption{o} // Allocate vararg slice once.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we track that these should be pooled?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made #5968 to track this

s.requestBytesCounter.Add(ctx, md.RequestSize, addOpts...)
s.responseBytesCounter.Add(ctx, md.ResponseSize, addOpts...)
s.serverLatencyMeasure.Record(ctx, md.ElapsedTime, o)

// TODO: Duplicate Metrics
}

func NewHTTPServer(meter metric.Meter) HTTPServer {
env := strings.ToLower(os.Getenv("OTEL_HTTP_CLIENT_COMPATIBILITY_MODE"))
duplicate := env == "http/dup"
server := HTTPServer{
duplicate: duplicate,
}
server.requestBytesCounter, server.responseBytesCounter, server.serverLatencyMeasure = oldHTTPServer{}.createMeasures(meter)
return server
}

type HTTPClient struct {
duplicate bool
}
Expand Down
83 changes: 83 additions & 0 deletions instrumentation/net/http/otelhttp/internal/semconv/env_test.go
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 (
"context"
"net/http"
"testing"

"github.com/stretchr/testify/require"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/metric/embedded"
"go.opentelemetry.io/otel/metric/noop"
)

func TestHTTPServerDoesNotPanic(t *testing.T) {
testCases := []struct {
name string
server HTTPServer
}{
{
name: "empty",
server: HTTPServer{},
},
{
name: "nil meter",
server: NewHTTPServer(nil),
},
{
name: "with Meter",
server: NewHTTPServer(noop.Meter{}),
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
require.NotPanics(t, func() {
req, err := http.NewRequest("GET", "http://example.com", nil)
require.NoError(t, err)

_ = tt.server.RequestTraceAttrs("stuff", req)
_ = tt.server.ResponseTraceAttrs(ResponseTelemetry{StatusCode: 200})
tt.server.RecordMetrics(context.Background(), MetricData{
ServerName: "stuff",
Req: req,
})
})
})
}
}

type testInst struct {
embedded.Int64Counter
embedded.Float64Histogram

intValue int64
floatValue float64
attributes []attribute.KeyValue
}

func (t *testInst) Add(ctx context.Context, incr int64, options ...metric.AddOption) {
t.intValue = incr
cfg := metric.NewAddConfig(options)
attr := cfg.Attributes()
t.attributes = attr.ToSlice()
}

func (t *testInst) Record(ctx context.Context, value float64, options ...metric.RecordOption) {
t.floatValue = value
cfg := metric.NewRecordConfig(options)
attr := cfg.Attributes()
t.attributes = attr.ToSlice()
}

func NewTestHTTPServer() HTTPServer {
return HTTPServer{
requestBytesCounter: &testInst{},
responseBytesCounter: &testInst{},
serverLatencyMeasure: &testInst{},
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (

func TestNewTraceRequest(t *testing.T) {
t.Setenv("OTEL_HTTP_CLIENT_COMPATIBILITY_MODE", "http/dup")
serv := NewHTTPServer()
serv := NewHTTPServer(nil)
want := func(req testServerReq) []attribute.KeyValue {
return []attribute.KeyValue{
attribute.String("http.request.method", "GET"),
Expand Down
7 changes: 7 additions & 0 deletions instrumentation/net/http/otelhttp/internal/semconv/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"strconv"
"strings"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
semconvNew "go.opentelemetry.io/otel/semconv/v1.26.0"
)
Expand Down Expand Up @@ -89,3 +90,9 @@
http.MethodPut: semconvNew.HTTPRequestMethodPut,
http.MethodTrace: semconvNew.HTTPRequestMethodTrace,
}

func handleErr(err error) {
if err != nil {
otel.Handle(err)

Check warning on line 96 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#L96

Added line #L96 was not covered by tests
}
}
Loading