diff --git a/.chloggen/add-new-server-option.yaml b/.chloggen/add-new-server-option.yaml new file mode 100644 index 00000000000..7b1e066a8e4 --- /dev/null +++ b/.chloggen/add-new-server-option.yaml @@ -0,0 +1,25 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver) +component: confighttp + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add an option to add prefix for span name for components + +# One or more tracking issues or pull requests related to the change +issues: [9382] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [] diff --git a/config/confighttp/confighttp.go b/config/confighttp/confighttp.go index 69ac3900fe5..961c9300c14 100644 --- a/config/confighttp/confighttp.go +++ b/config/confighttp/confighttp.go @@ -379,6 +379,7 @@ func (hss *ServerConfig) ToListener(ctx context.Context) (net.Listener, error) { type toServerOptions struct { errHandler func(w http.ResponseWriter, r *http.Request, errorMsg string, statusCode int) decoders map[string]func(body io.ReadCloser) (io.ReadCloser, error) + formater func(string, *http.Request) string } // ToServerOption is an option to change the behavior of the HTTP server @@ -412,11 +413,21 @@ func WithDecoder(key string, dec func(body io.ReadCloser) (io.ReadCloser, error) }) } +// WithSpanFormatter specifies which formater to use for span. +// Ideally, this prefix in span name should be the component's ID. +func WithSpanFormatter(formater func(string, *http.Request) string) ToServerOption { + return toServerOptionFunc(func(opts *toServerOptions) { + opts.formater = formater + }) +} + // ToServer creates an http.Server from settings object. func (hss *ServerConfig) ToServer(_ context.Context, host component.Host, settings component.TelemetrySettings, handler http.Handler, opts ...ToServerOption) (*http.Server, error) { internal.WarnOnUnspecifiedHost(settings.Logger, hss.Endpoint) - serverOpts := &toServerOptions{} + serverOpts := &toServerOptions{ + formater: PrefixFormatter(""), // use empty-prefix formater by default + } for _, o := range opts { o.apply(serverOpts) } @@ -464,14 +475,11 @@ func (hss *ServerConfig) ToServer(_ context.Context, host component.Host, settin otelOpts := []otelhttp.Option{ otelhttp.WithTracerProvider(settings.TracerProvider), otelhttp.WithPropagators(otel.GetTextMapPropagator()), - otelhttp.WithSpanNameFormatter(func(_ string, r *http.Request) string { - return r.URL.Path - }), + otelhttp.WithSpanNameFormatter(serverOpts.formater), otelhttp.WithMeterProvider(getLeveledMeterProvider(settings)), } // Enable OpenTelemetry observability plugin. - // TODO: Consider to use component ID string as prefix for all the operations. handler = otelhttp.NewHandler(handler, "", otelOpts...) // wrap the current handler in an interceptor that will add client.Info to the request's context @@ -556,6 +564,15 @@ func maxRequestBodySizeInterceptor(next http.Handler, maxRecvSize int64) http.Ha }) } +func PrefixFormatter(prefix string) func(string, *http.Request) string { + return func(_ string, r *http.Request) string { + if len(prefix) > 0 { + return fmt.Sprintf("%s:%s", prefix, r.URL.Path) + } + return r.URL.Path + } +} + func getLeveledMeterProvider(settings component.TelemetrySettings) metric.MeterProvider { if configtelemetry.LevelDetailed <= settings.MetricsLevel { return settings.MeterProvider diff --git a/config/confighttp/confighttp_test.go b/config/confighttp/confighttp_test.go index 5671bcb5fd1..54a2ce46b3a 100644 --- a/config/confighttp/confighttp_test.go +++ b/config/confighttp/confighttp_test.go @@ -21,6 +21,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/embedded" + "go.opentelemetry.io/otel/trace/noop" "go.uber.org/zap" "go.uber.org/zap/zaptest/observer" @@ -1530,3 +1533,90 @@ func TestDefaultHTTPServerSettings(t *testing.T) { assert.Equal(t, time.Duration(0), httpServerSettings.ReadTimeout) assert.Equal(t, 1*time.Minute, httpServerSettings.ReadHeaderTimeout) } + +// TracerProvider is an OpenTelemetry No-Op TracerProvider. +type TracerProvider struct { + embedded.TracerProvider + ch chan string +} + +// NewTracerProvider returns a TracerProvider that does not record any telemetry. +func NewTracerProvider(ch chan string) TracerProvider { + return TracerProvider{ch: ch} +} + +// Tracer returns an OpenTelemetry Tracer that does not record any telemetry. +func (t TracerProvider) Tracer(string, ...trace.TracerOption) trace.Tracer { + return Tracer{ch: t.ch} +} + +// Tracer is an OpenTelemetry No-Op Tracer. +type Tracer struct { + embedded.Tracer + ch chan string +} + +type Span struct { + noop.Span + name string +} + +func (t Tracer) Start(ctx context.Context, name string, _ ...trace.SpanStartOption) (context.Context, trace.Span) { + t.ch <- name + return ctx, Span{name: name} +} + +func TestOperationPrefix(t *testing.T) { + tests := []struct { + name string + prefix string + expectedSpanName string + }{ + { + name: "componentID prefix", + prefix: "otlphttpreceiver", + expectedSpanName: "otlphttpreceiver:/", + }, + { + name: "empty prefix", + prefix: "", + expectedSpanName: "/", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + nameChan := make(chan string, 1) + set := componenttest.NewNopTelemetrySettings() + set.TracerProvider = TracerProvider{ch: nameChan} + logger, _ := observer.New(zap.DebugLevel) + set.Logger = zap.New(logger) + setting := &ServerConfig{ + Endpoint: "localhost:0", + } + + ln, err := setting.ToListener(context.Background()) + require.NoError(t, err) + s, err := setting.ToServer( + context.Background(), + componenttest.NewNopHost(), + set, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }), + WithSpanFormatter(PrefixFormatter(tt.prefix)), + ) + require.NoError(t, err) + go func() { + _ = s.Serve(ln) + }() + status, err := http.Get("http://" + ln.Addr().String()) + require.NoError(t, err) + require.Equal(t, http.StatusOK, status.StatusCode) + require.NoError(t, s.Close()) + + spanName := <-nameChan + require.Equal(t, tt.expectedSpanName, spanName) + require.False(t, strings.HasPrefix(spanName, ":")) + }) + } +} diff --git a/config/confighttp/go.mod b/config/confighttp/go.mod index f346a77dfdf..9d9eb685c54 100644 --- a/config/confighttp/go.mod +++ b/config/confighttp/go.mod @@ -21,6 +21,7 @@ require ( go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 go.opentelemetry.io/otel v1.32.0 go.opentelemetry.io/otel/metric v1.32.0 + go.opentelemetry.io/otel/trace v1.32.0 go.uber.org/goleak v1.3.0 go.uber.org/zap v1.27.0 golang.org/x/net v0.31.0 @@ -39,7 +40,6 @@ require ( go.opentelemetry.io/collector/pdata v1.20.0 // indirect go.opentelemetry.io/otel/sdk v1.32.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect - go.opentelemetry.io/otel/trace v1.32.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/sys v0.27.0 // indirect golang.org/x/text v0.20.0 // indirect diff --git a/receiver/otlpreceiver/otlp.go b/receiver/otlpreceiver/otlp.go index a4c5f08c7a9..b289f287f4f 100644 --- a/receiver/otlpreceiver/otlp.go +++ b/receiver/otlpreceiver/otlp.go @@ -162,7 +162,10 @@ func (r *otlpReceiver) startHTTPServer(ctx context.Context, host component.Host) } var err error - if r.serverHTTP, err = r.cfg.HTTP.ToServer(ctx, host, r.settings.TelemetrySettings, httpMux, confighttp.WithErrorHandler(errorHandler)); err != nil { + if r.serverHTTP, err = r.cfg.HTTP.ToServer(ctx, host, r.settings.TelemetrySettings, httpMux, + confighttp.WithErrorHandler(errorHandler), + confighttp.WithSpanFormatter(confighttp.PrefixFormatter(r.settings.ID.String())), + ); err != nil { return err }