Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Add support for configuring `ClientCertificate` and `ClientKey` field for OTLP exporters in `go.opentelemetry.io/contrib/config`. (#6378)
- Add `WithAttributeBuilder`, `AttributeBuilder`, `DefaultAttributeBuilder`, `DynamoDBAttributeBuilder`, `SNSAttributeBuilder` to support adding attributes based on SDK input and output in `go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws`. (#6543)
- Support for the OTEL_HTTP_CLIENT_COMPATIBILITY_MODE=http/dup environment variable in `go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux` to emit attributes for both the v1.20.0 and v1.26.0 semantic conventions. (#6652)
- Added the `WithMeterProvider` option to allow passing a custom meter provider to `go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux`. (#6648)
- Added the `WithMetricAttributesFn` option to allow setting dynamic, per-request metric attributes in `go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux`. (#6648)
- Added metrics support, and emit all stable metrics from the [Semantic Conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-metrics.md) in `go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux`. (#6648)

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,22 @@ package otelmux // import "go.opentelemetry.io/contrib/instrumentation/github.co
import (
"net/http"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/propagation"
oteltrace "go.opentelemetry.io/otel/trace"
)

// config is used to configure the mux middleware.
type config struct {
TracerProvider oteltrace.TracerProvider
Propagators propagation.TextMapPropagator
spanNameFormatter func(string, *http.Request) string
PublicEndpoint bool
PublicEndpointFn func(*http.Request) bool
Filters []Filter
TracerProvider oteltrace.TracerProvider
Propagators propagation.TextMapPropagator
spanNameFormatter func(string, *http.Request) string
PublicEndpoint bool
PublicEndpointFn func(*http.Request) bool
Filters []Filter
MeterProvider metric.MeterProvider
MetricAttributesFn func(*http.Request) []attribute.KeyValue
}

// Option specifies instrumentation configuration options.
Expand Down Expand Up @@ -97,3 +101,21 @@ func WithFilter(f Filter) Option {
c.Filters = append(c.Filters, f)
})
}

// WithMeterProvider specifies a meter provider to use for creating a metric.
// If none is specified, the global provider is used.
func WithMeterProvider(provider metric.MeterProvider) Option {
return optionFunc(func(cfg *config) {
if provider != nil {
cfg.MeterProvider = provider
}
})
}

// WithMetricAttributesFn returns an Option to set a function that maps an HTTP request to a slice of attribute.KeyValue.
// These attributes will be included in metrics for every request.
func WithMetricAttributesFn(metricAttributesFn func(r *http.Request) []attribute.KeyValue) Option {
return optionFunc(func(c *config) {
c.MetricAttributesFn = metricAttributesFn
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package otelmux // import "go.opentelemetry.io/contrib/instrumentation/github.co
import (
"fmt"
"net/http"
"time"

"github.com/felixge/httpsnoop"
"github.com/gorilla/mux"
Expand All @@ -14,7 +15,8 @@ import (
"go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux/internal/semconv"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/metric/noop"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
)
Expand Down Expand Up @@ -45,31 +47,42 @@ func Middleware(service string, opts ...Option) mux.MiddlewareFunc {
if cfg.spanNameFormatter == nil {
cfg.spanNameFormatter = defaultSpanNameFunc
}
if cfg.MeterProvider == nil {
cfg.MeterProvider = otel.GetMeterProvider()
}
meter := cfg.MeterProvider.Meter(
ScopeName,
metric.WithInstrumentationVersion(Version()),
)
return func(handler http.Handler) http.Handler {
return traceware{
service: service,
tracer: tracer,
propagators: cfg.Propagators,
handler: handler,
spanNameFormatter: cfg.spanNameFormatter,
publicEndpoint: cfg.PublicEndpoint,
publicEndpointFn: cfg.PublicEndpointFn,
filters: cfg.Filters,
semconv: semconv.NewHTTPServer(noop.Meter{}),
service: service,
tracer: tracer,
propagators: cfg.Propagators,
handler: handler,
spanNameFormatter: cfg.spanNameFormatter,
publicEndpoint: cfg.PublicEndpoint,
publicEndpointFn: cfg.PublicEndpointFn,
filters: cfg.Filters,
meter: meter,
semconv: semconv.NewHTTPServer(meter),
metricAttributesFn: cfg.MetricAttributesFn,
}
}
}

type traceware struct {
service string
tracer trace.Tracer
propagators propagation.TextMapPropagator
handler http.Handler
spanNameFormatter func(string, *http.Request) string
publicEndpoint bool
publicEndpointFn func(*http.Request) bool
filters []Filter
semconv semconv.HTTPServer
service string
tracer trace.Tracer
propagators propagation.TextMapPropagator
handler http.Handler
spanNameFormatter func(string, *http.Request) string
publicEndpoint bool
publicEndpointFn func(*http.Request) bool
filters []Filter
meter metric.Meter
semconv semconv.HTTPServer
metricAttributesFn func(*http.Request) []attribute.KeyValue
}

// defaultSpanNameFunc just reuses the route name as the span name.
Expand All @@ -78,6 +91,7 @@ func defaultSpanNameFunc(routeName string, _ *http.Request) string { return rout
// ServeHTTP implements the http.Handler interface. It does the actual
// tracing of the request.
func (tw traceware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
requestStartTime := time.Now()
for _, f := range tw.filters {
if !f(r) {
// Simply pass through to the handler if a filter rejects the request
Expand Down Expand Up @@ -158,4 +172,31 @@ func (tw traceware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
WriteBytes: rww.BytesWritten(),
WriteError: rww.Error(),
})...)

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

metricAttributes := semconv.MetricAttributes{
Req: r,
StatusCode: statusCode,
AdditionalAttributes: tw.metricAttributesFromRequest(r),
}

tw.semconv.RecordMetrics(ctx, semconv.ServerMetricData{
ServerName: tw.service,
ResponseSize: rww.BytesWritten(),
MetricAttributes: metricAttributes,
MetricData: semconv.MetricData{
RequestSize: bw.BytesRead(),
ElapsedTime: elapsedTime,
},
})
}

func (tw traceware) metricAttributesFromRequest(r *http.Request) []attribute.KeyValue {
var attributeForRequest []attribute.KeyValue
if tw.metricAttributesFn != nil {
attributeForRequest = tw.metricAttributesFn(r)
}
return attributeForRequest
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.59.0
go.opentelemetry.io/otel v1.34.0
go.opentelemetry.io/otel/sdk v1.34.0
go.opentelemetry.io/otel/sdk/metric v1.34.0
go.opentelemetry.io/otel/trace v1.34.0
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/propagation"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
"go.opentelemetry.io/otel/trace"
Expand Down Expand Up @@ -81,8 +83,14 @@ func TestSDKIntegration(t *testing.T) {
provider := sdktrace.NewTracerProvider()
provider.RegisterSpanProcessor(sr)

reader := sdkmetric.NewManualReader()
meterProvider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader))

router := mux.NewRouter()
router.Use(otelmux.Middleware("foobar", otelmux.WithTracerProvider(provider)))
router.Use(otelmux.Middleware("foobar",
otelmux.WithTracerProvider(provider),
otelmux.WithMeterProvider(meterProvider)))

router.HandleFunc("/user/{id:[0-9]+}", ok)
router.HandleFunc("/book/{title}", ok)

Expand Down Expand Up @@ -282,3 +290,82 @@ func TestWithPublicEndpointFn(t *testing.T) {
})
}
}

func TestHandlerWithMetricAttributesFn(t *testing.T) {
const (
serverRequestSize = "http.server.request.size"
serverResponseSize = "http.server.response.size"
serverDuration = "http.server.duration"
)
testCases := []struct {
name string
fn func(r *http.Request) []attribute.KeyValue
expectedAdditionalAttribute []attribute.KeyValue
}{
{
name: "With a nil function",
fn: nil,
expectedAdditionalAttribute: []attribute.KeyValue{},
},
{
name: "With a function that returns an additional attribute",
fn: func(r *http.Request) []attribute.KeyValue {
return []attribute.KeyValue{
attribute.String("fooKey", "fooValue"),
attribute.String("barKey", "barValue"),
}
},
expectedAdditionalAttribute: []attribute.KeyValue{
attribute.String("fooKey", "fooValue"),
attribute.String("barKey", "barValue"),
},
},
}

for _, tc := range testCases {
reader := sdkmetric.NewManualReader()
meterProvider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader))

router := mux.NewRouter()
router.Use(otelmux.Middleware("foobar",
otelmux.WithMeterProvider(meterProvider),
otelmux.WithMetricAttributesFn(tc.fn),
))

router.HandleFunc("/user/{id:[0-9]+}", ok)
r, err := http.NewRequest(http.MethodGet, "http://localhost/user/123", nil)
require.NoError(t, err)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, r)

rm := metricdata.ResourceMetrics{}
err = reader.Collect(context.Background(), &rm)
require.NoError(t, err)
require.Len(t, rm.ScopeMetrics, 1)
assert.Len(t, rm.ScopeMetrics[0].Metrics, 3)

// Verify that the additional attribute is present in the metrics.
for _, m := range rm.ScopeMetrics[0].Metrics {
switch m.Name {
case serverRequestSize, serverResponseSize:
d, ok := m.Data.(metricdata.Sum[int64])
assert.True(t, ok)
assert.Len(t, d.DataPoints, 1)
containsAttributes(t, d.DataPoints[0].Attributes, testCases[0].expectedAdditionalAttribute)
case serverDuration:
d, ok := m.Data.(metricdata.Histogram[float64])
assert.True(t, ok)
assert.Len(t, d.DataPoints, 1)
containsAttributes(t, d.DataPoints[0].Attributes, testCases[0].expectedAdditionalAttribute)
}
}
}
}

func containsAttributes(t *testing.T, attrSet attribute.Set, expected []attribute.KeyValue) {
for _, att := range expected {
actualValue, ok := attrSet.Value(att.Key)
assert.True(t, ok)
assert.Equal(t, att.Value.AsString(), actualValue.AsString())
}
}