diff --git a/go.mod b/go.mod index 4dd5736013..8b93b339e6 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/spf13/pflag v1.0.6 github.com/tsenart/vegeta/v12 v12.12.0 go.opencensus.io v0.24.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 go.opentelemetry.io/contrib/instrumentation/runtime v0.62.0 go.opentelemetry.io/otel v1.37.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0 diff --git a/go.sum b/go.sum index f817e8b23e..36e61b3518 100644 --- a/go.sum +++ b/go.sum @@ -367,8 +367,8 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= go.opentelemetry.io/contrib/instrumentation/runtime v0.62.0 h1:ZIt0ya9/y4WyRIzfLC8hQRRsWg0J9M9GyaGtIMiElZI= go.opentelemetry.io/contrib/instrumentation/runtime v0.62.0/go.mod h1:F1aJ9VuiKWOlWwKdTYDUp1aoS0HzQxg38/VLxKmhm5U= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/config.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/config.go index a01bfafbe0..6bd50d4c9b 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/config.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/config.go @@ -176,6 +176,10 @@ func WithMessageEvents(events ...event) Option { // WithSpanNameFormatter takes a function that will be called on every // request and the returned string will become the Span Name. +// +// When using [http.ServeMux] (or any middleware that sets the Pattern of [http.Request]), +// the span name formatter will run twice. Once when the span is created, and +// second time after the middleware, so the pattern can be used. func WithSpanNameFormatter(f func(operation string, r *http.Request) string) Option { return optionFunc(func(c *config) { c.SpanNameFormatter = f diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/handler.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/handler.go index e555a475f1..937f9b4e73 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/handler.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/handler.go @@ -12,6 +12,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/otel" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" ) @@ -21,15 +22,16 @@ type middleware struct { operation string server string - tracer trace.Tracer - propagators propagation.TextMapPropagator - spanStartOptions []trace.SpanStartOption - readEvent bool - writeEvent bool - filters []Filter - spanNameFormatter func(string, *http.Request) string - publicEndpoint bool - publicEndpointFn func(*http.Request) bool + tracer trace.Tracer + propagators propagation.TextMapPropagator + spanStartOptions []trace.SpanStartOption + readEvent bool + writeEvent bool + filters []Filter + spanNameFormatter func(string, *http.Request) string + publicEndpoint bool + publicEndpointFn func(*http.Request) bool + metricAttributesFn func(*http.Request) []attribute.KeyValue semconv semconv.HTTPServer } @@ -79,6 +81,7 @@ func (h *middleware) configure(c *config) { h.publicEndpointFn = c.PublicEndpointFn h.server = c.ServerName h.semconv = semconv.NewHTTPServer(c.Meter) + h.metricAttributesFn = c.MetricAttributesFn } // serveHTTP sets up tracing and calls the given next http.Handler with the span @@ -95,7 +98,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.semconv.RequestTraceAttrs(h.server, r)...), + trace.WithAttributes(h.semconv.RequestTraceAttrs(h.server, r, semconv.RequestTraceAttrsOpts{})...), } opts = append(opts, h.spanStartOptions...) @@ -173,7 +176,12 @@ func (h *middleware) serveHTTP(w http.ResponseWriter, r *http.Request, next http ctx = ContextWithLabeler(ctx, labeler) } - next.ServeHTTP(w, r.WithContext(ctx)) + r = r.WithContext(ctx) + next.ServeHTTP(w, r) + + if r.Pattern != "" { + span.SetName(h.spanNameFormatter(h.operation, r)) + } statusCode := rww.StatusCode() bytesWritten := rww.BytesWritten() @@ -189,14 +197,16 @@ func (h *middleware) serveHTTP(w http.ResponseWriter, r *http.Request, next http // 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: append(labeler.Get(), h.metricAttributesFromRequest(r)...), + } + h.semconv.RecordMetrics(ctx, semconv.ServerMetricData{ - ServerName: h.server, - ResponseSize: bytesWritten, - MetricAttributes: semconv.MetricAttributes{ - Req: r, - StatusCode: statusCode, - AdditionalAttributes: labeler.Get(), - }, + ServerName: h.server, + ResponseSize: bytesWritten, + MetricAttributes: metricAttributes, MetricData: semconv.MetricData{ RequestSize: bw.BytesRead(), ElapsedTime: elapsedTime, @@ -204,6 +214,14 @@ func (h *middleware) serveHTTP(w http.ResponseWriter, r *http.Request, next http }) } +func (h *middleware) metricAttributesFromRequest(r *http.Request) []attribute.KeyValue { + var attributeForRequest []attribute.KeyValue + if h.metricAttributesFn != nil { + attributeForRequest = h.metricAttributesFn(r) + } + return attributeForRequest +} + // WithRouteTag annotates spans and metrics with the provided route name // with HTTP route attribute. func WithRouteTag(route string, h http.Handler) http.Handler { diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request/body_wrapper.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request/body_wrapper.go index a945f55661..d032aa841b 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request/body_wrapper.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request/body_wrapper.go @@ -1,6 +1,11 @@ +// Code generated by gotmpl. DO NOT MODIFY. +// source: internal/shared/request/body_wrapper.go.tmpl + // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +// Package request provides types and functionality to handle HTTP request +// handling. package request // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request" import ( @@ -53,7 +58,7 @@ func (w *BodyWrapper) updateReadData(n int64, err error) { } } -// Closes closes the io.ReadCloser. +// Close closes the io.ReadCloser. func (w *BodyWrapper) Close() error { return w.ReadCloser.Close() } diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request/gen.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request/gen.go new file mode 100644 index 0000000000..9e00dd2fce --- /dev/null +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request/gen.go @@ -0,0 +1,10 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package request // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request" + +// Generate request package: +//go:generate gotmpl --body=../../../../../../internal/shared/request/body_wrapper.go.tmpl "--data={}" --out=body_wrapper.go +//go:generate gotmpl --body=../../../../../../internal/shared/request/body_wrapper_test.go.tmpl "--data={}" --out=body_wrapper_test.go +//go:generate gotmpl --body=../../../../../../internal/shared/request/resp_writer_wrapper.go.tmpl "--data={}" --out=resp_writer_wrapper.go +//go:generate gotmpl --body=../../../../../../internal/shared/request/resp_writer_wrapper_test.go.tmpl "--data={}" --out=resp_writer_wrapper_test.go diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request/resp_writer_wrapper.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request/resp_writer_wrapper.go index fbc344cbdd..ca2e4c14c7 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request/resp_writer_wrapper.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request/resp_writer_wrapper.go @@ -1,3 +1,6 @@ +// Code generated by gotmpl. DO NOT MODIFY. +// source: internal/shared/request/resp_writer_wrapper.go.tmpl + // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 @@ -102,7 +105,7 @@ func (w *RespWriterWrapper) BytesWritten() int64 { return w.written } -// BytesWritten returns the HTTP status code that was sent. +// StatusCode returns the HTTP status code that was sent. func (w *RespWriterWrapper) StatusCode() int { w.mu.RLock() defer w.mu.RUnlock() diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/env.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/env.go index 3b036f8a37..7edc8d10ff 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/env.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/env.go @@ -1,3 +1,6 @@ +// Code generated by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconv/env.go.tmpl + // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 @@ -14,8 +17,13 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/semconv/v1.34.0/httpconv" ) +// OTelSemConvStabilityOptIn is an environment variable. +// That can be set to "http/dup" to keep getting the old HTTP semantic conventions. +const OTelSemConvStabilityOptIn = "OTEL_SEMCONV_STABILITY_OPT_IN" + type ResponseTelemetry struct { StatusCode int ReadBytes int64 @@ -31,6 +39,11 @@ type HTTPServer struct { requestBytesCounter metric.Int64Counter responseBytesCounter metric.Int64Counter serverLatencyMeasure metric.Float64Histogram + + // New metrics + requestBodySizeHistogram httpconv.ServerRequestBodySize + responseBodySizeHistogram httpconv.ServerResponseBodySize + requestDurationHistogram httpconv.ServerRequestDuration } // RequestTraceAttrs returns trace attributes for an HTTP request received by a @@ -49,26 +62,40 @@ type HTTPServer struct { // // 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 (s HTTPServer) RequestTraceAttrs(server string, req *http.Request) []attribute.KeyValue { +func (s HTTPServer) RequestTraceAttrs(server string, req *http.Request, opts RequestTraceAttrsOpts) []attribute.KeyValue { + attrs := CurrentHTTPServer{}.RequestTraceAttrs(server, req, opts) + if s.duplicate { + return OldHTTPServer{}.RequestTraceAttrs(server, req, attrs) + } + return attrs +} + +func (s HTTPServer) NetworkTransportAttr(network string) []attribute.KeyValue { if s.duplicate { - return append(OldHTTPServer{}.RequestTraceAttrs(server, req), CurrentHTTPServer{}.RequestTraceAttrs(server, req)...) + return []attribute.KeyValue{ + OldHTTPServer{}.NetworkTransportAttr(network), + CurrentHTTPServer{}.NetworkTransportAttr(network), + } + } + return []attribute.KeyValue{ + CurrentHTTPServer{}.NetworkTransportAttr(network), } - return OldHTTPServer{}.RequestTraceAttrs(server, req) } // 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. func (s HTTPServer) ResponseTraceAttrs(resp ResponseTelemetry) []attribute.KeyValue { + attrs := CurrentHTTPServer{}.ResponseTraceAttrs(resp) if s.duplicate { - return append(OldHTTPServer{}.ResponseTraceAttrs(resp), CurrentHTTPServer{}.ResponseTraceAttrs(resp)...) + return OldHTTPServer{}.ResponseTraceAttrs(resp, attrs) } - return OldHTTPServer{}.ResponseTraceAttrs(resp) + return attrs } // Route returns the attribute for the route. func (s HTTPServer) Route(route string) attribute.KeyValue { - return OldHTTPServer{}.Route(route) + return CurrentHTTPServer{}.Route(route) } // Status returns a span status code and message for an HTTP status code @@ -100,41 +127,86 @@ type MetricAttributes struct { type MetricData struct { RequestSize int64 + + // The request duration, in milliseconds ElapsedTime float64 } -var metricAddOptionPool = &sync.Pool{ - New: func() interface{} { - return &[]metric.AddOption{} - }, -} +var ( + metricAddOptionPool = &sync.Pool{ + New: func() interface{} { + return &[]metric.AddOption{} + }, + } -func (s HTTPServer) RecordMetrics(ctx context.Context, md ServerMetricData) { - if s.requestBytesCounter == nil || s.responseBytesCounter == nil || s.serverLatencyMeasure == nil { - // This will happen if an HTTPServer{} is used instead of NewHTTPServer. - return + metricRecordOptionPool = &sync.Pool{ + New: func() interface{} { + return &[]metric.RecordOption{} + }, } +) - attributes := OldHTTPServer{}.MetricAttributes(md.ServerName, md.Req, md.StatusCode, md.AdditionalAttributes) +func (s HTTPServer) RecordMetrics(ctx context.Context, md ServerMetricData) { + attributes := CurrentHTTPServer{}.MetricAttributes(md.ServerName, md.Req, md.StatusCode, md.AdditionalAttributes) o := metric.WithAttributeSet(attribute.NewSet(attributes...)) - addOpts := metricAddOptionPool.Get().(*[]metric.AddOption) - *addOpts = append(*addOpts, o) - s.requestBytesCounter.Add(ctx, md.RequestSize, *addOpts...) - s.responseBytesCounter.Add(ctx, md.ResponseSize, *addOpts...) - s.serverLatencyMeasure.Record(ctx, md.ElapsedTime, o) - *addOpts = (*addOpts)[:0] - metricAddOptionPool.Put(addOpts) + recordOpts := metricRecordOptionPool.Get().(*[]metric.RecordOption) + *recordOpts = append(*recordOpts, o) + s.requestBodySizeHistogram.Inst().Record(ctx, md.RequestSize, *recordOpts...) + s.responseBodySizeHistogram.Inst().Record(ctx, md.ResponseSize, *recordOpts...) + s.requestDurationHistogram.Inst().Record(ctx, md.ElapsedTime/1000.0, o) + *recordOpts = (*recordOpts)[:0] + metricRecordOptionPool.Put(recordOpts) + + if s.duplicate && s.requestBytesCounter != nil && s.responseBytesCounter != nil && s.serverLatencyMeasure != nil { + attributes := OldHTTPServer{}.MetricAttributes(md.ServerName, md.Req, md.StatusCode, md.AdditionalAttributes) + o := metric.WithAttributeSet(attribute.NewSet(attributes...)) + addOpts := metricAddOptionPool.Get().(*[]metric.AddOption) + *addOpts = append(*addOpts, o) + s.requestBytesCounter.Add(ctx, md.RequestSize, *addOpts...) + s.responseBytesCounter.Add(ctx, md.ResponseSize, *addOpts...) + s.serverLatencyMeasure.Record(ctx, md.ElapsedTime, o) + *addOpts = (*addOpts)[:0] + metricAddOptionPool.Put(addOpts) + } +} - // TODO: Duplicate Metrics +// hasOptIn returns true if the comma-separated version string contains the +// exact optIn value. +func hasOptIn(version, optIn string) bool { + for _, v := range strings.Split(version, ",") { + if strings.TrimSpace(v) == optIn { + return true + } + } + return false } func NewHTTPServer(meter metric.Meter) HTTPServer { - env := strings.ToLower(os.Getenv("OTEL_SEMCONV_STABILITY_OPT_IN")) - duplicate := env == "http/dup" + env := strings.ToLower(os.Getenv(OTelSemConvStabilityOptIn)) + duplicate := hasOptIn(env, "http/dup") server := HTTPServer{ duplicate: duplicate, } - server.requestBytesCounter, server.responseBytesCounter, server.serverLatencyMeasure = OldHTTPServer{}.createMeasures(meter) + + var err error + server.requestBodySizeHistogram, err = httpconv.NewServerRequestBodySize(meter) + handleErr(err) + + server.responseBodySizeHistogram, err = httpconv.NewServerResponseBodySize(meter) + handleErr(err) + + server.requestDurationHistogram, err = httpconv.NewServerRequestDuration( + meter, + metric.WithExplicitBucketBoundaries( + 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, + 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, + ), + ) + handleErr(err) + + if duplicate { + server.requestBytesCounter, server.responseBytesCounter, server.serverLatencyMeasure = OldHTTPServer{}.createMeasures(meter) + } return server } @@ -145,32 +217,52 @@ type HTTPClient struct { requestBytesCounter metric.Int64Counter responseBytesCounter metric.Int64Counter latencyMeasure metric.Float64Histogram + + // new metrics + requestBodySize httpconv.ClientRequestBodySize + requestDuration httpconv.ClientRequestDuration } func NewHTTPClient(meter metric.Meter) HTTPClient { - env := strings.ToLower(os.Getenv("OTEL_SEMCONV_STABILITY_OPT_IN")) + env := strings.ToLower(os.Getenv(OTelSemConvStabilityOptIn)) + duplicate := hasOptIn(env, "http/dup") client := HTTPClient{ - duplicate: env == "http/dup", + duplicate: duplicate, + } + + var err error + client.requestBodySize, err = httpconv.NewClientRequestBodySize(meter) + handleErr(err) + + client.requestDuration, err = httpconv.NewClientRequestDuration( + meter, + metric.WithExplicitBucketBoundaries(0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10), + ) + handleErr(err) + + if duplicate { + client.requestBytesCounter, client.responseBytesCounter, client.latencyMeasure = OldHTTPClient{}.createMeasures(meter) } - client.requestBytesCounter, client.responseBytesCounter, client.latencyMeasure = OldHTTPClient{}.createMeasures(meter) + return client } // RequestTraceAttrs returns attributes for an HTTP request made by a client. func (c HTTPClient) RequestTraceAttrs(req *http.Request) []attribute.KeyValue { + attrs := CurrentHTTPClient{}.RequestTraceAttrs(req) if c.duplicate { - return append(OldHTTPClient{}.RequestTraceAttrs(req), CurrentHTTPClient{}.RequestTraceAttrs(req)...) + return OldHTTPClient{}.RequestTraceAttrs(req, attrs) } - return OldHTTPClient{}.RequestTraceAttrs(req) + return attrs } // ResponseTraceAttrs returns metric attributes for an HTTP request made by a client. func (c HTTPClient) ResponseTraceAttrs(resp *http.Response) []attribute.KeyValue { + attrs := CurrentHTTPClient{}.ResponseTraceAttrs(resp) if c.duplicate { - return append(OldHTTPClient{}.ResponseTraceAttrs(resp), CurrentHTTPClient{}.ResponseTraceAttrs(resp)...) + return OldHTTPClient{}.ResponseTraceAttrs(resp, attrs) } - - return OldHTTPClient{}.ResponseTraceAttrs(resp) + return attrs } func (c HTTPClient) Status(code int) (codes.Code, string) { @@ -184,11 +276,7 @@ func (c HTTPClient) Status(code int) (codes.Code, string) { } func (c HTTPClient) ErrorType(err error) attribute.KeyValue { - if c.duplicate { - return CurrentHTTPClient{}.ErrorType(err) - } - - return attribute.KeyValue{} + return CurrentHTTPClient{}.ErrorType(err) } type MetricOpts struct { @@ -204,34 +292,52 @@ func (o MetricOpts) AddOptions() metric.AddOption { return o.addOptions } -func (c HTTPClient) MetricOptions(ma MetricAttributes) MetricOpts { - attributes := OldHTTPClient{}.MetricAttributes(ma.Req, ma.StatusCode, ma.AdditionalAttributes) - // TODO: Duplicate Metrics +func (c HTTPClient) MetricOptions(ma MetricAttributes) map[string]MetricOpts { + opts := map[string]MetricOpts{} + + attributes := CurrentHTTPClient{}.MetricAttributes(ma.Req, ma.StatusCode, ma.AdditionalAttributes) set := metric.WithAttributeSet(attribute.NewSet(attributes...)) - return MetricOpts{ + opts["new"] = MetricOpts{ measurement: set, addOptions: set, } -} -func (s HTTPClient) RecordMetrics(ctx context.Context, md MetricData, opts MetricOpts) { - if s.requestBytesCounter == nil || s.latencyMeasure == nil { - // This will happen if an HTTPClient{} is used instead of NewHTTPClient(). - return + if c.duplicate { + attributes := OldHTTPClient{}.MetricAttributes(ma.Req, ma.StatusCode, ma.AdditionalAttributes) + set := metric.WithAttributeSet(attribute.NewSet(attributes...)) + opts["old"] = MetricOpts{ + measurement: set, + addOptions: set, + } } - s.requestBytesCounter.Add(ctx, md.RequestSize, opts.AddOptions()) - s.latencyMeasure.Record(ctx, md.ElapsedTime, opts.MeasurementOption()) + return opts +} + +func (s HTTPClient) RecordMetrics(ctx context.Context, md MetricData, opts map[string]MetricOpts) { + s.requestBodySize.Inst().Record(ctx, md.RequestSize, opts["new"].MeasurementOption()) + s.requestDuration.Inst().Record(ctx, md.ElapsedTime/1000, opts["new"].MeasurementOption()) - // TODO: Duplicate Metrics + if s.duplicate { + s.requestBytesCounter.Add(ctx, md.RequestSize, opts["old"].AddOptions()) + s.latencyMeasure.Record(ctx, md.ElapsedTime, opts["old"].MeasurementOption()) + } } -func (s HTTPClient) RecordResponseSize(ctx context.Context, responseData int64, opts metric.AddOption) { +func (s HTTPClient) RecordResponseSize(ctx context.Context, responseData int64, opts map[string]MetricOpts) { if s.responseBytesCounter == nil { // This will happen if an HTTPClient{} is used instead of NewHTTPClient(). return } - s.responseBytesCounter.Add(ctx, responseData, opts) - // TODO: Duplicate Metrics + s.responseBytesCounter.Add(ctx, responseData, opts["old"].AddOptions()) +} + +func (s HTTPClient) TraceAttributes(host string) []attribute.KeyValue { + attrs := CurrentHTTPClient{}.TraceAttributes(host) + if s.duplicate { + return OldHTTPClient{}.TraceAttributes(host, attrs) + } + + return attrs } diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/gen.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/gen.go new file mode 100644 index 0000000000..b4036dd906 --- /dev/null +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/gen.go @@ -0,0 +1,16 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package semconv // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv" + +// Generate semconv package: +//go:generate gotmpl --body=../../../../../../internal/shared/semconv/bench_test.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\" }" --out=bench_test.go +//go:generate gotmpl --body=../../../../../../internal/shared/semconv/common_test.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\" }" --out=common_test.go +//go:generate gotmpl --body=../../../../../../internal/shared/semconv/env.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\" }" --out=env.go +//go:generate gotmpl --body=../../../../../../internal/shared/semconv/env_test.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\" }" --out=env_test.go +//go:generate gotmpl --body=../../../../../../internal/shared/semconv/httpconv.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\" }" --out=httpconv.go +//go:generate gotmpl --body=../../../../../../internal/shared/semconv/httpconv_test.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\" }" --out=httpconv_test.go +//go:generate gotmpl --body=../../../../../../internal/shared/semconv/httpconvtest_test.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\" }" --out=httpconvtest_test.go +//go:generate gotmpl --body=../../../../../../internal/shared/semconv/util.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\" }" --out=util.go +//go:generate gotmpl --body=../../../../../../internal/shared/semconv/util_test.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\" }" --out=util_test.go +//go:generate gotmpl --body=../../../../../../internal/shared/semconv/v1.20.0.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\" }" --out=v1.20.0.go diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/httpconv.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/httpconv.go index dc9ec7bc39..9766f3ac85 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/httpconv.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/httpconv.go @@ -1,22 +1,33 @@ +// Code generated by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconv/httpconv.go.tmpl + // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +// Package semconv provides OpenTelemetry semantic convention types and +// functionality. package semconv // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv" import ( "fmt" "net/http" "reflect" + "slices" "strconv" "strings" "go.opentelemetry.io/otel/attribute" - semconvNew "go.opentelemetry.io/otel/semconv/v1.26.0" + semconvNew "go.opentelemetry.io/otel/semconv/v1.34.0" ) +type RequestTraceAttrsOpts struct { + // If set, this is used as value for the "http.client_ip" attribute. + HTTPClientIP string +} + type CurrentHTTPServer struct{} -// TraceRequest returns trace attributes for an HTTP request received by a +// 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 @@ -32,7 +43,7 @@ type CurrentHTTPServer struct{} // // 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 CurrentHTTPServer) RequestTraceAttrs(server string, req *http.Request) []attribute.KeyValue { +func (n CurrentHTTPServer) RequestTraceAttrs(server string, req *http.Request, opts RequestTraceAttrsOpts) []attribute.KeyValue { count := 3 // ServerAddress, Method, Scheme var host string @@ -59,7 +70,8 @@ func (n CurrentHTTPServer) RequestTraceAttrs(server string, req *http.Request) [ scheme := n.scheme(req.TLS != nil) - if peer, peerPort := SplitHostPort(req.RemoteAddr); peer != "" { + peer, peerPort := SplitHostPort(req.RemoteAddr) + if 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. count++ @@ -73,7 +85,17 @@ func (n CurrentHTTPServer) RequestTraceAttrs(server string, req *http.Request) [ count++ } - clientIP := serverClientIP(req.Header.Get("X-Forwarded-For")) + // For client IP, use, in order: + // 1. The value passed in the options + // 2. The value in the X-Forwarded-For header + // 3. The peer address + clientIP := opts.HTTPClientIP + if clientIP == "" { + clientIP = serverClientIP(req.Header.Get("X-Forwarded-For")) + if clientIP == "" { + clientIP = peer + } + } if clientIP != "" { count++ } @@ -90,6 +112,11 @@ func (n CurrentHTTPServer) RequestTraceAttrs(server string, req *http.Request) [ count++ } + route := httpRoute(req.Pattern) + if route != "" { + count++ + } + attrs := make([]attribute.KeyValue, 0, count) attrs = append(attrs, semconvNew.ServerAddress(host), @@ -113,7 +140,7 @@ func (n CurrentHTTPServer) RequestTraceAttrs(server string, req *http.Request) [ } } - if useragent := req.UserAgent(); useragent != "" { + if useragent != "" { attrs = append(attrs, semconvNew.UserAgentOriginal(useragent)) } @@ -132,9 +159,26 @@ func (n CurrentHTTPServer) RequestTraceAttrs(server string, req *http.Request) [ attrs = append(attrs, semconvNew.NetworkProtocolVersion(protoVersion)) } + if route != "" { + attrs = append(attrs, n.Route(route)) + } + return attrs } +func (n CurrentHTTPServer) NetworkTransportAttr(network string) attribute.KeyValue { + switch network { + case "tcp", "tcp4", "tcp6": + return semconvNew.NetworkTransportTCP + case "udp", "udp4", "udp6": + return semconvNew.NetworkTransportUDP + case "unix", "unixgram", "unixpacket": + return semconvNew.NetworkTransportUnix + default: + return semconvNew.NetworkTransportPipe + } +} + func (n CurrentHTTPServer) method(method string) (attribute.KeyValue, attribute.KeyValue) { if method == "" { return semconvNew.HTTPRequestMethodGet, attribute.KeyValue{} @@ -157,9 +201,11 @@ func (n CurrentHTTPServer) scheme(https bool) attribute.KeyValue { // nolint:rev return semconvNew.URLScheme("http") } -// TraceResponse returns trace attributes for telemetry from an HTTP response. +// 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. +// If any of the fields in the ResponseTelemetry are not set the attribute will +// be omitted. func (n CurrentHTTPServer) ResponseTraceAttrs(resp ResponseTelemetry) []attribute.KeyValue { var count int @@ -199,6 +245,57 @@ func (n CurrentHTTPServer) Route(route string) attribute.KeyValue { return semconvNew.HTTPRoute(route) } +func (n CurrentHTTPServer) MetricAttributes(server string, req *http.Request, statusCode int, additionalAttributes []attribute.KeyValue) []attribute.KeyValue { + num := len(additionalAttributes) + 3 + 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 { + _, p = SplitHostPort(req.Host) + } + } + hostPort := requiredHTTPPort(req.TLS != nil, p) + if hostPort > 0 { + num++ + } + protoName, protoVersion := netProtocol(req.Proto) + if protoName != "" { + num++ + } + if protoVersion != "" { + num++ + } + + if statusCode > 0 { + num++ + } + + attributes := slices.Grow(additionalAttributes, num) + attributes = append(attributes, + semconvNew.HTTPRequestMethodKey.String(standardizeHTTPMethod(req.Method)), + n.scheme(req.TLS != nil), + semconvNew.ServerAddress(host)) + + if hostPort > 0 { + attributes = append(attributes, semconvNew.ServerPort(hostPort)) + } + if protoName != "" { + attributes = append(attributes, semconvNew.NetworkProtocolName(protoName)) + } + if protoVersion != "" { + attributes = append(attributes, semconvNew.NetworkProtocolVersion(protoVersion)) + } + + if statusCode > 0 { + attributes = append(attributes, semconvNew.HTTPResponseStatusCode(statusCode)) + } + return attributes +} + type CurrentHTTPClient struct{} // RequestTraceAttrs returns trace attributes for an HTTP request made by a client. @@ -343,6 +440,78 @@ func (n CurrentHTTPClient) method(method string) (attribute.KeyValue, attribute. return semconvNew.HTTPRequestMethodGet, orig } +func (n CurrentHTTPClient) MetricAttributes(req *http.Request, statusCode int, additionalAttributes []attribute.KeyValue) []attribute.KeyValue { + num := len(additionalAttributes) + 2 + var h string + if req.URL != nil { + h = req.URL.Host + } + var requestHost string + var requestPort int + for _, hostport := range []string{h, req.Header.Get("Host")} { + requestHost, requestPort = SplitHostPort(hostport) + if requestHost != "" || requestPort > 0 { + break + } + } + + port := requiredHTTPPort(req.URL != nil && req.URL.Scheme == "https", requestPort) + if port > 0 { + num++ + } + + protoName, protoVersion := netProtocol(req.Proto) + if protoName != "" { + num++ + } + if protoVersion != "" { + num++ + } + + if statusCode > 0 { + num++ + } + + attributes := slices.Grow(additionalAttributes, num) + attributes = append(attributes, + semconvNew.HTTPRequestMethodKey.String(standardizeHTTPMethod(req.Method)), + semconvNew.ServerAddress(requestHost), + n.scheme(req), + ) + + if port > 0 { + attributes = append(attributes, semconvNew.ServerPort(port)) + } + if protoName != "" { + attributes = append(attributes, semconvNew.NetworkProtocolName(protoName)) + } + if protoVersion != "" { + attributes = append(attributes, semconvNew.NetworkProtocolVersion(protoVersion)) + } + + if statusCode > 0 { + attributes = append(attributes, semconvNew.HTTPResponseStatusCode(statusCode)) + } + return attributes +} + +// TraceAttributes returns attributes for httptrace. +func (n CurrentHTTPClient) TraceAttributes(host string) []attribute.KeyValue { + return []attribute.KeyValue{ + semconvNew.ServerAddress(host), + } +} + +func (n CurrentHTTPClient) scheme(req *http.Request) attribute.KeyValue { + if req.URL != nil && req.URL.Scheme != "" { + return semconvNew.URLScheme(req.URL.Scheme) + } + if req.TLS != nil { + return semconvNew.URLScheme("https") + } + return semconvNew.URLScheme("http") +} + func isErrorStatusCode(code int) bool { return code >= 400 || code < 100 } diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/util.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/util.go index 93e8d0f94c..e4ebf85814 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/util.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/util.go @@ -1,3 +1,6 @@ +// Code generated by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconv/util.go.tmpl + // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 @@ -11,7 +14,7 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" - semconvNew "go.opentelemetry.io/otel/semconv/v1.26.0" + semconvNew "go.opentelemetry.io/otel/semconv/v1.34.0" ) // SplitHostPort splits a network address hostport of the form "host", @@ -25,17 +28,17 @@ func SplitHostPort(hostport string) (host string, port int) { port = -1 if strings.HasPrefix(hostport, "[") { - addrEnd := strings.LastIndex(hostport, "]") + addrEnd := strings.LastIndexByte(hostport, ']') if addrEnd < 0 { // Invalid hostport. return } - if i := strings.LastIndex(hostport[addrEnd:], ":"); i < 0 { + if i := strings.LastIndexByte(hostport[addrEnd:], ':'); i < 0 { host = hostport[1:addrEnd] return } } else { - if i := strings.LastIndex(hostport, ":"); i < 0 { + if i := strings.LastIndexByte(hostport, ':'); i < 0 { host = hostport return } @@ -67,15 +70,31 @@ func requiredHTTPPort(https bool, port int) int { // nolint:revive } func serverClientIP(xForwardedFor string) string { - if idx := strings.Index(xForwardedFor, ","); idx >= 0 { + if idx := strings.IndexByte(xForwardedFor, ','); idx >= 0 { xForwardedFor = xForwardedFor[:idx] } return xForwardedFor } +func httpRoute(pattern string) string { + if idx := strings.IndexByte(pattern, '/'); idx >= 0 { + return pattern[idx:] + } + return "" +} + func netProtocol(proto string) (name string, version string) { name, version, _ = strings.Cut(proto, "/") - name = strings.ToLower(name) + switch name { + case "HTTP": + name = "http" + case "QUIC": + name = "quic" + case "SPDY": + name = "spdy" + default: + name = strings.ToLower(name) + } return name, version } @@ -96,3 +115,13 @@ func handleErr(err error) { otel.Handle(err) } } + +func standardizeHTTPMethod(method string) string { + method = strings.ToUpper(method) + switch method { + case http.MethodConnect, http.MethodDelete, http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodPatch, http.MethodPost, http.MethodPut, http.MethodTrace: + default: + method = "_OTHER" + } + return method +} diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/v1.20.0.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/v1.20.0.go index c042249dd7..ba7fccf1ef 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/v1.20.0.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/v1.20.0.go @@ -1,3 +1,6 @@ +// Code generated by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconv/v120.0.go.tmpl + // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 @@ -8,7 +11,6 @@ import ( "io" "net/http" "slices" - "strings" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconvutil" "go.opentelemetry.io/otel/attribute" @@ -35,16 +37,18 @@ type OldHTTPServer struct{} // // 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 (o OldHTTPServer) RequestTraceAttrs(server string, req *http.Request) []attribute.KeyValue { - return semconvutil.HTTPServerRequest(server, req) +func (o OldHTTPServer) RequestTraceAttrs(server string, req *http.Request, attrs []attribute.KeyValue) []attribute.KeyValue { + return semconvutil.HTTPServerRequest(server, req, semconvutil.HTTPServerRequestOptions{}, attrs) +} + +func (o OldHTTPServer) NetworkTransportAttr(network string) attribute.KeyValue { + return semconvutil.NetTransport(network) } // 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. -func (o OldHTTPServer) ResponseTraceAttrs(resp ResponseTelemetry) []attribute.KeyValue { - attributes := []attribute.KeyValue{} - +func (o OldHTTPServer) ResponseTraceAttrs(resp ResponseTelemetry, attributes []attribute.KeyValue) []attribute.KeyValue { if resp.ReadBytes > 0 { attributes = append(attributes, semconv.HTTPRequestContentLength(int(resp.ReadBytes))) } @@ -144,7 +148,7 @@ func (o OldHTTPServer) MetricAttributes(server string, req *http.Request, status attributes := slices.Grow(additionalAttributes, n) attributes = append(attributes, - standardizeHTTPMethodMetric(req.Method), + semconv.HTTPMethod(standardizeHTTPMethod(req.Method)), o.scheme(req.TLS != nil), semconv.NetHostName(host)) @@ -173,12 +177,12 @@ func (o OldHTTPServer) scheme(https bool) attribute.KeyValue { // nolint:revive type OldHTTPClient struct{} -func (o OldHTTPClient) RequestTraceAttrs(req *http.Request) []attribute.KeyValue { - return semconvutil.HTTPClientRequest(req) +func (o OldHTTPClient) RequestTraceAttrs(req *http.Request, attrs []attribute.KeyValue) []attribute.KeyValue { + return semconvutil.HTTPClientRequest(req, attrs) } -func (o OldHTTPClient) ResponseTraceAttrs(resp *http.Response) []attribute.KeyValue { - return semconvutil.HTTPClientResponse(resp) +func (o OldHTTPClient) ResponseTraceAttrs(resp *http.Response, attrs []attribute.KeyValue) []attribute.KeyValue { + return semconvutil.HTTPClientResponse(resp, attrs) } func (o OldHTTPClient) MetricAttributes(req *http.Request, statusCode int, additionalAttributes []attribute.KeyValue) []attribute.KeyValue { @@ -214,7 +218,7 @@ func (o OldHTTPClient) MetricAttributes(req *http.Request, statusCode int, addit attributes := slices.Grow(additionalAttributes, n) attributes = append(attributes, - standardizeHTTPMethodMetric(req.Method), + semconv.HTTPMethod(standardizeHTTPMethod(req.Method)), semconv.NetPeerName(requestHost), ) @@ -263,12 +267,7 @@ func (o OldHTTPClient) createMeasures(meter metric.Meter) (metric.Int64Counter, return requestBytesCounter, responseBytesCounter, latencyMeasure } -func standardizeHTTPMethodMetric(method string) attribute.KeyValue { - method = strings.ToUpper(method) - switch method { - case http.MethodConnect, http.MethodDelete, http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodPatch, http.MethodPost, http.MethodPut, http.MethodTrace: - default: - method = "_OTHER" - } - return semconv.HTTPMethod(method) +// TraceAttributes returns attributes for httptrace. +func (c OldHTTPClient) TraceAttributes(host string, attrs []attribute.KeyValue) []attribute.KeyValue { + return append(attrs, semconv.NetHostName(host)) } diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconvutil/httpconv.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconvutil/httpconv.go index a73bb06e90..b997354793 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconvutil/httpconv.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconvutil/httpconv.go @@ -1,14 +1,16 @@ -// Code created by gotmpl. DO NOT MODIFY. +// Code generated by gotmpl. DO NOT MODIFY. // source: internal/shared/semconvutil/httpconv.go.tmpl // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +// Package semconvutil provides OpenTelemetry semantic convention utilities. package semconvutil // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconvutil" import ( "fmt" "net/http" + "slices" "strings" "go.opentelemetry.io/otel/attribute" @@ -16,6 +18,11 @@ import ( semconv "go.opentelemetry.io/otel/semconv/v1.20.0" ) +type HTTPServerRequestOptions struct { + // If set, this is used as value for the "http.client_ip" attribute. + HTTPClientIP string +} + // HTTPClientResponse returns trace attributes for an HTTP response received by a // client from a server. It will return the following attributes if the related // values are defined in resp: "http.status.code", @@ -26,9 +33,9 @@ import ( // attributes. If a complete set of attributes can be generated using the // request contained in resp. For example: // -// append(HTTPClientResponse(resp), ClientRequest(resp.Request)...) -func HTTPClientResponse(resp *http.Response) []attribute.KeyValue { - return hc.ClientResponse(resp) +// HTTPClientResponse(resp, ClientRequest(resp.Request))) +func HTTPClientResponse(resp *http.Response, attrs []attribute.KeyValue) []attribute.KeyValue { + return hc.ClientResponse(resp, attrs) } // HTTPClientRequest returns trace attributes for an HTTP request made by a client. @@ -36,8 +43,8 @@ func HTTPClientResponse(resp *http.Response) []attribute.KeyValue { // "net.peer.name". The following attributes are returned if the related values // are defined in req: "net.peer.port", "user_agent.original", // "http.request_content_length". -func HTTPClientRequest(req *http.Request) []attribute.KeyValue { - return hc.ClientRequest(req) +func HTTPClientRequest(req *http.Request, attrs []attribute.KeyValue) []attribute.KeyValue { + return hc.ClientRequest(req, attrs) } // HTTPClientRequestMetrics returns metric attributes for an HTTP request made by a client. @@ -75,8 +82,8 @@ func HTTPClientStatus(code int) (codes.Code, string) { // "http.target", "net.host.name". The following attributes are returned if // they related values are defined in req: "net.host.port", "net.sock.peer.addr", // "net.sock.peer.port", "user_agent.original", "http.client_ip". -func HTTPServerRequest(server string, req *http.Request) []attribute.KeyValue { - return hc.ServerRequest(server, req) +func HTTPServerRequest(server string, req *http.Request, opts HTTPServerRequestOptions, attrs []attribute.KeyValue) []attribute.KeyValue { + return hc.ServerRequest(server, req, opts, attrs) } // HTTPServerRequestMetrics returns metric attributes for an HTTP request received by a @@ -153,8 +160,8 @@ var hc = &httpConv{ // attributes. If a complete set of attributes can be generated using the // request contained in resp. For example: // -// append(ClientResponse(resp), ClientRequest(resp.Request)...) -func (c *httpConv) ClientResponse(resp *http.Response) []attribute.KeyValue { +// ClientResponse(resp, ClientRequest(resp.Request)) +func (c *httpConv) ClientResponse(resp *http.Response, attrs []attribute.KeyValue) []attribute.KeyValue { /* The following semantic conventions are returned if present: http.status_code int http.response_content_length int @@ -166,8 +173,11 @@ func (c *httpConv) ClientResponse(resp *http.Response) []attribute.KeyValue { if resp.ContentLength > 0 { n++ } + if n == 0 { + return attrs + } - attrs := make([]attribute.KeyValue, 0, n) + attrs = slices.Grow(attrs, n) if resp.StatusCode > 0 { attrs = append(attrs, c.HTTPStatusCodeKey.Int(resp.StatusCode)) } @@ -182,7 +192,7 @@ func (c *httpConv) ClientResponse(resp *http.Response) []attribute.KeyValue { // "net.peer.name". The following attributes are returned if the related values // are defined in req: "net.peer.port", "user_agent.original", // "http.request_content_length", "user_agent.original". -func (c *httpConv) ClientRequest(req *http.Request) []attribute.KeyValue { +func (c *httpConv) ClientRequest(req *http.Request, attrs []attribute.KeyValue) []attribute.KeyValue { /* The following semantic conventions are returned if present: http.method string user_agent.original string @@ -221,8 +231,7 @@ func (c *httpConv) ClientRequest(req *http.Request) []attribute.KeyValue { n++ } - attrs := make([]attribute.KeyValue, 0, n) - + attrs = slices.Grow(attrs, n) attrs = append(attrs, c.method(req.Method)) var u string @@ -305,7 +314,7 @@ func (c *httpConv) ClientRequestMetrics(req *http.Request) []attribute.KeyValue // related values are defined in req: "net.host.port", "net.sock.peer.addr", // "net.sock.peer.port", "user_agent.original", "http.client_ip", // "net.protocol.name", "net.protocol.version". -func (c *httpConv) ServerRequest(server string, req *http.Request) []attribute.KeyValue { +func (c *httpConv) ServerRequest(server string, req *http.Request, opts HTTPServerRequestOptions, attrs []attribute.KeyValue) []attribute.KeyValue { /* The following semantic conventions are returned if present: http.method string http.scheme string @@ -358,7 +367,17 @@ func (c *httpConv) ServerRequest(server string, req *http.Request) []attribute.K n++ } - clientIP := serverClientIP(req.Header.Get("X-Forwarded-For")) + // For client IP, use, in order: + // 1. The value passed in the options + // 2. The value in the X-Forwarded-For header + // 3. The peer address + clientIP := opts.HTTPClientIP + if clientIP == "" { + clientIP = serverClientIP(req.Header.Get("X-Forwarded-For")) + if clientIP == "" { + clientIP = peer + } + } if clientIP != "" { n++ } @@ -378,7 +397,7 @@ func (c *httpConv) ServerRequest(server string, req *http.Request) []attribute.K n++ } - attrs := make([]attribute.KeyValue, 0, n) + attrs = slices.Grow(attrs, n) attrs = append(attrs, c.method(req.Method)) attrs = append(attrs, c.scheme(req.TLS != nil)) diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconvutil/netconv.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconvutil/netconv.go index b80a1db61f..df97255e41 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconvutil/netconv.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconvutil/netconv.go @@ -1,4 +1,4 @@ -// Code created by gotmpl. DO NOT MODIFY. +// Code generated by gotmpl. DO NOT MODIFY. // source: internal/shared/semconvutil/netconv.go.tmpl // Copyright The OpenTelemetry Authors @@ -200,6 +200,15 @@ func splitHostPort(hostport string) (host string, port int) { func netProtocol(proto string) (name string, version string) { name, version, _ = strings.Cut(proto, "/") - name = strings.ToLower(name) + switch name { + case "HTTP": + name = "http" + case "QUIC": + name = "quic" + case "SPDY": + name = "spdy" + default: + name = strings.ToLower(name) + } return name, version } diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/labeler.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/labeler.go index ea504e396f..d62ce44b00 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/labeler.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/labeler.go @@ -35,14 +35,14 @@ func (l *Labeler) Get() []attribute.KeyValue { type labelerContextKeyType int -const lablelerContextKey labelerContextKeyType = 0 +const labelerContextKey labelerContextKeyType = 0 // ContextWithLabeler returns a new context with the provided Labeler instance. // Attributes added to the specified labeler will be injected into metrics // emitted by the instrumentation. Only one labeller can be injected into the // context. Injecting it multiple times will override the previous calls. func ContextWithLabeler(parent context.Context, l *Labeler) context.Context { - return context.WithValue(parent, lablelerContextKey, l) + return context.WithValue(parent, labelerContextKey, l) } // LabelerFromContext retrieves a Labeler instance from the provided context if @@ -50,7 +50,7 @@ func ContextWithLabeler(parent context.Context, l *Labeler) context.Context { // Labeler is returned and the second return value is false. In this case it is // safe to use the Labeler but any attributes added to it will not be used. func LabelerFromContext(ctx context.Context) (*Labeler, bool) { - l, ok := ctx.Value(lablelerContextKey).(*Labeler) + l, ok := ctx.Value(labelerContextKey).(*Labeler) if !ok { l = &Labeler{} } diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/transport.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/transport.go index 39681ad4b0..e4c02a4296 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/transport.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/transport.go @@ -129,6 +129,41 @@ func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) { t.propagators.Inject(ctx, propagation.HeaderCarrier(r.Header)) res, err := t.rt.RoundTrip(r) + + // Defer metrics recording function to record the metrics on error or no error. + defer func() { + metricAttributes := semconv.MetricAttributes{ + Req: r, + AdditionalAttributes: append(labeler.Get(), t.metricAttributesFromRequest(r)...), + } + + if err == nil { + metricAttributes.StatusCode = res.StatusCode + } + + metricOpts := t.semconv.MetricOptions(metricAttributes) + + metricData := semconv.MetricData{ + RequestSize: bw.BytesRead(), + } + + if err == nil { + // For handling response bytes we leverage a callback when the client reads the http response + readRecordFunc := func(n int64) { + t.semconv.RecordResponseSize(ctx, n, metricOpts) + } + + res.Body = newWrappedBody(span, readRecordFunc, res.Body) + } + + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedTime := float64(time.Since(requestStartTime)) / float64(time.Millisecond) + + metricData.ElapsedTime = elapsedTime + + t.semconv.RecordMetrics(ctx, metricData, metricOpts) + }() + if err != nil { // set error type attribute if the error is part of the predefined // error types. @@ -141,35 +176,14 @@ func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) { span.SetStatus(codes.Error, err.Error()) span.End() - return res, err - } - // metrics - metricOpts := t.semconv.MetricOptions(semconv.MetricAttributes{ - Req: r, - StatusCode: res.StatusCode, - AdditionalAttributes: append(labeler.Get(), t.metricAttributesFromRequest(r)...), - }) - - // For handling response bytes we leverage a callback when the client reads the http response - readRecordFunc := func(n int64) { - t.semconv.RecordResponseSize(ctx, n, metricOpts.AddOptions()) + return res, err } // traces span.SetAttributes(t.semconv.ResponseTraceAttrs(res)...) span.SetStatus(t.semconv.Status(res.StatusCode)) - res.Body = newWrappedBody(span, readRecordFunc, res.Body) - - // Use floating point division here for higher precision (instead of Millisecond method). - elapsedTime := float64(time.Since(requestStartTime)) / float64(time.Millisecond) - - t.semconv.RecordMetrics(ctx, semconv.MetricData{ - RequestSize: bw.BytesRead(), - ElapsedTime: elapsedTime, - }, metricOpts) - return res, nil } diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/version.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/version.go index 353e43b91f..2fe5a13618 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/version.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/version.go @@ -5,13 +5,6 @@ package otelhttp // import "go.opentelemetry.io/contrib/instrumentation/net/http // Version is the current release version of the otelhttp instrumentation. func Version() string { - return "0.58.0" + return "0.62.0" // This string is updated by the pre_release.sh script during release } - -// SemVersion is the semantic version to be supplied to tracer/meter creation. -// -// Deprecated: Use [Version] instead. -func SemVersion() string { - return Version() -} diff --git a/vendor/go.opentelemetry.io/otel/semconv/v1.34.0/httpconv/metric.go b/vendor/go.opentelemetry.io/otel/semconv/v1.34.0/httpconv/metric.go new file mode 100644 index 0000000000..79843adbb5 --- /dev/null +++ b/vendor/go.opentelemetry.io/otel/semconv/v1.34.0/httpconv/metric.go @@ -0,0 +1,1418 @@ +// Code generated from semantic convention specification. DO NOT EDIT. + +// Package httpconv provides types and functionality for OpenTelemetry semantic +// conventions in the "http" namespace. +package httpconv + +import ( + "context" + "sync" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/metric/noop" +) + +var ( + addOptPool = &sync.Pool{New: func() any { return &[]metric.AddOption{} }} + recOptPool = &sync.Pool{New: func() any { return &[]metric.RecordOption{} }} +) + +// ErrorTypeAttr is an attribute conforming to the error.type semantic +// conventions. It represents the describes a class of error the operation ended +// with. +type ErrorTypeAttr string + +var ( + // ErrorTypeOther is a fallback error value to be used when the instrumentation + // doesn't define a custom value. + ErrorTypeOther ErrorTypeAttr = "_OTHER" +) + +// ConnectionStateAttr is an attribute conforming to the http.connection.state +// semantic conventions. It represents the state of the HTTP connection in the +// HTTP connection pool. +type ConnectionStateAttr string + +var ( + // ConnectionStateActive is the active state. + ConnectionStateActive ConnectionStateAttr = "active" + // ConnectionStateIdle is the idle state. + ConnectionStateIdle ConnectionStateAttr = "idle" +) + +// RequestMethodAttr is an attribute conforming to the http.request.method +// semantic conventions. It represents the HTTP request method. +type RequestMethodAttr string + +var ( + // RequestMethodConnect is the CONNECT method. + RequestMethodConnect RequestMethodAttr = "CONNECT" + // RequestMethodDelete is the DELETE method. + RequestMethodDelete RequestMethodAttr = "DELETE" + // RequestMethodGet is the GET method. + RequestMethodGet RequestMethodAttr = "GET" + // RequestMethodHead is the HEAD method. + RequestMethodHead RequestMethodAttr = "HEAD" + // RequestMethodOptions is the OPTIONS method. + RequestMethodOptions RequestMethodAttr = "OPTIONS" + // RequestMethodPatch is the PATCH method. + RequestMethodPatch RequestMethodAttr = "PATCH" + // RequestMethodPost is the POST method. + RequestMethodPost RequestMethodAttr = "POST" + // RequestMethodPut is the PUT method. + RequestMethodPut RequestMethodAttr = "PUT" + // RequestMethodTrace is the TRACE method. + RequestMethodTrace RequestMethodAttr = "TRACE" + // RequestMethodOther is the any HTTP method that the instrumentation has no + // prior knowledge of. + RequestMethodOther RequestMethodAttr = "_OTHER" +) + +// UserAgentSyntheticTypeAttr is an attribute conforming to the +// user_agent.synthetic.type semantic conventions. It represents the specifies +// the category of synthetic traffic, such as tests or bots. +type UserAgentSyntheticTypeAttr string + +var ( + // UserAgentSyntheticTypeBot is the bot source. + UserAgentSyntheticTypeBot UserAgentSyntheticTypeAttr = "bot" + // UserAgentSyntheticTypeTest is the synthetic test source. + UserAgentSyntheticTypeTest UserAgentSyntheticTypeAttr = "test" +) + +// ClientActiveRequests is an instrument used to record metric values conforming +// to the "http.client.active_requests" semantic conventions. It represents the +// number of active HTTP requests. +type ClientActiveRequests struct { + metric.Int64UpDownCounter +} + +// NewClientActiveRequests returns a new ClientActiveRequests instrument. +func NewClientActiveRequests( + m metric.Meter, + opt ...metric.Int64UpDownCounterOption, +) (ClientActiveRequests, error) { + // Check if the meter is nil. + if m == nil { + return ClientActiveRequests{noop.Int64UpDownCounter{}}, nil + } + + i, err := m.Int64UpDownCounter( + "http.client.active_requests", + append([]metric.Int64UpDownCounterOption{ + metric.WithDescription("Number of active HTTP requests."), + metric.WithUnit("{request}"), + }, opt...)..., + ) + if err != nil { + return ClientActiveRequests{noop.Int64UpDownCounter{}}, err + } + return ClientActiveRequests{i}, nil +} + +// Inst returns the underlying metric instrument. +func (m ClientActiveRequests) Inst() metric.Int64UpDownCounter { + return m.Int64UpDownCounter +} + +// Name returns the semantic convention name of the instrument. +func (ClientActiveRequests) Name() string { + return "http.client.active_requests" +} + +// Unit returns the semantic convention unit of the instrument +func (ClientActiveRequests) Unit() string { + return "{request}" +} + +// Description returns the semantic convention description of the instrument +func (ClientActiveRequests) Description() string { + return "Number of active HTTP requests." +} + +// Add adds incr to the existing count. +// +// The serverAddress is the server domain name if available without reverse DNS +// lookup; otherwise, IP address or Unix domain socket name. +// +// The serverPort is the port identifier of the ["URI origin"] HTTP request is +// sent to. +// +// All additional attrs passed are included in the recorded value. +// +// ["URI origin"]: https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin +func (m ClientActiveRequests) Add( + ctx context.Context, + incr int64, + serverAddress string, + serverPort int, + attrs ...attribute.KeyValue, +) { + o := addOptPool.Get().(*[]metric.AddOption) + defer func() { + *o = (*o)[:0] + addOptPool.Put(o) + }() + + *o = append( + *o, + metric.WithAttributes( + append( + attrs, + attribute.String("server.address", serverAddress), + attribute.Int("server.port", serverPort), + )..., + ), + ) + + m.Int64UpDownCounter.Add(ctx, incr, *o...) +} + +// AttrURLTemplate returns an optional attribute for the "url.template" semantic +// convention. It represents the low-cardinality template of an +// [absolute path reference]. +// +// [absolute path reference]: https://www.rfc-editor.org/rfc/rfc3986#section-4.2 +func (ClientActiveRequests) AttrURLTemplate(val string) attribute.KeyValue { + return attribute.String("url.template", val) +} + +// AttrRequestMethod returns an optional attribute for the "http.request.method" +// semantic convention. It represents the HTTP request method. +func (ClientActiveRequests) AttrRequestMethod(val RequestMethodAttr) attribute.KeyValue { + return attribute.String("http.request.method", string(val)) +} + +// AttrURLScheme returns an optional attribute for the "url.scheme" semantic +// convention. It represents the [URI scheme] component identifying the used +// protocol. +// +// [URI scheme]: https://www.rfc-editor.org/rfc/rfc3986#section-3.1 +func (ClientActiveRequests) AttrURLScheme(val string) attribute.KeyValue { + return attribute.String("url.scheme", val) +} + +// ClientConnectionDuration is an instrument used to record metric values +// conforming to the "http.client.connection.duration" semantic conventions. It +// represents the duration of the successfully established outbound HTTP +// connections. +type ClientConnectionDuration struct { + metric.Float64Histogram +} + +// NewClientConnectionDuration returns a new ClientConnectionDuration instrument. +func NewClientConnectionDuration( + m metric.Meter, + opt ...metric.Float64HistogramOption, +) (ClientConnectionDuration, error) { + // Check if the meter is nil. + if m == nil { + return ClientConnectionDuration{noop.Float64Histogram{}}, nil + } + + i, err := m.Float64Histogram( + "http.client.connection.duration", + append([]metric.Float64HistogramOption{ + metric.WithDescription("The duration of the successfully established outbound HTTP connections."), + metric.WithUnit("s"), + }, opt...)..., + ) + if err != nil { + return ClientConnectionDuration{noop.Float64Histogram{}}, err + } + return ClientConnectionDuration{i}, nil +} + +// Inst returns the underlying metric instrument. +func (m ClientConnectionDuration) Inst() metric.Float64Histogram { + return m.Float64Histogram +} + +// Name returns the semantic convention name of the instrument. +func (ClientConnectionDuration) Name() string { + return "http.client.connection.duration" +} + +// Unit returns the semantic convention unit of the instrument +func (ClientConnectionDuration) Unit() string { + return "s" +} + +// Description returns the semantic convention description of the instrument +func (ClientConnectionDuration) Description() string { + return "The duration of the successfully established outbound HTTP connections." +} + +// Record records val to the current distribution. +// +// The serverAddress is the server domain name if available without reverse DNS +// lookup; otherwise, IP address or Unix domain socket name. +// +// The serverPort is the port identifier of the ["URI origin"] HTTP request is +// sent to. +// +// All additional attrs passed are included in the recorded value. +// +// ["URI origin"]: https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin +func (m ClientConnectionDuration) Record( + ctx context.Context, + val float64, + serverAddress string, + serverPort int, + attrs ...attribute.KeyValue, +) { + o := recOptPool.Get().(*[]metric.RecordOption) + defer func() { + *o = (*o)[:0] + recOptPool.Put(o) + }() + + *o = append( + *o, + metric.WithAttributes( + append( + attrs, + attribute.String("server.address", serverAddress), + attribute.Int("server.port", serverPort), + )..., + ), + ) + + m.Float64Histogram.Record(ctx, val, *o...) +} + +// AttrNetworkPeerAddress returns an optional attribute for the +// "network.peer.address" semantic convention. It represents the peer address of +// the network connection - IP address or Unix domain socket name. +func (ClientConnectionDuration) AttrNetworkPeerAddress(val string) attribute.KeyValue { + return attribute.String("network.peer.address", val) +} + +// AttrNetworkProtocolVersion returns an optional attribute for the +// "network.protocol.version" semantic convention. It represents the actual +// version of the protocol used for network communication. +func (ClientConnectionDuration) AttrNetworkProtocolVersion(val string) attribute.KeyValue { + return attribute.String("network.protocol.version", val) +} + +// AttrURLScheme returns an optional attribute for the "url.scheme" semantic +// convention. It represents the [URI scheme] component identifying the used +// protocol. +// +// [URI scheme]: https://www.rfc-editor.org/rfc/rfc3986#section-3.1 +func (ClientConnectionDuration) AttrURLScheme(val string) attribute.KeyValue { + return attribute.String("url.scheme", val) +} + +// ClientOpenConnections is an instrument used to record metric values conforming +// to the "http.client.open_connections" semantic conventions. It represents the +// number of outbound HTTP connections that are currently active or idle on the +// client. +type ClientOpenConnections struct { + metric.Int64UpDownCounter +} + +// NewClientOpenConnections returns a new ClientOpenConnections instrument. +func NewClientOpenConnections( + m metric.Meter, + opt ...metric.Int64UpDownCounterOption, +) (ClientOpenConnections, error) { + // Check if the meter is nil. + if m == nil { + return ClientOpenConnections{noop.Int64UpDownCounter{}}, nil + } + + i, err := m.Int64UpDownCounter( + "http.client.open_connections", + append([]metric.Int64UpDownCounterOption{ + metric.WithDescription("Number of outbound HTTP connections that are currently active or idle on the client."), + metric.WithUnit("{connection}"), + }, opt...)..., + ) + if err != nil { + return ClientOpenConnections{noop.Int64UpDownCounter{}}, err + } + return ClientOpenConnections{i}, nil +} + +// Inst returns the underlying metric instrument. +func (m ClientOpenConnections) Inst() metric.Int64UpDownCounter { + return m.Int64UpDownCounter +} + +// Name returns the semantic convention name of the instrument. +func (ClientOpenConnections) Name() string { + return "http.client.open_connections" +} + +// Unit returns the semantic convention unit of the instrument +func (ClientOpenConnections) Unit() string { + return "{connection}" +} + +// Description returns the semantic convention description of the instrument +func (ClientOpenConnections) Description() string { + return "Number of outbound HTTP connections that are currently active or idle on the client." +} + +// Add adds incr to the existing count. +// +// The connectionState is the state of the HTTP connection in the HTTP connection +// pool. +// +// The serverAddress is the server domain name if available without reverse DNS +// lookup; otherwise, IP address or Unix domain socket name. +// +// The serverPort is the port identifier of the ["URI origin"] HTTP request is +// sent to. +// +// All additional attrs passed are included in the recorded value. +// +// ["URI origin"]: https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin +func (m ClientOpenConnections) Add( + ctx context.Context, + incr int64, + connectionState ConnectionStateAttr, + serverAddress string, + serverPort int, + attrs ...attribute.KeyValue, +) { + o := addOptPool.Get().(*[]metric.AddOption) + defer func() { + *o = (*o)[:0] + addOptPool.Put(o) + }() + + *o = append( + *o, + metric.WithAttributes( + append( + attrs, + attribute.String("http.connection.state", string(connectionState)), + attribute.String("server.address", serverAddress), + attribute.Int("server.port", serverPort), + )..., + ), + ) + + m.Int64UpDownCounter.Add(ctx, incr, *o...) +} + +// AttrNetworkPeerAddress returns an optional attribute for the +// "network.peer.address" semantic convention. It represents the peer address of +// the network connection - IP address or Unix domain socket name. +func (ClientOpenConnections) AttrNetworkPeerAddress(val string) attribute.KeyValue { + return attribute.String("network.peer.address", val) +} + +// AttrNetworkProtocolVersion returns an optional attribute for the +// "network.protocol.version" semantic convention. It represents the actual +// version of the protocol used for network communication. +func (ClientOpenConnections) AttrNetworkProtocolVersion(val string) attribute.KeyValue { + return attribute.String("network.protocol.version", val) +} + +// AttrURLScheme returns an optional attribute for the "url.scheme" semantic +// convention. It represents the [URI scheme] component identifying the used +// protocol. +// +// [URI scheme]: https://www.rfc-editor.org/rfc/rfc3986#section-3.1 +func (ClientOpenConnections) AttrURLScheme(val string) attribute.KeyValue { + return attribute.String("url.scheme", val) +} + +// ClientRequestBodySize is an instrument used to record metric values conforming +// to the "http.client.request.body.size" semantic conventions. It represents the +// size of HTTP client request bodies. +type ClientRequestBodySize struct { + metric.Int64Histogram +} + +// NewClientRequestBodySize returns a new ClientRequestBodySize instrument. +func NewClientRequestBodySize( + m metric.Meter, + opt ...metric.Int64HistogramOption, +) (ClientRequestBodySize, error) { + // Check if the meter is nil. + if m == nil { + return ClientRequestBodySize{noop.Int64Histogram{}}, nil + } + + i, err := m.Int64Histogram( + "http.client.request.body.size", + append([]metric.Int64HistogramOption{ + metric.WithDescription("Size of HTTP client request bodies."), + metric.WithUnit("By"), + }, opt...)..., + ) + if err != nil { + return ClientRequestBodySize{noop.Int64Histogram{}}, err + } + return ClientRequestBodySize{i}, nil +} + +// Inst returns the underlying metric instrument. +func (m ClientRequestBodySize) Inst() metric.Int64Histogram { + return m.Int64Histogram +} + +// Name returns the semantic convention name of the instrument. +func (ClientRequestBodySize) Name() string { + return "http.client.request.body.size" +} + +// Unit returns the semantic convention unit of the instrument +func (ClientRequestBodySize) Unit() string { + return "By" +} + +// Description returns the semantic convention description of the instrument +func (ClientRequestBodySize) Description() string { + return "Size of HTTP client request bodies." +} + +// Record records val to the current distribution. +// +// The requestMethod is the HTTP request method. +// +// The serverAddress is the host identifier of the ["URI origin"] HTTP request is +// sent to. +// +// The serverPort is the port identifier of the ["URI origin"] HTTP request is +// sent to. +// +// All additional attrs passed are included in the recorded value. +// +// ["URI origin"]: https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin +// ["URI origin"]: https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin +// +// The size of the request payload body in bytes. This is the number of bytes +// transferred excluding headers and is often, but not always, present as the +// [Content-Length] header. For requests using transport encoding, this should be +// the compressed size. +// +// [Content-Length]: https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length +func (m ClientRequestBodySize) Record( + ctx context.Context, + val int64, + requestMethod RequestMethodAttr, + serverAddress string, + serverPort int, + attrs ...attribute.KeyValue, +) { + o := recOptPool.Get().(*[]metric.RecordOption) + defer func() { + *o = (*o)[:0] + recOptPool.Put(o) + }() + + *o = append( + *o, + metric.WithAttributes( + append( + attrs, + attribute.String("http.request.method", string(requestMethod)), + attribute.String("server.address", serverAddress), + attribute.Int("server.port", serverPort), + )..., + ), + ) + + m.Int64Histogram.Record(ctx, val, *o...) +} + +// AttrErrorType returns an optional attribute for the "error.type" semantic +// convention. It represents the describes a class of error the operation ended +// with. +func (ClientRequestBodySize) AttrErrorType(val ErrorTypeAttr) attribute.KeyValue { + return attribute.String("error.type", string(val)) +} + +// AttrResponseStatusCode returns an optional attribute for the +// "http.response.status_code" semantic convention. It represents the +// [HTTP response status code]. +// +// [HTTP response status code]: https://tools.ietf.org/html/rfc7231#section-6 +func (ClientRequestBodySize) AttrResponseStatusCode(val int) attribute.KeyValue { + return attribute.Int("http.response.status_code", val) +} + +// AttrNetworkProtocolName returns an optional attribute for the +// "network.protocol.name" semantic convention. It represents the +// [OSI application layer] or non-OSI equivalent. +// +// [OSI application layer]: https://wikipedia.org/wiki/Application_layer +func (ClientRequestBodySize) AttrNetworkProtocolName(val string) attribute.KeyValue { + return attribute.String("network.protocol.name", val) +} + +// AttrURLTemplate returns an optional attribute for the "url.template" semantic +// convention. It represents the low-cardinality template of an +// [absolute path reference]. +// +// [absolute path reference]: https://www.rfc-editor.org/rfc/rfc3986#section-4.2 +func (ClientRequestBodySize) AttrURLTemplate(val string) attribute.KeyValue { + return attribute.String("url.template", val) +} + +// AttrNetworkProtocolVersion returns an optional attribute for the +// "network.protocol.version" semantic convention. It represents the actual +// version of the protocol used for network communication. +func (ClientRequestBodySize) AttrNetworkProtocolVersion(val string) attribute.KeyValue { + return attribute.String("network.protocol.version", val) +} + +// AttrURLScheme returns an optional attribute for the "url.scheme" semantic +// convention. It represents the [URI scheme] component identifying the used +// protocol. +// +// [URI scheme]: https://www.rfc-editor.org/rfc/rfc3986#section-3.1 +func (ClientRequestBodySize) AttrURLScheme(val string) attribute.KeyValue { + return attribute.String("url.scheme", val) +} + +// ClientRequestDuration is an instrument used to record metric values conforming +// to the "http.client.request.duration" semantic conventions. It represents the +// duration of HTTP client requests. +type ClientRequestDuration struct { + metric.Float64Histogram +} + +// NewClientRequestDuration returns a new ClientRequestDuration instrument. +func NewClientRequestDuration( + m metric.Meter, + opt ...metric.Float64HistogramOption, +) (ClientRequestDuration, error) { + // Check if the meter is nil. + if m == nil { + return ClientRequestDuration{noop.Float64Histogram{}}, nil + } + + i, err := m.Float64Histogram( + "http.client.request.duration", + append([]metric.Float64HistogramOption{ + metric.WithDescription("Duration of HTTP client requests."), + metric.WithUnit("s"), + }, opt...)..., + ) + if err != nil { + return ClientRequestDuration{noop.Float64Histogram{}}, err + } + return ClientRequestDuration{i}, nil +} + +// Inst returns the underlying metric instrument. +func (m ClientRequestDuration) Inst() metric.Float64Histogram { + return m.Float64Histogram +} + +// Name returns the semantic convention name of the instrument. +func (ClientRequestDuration) Name() string { + return "http.client.request.duration" +} + +// Unit returns the semantic convention unit of the instrument +func (ClientRequestDuration) Unit() string { + return "s" +} + +// Description returns the semantic convention description of the instrument +func (ClientRequestDuration) Description() string { + return "Duration of HTTP client requests." +} + +// Record records val to the current distribution. +// +// The requestMethod is the HTTP request method. +// +// The serverAddress is the host identifier of the ["URI origin"] HTTP request is +// sent to. +// +// The serverPort is the port identifier of the ["URI origin"] HTTP request is +// sent to. +// +// All additional attrs passed are included in the recorded value. +// +// ["URI origin"]: https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin +// ["URI origin"]: https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin +func (m ClientRequestDuration) Record( + ctx context.Context, + val float64, + requestMethod RequestMethodAttr, + serverAddress string, + serverPort int, + attrs ...attribute.KeyValue, +) { + o := recOptPool.Get().(*[]metric.RecordOption) + defer func() { + *o = (*o)[:0] + recOptPool.Put(o) + }() + + *o = append( + *o, + metric.WithAttributes( + append( + attrs, + attribute.String("http.request.method", string(requestMethod)), + attribute.String("server.address", serverAddress), + attribute.Int("server.port", serverPort), + )..., + ), + ) + + m.Float64Histogram.Record(ctx, val, *o...) +} + +// AttrErrorType returns an optional attribute for the "error.type" semantic +// convention. It represents the describes a class of error the operation ended +// with. +func (ClientRequestDuration) AttrErrorType(val ErrorTypeAttr) attribute.KeyValue { + return attribute.String("error.type", string(val)) +} + +// AttrResponseStatusCode returns an optional attribute for the +// "http.response.status_code" semantic convention. It represents the +// [HTTP response status code]. +// +// [HTTP response status code]: https://tools.ietf.org/html/rfc7231#section-6 +func (ClientRequestDuration) AttrResponseStatusCode(val int) attribute.KeyValue { + return attribute.Int("http.response.status_code", val) +} + +// AttrNetworkProtocolName returns an optional attribute for the +// "network.protocol.name" semantic convention. It represents the +// [OSI application layer] or non-OSI equivalent. +// +// [OSI application layer]: https://wikipedia.org/wiki/Application_layer +func (ClientRequestDuration) AttrNetworkProtocolName(val string) attribute.KeyValue { + return attribute.String("network.protocol.name", val) +} + +// AttrNetworkProtocolVersion returns an optional attribute for the +// "network.protocol.version" semantic convention. It represents the actual +// version of the protocol used for network communication. +func (ClientRequestDuration) AttrNetworkProtocolVersion(val string) attribute.KeyValue { + return attribute.String("network.protocol.version", val) +} + +// AttrURLScheme returns an optional attribute for the "url.scheme" semantic +// convention. It represents the [URI scheme] component identifying the used +// protocol. +// +// [URI scheme]: https://www.rfc-editor.org/rfc/rfc3986#section-3.1 +func (ClientRequestDuration) AttrURLScheme(val string) attribute.KeyValue { + return attribute.String("url.scheme", val) +} + +// AttrURLTemplate returns an optional attribute for the "url.template" semantic +// convention. It represents the low-cardinality template of an +// [absolute path reference]. +// +// [absolute path reference]: https://www.rfc-editor.org/rfc/rfc3986#section-4.2 +func (ClientRequestDuration) AttrURLTemplate(val string) attribute.KeyValue { + return attribute.String("url.template", val) +} + +// ClientResponseBodySize is an instrument used to record metric values +// conforming to the "http.client.response.body.size" semantic conventions. It +// represents the size of HTTP client response bodies. +type ClientResponseBodySize struct { + metric.Int64Histogram +} + +// NewClientResponseBodySize returns a new ClientResponseBodySize instrument. +func NewClientResponseBodySize( + m metric.Meter, + opt ...metric.Int64HistogramOption, +) (ClientResponseBodySize, error) { + // Check if the meter is nil. + if m == nil { + return ClientResponseBodySize{noop.Int64Histogram{}}, nil + } + + i, err := m.Int64Histogram( + "http.client.response.body.size", + append([]metric.Int64HistogramOption{ + metric.WithDescription("Size of HTTP client response bodies."), + metric.WithUnit("By"), + }, opt...)..., + ) + if err != nil { + return ClientResponseBodySize{noop.Int64Histogram{}}, err + } + return ClientResponseBodySize{i}, nil +} + +// Inst returns the underlying metric instrument. +func (m ClientResponseBodySize) Inst() metric.Int64Histogram { + return m.Int64Histogram +} + +// Name returns the semantic convention name of the instrument. +func (ClientResponseBodySize) Name() string { + return "http.client.response.body.size" +} + +// Unit returns the semantic convention unit of the instrument +func (ClientResponseBodySize) Unit() string { + return "By" +} + +// Description returns the semantic convention description of the instrument +func (ClientResponseBodySize) Description() string { + return "Size of HTTP client response bodies." +} + +// Record records val to the current distribution. +// +// The requestMethod is the HTTP request method. +// +// The serverAddress is the host identifier of the ["URI origin"] HTTP request is +// sent to. +// +// The serverPort is the port identifier of the ["URI origin"] HTTP request is +// sent to. +// +// All additional attrs passed are included in the recorded value. +// +// ["URI origin"]: https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin +// ["URI origin"]: https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin +// +// The size of the response payload body in bytes. This is the number of bytes +// transferred excluding headers and is often, but not always, present as the +// [Content-Length] header. For requests using transport encoding, this should be +// the compressed size. +// +// [Content-Length]: https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length +func (m ClientResponseBodySize) Record( + ctx context.Context, + val int64, + requestMethod RequestMethodAttr, + serverAddress string, + serverPort int, + attrs ...attribute.KeyValue, +) { + o := recOptPool.Get().(*[]metric.RecordOption) + defer func() { + *o = (*o)[:0] + recOptPool.Put(o) + }() + + *o = append( + *o, + metric.WithAttributes( + append( + attrs, + attribute.String("http.request.method", string(requestMethod)), + attribute.String("server.address", serverAddress), + attribute.Int("server.port", serverPort), + )..., + ), + ) + + m.Int64Histogram.Record(ctx, val, *o...) +} + +// AttrErrorType returns an optional attribute for the "error.type" semantic +// convention. It represents the describes a class of error the operation ended +// with. +func (ClientResponseBodySize) AttrErrorType(val ErrorTypeAttr) attribute.KeyValue { + return attribute.String("error.type", string(val)) +} + +// AttrResponseStatusCode returns an optional attribute for the +// "http.response.status_code" semantic convention. It represents the +// [HTTP response status code]. +// +// [HTTP response status code]: https://tools.ietf.org/html/rfc7231#section-6 +func (ClientResponseBodySize) AttrResponseStatusCode(val int) attribute.KeyValue { + return attribute.Int("http.response.status_code", val) +} + +// AttrNetworkProtocolName returns an optional attribute for the +// "network.protocol.name" semantic convention. It represents the +// [OSI application layer] or non-OSI equivalent. +// +// [OSI application layer]: https://wikipedia.org/wiki/Application_layer +func (ClientResponseBodySize) AttrNetworkProtocolName(val string) attribute.KeyValue { + return attribute.String("network.protocol.name", val) +} + +// AttrURLTemplate returns an optional attribute for the "url.template" semantic +// convention. It represents the low-cardinality template of an +// [absolute path reference]. +// +// [absolute path reference]: https://www.rfc-editor.org/rfc/rfc3986#section-4.2 +func (ClientResponseBodySize) AttrURLTemplate(val string) attribute.KeyValue { + return attribute.String("url.template", val) +} + +// AttrNetworkProtocolVersion returns an optional attribute for the +// "network.protocol.version" semantic convention. It represents the actual +// version of the protocol used for network communication. +func (ClientResponseBodySize) AttrNetworkProtocolVersion(val string) attribute.KeyValue { + return attribute.String("network.protocol.version", val) +} + +// AttrURLScheme returns an optional attribute for the "url.scheme" semantic +// convention. It represents the [URI scheme] component identifying the used +// protocol. +// +// [URI scheme]: https://www.rfc-editor.org/rfc/rfc3986#section-3.1 +func (ClientResponseBodySize) AttrURLScheme(val string) attribute.KeyValue { + return attribute.String("url.scheme", val) +} + +// ServerActiveRequests is an instrument used to record metric values conforming +// to the "http.server.active_requests" semantic conventions. It represents the +// number of active HTTP server requests. +type ServerActiveRequests struct { + metric.Int64UpDownCounter +} + +// NewServerActiveRequests returns a new ServerActiveRequests instrument. +func NewServerActiveRequests( + m metric.Meter, + opt ...metric.Int64UpDownCounterOption, +) (ServerActiveRequests, error) { + // Check if the meter is nil. + if m == nil { + return ServerActiveRequests{noop.Int64UpDownCounter{}}, nil + } + + i, err := m.Int64UpDownCounter( + "http.server.active_requests", + append([]metric.Int64UpDownCounterOption{ + metric.WithDescription("Number of active HTTP server requests."), + metric.WithUnit("{request}"), + }, opt...)..., + ) + if err != nil { + return ServerActiveRequests{noop.Int64UpDownCounter{}}, err + } + return ServerActiveRequests{i}, nil +} + +// Inst returns the underlying metric instrument. +func (m ServerActiveRequests) Inst() metric.Int64UpDownCounter { + return m.Int64UpDownCounter +} + +// Name returns the semantic convention name of the instrument. +func (ServerActiveRequests) Name() string { + return "http.server.active_requests" +} + +// Unit returns the semantic convention unit of the instrument +func (ServerActiveRequests) Unit() string { + return "{request}" +} + +// Description returns the semantic convention description of the instrument +func (ServerActiveRequests) Description() string { + return "Number of active HTTP server requests." +} + +// Add adds incr to the existing count. +// +// The requestMethod is the HTTP request method. +// +// The urlScheme is the the [URI scheme] component identifying the used protocol. +// +// All additional attrs passed are included in the recorded value. +// +// [URI scheme]: https://www.rfc-editor.org/rfc/rfc3986#section-3.1 +func (m ServerActiveRequests) Add( + ctx context.Context, + incr int64, + requestMethod RequestMethodAttr, + urlScheme string, + attrs ...attribute.KeyValue, +) { + o := addOptPool.Get().(*[]metric.AddOption) + defer func() { + *o = (*o)[:0] + addOptPool.Put(o) + }() + + *o = append( + *o, + metric.WithAttributes( + append( + attrs, + attribute.String("http.request.method", string(requestMethod)), + attribute.String("url.scheme", urlScheme), + )..., + ), + ) + + m.Int64UpDownCounter.Add(ctx, incr, *o...) +} + +// AttrServerAddress returns an optional attribute for the "server.address" +// semantic convention. It represents the name of the local HTTP server that +// received the request. +func (ServerActiveRequests) AttrServerAddress(val string) attribute.KeyValue { + return attribute.String("server.address", val) +} + +// AttrServerPort returns an optional attribute for the "server.port" semantic +// convention. It represents the port of the local HTTP server that received the +// request. +func (ServerActiveRequests) AttrServerPort(val int) attribute.KeyValue { + return attribute.Int("server.port", val) +} + +// ServerRequestBodySize is an instrument used to record metric values conforming +// to the "http.server.request.body.size" semantic conventions. It represents the +// size of HTTP server request bodies. +type ServerRequestBodySize struct { + metric.Int64Histogram +} + +// NewServerRequestBodySize returns a new ServerRequestBodySize instrument. +func NewServerRequestBodySize( + m metric.Meter, + opt ...metric.Int64HistogramOption, +) (ServerRequestBodySize, error) { + // Check if the meter is nil. + if m == nil { + return ServerRequestBodySize{noop.Int64Histogram{}}, nil + } + + i, err := m.Int64Histogram( + "http.server.request.body.size", + append([]metric.Int64HistogramOption{ + metric.WithDescription("Size of HTTP server request bodies."), + metric.WithUnit("By"), + }, opt...)..., + ) + if err != nil { + return ServerRequestBodySize{noop.Int64Histogram{}}, err + } + return ServerRequestBodySize{i}, nil +} + +// Inst returns the underlying metric instrument. +func (m ServerRequestBodySize) Inst() metric.Int64Histogram { + return m.Int64Histogram +} + +// Name returns the semantic convention name of the instrument. +func (ServerRequestBodySize) Name() string { + return "http.server.request.body.size" +} + +// Unit returns the semantic convention unit of the instrument +func (ServerRequestBodySize) Unit() string { + return "By" +} + +// Description returns the semantic convention description of the instrument +func (ServerRequestBodySize) Description() string { + return "Size of HTTP server request bodies." +} + +// Record records val to the current distribution. +// +// The requestMethod is the HTTP request method. +// +// The urlScheme is the the [URI scheme] component identifying the used protocol. +// +// All additional attrs passed are included in the recorded value. +// +// [URI scheme]: https://www.rfc-editor.org/rfc/rfc3986#section-3.1 +// +// The size of the request payload body in bytes. This is the number of bytes +// transferred excluding headers and is often, but not always, present as the +// [Content-Length] header. For requests using transport encoding, this should be +// the compressed size. +// +// [Content-Length]: https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length +func (m ServerRequestBodySize) Record( + ctx context.Context, + val int64, + requestMethod RequestMethodAttr, + urlScheme string, + attrs ...attribute.KeyValue, +) { + o := recOptPool.Get().(*[]metric.RecordOption) + defer func() { + *o = (*o)[:0] + recOptPool.Put(o) + }() + + *o = append( + *o, + metric.WithAttributes( + append( + attrs, + attribute.String("http.request.method", string(requestMethod)), + attribute.String("url.scheme", urlScheme), + )..., + ), + ) + + m.Int64Histogram.Record(ctx, val, *o...) +} + +// AttrErrorType returns an optional attribute for the "error.type" semantic +// convention. It represents the describes a class of error the operation ended +// with. +func (ServerRequestBodySize) AttrErrorType(val ErrorTypeAttr) attribute.KeyValue { + return attribute.String("error.type", string(val)) +} + +// AttrResponseStatusCode returns an optional attribute for the +// "http.response.status_code" semantic convention. It represents the +// [HTTP response status code]. +// +// [HTTP response status code]: https://tools.ietf.org/html/rfc7231#section-6 +func (ServerRequestBodySize) AttrResponseStatusCode(val int) attribute.KeyValue { + return attribute.Int("http.response.status_code", val) +} + +// AttrRoute returns an optional attribute for the "http.route" semantic +// convention. It represents the matched route, that is, the path template in the +// format used by the respective server framework. +func (ServerRequestBodySize) AttrRoute(val string) attribute.KeyValue { + return attribute.String("http.route", val) +} + +// AttrNetworkProtocolName returns an optional attribute for the +// "network.protocol.name" semantic convention. It represents the +// [OSI application layer] or non-OSI equivalent. +// +// [OSI application layer]: https://wikipedia.org/wiki/Application_layer +func (ServerRequestBodySize) AttrNetworkProtocolName(val string) attribute.KeyValue { + return attribute.String("network.protocol.name", val) +} + +// AttrNetworkProtocolVersion returns an optional attribute for the +// "network.protocol.version" semantic convention. It represents the actual +// version of the protocol used for network communication. +func (ServerRequestBodySize) AttrNetworkProtocolVersion(val string) attribute.KeyValue { + return attribute.String("network.protocol.version", val) +} + +// AttrServerAddress returns an optional attribute for the "server.address" +// semantic convention. It represents the name of the local HTTP server that +// received the request. +func (ServerRequestBodySize) AttrServerAddress(val string) attribute.KeyValue { + return attribute.String("server.address", val) +} + +// AttrServerPort returns an optional attribute for the "server.port" semantic +// convention. It represents the port of the local HTTP server that received the +// request. +func (ServerRequestBodySize) AttrServerPort(val int) attribute.KeyValue { + return attribute.Int("server.port", val) +} + +// AttrUserAgentSyntheticType returns an optional attribute for the +// "user_agent.synthetic.type" semantic convention. It represents the specifies +// the category of synthetic traffic, such as tests or bots. +func (ServerRequestBodySize) AttrUserAgentSyntheticType(val UserAgentSyntheticTypeAttr) attribute.KeyValue { + return attribute.String("user_agent.synthetic.type", string(val)) +} + +// ServerRequestDuration is an instrument used to record metric values conforming +// to the "http.server.request.duration" semantic conventions. It represents the +// duration of HTTP server requests. +type ServerRequestDuration struct { + metric.Float64Histogram +} + +// NewServerRequestDuration returns a new ServerRequestDuration instrument. +func NewServerRequestDuration( + m metric.Meter, + opt ...metric.Float64HistogramOption, +) (ServerRequestDuration, error) { + // Check if the meter is nil. + if m == nil { + return ServerRequestDuration{noop.Float64Histogram{}}, nil + } + + i, err := m.Float64Histogram( + "http.server.request.duration", + append([]metric.Float64HistogramOption{ + metric.WithDescription("Duration of HTTP server requests."), + metric.WithUnit("s"), + }, opt...)..., + ) + if err != nil { + return ServerRequestDuration{noop.Float64Histogram{}}, err + } + return ServerRequestDuration{i}, nil +} + +// Inst returns the underlying metric instrument. +func (m ServerRequestDuration) Inst() metric.Float64Histogram { + return m.Float64Histogram +} + +// Name returns the semantic convention name of the instrument. +func (ServerRequestDuration) Name() string { + return "http.server.request.duration" +} + +// Unit returns the semantic convention unit of the instrument +func (ServerRequestDuration) Unit() string { + return "s" +} + +// Description returns the semantic convention description of the instrument +func (ServerRequestDuration) Description() string { + return "Duration of HTTP server requests." +} + +// Record records val to the current distribution. +// +// The requestMethod is the HTTP request method. +// +// The urlScheme is the the [URI scheme] component identifying the used protocol. +// +// All additional attrs passed are included in the recorded value. +// +// [URI scheme]: https://www.rfc-editor.org/rfc/rfc3986#section-3.1 +func (m ServerRequestDuration) Record( + ctx context.Context, + val float64, + requestMethod RequestMethodAttr, + urlScheme string, + attrs ...attribute.KeyValue, +) { + o := recOptPool.Get().(*[]metric.RecordOption) + defer func() { + *o = (*o)[:0] + recOptPool.Put(o) + }() + + *o = append( + *o, + metric.WithAttributes( + append( + attrs, + attribute.String("http.request.method", string(requestMethod)), + attribute.String("url.scheme", urlScheme), + )..., + ), + ) + + m.Float64Histogram.Record(ctx, val, *o...) +} + +// AttrErrorType returns an optional attribute for the "error.type" semantic +// convention. It represents the describes a class of error the operation ended +// with. +func (ServerRequestDuration) AttrErrorType(val ErrorTypeAttr) attribute.KeyValue { + return attribute.String("error.type", string(val)) +} + +// AttrResponseStatusCode returns an optional attribute for the +// "http.response.status_code" semantic convention. It represents the +// [HTTP response status code]. +// +// [HTTP response status code]: https://tools.ietf.org/html/rfc7231#section-6 +func (ServerRequestDuration) AttrResponseStatusCode(val int) attribute.KeyValue { + return attribute.Int("http.response.status_code", val) +} + +// AttrRoute returns an optional attribute for the "http.route" semantic +// convention. It represents the matched route, that is, the path template in the +// format used by the respective server framework. +func (ServerRequestDuration) AttrRoute(val string) attribute.KeyValue { + return attribute.String("http.route", val) +} + +// AttrNetworkProtocolName returns an optional attribute for the +// "network.protocol.name" semantic convention. It represents the +// [OSI application layer] or non-OSI equivalent. +// +// [OSI application layer]: https://wikipedia.org/wiki/Application_layer +func (ServerRequestDuration) AttrNetworkProtocolName(val string) attribute.KeyValue { + return attribute.String("network.protocol.name", val) +} + +// AttrNetworkProtocolVersion returns an optional attribute for the +// "network.protocol.version" semantic convention. It represents the actual +// version of the protocol used for network communication. +func (ServerRequestDuration) AttrNetworkProtocolVersion(val string) attribute.KeyValue { + return attribute.String("network.protocol.version", val) +} + +// AttrServerAddress returns an optional attribute for the "server.address" +// semantic convention. It represents the name of the local HTTP server that +// received the request. +func (ServerRequestDuration) AttrServerAddress(val string) attribute.KeyValue { + return attribute.String("server.address", val) +} + +// AttrServerPort returns an optional attribute for the "server.port" semantic +// convention. It represents the port of the local HTTP server that received the +// request. +func (ServerRequestDuration) AttrServerPort(val int) attribute.KeyValue { + return attribute.Int("server.port", val) +} + +// AttrUserAgentSyntheticType returns an optional attribute for the +// "user_agent.synthetic.type" semantic convention. It represents the specifies +// the category of synthetic traffic, such as tests or bots. +func (ServerRequestDuration) AttrUserAgentSyntheticType(val UserAgentSyntheticTypeAttr) attribute.KeyValue { + return attribute.String("user_agent.synthetic.type", string(val)) +} + +// ServerResponseBodySize is an instrument used to record metric values +// conforming to the "http.server.response.body.size" semantic conventions. It +// represents the size of HTTP server response bodies. +type ServerResponseBodySize struct { + metric.Int64Histogram +} + +// NewServerResponseBodySize returns a new ServerResponseBodySize instrument. +func NewServerResponseBodySize( + m metric.Meter, + opt ...metric.Int64HistogramOption, +) (ServerResponseBodySize, error) { + // Check if the meter is nil. + if m == nil { + return ServerResponseBodySize{noop.Int64Histogram{}}, nil + } + + i, err := m.Int64Histogram( + "http.server.response.body.size", + append([]metric.Int64HistogramOption{ + metric.WithDescription("Size of HTTP server response bodies."), + metric.WithUnit("By"), + }, opt...)..., + ) + if err != nil { + return ServerResponseBodySize{noop.Int64Histogram{}}, err + } + return ServerResponseBodySize{i}, nil +} + +// Inst returns the underlying metric instrument. +func (m ServerResponseBodySize) Inst() metric.Int64Histogram { + return m.Int64Histogram +} + +// Name returns the semantic convention name of the instrument. +func (ServerResponseBodySize) Name() string { + return "http.server.response.body.size" +} + +// Unit returns the semantic convention unit of the instrument +func (ServerResponseBodySize) Unit() string { + return "By" +} + +// Description returns the semantic convention description of the instrument +func (ServerResponseBodySize) Description() string { + return "Size of HTTP server response bodies." +} + +// Record records val to the current distribution. +// +// The requestMethod is the HTTP request method. +// +// The urlScheme is the the [URI scheme] component identifying the used protocol. +// +// All additional attrs passed are included in the recorded value. +// +// [URI scheme]: https://www.rfc-editor.org/rfc/rfc3986#section-3.1 +// +// The size of the response payload body in bytes. This is the number of bytes +// transferred excluding headers and is often, but not always, present as the +// [Content-Length] header. For requests using transport encoding, this should be +// the compressed size. +// +// [Content-Length]: https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length +func (m ServerResponseBodySize) Record( + ctx context.Context, + val int64, + requestMethod RequestMethodAttr, + urlScheme string, + attrs ...attribute.KeyValue, +) { + o := recOptPool.Get().(*[]metric.RecordOption) + defer func() { + *o = (*o)[:0] + recOptPool.Put(o) + }() + + *o = append( + *o, + metric.WithAttributes( + append( + attrs, + attribute.String("http.request.method", string(requestMethod)), + attribute.String("url.scheme", urlScheme), + )..., + ), + ) + + m.Int64Histogram.Record(ctx, val, *o...) +} + +// AttrErrorType returns an optional attribute for the "error.type" semantic +// convention. It represents the describes a class of error the operation ended +// with. +func (ServerResponseBodySize) AttrErrorType(val ErrorTypeAttr) attribute.KeyValue { + return attribute.String("error.type", string(val)) +} + +// AttrResponseStatusCode returns an optional attribute for the +// "http.response.status_code" semantic convention. It represents the +// [HTTP response status code]. +// +// [HTTP response status code]: https://tools.ietf.org/html/rfc7231#section-6 +func (ServerResponseBodySize) AttrResponseStatusCode(val int) attribute.KeyValue { + return attribute.Int("http.response.status_code", val) +} + +// AttrRoute returns an optional attribute for the "http.route" semantic +// convention. It represents the matched route, that is, the path template in the +// format used by the respective server framework. +func (ServerResponseBodySize) AttrRoute(val string) attribute.KeyValue { + return attribute.String("http.route", val) +} + +// AttrNetworkProtocolName returns an optional attribute for the +// "network.protocol.name" semantic convention. It represents the +// [OSI application layer] or non-OSI equivalent. +// +// [OSI application layer]: https://wikipedia.org/wiki/Application_layer +func (ServerResponseBodySize) AttrNetworkProtocolName(val string) attribute.KeyValue { + return attribute.String("network.protocol.name", val) +} + +// AttrNetworkProtocolVersion returns an optional attribute for the +// "network.protocol.version" semantic convention. It represents the actual +// version of the protocol used for network communication. +func (ServerResponseBodySize) AttrNetworkProtocolVersion(val string) attribute.KeyValue { + return attribute.String("network.protocol.version", val) +} + +// AttrServerAddress returns an optional attribute for the "server.address" +// semantic convention. It represents the name of the local HTTP server that +// received the request. +func (ServerResponseBodySize) AttrServerAddress(val string) attribute.KeyValue { + return attribute.String("server.address", val) +} + +// AttrServerPort returns an optional attribute for the "server.port" semantic +// convention. It represents the port of the local HTTP server that received the +// request. +func (ServerResponseBodySize) AttrServerPort(val int) attribute.KeyValue { + return attribute.Int("server.port", val) +} + +// AttrUserAgentSyntheticType returns an optional attribute for the +// "user_agent.synthetic.type" semantic convention. It represents the specifies +// the category of synthetic traffic, such as tests or bots. +func (ServerResponseBodySize) AttrUserAgentSyntheticType(val UserAgentSyntheticTypeAttr) attribute.KeyValue { + return attribute.String("user_agent.synthetic.type", string(val)) +} \ No newline at end of file diff --git a/vendor/modules.txt b/vendor/modules.txt index 10322dabc6..849b17cce0 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -221,8 +221,8 @@ go.opencensus.io/trace/tracestate ## explicit; go 1.22.0 go.opentelemetry.io/auto/sdk go.opentelemetry.io/auto/sdk/internal/telemetry -# go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 -## explicit; go 1.22.0 +# go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 +## explicit; go 1.23.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv @@ -246,6 +246,7 @@ go.opentelemetry.io/otel/semconv/v1.20.0 go.opentelemetry.io/otel/semconv/v1.26.0 go.opentelemetry.io/otel/semconv/v1.34.0 go.opentelemetry.io/otel/semconv/v1.34.0/goconv +go.opentelemetry.io/otel/semconv/v1.34.0/httpconv # go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0 ## explicit; go 1.23.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc diff --git a/webhook/admission.go b/webhook/admission.go index 9e14eae89b..14a25fd3cd 100644 --- a/webhook/admission.go +++ b/webhook/admission.go @@ -26,12 +26,15 @@ import ( "strings" "time" - "go.uber.org/zap" admissionv1 "k8s.io/api/admission/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "knative.dev/pkg/apis" "knative.dev/pkg/logging" "knative.dev/pkg/logging/logkey" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/metric" ) const ( @@ -69,7 +72,7 @@ type StatelessAdmissionController interface { } // MakeErrorStatus creates an 'BadRequest' error AdmissionResponse -func MakeErrorStatus(reason string, args ...interface{}) *admissionv1.AdmissionResponse { +func MakeErrorStatus(reason string, args ...any) *admissionv1.AdmissionResponse { result := apierrors.NewBadRequest(fmt.Sprintf(reason, args...)).Status() return &admissionv1.AdmissionResponse{ Result: &result, @@ -77,7 +80,7 @@ func MakeErrorStatus(reason string, args ...interface{}) *admissionv1.AdmissionR } } -func admissionHandler(rootLogger *zap.SugaredLogger, stats StatsReporter, c AdmissionController, synced <-chan struct{}) http.HandlerFunc { +func admissionHandler(wh *Webhook, c AdmissionController, synced <-chan struct{}) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if _, ok := c.(StatelessAdmissionController); ok { // Stateless admission controllers do not require Informers to have @@ -88,8 +91,7 @@ func admissionHandler(rootLogger *zap.SugaredLogger, stats StatsReporter, c Admi <-synced } - ttStart := time.Now() - logger := rootLogger + logger := wh.Logger logger.Infof("Webhook ServeHTTP request=%#v", r) var review admissionv1.AdmissionReview @@ -100,6 +102,18 @@ func admissionHandler(rootLogger *zap.SugaredLogger, stats StatsReporter, c Admi } r.Body = io.NopCloser(&bodyBuffer) + // otelhttp middleware creates the labeler + labeler, _ := otelhttp.LabelerFromContext(r.Context()) + + labeler.Add( + KindAttr.With(review.Request.Kind.Kind), + GroupAttr.With(review.Request.Kind.Group), + VersionAttr.With(review.Request.Kind.Version), + OperationAttr.With(string(review.Request.Operation)), + SubresourceAttr.With(review.Request.SubResource), + WebhookTypeAttr.With(WebhookTypeAdmission), + ) + logger = logger.With( logkey.Kind, review.Request.Kind.String(), logkey.Namespace, review.Request.Namespace, @@ -120,12 +134,26 @@ func admissionHandler(rootLogger *zap.SugaredLogger, stats StatsReporter, c Admi TypeMeta: review.TypeMeta, } + ttStart := time.Now() reviewResponse := c.Admit(ctx, review.Request) + var patchType string if reviewResponse.PatchType != nil { patchType = string(*reviewResponse.PatchType) } + status := metav1.StatusFailure + if reviewResponse.Allowed { + status = metav1.StatusSuccess + } + + labeler.Add(StatusAttr.With(strings.ToLower(status))) + + wh.metrics.recordHandlerDuration(ctx, + time.Since(ttStart), + metric.WithAttributes(labeler.Get()...), + ) + if !reviewResponse.Allowed || reviewResponse.PatchType != nil || response.Response == nil { response.Response = reviewResponse } @@ -155,11 +183,6 @@ func admissionHandler(rootLogger *zap.SugaredLogger, stats StatsReporter, c Admi http.Error(w, fmt.Sprint("could not encode response:", err), http.StatusInternalServerError) return } - - if stats != nil { - // Only report valid requests - stats.ReportAdmissionRequest(review.Request, response.Response, time.Since(ttStart)) - } } } diff --git a/webhook/admission_integration_test.go b/webhook/admission_integration_test.go index 96fe44e7b5..2eada24ba0 100644 --- a/webhook/admission_integration_test.go +++ b/webhook/admission_integration_test.go @@ -29,15 +29,17 @@ import ( "time" "github.com/google/go-cmp/cmp" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "golang.org/x/sync/errgroup" jsonpatch "gomodules.xyz/jsonpatch/v2" + admissionv1 "k8s.io/api/admission/v1" authenticationv1 "k8s.io/api/authentication/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "knative.dev/pkg/apis" kubeclient "knative.dev/pkg/client/injection/kube/client/fake" - "knative.dev/pkg/metrics/metricstest" - _ "knative.dev/pkg/metrics/testing" + "knative.dev/pkg/observability/metrics/metricstest" ) type fixedAdmissionController struct { @@ -99,7 +101,7 @@ func TestAdmissionEmptyRequestBody(t *testing.T) { func TestAdmissionValidResponseForResourceTLS(t *testing.T) { ac := &fixedAdmissionController{ path: "/bazinga", - response: &admissionv1.AdmissionResponse{}, + response: &admissionv1.AdmissionResponse{Allowed: true}, } test := testSetup(t, withController(ac)) @@ -195,6 +197,8 @@ func TestAdmissionValidResponseForResourceTLS(t *testing.T) { t.Errorf("expected the response typeMeta to be the same as the request (-want, +got)\n%s", diff) return } + + assertAdmissionMetrics(t, test, ac.response.Allowed) }() // Wait for the goroutine to launch. @@ -216,14 +220,12 @@ func TestAdmissionValidResponseForResourceTLS(t *testing.T) { case <-time.After(5 * time.Second): t.Error("Timed out waiting on Admit to complete after informers synced.") } - - metricstest.CheckStatsReported(t, requestCountName, requestLatenciesName) } func TestAdmissionValidResponseForResource(t *testing.T) { ac := &fixedAdmissionController{ path: "/bazinga", - response: &admissionv1.AdmissionResponse{}, + response: &admissionv1.AdmissionResponse{Allowed: true}, } test := testSetup(t, withController(ac), withNoTLS()) @@ -316,6 +318,8 @@ func TestAdmissionValidResponseForResource(t *testing.T) { t.Errorf("expected the response typeMeta to be the same as the request (-want, +got)\n%s", diff) return } + + assertAdmissionMetrics(t, test, ac.response.Allowed) }() // Wait for the goroutine to launch. @@ -337,8 +341,6 @@ func TestAdmissionValidResponseForResource(t *testing.T) { case <-time.After(5 * time.Second): t.Error("Timed out waiting on Admit to complete after informers synced.") } - - metricstest.CheckStatsReported(t, requestCountName, requestLatenciesName) } func TestAdmissionInvalidResponseForResource(t *testing.T) { @@ -449,8 +451,7 @@ func TestAdmissionInvalidResponseForResource(t *testing.T) { t.Error("Received unexpected response status message", reviewResponse.Response.Result.Message) } - // Stats should be reported for requests that have admission disallowed - metricstest.CheckStatsReported(t, requestCountName, requestLatenciesName) + assertAdmissionMetrics(t, test, ac.response.Allowed) } func TestAdmissionWarningResponseForResource(t *testing.T) { @@ -458,8 +459,11 @@ func TestAdmissionWarningResponseForResource(t *testing.T) { // these three warnings expectedWarnings := []string{"everything is not fine.", "like really", "for sure"} ac := &fixedAdmissionController{ - path: "/warnmeplease", - response: &admissionv1.AdmissionResponse{Warnings: []string{"everything is not fine.\nlike really\nfor sure"}}, + path: "/warnmeplease", + response: &admissionv1.AdmissionResponse{ + Allowed: true, + Warnings: []string{"everything is not fine.\nlike really\nfor sure"}, + }, } test := testSetup(t, withController(ac)) @@ -557,12 +561,16 @@ func TestAdmissionWarningResponseForResource(t *testing.T) { t.Errorf("Unexpected warning want %s got %s", expectedWarnings[i], w) } } + + assertAdmissionMetrics(t, test, ac.response.Allowed) } func TestAdmissionValidResponseForRequestBody(t *testing.T) { ac := &readBodyTwiceAdmissionController{ - path: "/bazinga", - response: &admissionv1.AdmissionResponse{}, + path: "/bazinga", + response: &admissionv1.AdmissionResponse{ + Allowed: true, + }, } test := testSetup(t, withController(ac), withNoTLS()) @@ -655,6 +663,8 @@ func TestAdmissionValidResponseForRequestBody(t *testing.T) { t.Errorf("expected the response typeMeta to be the same as the request (-want, +got)\n%s", diff) return } + + assertAdmissionMetrics(t, test, ac.response.Allowed) }() // Wait for the goroutine to launch. @@ -676,6 +686,34 @@ func TestAdmissionValidResponseForRequestBody(t *testing.T) { case <-time.After(5 * time.Second): t.Error("Timed out waiting on Admit to complete after informers synced.") } +} - metricstest.CheckStatsReported(t, requestCountName, requestLatenciesName) +func assertAdmissionMetrics(t *testing.T, tc testContext, allowed bool) { + status := metav1.StatusFailure + if allowed { + status = metav1.StatusSuccess + } + metricstest.AssertMetrics(t, tc.metricReader, + metricstest.MetricsPresent( + otelhttp.ScopeName, + "http.server.request.body.size", + "http.server.response.body.size", + "http.server.request.duration", + ), + metricstest.MetricsPresent( + scopeName, + "kn.webhook.handler.duration", + ), + metricstest.HasAttributes( + "", // any scope + "", // any metric + WebhookTypeAttr.With(WebhookTypeAdmission), + OperationAttr.With("CREATE"), + GroupAttr.With("pkg.knative.dev"), + VersionAttr.With("v1alpha1"), + KindAttr.With("Resource"), + SubresourceAttr.With(""), + StatusAttr.With(strings.ToLower(status)), + ), + ) } diff --git a/webhook/conversion.go b/webhook/conversion.go index fa3c76a702..7d72b48963 100644 --- a/webhook/conversion.go +++ b/webhook/conversion.go @@ -21,10 +21,15 @@ import ( "encoding/json" "fmt" "net/http" + "strings" "time" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/metric" "go.uber.org/zap" apixv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "knative.dev/pkg/apis" "knative.dev/pkg/logging" ) @@ -38,10 +43,9 @@ type ConversionController interface { Convert(context.Context, *apixv1.ConversionRequest) *apixv1.ConversionResponse } -func conversionHandler(rootLogger *zap.SugaredLogger, stats StatsReporter, c ConversionController) http.HandlerFunc { +func conversionHandler(wh *Webhook, c ConversionController) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ttStart := time.Now() - logger := rootLogger + logger := wh.Logger logger.Infof("Webhook ServeHTTP request=%#v", r) var review apixv1.ConversionReview @@ -50,6 +54,20 @@ func conversionHandler(rootLogger *zap.SugaredLogger, stats StatsReporter, c Con return } + gv, err := parseAPIVersion(review.Request.DesiredAPIVersion) + if err != nil { + http.Error(w, fmt.Sprint("could parse desired api version:", err), http.StatusBadRequest) + return + } + + // otelhttp middleware creates the labeler + labeler, _ := otelhttp.LabelerFromContext(r.Context()) + labeler.Add( + WebhookTypeAttr.With(WebhookTypeConversion), + GroupAttr.With(gv.Group), + VersionAttr.With(gv.Version), + ) + logger = logger.With( zap.String("uid", string(review.Request.UID)), zap.String("desiredAPIVersion", review.Request.DesiredAPIVersion), @@ -58,6 +76,7 @@ func conversionHandler(rootLogger *zap.SugaredLogger, stats StatsReporter, c Con ctx := logging.WithLogger(r.Context(), logger) ctx = apis.WithHTTPRequest(ctx, r) + ttStart := time.Now() response := apixv1.ConversionReview{ // Use the same type meta as the request - this is required by the K8s API // note: v1beta1 & v1 ConversionReview shapes are identical so even though @@ -66,14 +85,36 @@ func conversionHandler(rootLogger *zap.SugaredLogger, stats StatsReporter, c Con Response: c.Convert(ctx, review.Request), } + labeler.Add( + StatusAttr.With(strings.ToLower(response.Response.Result.Status)), + ) + + wh.metrics.recordHandlerDuration(ctx, time.Since(ttStart), + metric.WithAttributes(labeler.Get()...), + ) + if err := json.NewEncoder(w).Encode(response); err != nil { http.Error(w, fmt.Sprint("could not encode response:", err), http.StatusInternalServerError) return } + } +} - if stats != nil { - // Only report valid requests - stats.ReportConversionRequest(review.Request, response.Response, time.Since(ttStart)) - } +func parseAPIVersion(apiVersion string) (schema.GroupVersion, error) { + gv, err := schema.ParseGroupVersion(apiVersion) + if err != nil { + err = fmt.Errorf("desired API version %q is not valid", apiVersion) + return schema.GroupVersion{}, err } + + if !isValidGV(gv) { + err = fmt.Errorf("desired API version %q is not valid", apiVersion) + return schema.GroupVersion{}, err + } + + return gv, nil +} + +func isValidGV(gk schema.GroupVersion) bool { + return gk.Group != "" && gk.Version != "" } diff --git a/webhook/conversion_integration_test.go b/webhook/conversion_integration_test.go index 3581d20131..b2a9cc789e 100644 --- a/webhook/conversion_integration_test.go +++ b/webhook/conversion_integration_test.go @@ -23,16 +23,20 @@ import ( "fmt" "io" "net/http" + "strings" "testing" "github.com/google/go-cmp/cmp" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "golang.org/x/sync/errgroup" + apixv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + kubeclient "knative.dev/pkg/client/injection/kube/client/fake" - _ "knative.dev/pkg/metrics/testing" + "knative.dev/pkg/observability/metrics/metricstest" ) type fixedConversionController struct { @@ -64,6 +68,9 @@ func TestConversionValidResponse(t *testing.T) { path: "/bazinga", response: &apixv1.ConversionResponse{ UID: types.UID("some-uid"), + Result: metav1.Status{ + Status: metav1.StatusSuccess, + }, }, } test := testSetup(t, withController(cc)) @@ -139,6 +146,8 @@ func TestConversionValidResponse(t *testing.T) { if diff := cmp.Diff(review.TypeMeta, reviewResponse.TypeMeta); diff != "" { t.Errorf("expected the response typeMeta to be the same as the request (-want, +got)\n%s", diff) } + + assertConversionMetrics(t, test, cc.response.Result.Status) } func TestConversionInvalidResponse(t *testing.T) { @@ -224,4 +233,29 @@ func TestConversionInvalidResponse(t *testing.T) { if reviewResponse.Response.Result.Status != metav1.StatusFailure { t.Errorf("expected the response uid to be the stubbed version") } + + assertConversionMetrics(t, test, cc.response.Result.Status) +} + +func assertConversionMetrics(t *testing.T, tc testContext, status string) { + metricstest.AssertMetrics(t, tc.metricReader, + metricstest.MetricsPresent( + otelhttp.ScopeName, + "http.server.request.body.size", + "http.server.response.body.size", + "http.server.request.duration", + ), + metricstest.MetricsPresent( + scopeName, + "kn.webhook.handler.duration", + ), + metricstest.HasAttributes( + "", // any scope + "", // any metric + WebhookTypeAttr.With(WebhookTypeConversion), + GroupAttr.With("example.com"), + VersionAttr.With("v1"), + StatusAttr.With(strings.ToLower(status)), + ), + ) } diff --git a/webhook/metrics.go b/webhook/metrics.go new file mode 100644 index 0000000000..958eb96cb7 --- /dev/null +++ b/webhook/metrics.go @@ -0,0 +1,84 @@ +/* +Copyright 2025 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webhook + +import ( + "context" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/metric" + + "knative.dev/pkg/observability/attributekey" +) + +const ( + scopeName = "knative.dev/pkg/webhook" + + WebhookTypeAdmission = "admission" + WebhookTypeDefaulting = "defaulting" + WebhookTypeValidation = "validation" + WebhookTypeConversion = "conversion" +) + +var ( + // WebhookType is an attribute that specifies whether the type of webhook is an admission + // eg. (defaulting/validation) or conversion + WebhookTypeAttr = attributekey.String("kn.webhook.type") + + GroupAttr = attributekey.String("kn.webhook.resource.group") + VersionAttr = attributekey.String("kn.webhook.resource.version") + KindAttr = attributekey.String("kn.webhook.resource.kind") + SubresourceAttr = attributekey.String("kn.webhook.subresource") + OperationAttr = attributekey.String("kn.webhook.operation.type") + StatusAttr = attributekey.String("kn.webhook.operation.status") +) + +type metrics struct { + handlerDuration metric.Float64Histogram +} + +func newMetrics(o Options) *metrics { + var ( + m metrics + err error + provider = o.MeterProvider + ) + + if provider == nil { + provider = otel.GetMeterProvider() + } + + meter := provider.Meter(scopeName) + + m.handlerDuration, err = meter.Float64Histogram( + "kn.webhook.handler.duration", + metric.WithDescription("The duration of task execution."), + metric.WithUnit("s"), + metric.WithExplicitBucketBoundaries(0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10), + ) + if err != nil { + panic(err) + } + + return &m +} + +func (m *metrics) recordHandlerDuration(ctx context.Context, d time.Duration, ro ...metric.RecordOption) { + elapsedTime := float64(d) / float64(time.Second) + m.handlerDuration.Record(ctx, elapsedTime, ro...) +} diff --git a/webhook/resourcesemantics/conversion/conversion.go b/webhook/resourcesemantics/conversion/conversion.go index 29830a8751..90412392ae 100644 --- a/webhook/resourcesemantics/conversion/conversion.go +++ b/webhook/resourcesemantics/conversion/conversion.go @@ -21,8 +21,8 @@ import ( "encoding/json" "fmt" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.uber.org/zap" - apixv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -32,6 +32,7 @@ import ( "knative.dev/pkg/kmeta" "knative.dev/pkg/logging" "knative.dev/pkg/logging/logkey" + "knative.dev/pkg/webhook" ) // Convert implements webhook.ConversionController @@ -81,6 +82,12 @@ func (r *reconciler) convert( return ret, err } + // otelhttp middleware creates the labeler + labeler, _ := otelhttp.LabelerFromContext(ctx) + labeler.Add( + webhook.KindAttr.With(inGVK.Kind), + ) + inGK := inGVK.GroupKind() conv, ok := r.kinds[inGK] if !ok { diff --git a/webhook/resourcesemantics/defaulting/defaulting.go b/webhook/resourcesemantics/defaulting/defaulting.go index 6aa08b4b94..670649b2e1 100644 --- a/webhook/resourcesemantics/defaulting/defaulting.go +++ b/webhook/resourcesemantics/defaulting/defaulting.go @@ -26,6 +26,9 @@ import ( "github.com/gobuffalo/flect" "go.uber.org/zap" "gomodules.xyz/jsonpatch/v2" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + admissionv1 "k8s.io/api/admission/v1" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" @@ -142,6 +145,10 @@ func (ac *reconciler) Path() string { // Admit implements AdmissionController func (ac *reconciler) Admit(ctx context.Context, request *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse { + // otelhttp middleware creates the labeler + labeler, _ := otelhttp.LabelerFromContext(ctx) + labeler.Add(webhook.WebhookTypeAttr.With(webhook.WebhookTypeDefaulting)) + if ac.withContext != nil { ctx = ac.withContext(ctx) } diff --git a/webhook/resourcesemantics/validation/validation_admit.go b/webhook/resourcesemantics/validation/validation_admit.go index ee27e3c35c..6d26212caa 100644 --- a/webhook/resourcesemantics/validation/validation_admit.go +++ b/webhook/resourcesemantics/validation/validation_admit.go @@ -21,10 +21,13 @@ import ( "errors" "fmt" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.uber.org/zap" + admissionv1 "k8s.io/api/admission/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" + "knative.dev/pkg/apis" kubeclient "knative.dev/pkg/client/injection/kube/client" "knative.dev/pkg/logging" @@ -60,6 +63,10 @@ var _ webhook.AdmissionController = (*reconciler)(nil) // Admit implements AdmissionController func (ac *reconciler) Admit(ctx context.Context, request *admissionv1.AdmissionRequest) (resp *admissionv1.AdmissionResponse) { + // otelhttp middleware creates the labeler + labeler, _ := otelhttp.LabelerFromContext(ctx) + labeler.Add(webhook.WebhookTypeAttr.With(webhook.WebhookTypeValidation)) + if ac.withContext != nil { ctx = ac.withContext(ctx) } diff --git a/webhook/stats_reporter.go b/webhook/stats_reporter.go deleted file mode 100644 index bc62820b98..0000000000 --- a/webhook/stats_reporter.go +++ /dev/null @@ -1,260 +0,0 @@ -/* -Copyright 2019 The Knative Authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package webhook - -import ( - "context" - "strconv" - "time" - - "go.opencensus.io/stats" - "go.opencensus.io/stats/view" - "go.opencensus.io/tag" - admissionv1 "k8s.io/api/admission/v1" - apixv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - "k8s.io/apimachinery/pkg/util/sets" - "knative.dev/pkg/metrics" -) - -const ( - requestCountName = "request_count" - requestLatenciesName = "request_latencies" -) - -var ( - requestCountM = stats.Int64( - requestCountName, - "The number of requests that are routed to webhook", - stats.UnitDimensionless) - responseTimeInMsecM = stats.Float64( - requestLatenciesName, - "The response time in milliseconds", - stats.UnitMilliseconds) - - // Create the tag keys that will be used to add tags to our measurements. - // Tag keys must conform to the restrictions described in - // go.opencensus.io/tag/validate.go. Currently those restrictions are: - // - length between 1 and 255 inclusive - // - characters are printable US-ASCII - requestOperationKey = tag.MustNewKey("request_operation") - kindGroupKey = tag.MustNewKey("kind_group") - kindVersionKey = tag.MustNewKey("kind_version") - kindKindKey = tag.MustNewKey("kind_kind") - resourceGroupKey = tag.MustNewKey("resource_group") - resourceVersionKey = tag.MustNewKey("resource_version") - resourceResourceKey = tag.MustNewKey("resource_resource") - resourceNamespaceKey = tag.MustNewKey("resource_namespace") - admissionAllowedKey = tag.MustNewKey("admission_allowed") - - desiredAPIVersionKey = tag.MustNewKey("desired_api_version") - resultStatusKey = tag.MustNewKey("result_status") - resultReasonKey = tag.MustNewKey("result_reason") - resultCodeKey = tag.MustNewKey("result_code") -) - -type ( - admissionToValue func(*admissionv1.AdmissionRequest, *admissionv1.AdmissionResponse) string - conversionToValue func(*apixv1.ConversionRequest, *apixv1.ConversionResponse) string -) - -var ( - allAdmissionTags = map[tag.Key]admissionToValue{ - requestOperationKey: func(req *admissionv1.AdmissionRequest, _ *admissionv1.AdmissionResponse) string { - return string(req.Operation) - }, - kindGroupKey: func(req *admissionv1.AdmissionRequest, _ *admissionv1.AdmissionResponse) string { - return req.Kind.Group - }, - kindVersionKey: func(req *admissionv1.AdmissionRequest, _ *admissionv1.AdmissionResponse) string { - return req.Kind.Version - }, - kindKindKey: func(req *admissionv1.AdmissionRequest, _ *admissionv1.AdmissionResponse) string { - return req.Kind.Kind - }, - resourceGroupKey: func(req *admissionv1.AdmissionRequest, _ *admissionv1.AdmissionResponse) string { - return req.Resource.Group - }, - resourceVersionKey: func(req *admissionv1.AdmissionRequest, _ *admissionv1.AdmissionResponse) string { - return req.Resource.Version - }, - resourceResourceKey: func(req *admissionv1.AdmissionRequest, _ *admissionv1.AdmissionResponse) string { - return req.Resource.Resource - }, - resourceNamespaceKey: func(req *admissionv1.AdmissionRequest, _ *admissionv1.AdmissionResponse) string { - return req.Namespace - }, - admissionAllowedKey: func(_ *admissionv1.AdmissionRequest, resp *admissionv1.AdmissionResponse) string { - return strconv.FormatBool(resp.Allowed) - }, - } - allConversionTags = map[tag.Key]conversionToValue{ - desiredAPIVersionKey: func(req *apixv1.ConversionRequest, _ *apixv1.ConversionResponse) string { - return req.DesiredAPIVersion - }, - resultStatusKey: func(_ *apixv1.ConversionRequest, resp *apixv1.ConversionResponse) string { - return resp.Result.Status - }, - resultReasonKey: func(_ *apixv1.ConversionRequest, resp *apixv1.ConversionResponse) string { - return string(resp.Result.Reason) - }, - resultCodeKey: func(_ *apixv1.ConversionRequest, resp *apixv1.ConversionResponse) string { - return strconv.Itoa(int(resp.Result.Code)) - }, - } -) - -// StatsReporter reports webhook metrics -type StatsReporter interface { - ReportAdmissionRequest(request *admissionv1.AdmissionRequest, response *admissionv1.AdmissionResponse, d time.Duration) error - ReportConversionRequest(request *apixv1.ConversionRequest, response *apixv1.ConversionResponse, d time.Duration) error -} - -type statsReporterOptions struct { - tagsToExclude sets.Set[string] -} - -type StatsReporterOption func(_ *statsReporterOptions) - -func WithoutTags(tags ...string) StatsReporterOption { - return func(opts *statsReporterOptions) { - opts.tagsToExclude.Insert(tags...) - } -} - -// reporter implements StatsReporter interface -type reporter struct { - ctx context.Context - - admissionTags map[tag.Key]admissionToValue - conversionTags map[tag.Key]conversionToValue -} - -// NewStatsReporter creates a reporter for webhook metrics -func NewStatsReporter(opts ...StatsReporterOption) (StatsReporter, error) { - ctx, err := tag.New( - context.Background(), - ) - if err != nil { - return nil, err - } - - options := statsReporterOptions{ - tagsToExclude: sets.New[string](), - } - for _, opt := range opts { - opt(&options) - } - - admissionTags := make(map[tag.Key]admissionToValue) - for key, f := range allAdmissionTags { - if options.tagsToExclude.Has(key.Name()) { - continue - } - admissionTags[key] = f - } - conversionTags := make(map[tag.Key]conversionToValue) - for key, f := range allConversionTags { - if options.tagsToExclude.Has(key.Name()) { - continue - } - conversionTags[key] = f - } - - return &reporter{ - ctx: ctx, - admissionTags: admissionTags, - conversionTags: conversionTags, - }, nil -} - -// Captures req count metric, recording the count and the duration -func (r *reporter) ReportAdmissionRequest(req *admissionv1.AdmissionRequest, resp *admissionv1.AdmissionResponse, d time.Duration) error { - mutators := make([]tag.Mutator, 0, len(r.admissionTags)) - - for key, f := range r.admissionTags { - mutators = append(mutators, tag.Insert(key, f(req, resp))) - } - - ctx, err := tag.New(r.ctx, mutators...) - if err != nil { - return err - } - - metrics.RecordBatch(ctx, requestCountM.M(1), - // Convert time.Duration in nanoseconds to milliseconds - responseTimeInMsecM.M(float64(d.Milliseconds()))) - return nil -} - -// Captures req count metric, recording the count and the duration -func (r *reporter) ReportConversionRequest(req *apixv1.ConversionRequest, resp *apixv1.ConversionResponse, d time.Duration) error { - mutators := make([]tag.Mutator, 0, len(r.conversionTags)) - - for key, f := range r.conversionTags { - mutators = append(mutators, tag.Insert(key, f(req, resp))) - } - - ctx, err := tag.New(r.ctx, mutators...) - if err != nil { - return err - } - - metrics.RecordBatch(ctx, requestCountM.M(1), - // Convert time.Duration in nanoseconds to milliseconds - responseTimeInMsecM.M(float64(d.Milliseconds()))) - return nil -} - -func RegisterMetrics(opts ...StatsReporterOption) { - options := statsReporterOptions{ - tagsToExclude: sets.New[string](), - } - for _, opt := range opts { - opt(&options) - } - - tagKeys := []tag.Key{} - for tag := range allAdmissionTags { - if options.tagsToExclude.Has(tag.Name()) { - continue - } - tagKeys = append(tagKeys, tag) - } - for tag := range allConversionTags { - if options.tagsToExclude.Has(tag.Name()) { - continue - } - tagKeys = append(tagKeys, tag) - } - - if err := view.Register( - &view.View{ - Description: requestCountM.Description(), - Measure: requestCountM, - Aggregation: view.Count(), - TagKeys: tagKeys, - }, - &view.View{ - Description: responseTimeInMsecM.Description(), - Measure: responseTimeInMsecM, - Aggregation: view.Distribution(metrics.Buckets125(1, 100000)...), // [1 2 5 10 20 50 100 200 500 1000 2000 5000 10000 20000 50000 100000]ms - TagKeys: tagKeys, - }, - ); err != nil { - panic(err) - } -} diff --git a/webhook/stats_reporter_test.go b/webhook/stats_reporter_test.go deleted file mode 100644 index 84d63a3707..0000000000 --- a/webhook/stats_reporter_test.go +++ /dev/null @@ -1,155 +0,0 @@ -/* -Copyright 2019 The Knative Authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package webhook - -import ( - "strconv" - "testing" - "time" - - admissionv1 "k8s.io/api/admission/v1" - apixv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "knative.dev/pkg/metrics/metricstest" - _ "knative.dev/pkg/metrics/testing" -) - -func TestWebhookStatsReporterAdmission(t *testing.T) { - setup() - req := &admissionv1.AdmissionRequest{ - UID: "705ab4f5-6393-11e8-b7cc-42010a800002", - Kind: metav1.GroupVersionKind{Group: "autoscaling", Version: "v1", Kind: "Scale"}, - Resource: metav1.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}, - Name: "my-deployment", - Namespace: "my-namespace", - Operation: admissionv1.Update, - } - - resp := &admissionv1.AdmissionResponse{ - UID: req.UID, - Allowed: true, - } - - r, _ := NewStatsReporter() - - shortTime, longTime := 1100.0, 9100.0 - expectedTags := map[string]string{ - requestOperationKey.Name(): string(req.Operation), - kindGroupKey.Name(): req.Kind.Group, - kindVersionKey.Name(): req.Kind.Version, - kindKindKey.Name(): req.Kind.Kind, - resourceGroupKey.Name(): req.Resource.Group, - resourceVersionKey.Name(): req.Resource.Version, - resourceResourceKey.Name(): req.Resource.Resource, - resourceNamespaceKey.Name(): req.Namespace, - admissionAllowedKey.Name(): strconv.FormatBool(resp.Allowed), - } - - if err := r.ReportAdmissionRequest(req, resp, time.Duration(shortTime)*time.Millisecond); err != nil { - t.Fatalf("ReportAdmissionRequest() = %v", err) - } - if err := r.ReportAdmissionRequest(req, resp, time.Duration(longTime)*time.Millisecond); err != nil { - t.Fatalf("ReportAdmissionRequest() = %v", err) - } - - metricstest.CheckCountData(t, requestCountName, expectedTags, 2) - metricstest.CheckDistributionData(t, requestLatenciesName, expectedTags, 2, shortTime, longTime) -} - -func TestWebhookStatsReporterAdmissionWithoutNamespaceTag(t *testing.T) { - setup(WithoutTags(resourceNamespaceKey.Name())) - req := &admissionv1.AdmissionRequest{ - UID: "705ab4f5-6393-11e8-b7cc-42010a800002", - Kind: metav1.GroupVersionKind{Group: "autoscaling", Version: "v1", Kind: "Scale"}, - Resource: metav1.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}, - Name: "my-deployment", - Namespace: "my-namespace", - Operation: admissionv1.Update, - } - - resp := &admissionv1.AdmissionResponse{ - UID: req.UID, - Allowed: true, - } - - r, _ := NewStatsReporter(WithoutTags(resourceNamespaceKey.Name())) - - shortTime, longTime := 1100.0, 9100.0 - expectedTags := map[string]string{ - requestOperationKey.Name(): string(req.Operation), - kindGroupKey.Name(): req.Kind.Group, - kindVersionKey.Name(): req.Kind.Version, - kindKindKey.Name(): req.Kind.Kind, - resourceGroupKey.Name(): req.Resource.Group, - resourceVersionKey.Name(): req.Resource.Version, - resourceResourceKey.Name(): req.Resource.Resource, - admissionAllowedKey.Name(): strconv.FormatBool(resp.Allowed), - } - - if err := r.ReportAdmissionRequest(req, resp, time.Duration(shortTime)*time.Millisecond); err != nil { - t.Fatalf("ReportAdmissionRequest() = %v", err) - } - if err := r.ReportAdmissionRequest(req, resp, time.Duration(longTime)*time.Millisecond); err != nil { - t.Fatalf("ReportAdmissionRequest() = %v", err) - } - - metricstest.CheckCountData(t, requestCountName, expectedTags, 2) - metricstest.CheckDistributionData(t, requestLatenciesName, expectedTags, 2, shortTime, longTime) -} - -func TestWebhookStatsReporterConversion(t *testing.T) { - setup() - req := &apixv1.ConversionRequest{ - UID: "705ab4f5-6393-11e8-b7cc-42010a800003", - DesiredAPIVersion: "knative.dev/v1", - } - - resp := &apixv1.ConversionResponse{ - UID: req.UID, - Result: metav1.Status{Status: "Failure", Reason: metav1.StatusReasonNotFound, Code: 404}, - } - - r, _ := NewStatsReporter() - - shortTime, longTime := 1100.0, 9100.0 - expectedTags := map[string]string{ - desiredAPIVersionKey.Name(): req.DesiredAPIVersion, - resultStatusKey.Name(): resp.Result.Status, - resultReasonKey.Name(): string(resp.Result.Reason), - resultCodeKey.Name(): strconv.Itoa(int(resp.Result.Code)), - } - - if err := r.ReportConversionRequest(req, resp, time.Duration(shortTime)*time.Millisecond); err != nil { - t.Fatalf("ReportConversionRequest() = %v", err) - } - if err := r.ReportConversionRequest(req, resp, time.Duration(longTime)*time.Millisecond); err != nil { - t.Fatalf("ReportConversionRequest() = %v", err) - } - - metricstest.CheckCountData(t, requestCountName, expectedTags, 2) - metricstest.CheckDistributionData(t, requestLatenciesName, expectedTags, 2, shortTime, longTime) -} - -func setup(opts ...StatsReporterOption) { - resetMetrics(opts...) -} - -// opencensus metrics carry global state that need to be reset between unit tests -func resetMetrics(opts ...StatsReporterOption) { - metricstest.Unregister(requestCountName, requestLatenciesName) - RegisterMetrics(opts...) -} diff --git a/webhook/webhook.go b/webhook/webhook.go index 9dc736b40c..a3acaf248e 100644 --- a/webhook/webhook.go +++ b/webhook/webhook.go @@ -33,6 +33,11 @@ import ( kubeinformerfactory "knative.dev/pkg/injection/clients/namespacedkube/informers/factory" "knative.dev/pkg/network/handlers" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" "golang.org/x/sync/errgroup" admissionv1 "k8s.io/api/admission/v1" @@ -70,13 +75,6 @@ type Options struct { // only a single port for the service. Port int - // StatsReporterOptions are the options used to initialize the default StatsReporter - StatsReporterOptions []StatsReporterOption - - // StatsReporter reports metrics about the webhook. - // This will be automatically initialized by the constructor if left uninitialized. - StatsReporter StatsReporter - // GracePeriod is how long to wait after failing readiness probes // before shutting down. GracePeriod time.Duration @@ -101,6 +99,18 @@ type Options struct { // * https://github.com/kubernetes/kubernetes/issues/121197 // * https://github.com/golang/go/issues/63417#issuecomment-1758858612 EnableHTTP2 bool + + // MeterProvider is used to configure the MeterProvider used by the webhook + // If nil it will use the global meter provider + MeterProvider metric.MeterProvider + + // TracerProvider is used to config the TracerProvider used by the webhook + // if nil it will use the global tracer provider + TracerProvider trace.TracerProvider + + // TextMapPropagator is used to configure the TextMapPropagator used by the webhook + // if nil it will use the global text map propagator + TextMapPropagator propagation.TextMapPropagator } // Operation is the verb being operated on @@ -131,6 +141,8 @@ type Webhook struct { // testListener is only used in testing so we don't get port conflicts testListener net.Listener + + metrics *metrics } // New constructs a Webhook @@ -149,15 +161,8 @@ func New( if opts == nil { return nil, errors.New("context must have Options specified") } - logger := logging.FromContext(ctx) - if opts.StatsReporter == nil { - reporter, err := NewStatsReporter(opts.StatsReporterOptions...) - if err != nil { - return nil, err - } - opts.StatsReporter = reporter - } + logger := logging.FromContext(ctx) defaultTLSMinVersion := uint16(tls.VersionTLS13) if opts.TLSMinVersion == 0 { @@ -172,6 +177,7 @@ func New( Options: *opts, Logger: logger, synced: cancel, + metrics: newMetrics(*opts), } if opts.SecretName != "" { @@ -224,12 +230,12 @@ func New( for _, controller := range controllers { switch c := controller.(type) { case AdmissionController: - handler := admissionHandler(logger, opts.StatsReporter, c, syncCtx.Done()) - webhook.mux.Handle(c.Path(), handler) + handler := admissionHandler(webhook, c, syncCtx.Done()) + webhook.mux.Handle(c.Path(), otelhttp.WithRouteTag(c.Path(), handler)) case ConversionController: - handler := conversionHandler(logger, opts.StatsReporter, c) - webhook.mux.Handle(c.Path(), handler) + handler := conversionHandler(webhook, c) + webhook.mux.Handle(c.Path(), otelhttp.WithRouteTag(c.Path(), handler)) default: return nil, fmt.Errorf("unknown webhook controller type: %T", controller) @@ -265,6 +271,14 @@ func (wh *Webhook) Run(stop <-chan struct{}) error { QuietPeriod: wh.Options.GracePeriod, } + otelHandler := otelhttp.NewHandler( + drainer, + wh.Options.ServiceName, + otelhttp.WithMeterProvider(wh.Options.MeterProvider), + otelhttp.WithTracerProvider(wh.Options.TracerProvider), + otelhttp.WithPropagators(wh.Options.TextMapPropagator), + ) + // If TLSNextProto is not nil, HTTP/2 support is not enabled automatically. nextProto := map[string]func(*http.Server, *tls.Conn, http.Handler){} if wh.Options.EnableHTTP2 { @@ -273,7 +287,7 @@ func (wh *Webhook) Run(stop <-chan struct{}) error { server := &http.Server{ ErrorLog: log.New(&zapWrapper{logger}, "", 0), - Handler: drainer, + Handler: otelHandler, Addr: fmt.Sprint(":", wh.Options.Port), TLSConfig: wh.tlsConfig, ReadHeaderTimeout: time.Minute, // https://medium.com/a-journey-with-go/go-understand-and-mitigate-slowloris-attack-711c1b1403f6 diff --git a/webhook/webhook_integration_test.go b/webhook/webhook_integration_test.go index e34182b939..fc4c1b8ed9 100644 --- a/webhook/webhook_integration_test.go +++ b/webhook/webhook_integration_test.go @@ -26,15 +26,18 @@ import ( "testing" "time" - kubeclient "knative.dev/pkg/client/injection/kube/client/fake" - _ "knative.dev/pkg/injection/clients/namespacedkube/informers/core/v1/secret/fake" - "knative.dev/pkg/system" - "golang.org/x/sync/errgroup" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "knative.dev/pkg/metrics/metricstest" + kubeclient "knative.dev/pkg/client/injection/kube/client/fake" + + "go.opentelemetry.io/otel/sdk/metric" + + "knative.dev/pkg/system" pkgtest "knative.dev/pkg/testing" certresources "knative.dev/pkg/webhook/certificates/resources" + + _ "knative.dev/pkg/injection/clients/namespacedkube/informers/core/v1/secret/fake" ) // createResource creates a testing.Resource with the given name in the system namespace. @@ -97,9 +100,6 @@ func TestMissingContentType(t *testing.T) { if !strings.Contains(string(responseBody), "invalid Content-Type") { t.Errorf("Response body to contain 'invalid Content-Type' , got = '%s'", string(responseBody)) } - - // Stats are not reported for internal server errors - metricstest.CheckStatsNotReported(t, requestCountName, requestLatenciesName) } func TestServerWithCustomSecret(t *testing.T) { @@ -121,7 +121,7 @@ func TestServerWithCustomSecret(t *testing.T) { } } -func testEmptyRequestBody(t *testing.T, controller interface{}) { +func testEmptyRequestBody(t *testing.T, controller any) { test := testSetup(t, withController(controller)) eg, _ := errgroup.WithContext(test.ctx) @@ -217,6 +217,10 @@ func testSetup(t *testing.T, opts ...func(*testOptions)) testContext { Options: newDefaultOptions(), } + reader := metric.NewManualReader() + provider := metric.NewMeterProvider(metric.WithReader(reader)) + testOpts.Options.MeterProvider = provider + for _, opt := range opts { opt(testOpts) } @@ -246,8 +250,21 @@ func testSetup(t *testing.T, opts ...func(*testOptions)) testContext { t.Fatalf("failed to create secret") } - resetMetrics() - return testContext{wh, l.Addr().String(), ctx, cancel} + return testContext{ + webhook: wh, + addr: l.Addr().String(), + ctx: ctx, + cancel: cancel, + metricReader: reader, + } +} + +type testContext struct { + webhook *Webhook + addr string + ctx context.Context + cancel context.CancelFunc + metricReader *metric.ManualReader } type testOptions struct { diff --git a/webhook/webhook_test.go b/webhook/webhook_test.go index ec42a8e06d..fcc3bda1f0 100644 --- a/webhook/webhook_test.go +++ b/webhook/webhook_test.go @@ -124,10 +124,3 @@ func TestTLSMinVersionWebhookOption(t *testing.T) { } }) } - -type testContext struct { - webhook *Webhook - addr string - ctx context.Context - cancel context.CancelFunc -}