-
Notifications
You must be signed in to change notification settings - Fork 106
feat: [#726] Add HTTP server and client telemetry instrumentation [5] #1326
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds HTTP server and client telemetry instrumentation capabilities to the Goravel framework, enabling automatic tracing and metrics collection for HTTP requests and responses. This is part of the broader telemetry instrumentation feature set (issue #726).
Key Changes:
- Implemented HTTP middleware for server-side request instrumentation with configurable filtering
- Created HTTP client transport wrapper for outbound request instrumentation
- Added configuration support for HTTP server instrumentation with exclusion patterns
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
telemetry/setup/stubs.go |
Adds instrumentation configuration section with http_server settings for enabled state and path/method exclusions |
telemetry/instrumentation/http/middleware.go |
Implements Telemetry middleware that captures traces, metrics, and handles panics for incoming HTTP requests |
telemetry/instrumentation/http/middleware_test.go |
Provides test suite with test helpers for middleware functionality including success cases, exclusions, disabled state, and panic handling |
telemetry/instrumentation/http/config.go |
Defines ServerConfig structure and option functions for customizing HTTP server instrumentation behavior |
telemetry/instrumentation/http/transport.go |
Implements NewTransport function to wrap HTTP clients with telemetry instrumentation |
telemetry/instrumentation/http/transport_test.go |
Tests for transport wrapper covering fallback scenarios and successful wrapping cases |
go.mod |
Adds dependency on otelhttp instrumentation library |
go.sum |
Updates dependency checksums for otelhttp and its transitive dependency httpsnoop |
π‘ Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "github.com/goravel/framework/support/color" | ||
| "github.com/goravel/framework/telemetry" | ||
| ) | ||
|
|
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The NewTransport function lacks documentation. As a public API function, it should have a comment explaining its purpose, parameters, and return value. This is especially important for exported functions that developers will use to instrument their HTTP clients.
| // NewTransport returns an http.RoundTripper instrumented with OpenTelemetry. | |
| // It wraps the provided base RoundTripper with otelhttp using the configured | |
| // telemetry facade's tracer provider, meter provider, and propagator. | |
| // | |
| // If telemetry.TelemetryFacade is nil, a warning is logged and no | |
| // instrumentation is applied. In that case, http.DefaultTransport is returned | |
| // when base is nil; otherwise the provided base RoundTripper is returned. |
| func (s *MiddlewareTestSuite) TestTelemetry() { | ||
| defaultTelemetrySetup := func(mockTelemetry *telemetrymocks.Telemetry) { | ||
| mockTelemetry.EXPECT().Tracer(instrumentationName).Return(tracenoop.NewTracerProvider().Tracer("test")).Once() | ||
| mockTelemetry.EXPECT().Meter(instrumentationName).Return(metricnoop.NewMeterProvider().Meter("test")).Once() | ||
| mockTelemetry.EXPECT().Propagator().Return(propagation.NewCompositeTextMapPropagator()).Once() | ||
| } | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| configSetup func(*configmocks.Config) | ||
| telemetrySetup func(*telemetrymocks.Telemetry) | ||
| handler nethttp.HandlerFunc | ||
| requestPath string | ||
| expectPanic bool | ||
| }{ | ||
| { | ||
| name: "Success: Request is traced and metrics recorded", | ||
| requestPath: "/users", | ||
| configSetup: func(mockConfig *configmocks.Config) { | ||
| mockConfig.EXPECT().UnmarshalKey("telemetry.instrumentation.http_server", mock.Anything). | ||
| Run(func(_ string, cfg any) { | ||
| c := cfg.(*ServerConfig) | ||
| c.Enabled = true | ||
| }).Return(nil).Once() | ||
| }, | ||
| telemetrySetup: defaultTelemetrySetup, | ||
| handler: func(w nethttp.ResponseWriter, r *nethttp.Request) { | ||
| w.WriteHeader(nethttp.StatusOK) | ||
| _, _ = w.Write([]byte("OK")) | ||
| }, | ||
| }, | ||
| { | ||
| name: "Ignored: Excluded path is skipped", | ||
| requestPath: "/health", | ||
| configSetup: func(mockConfig *configmocks.Config) { | ||
| mockConfig.EXPECT().UnmarshalKey("telemetry.instrumentation.http_server", mock.Anything). | ||
| Run(func(_ string, cfg interface{}) { | ||
| c := cfg.(*ServerConfig) | ||
| c.Enabled = true | ||
| c.ExcludedPaths = []string{"/health"} | ||
| }).Return(nil).Once() | ||
| }, | ||
| telemetrySetup: defaultTelemetrySetup, | ||
| handler: func(w nethttp.ResponseWriter, r *nethttp.Request) { | ||
| w.WriteHeader(nethttp.StatusOK) | ||
| }, | ||
| }, | ||
| { | ||
| name: "Ignored: Disabled via config", | ||
| requestPath: "/users", | ||
| configSetup: func(mockConfig *configmocks.Config) { | ||
| mockConfig.EXPECT().UnmarshalKey("telemetry.instrumentation.http_server", mock.Anything). | ||
| Run(func(_ string, cfg interface{}) { | ||
| c := cfg.(*ServerConfig) | ||
| c.Enabled = false | ||
| }).Return(nil).Once() | ||
| }, | ||
| telemetrySetup: func(mockTelemetry *telemetrymocks.Telemetry) { | ||
| // If disabled, Tracer/Meter should NOT be initialized | ||
| }, | ||
| handler: func(w nethttp.ResponseWriter, r *nethttp.Request) { | ||
| w.WriteHeader(nethttp.StatusOK) | ||
| }, | ||
| }, | ||
| { | ||
| name: "Panic: Metrics recorded as 500 and panic re-thrown", | ||
| requestPath: "/crash", | ||
| expectPanic: true, | ||
| configSetup: func(mockConfig *configmocks.Config) { | ||
| mockConfig.EXPECT().UnmarshalKey("telemetry.instrumentation.http_server", mock.Anything). | ||
| Run(func(_ string, cfg any) { | ||
| c := cfg.(*ServerConfig) | ||
| c.Enabled = true | ||
| }).Return(nil).Once() | ||
| }, | ||
| telemetrySetup: defaultTelemetrySetup, | ||
| handler: func(w nethttp.ResponseWriter, r *nethttp.Request) { | ||
| panic("server crash") | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| s.Run(tt.name, func() { | ||
| mockConfig := configmocks.NewConfig(s.T()) | ||
| mockTelemetry := telemetrymocks.NewTelemetry(s.T()) | ||
|
|
||
| telemetry.ConfigFacade = mockConfig | ||
| telemetry.TelemetryFacade = mockTelemetry | ||
|
|
||
| tt.configSetup(mockConfig) | ||
| tt.telemetrySetup(mockTelemetry) | ||
|
|
||
| handler := testMiddleware(tt.handler) | ||
| server := httptest.NewServer(handler) | ||
| defer server.Close() | ||
|
|
||
| client := &nethttp.Client{} | ||
|
|
||
| action := func() { | ||
| _, err := client.Get(server.URL + tt.requestPath) | ||
| if !tt.expectPanic { | ||
| s.NoError(err) | ||
| } | ||
| } | ||
|
|
||
| action() | ||
| }) | ||
| } | ||
| } |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The SpanNameFormatter option is not covered by tests. While the middleware uses defaultSpanNameFormatter internally, there are no tests validating that custom SpanNameFormatter functions can be provided through the WithSpanNameFormatter option.
| func (s *MiddlewareTestSuite) TestTelemetry() { | ||
| defaultTelemetrySetup := func(mockTelemetry *telemetrymocks.Telemetry) { | ||
| mockTelemetry.EXPECT().Tracer(instrumentationName).Return(tracenoop.NewTracerProvider().Tracer("test")).Once() | ||
| mockTelemetry.EXPECT().Meter(instrumentationName).Return(metricnoop.NewMeterProvider().Meter("test")).Once() | ||
| mockTelemetry.EXPECT().Propagator().Return(propagation.NewCompositeTextMapPropagator()).Once() | ||
| } | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| configSetup func(*configmocks.Config) | ||
| telemetrySetup func(*telemetrymocks.Telemetry) | ||
| handler nethttp.HandlerFunc | ||
| requestPath string | ||
| expectPanic bool | ||
| }{ | ||
| { | ||
| name: "Success: Request is traced and metrics recorded", | ||
| requestPath: "/users", | ||
| configSetup: func(mockConfig *configmocks.Config) { | ||
| mockConfig.EXPECT().UnmarshalKey("telemetry.instrumentation.http_server", mock.Anything). | ||
| Run(func(_ string, cfg any) { | ||
| c := cfg.(*ServerConfig) | ||
| c.Enabled = true | ||
| }).Return(nil).Once() | ||
| }, | ||
| telemetrySetup: defaultTelemetrySetup, | ||
| handler: func(w nethttp.ResponseWriter, r *nethttp.Request) { | ||
| w.WriteHeader(nethttp.StatusOK) | ||
| _, _ = w.Write([]byte("OK")) | ||
| }, | ||
| }, | ||
| { | ||
| name: "Ignored: Excluded path is skipped", | ||
| requestPath: "/health", | ||
| configSetup: func(mockConfig *configmocks.Config) { | ||
| mockConfig.EXPECT().UnmarshalKey("telemetry.instrumentation.http_server", mock.Anything). | ||
| Run(func(_ string, cfg interface{}) { | ||
| c := cfg.(*ServerConfig) | ||
| c.Enabled = true | ||
| c.ExcludedPaths = []string{"/health"} | ||
| }).Return(nil).Once() | ||
| }, | ||
| telemetrySetup: defaultTelemetrySetup, | ||
| handler: func(w nethttp.ResponseWriter, r *nethttp.Request) { | ||
| w.WriteHeader(nethttp.StatusOK) | ||
| }, | ||
| }, | ||
| { | ||
| name: "Ignored: Disabled via config", | ||
| requestPath: "/users", | ||
| configSetup: func(mockConfig *configmocks.Config) { | ||
| mockConfig.EXPECT().UnmarshalKey("telemetry.instrumentation.http_server", mock.Anything). | ||
| Run(func(_ string, cfg interface{}) { | ||
| c := cfg.(*ServerConfig) | ||
| c.Enabled = false | ||
| }).Return(nil).Once() | ||
| }, | ||
| telemetrySetup: func(mockTelemetry *telemetrymocks.Telemetry) { | ||
| // If disabled, Tracer/Meter should NOT be initialized | ||
| }, | ||
| handler: func(w nethttp.ResponseWriter, r *nethttp.Request) { | ||
| w.WriteHeader(nethttp.StatusOK) | ||
| }, | ||
| }, | ||
| { | ||
| name: "Panic: Metrics recorded as 500 and panic re-thrown", | ||
| requestPath: "/crash", | ||
| expectPanic: true, | ||
| configSetup: func(mockConfig *configmocks.Config) { | ||
| mockConfig.EXPECT().UnmarshalKey("telemetry.instrumentation.http_server", mock.Anything). | ||
| Run(func(_ string, cfg any) { | ||
| c := cfg.(*ServerConfig) | ||
| c.Enabled = true | ||
| }).Return(nil).Once() | ||
| }, | ||
| telemetrySetup: defaultTelemetrySetup, | ||
| handler: func(w nethttp.ResponseWriter, r *nethttp.Request) { | ||
| panic("server crash") | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| s.Run(tt.name, func() { | ||
| mockConfig := configmocks.NewConfig(s.T()) | ||
| mockTelemetry := telemetrymocks.NewTelemetry(s.T()) | ||
|
|
||
| telemetry.ConfigFacade = mockConfig | ||
| telemetry.TelemetryFacade = mockTelemetry | ||
|
|
||
| tt.configSetup(mockConfig) | ||
| tt.telemetrySetup(mockTelemetry) | ||
|
|
||
| handler := testMiddleware(tt.handler) | ||
| server := httptest.NewServer(handler) | ||
| defer server.Close() | ||
|
|
||
| client := &nethttp.Client{} | ||
|
|
||
| action := func() { | ||
| _, err := client.Get(server.URL + tt.requestPath) | ||
| if !tt.expectPanic { | ||
| s.NoError(err) | ||
| } | ||
| } | ||
|
|
||
| action() | ||
| }) | ||
| } | ||
| } |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The MetricAttributes option is not covered by tests. While the middleware supports custom metric attributes through the WithMetricAttributes option, there are no tests validating this functionality works correctly.
| color.Warningln("[Telemetry] Facade not initialized. HTTP middleware disabled.") | ||
| return func(ctx http.Context) { ctx.Request().Next() } | ||
| } | ||
|
|
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code does not check if ConfigFacade is nil before calling UnmarshalKey, which could cause a panic. Based on the pattern used in the log instrumentation (telemetry/instrumentation/log/channel.go), ConfigFacade should be validated before use. Consider adding a nil check similar to the TelemetryFacade check.
| if telemetry.ConfigFacade == nil { | |
| color.Warningln("[Telemetry] Config facade not initialized. HTTP middleware disabled.") | |
| return func(ctx http.Context) { ctx.Request().Next() } | |
| } |
| originalTelemetry contractstelemetry.Telemetry | ||
| } | ||
|
|
||
| func (s *MiddlewareTestSuite) SetupTest() { | ||
| s.originalTelemetry = telemetry.TelemetryFacade | ||
| } | ||
|
|
||
| func (s *MiddlewareTestSuite) TearDownTest() { | ||
| telemetry.TelemetryFacade = s.originalTelemetry |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test suite modifies the global ConfigFacade in tests (line 128) but does not restore it in TearDownTest. This could cause test pollution and affect other tests. Consider storing the original ConfigFacade in SetupTest and restoring it in TearDownTest, similar to how TelemetryFacade is handled.
| originalTelemetry contractstelemetry.Telemetry | |
| } | |
| func (s *MiddlewareTestSuite) SetupTest() { | |
| s.originalTelemetry = telemetry.TelemetryFacade | |
| } | |
| func (s *MiddlewareTestSuite) TearDownTest() { | |
| telemetry.TelemetryFacade = s.originalTelemetry | |
| originalTelemetry contractstelemetry.Telemetry | |
| originalConfig interface{} | |
| } | |
| func (s *MiddlewareTestSuite) SetupTest() { | |
| s.originalTelemetry = telemetry.TelemetryFacade | |
| s.originalConfig = telemetry.ConfigFacade | |
| } | |
| func (s *MiddlewareTestSuite) TearDownTest() { | |
| telemetry.TelemetryFacade = s.originalTelemetry | |
| telemetry.ConfigFacade = s.originalConfig |
| func (s *MiddlewareTestSuite) TestTelemetry() { | ||
| defaultTelemetrySetup := func(mockTelemetry *telemetrymocks.Telemetry) { | ||
| mockTelemetry.EXPECT().Tracer(instrumentationName).Return(tracenoop.NewTracerProvider().Tracer("test")).Once() | ||
| mockTelemetry.EXPECT().Meter(instrumentationName).Return(metricnoop.NewMeterProvider().Meter("test")).Once() | ||
| mockTelemetry.EXPECT().Propagator().Return(propagation.NewCompositeTextMapPropagator()).Once() | ||
| } | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| configSetup func(*configmocks.Config) | ||
| telemetrySetup func(*telemetrymocks.Telemetry) | ||
| handler nethttp.HandlerFunc | ||
| requestPath string | ||
| expectPanic bool | ||
| }{ | ||
| { | ||
| name: "Success: Request is traced and metrics recorded", | ||
| requestPath: "/users", | ||
| configSetup: func(mockConfig *configmocks.Config) { | ||
| mockConfig.EXPECT().UnmarshalKey("telemetry.instrumentation.http_server", mock.Anything). | ||
| Run(func(_ string, cfg any) { | ||
| c := cfg.(*ServerConfig) | ||
| c.Enabled = true | ||
| }).Return(nil).Once() | ||
| }, | ||
| telemetrySetup: defaultTelemetrySetup, | ||
| handler: func(w nethttp.ResponseWriter, r *nethttp.Request) { | ||
| w.WriteHeader(nethttp.StatusOK) | ||
| _, _ = w.Write([]byte("OK")) | ||
| }, | ||
| }, | ||
| { | ||
| name: "Ignored: Excluded path is skipped", | ||
| requestPath: "/health", | ||
| configSetup: func(mockConfig *configmocks.Config) { | ||
| mockConfig.EXPECT().UnmarshalKey("telemetry.instrumentation.http_server", mock.Anything). | ||
| Run(func(_ string, cfg interface{}) { | ||
| c := cfg.(*ServerConfig) | ||
| c.Enabled = true | ||
| c.ExcludedPaths = []string{"/health"} | ||
| }).Return(nil).Once() | ||
| }, | ||
| telemetrySetup: defaultTelemetrySetup, | ||
| handler: func(w nethttp.ResponseWriter, r *nethttp.Request) { | ||
| w.WriteHeader(nethttp.StatusOK) | ||
| }, | ||
| }, | ||
| { | ||
| name: "Ignored: Disabled via config", | ||
| requestPath: "/users", | ||
| configSetup: func(mockConfig *configmocks.Config) { | ||
| mockConfig.EXPECT().UnmarshalKey("telemetry.instrumentation.http_server", mock.Anything). | ||
| Run(func(_ string, cfg interface{}) { | ||
| c := cfg.(*ServerConfig) | ||
| c.Enabled = false | ||
| }).Return(nil).Once() | ||
| }, | ||
| telemetrySetup: func(mockTelemetry *telemetrymocks.Telemetry) { | ||
| // If disabled, Tracer/Meter should NOT be initialized | ||
| }, | ||
| handler: func(w nethttp.ResponseWriter, r *nethttp.Request) { | ||
| w.WriteHeader(nethttp.StatusOK) | ||
| }, | ||
| }, | ||
| { | ||
| name: "Panic: Metrics recorded as 500 and panic re-thrown", | ||
| requestPath: "/crash", | ||
| expectPanic: true, | ||
| configSetup: func(mockConfig *configmocks.Config) { | ||
| mockConfig.EXPECT().UnmarshalKey("telemetry.instrumentation.http_server", mock.Anything). | ||
| Run(func(_ string, cfg any) { | ||
| c := cfg.(*ServerConfig) | ||
| c.Enabled = true | ||
| }).Return(nil).Once() | ||
| }, | ||
| telemetrySetup: defaultTelemetrySetup, | ||
| handler: func(w nethttp.ResponseWriter, r *nethttp.Request) { | ||
| panic("server crash") | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| s.Run(tt.name, func() { | ||
| mockConfig := configmocks.NewConfig(s.T()) | ||
| mockTelemetry := telemetrymocks.NewTelemetry(s.T()) | ||
|
|
||
| telemetry.ConfigFacade = mockConfig | ||
| telemetry.TelemetryFacade = mockTelemetry | ||
|
|
||
| tt.configSetup(mockConfig) | ||
| tt.telemetrySetup(mockTelemetry) | ||
|
|
||
| handler := testMiddleware(tt.handler) | ||
| server := httptest.NewServer(handler) | ||
| defer server.Close() | ||
|
|
||
| client := &nethttp.Client{} | ||
|
|
||
| action := func() { | ||
| _, err := client.Get(server.URL + tt.requestPath) | ||
| if !tt.expectPanic { | ||
| s.NoError(err) | ||
| } | ||
| } | ||
|
|
||
| action() | ||
| }) | ||
| } | ||
| } |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ExcludedMethods configuration feature is not covered by tests. While ExcludedPaths has test coverage, ExcludedMethods should also be tested to ensure HTTP methods can be properly filtered.
| func (s *MiddlewareTestSuite) TestTelemetry() { | ||
| defaultTelemetrySetup := func(mockTelemetry *telemetrymocks.Telemetry) { | ||
| mockTelemetry.EXPECT().Tracer(instrumentationName).Return(tracenoop.NewTracerProvider().Tracer("test")).Once() | ||
| mockTelemetry.EXPECT().Meter(instrumentationName).Return(metricnoop.NewMeterProvider().Meter("test")).Once() | ||
| mockTelemetry.EXPECT().Propagator().Return(propagation.NewCompositeTextMapPropagator()).Once() | ||
| } | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| configSetup func(*configmocks.Config) | ||
| telemetrySetup func(*telemetrymocks.Telemetry) | ||
| handler nethttp.HandlerFunc | ||
| requestPath string | ||
| expectPanic bool | ||
| }{ | ||
| { | ||
| name: "Success: Request is traced and metrics recorded", | ||
| requestPath: "/users", | ||
| configSetup: func(mockConfig *configmocks.Config) { | ||
| mockConfig.EXPECT().UnmarshalKey("telemetry.instrumentation.http_server", mock.Anything). | ||
| Run(func(_ string, cfg any) { | ||
| c := cfg.(*ServerConfig) | ||
| c.Enabled = true | ||
| }).Return(nil).Once() | ||
| }, | ||
| telemetrySetup: defaultTelemetrySetup, | ||
| handler: func(w nethttp.ResponseWriter, r *nethttp.Request) { | ||
| w.WriteHeader(nethttp.StatusOK) | ||
| _, _ = w.Write([]byte("OK")) | ||
| }, | ||
| }, | ||
| { | ||
| name: "Ignored: Excluded path is skipped", | ||
| requestPath: "/health", | ||
| configSetup: func(mockConfig *configmocks.Config) { | ||
| mockConfig.EXPECT().UnmarshalKey("telemetry.instrumentation.http_server", mock.Anything). | ||
| Run(func(_ string, cfg interface{}) { | ||
| c := cfg.(*ServerConfig) | ||
| c.Enabled = true | ||
| c.ExcludedPaths = []string{"/health"} | ||
| }).Return(nil).Once() | ||
| }, | ||
| telemetrySetup: defaultTelemetrySetup, | ||
| handler: func(w nethttp.ResponseWriter, r *nethttp.Request) { | ||
| w.WriteHeader(nethttp.StatusOK) | ||
| }, | ||
| }, | ||
| { | ||
| name: "Ignored: Disabled via config", | ||
| requestPath: "/users", | ||
| configSetup: func(mockConfig *configmocks.Config) { | ||
| mockConfig.EXPECT().UnmarshalKey("telemetry.instrumentation.http_server", mock.Anything). | ||
| Run(func(_ string, cfg interface{}) { | ||
| c := cfg.(*ServerConfig) | ||
| c.Enabled = false | ||
| }).Return(nil).Once() | ||
| }, | ||
| telemetrySetup: func(mockTelemetry *telemetrymocks.Telemetry) { | ||
| // If disabled, Tracer/Meter should NOT be initialized | ||
| }, | ||
| handler: func(w nethttp.ResponseWriter, r *nethttp.Request) { | ||
| w.WriteHeader(nethttp.StatusOK) | ||
| }, | ||
| }, | ||
| { | ||
| name: "Panic: Metrics recorded as 500 and panic re-thrown", | ||
| requestPath: "/crash", | ||
| expectPanic: true, | ||
| configSetup: func(mockConfig *configmocks.Config) { | ||
| mockConfig.EXPECT().UnmarshalKey("telemetry.instrumentation.http_server", mock.Anything). | ||
| Run(func(_ string, cfg any) { | ||
| c := cfg.(*ServerConfig) | ||
| c.Enabled = true | ||
| }).Return(nil).Once() | ||
| }, | ||
| telemetrySetup: defaultTelemetrySetup, | ||
| handler: func(w nethttp.ResponseWriter, r *nethttp.Request) { | ||
| panic("server crash") | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| s.Run(tt.name, func() { | ||
| mockConfig := configmocks.NewConfig(s.T()) | ||
| mockTelemetry := telemetrymocks.NewTelemetry(s.T()) | ||
|
|
||
| telemetry.ConfigFacade = mockConfig | ||
| telemetry.TelemetryFacade = mockTelemetry | ||
|
|
||
| tt.configSetup(mockConfig) | ||
| tt.telemetrySetup(mockTelemetry) | ||
|
|
||
| handler := testMiddleware(tt.handler) | ||
| server := httptest.NewServer(handler) | ||
| defer server.Close() | ||
|
|
||
| client := &nethttp.Client{} | ||
|
|
||
| action := func() { | ||
| _, err := client.Get(server.URL + tt.requestPath) | ||
| if !tt.expectPanic { | ||
| s.NoError(err) | ||
| } | ||
| } | ||
|
|
||
| action() | ||
| }) | ||
| } | ||
| } |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Filters configuration feature is not covered by tests. The middleware supports custom filters through the ServerConfig.Filters field, but there are no tests validating this functionality works correctly.
| func (s *MiddlewareTestSuite) TestTelemetry() { | ||
| defaultTelemetrySetup := func(mockTelemetry *telemetrymocks.Telemetry) { | ||
| mockTelemetry.EXPECT().Tracer(instrumentationName).Return(tracenoop.NewTracerProvider().Tracer("test")).Once() | ||
| mockTelemetry.EXPECT().Meter(instrumentationName).Return(metricnoop.NewMeterProvider().Meter("test")).Once() | ||
| mockTelemetry.EXPECT().Propagator().Return(propagation.NewCompositeTextMapPropagator()).Once() | ||
| } | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| configSetup func(*configmocks.Config) | ||
| telemetrySetup func(*telemetrymocks.Telemetry) | ||
| handler nethttp.HandlerFunc | ||
| requestPath string | ||
| expectPanic bool | ||
| }{ | ||
| { | ||
| name: "Success: Request is traced and metrics recorded", | ||
| requestPath: "/users", | ||
| configSetup: func(mockConfig *configmocks.Config) { | ||
| mockConfig.EXPECT().UnmarshalKey("telemetry.instrumentation.http_server", mock.Anything). | ||
| Run(func(_ string, cfg any) { | ||
| c := cfg.(*ServerConfig) | ||
| c.Enabled = true | ||
| }).Return(nil).Once() | ||
| }, | ||
| telemetrySetup: defaultTelemetrySetup, | ||
| handler: func(w nethttp.ResponseWriter, r *nethttp.Request) { | ||
| w.WriteHeader(nethttp.StatusOK) | ||
| _, _ = w.Write([]byte("OK")) | ||
| }, | ||
| }, | ||
| { | ||
| name: "Ignored: Excluded path is skipped", | ||
| requestPath: "/health", | ||
| configSetup: func(mockConfig *configmocks.Config) { | ||
| mockConfig.EXPECT().UnmarshalKey("telemetry.instrumentation.http_server", mock.Anything). | ||
| Run(func(_ string, cfg interface{}) { | ||
| c := cfg.(*ServerConfig) | ||
| c.Enabled = true | ||
| c.ExcludedPaths = []string{"/health"} | ||
| }).Return(nil).Once() | ||
| }, | ||
| telemetrySetup: defaultTelemetrySetup, | ||
| handler: func(w nethttp.ResponseWriter, r *nethttp.Request) { | ||
| w.WriteHeader(nethttp.StatusOK) | ||
| }, | ||
| }, | ||
| { | ||
| name: "Ignored: Disabled via config", | ||
| requestPath: "/users", | ||
| configSetup: func(mockConfig *configmocks.Config) { | ||
| mockConfig.EXPECT().UnmarshalKey("telemetry.instrumentation.http_server", mock.Anything). | ||
| Run(func(_ string, cfg interface{}) { | ||
| c := cfg.(*ServerConfig) | ||
| c.Enabled = false | ||
| }).Return(nil).Once() | ||
| }, | ||
| telemetrySetup: func(mockTelemetry *telemetrymocks.Telemetry) { | ||
| // If disabled, Tracer/Meter should NOT be initialized | ||
| }, | ||
| handler: func(w nethttp.ResponseWriter, r *nethttp.Request) { | ||
| w.WriteHeader(nethttp.StatusOK) | ||
| }, | ||
| }, | ||
| { | ||
| name: "Panic: Metrics recorded as 500 and panic re-thrown", | ||
| requestPath: "/crash", | ||
| expectPanic: true, | ||
| configSetup: func(mockConfig *configmocks.Config) { | ||
| mockConfig.EXPECT().UnmarshalKey("telemetry.instrumentation.http_server", mock.Anything). | ||
| Run(func(_ string, cfg any) { | ||
| c := cfg.(*ServerConfig) | ||
| c.Enabled = true | ||
| }).Return(nil).Once() | ||
| }, | ||
| telemetrySetup: defaultTelemetrySetup, | ||
| handler: func(w nethttp.ResponseWriter, r *nethttp.Request) { | ||
| panic("server crash") | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| s.Run(tt.name, func() { | ||
| mockConfig := configmocks.NewConfig(s.T()) | ||
| mockTelemetry := telemetrymocks.NewTelemetry(s.T()) | ||
|
|
||
| telemetry.ConfigFacade = mockConfig | ||
| telemetry.TelemetryFacade = mockTelemetry | ||
|
|
||
| tt.configSetup(mockConfig) | ||
| tt.telemetrySetup(mockTelemetry) | ||
|
|
||
| handler := testMiddleware(tt.handler) | ||
| server := httptest.NewServer(handler) | ||
| defer server.Close() | ||
|
|
||
| client := &nethttp.Client{} | ||
|
|
||
| action := func() { | ||
| _, err := client.Get(server.URL + tt.requestPath) | ||
| if !tt.expectPanic { | ||
| s.NoError(err) | ||
| } | ||
| } | ||
|
|
||
| action() | ||
| }) | ||
| } | ||
| } |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no test coverage for the case when TelemetryFacade is nil. The middleware includes a nil check that returns early and logs a warning, but this code path is not tested.
| unitSeconds = "s" | ||
| unitBytes = "By" | ||
| ) | ||
|
|
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Telemetry function lacks documentation. As a public API function that creates middleware, it should have a comment explaining its purpose, parameters, and return value. This is especially important for exported functions that will be used by developers.
| // Telemetry creates HTTP server telemetry middleware that instruments incoming | |
| // requests with tracing and metrics. The optional opts parameters allow | |
| // customizing the server configuration (such as span naming and enabling or | |
| // disabling instrumentation). It returns an http.Middleware that propagates | |
| // context, records spans and metrics when telemetry is enabled, and otherwise | |
| // transparently passes requests through when telemetry is disabled or not | |
| // initialized. |
Codecov Reportβ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #1326 +/- ##
==========================================
+ Coverage 69.41% 70.05% +0.64%
==========================================
Files 282 285 +3
Lines 16630 16868 +238
==========================================
+ Hits 11543 11817 +274
+ Misses 4596 4550 -46
- Partials 491 501 +10 β View full report in Codecov by Sentry. π New features to boost your workflow:
|
π Description
RelatedTo goravel/goravel#726
β Checks