-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add OTEL HTTP tracing middleware #159
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| package module | ||
|
|
||
| import ( | ||
| "context" | ||
| "net/http" | ||
|
|
||
| "github.com/CrisisTextLine/modular" | ||
| "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" | ||
| ) | ||
|
|
||
| // OTelMiddleware instruments HTTP requests with OpenTelemetry tracing. | ||
| type OTelMiddleware struct { | ||
| name string | ||
| serverName string | ||
| } | ||
|
|
||
| // NewOTelMiddleware creates a new OpenTelemetry HTTP tracing middleware. | ||
| func NewOTelMiddleware(name, serverName string) *OTelMiddleware { | ||
| return &OTelMiddleware{name: name, serverName: serverName} | ||
| } | ||
|
|
||
| // Name returns the module name. | ||
| func (m *OTelMiddleware) Name() string { return m.name } | ||
|
|
||
| // Init initializes the middleware. | ||
| func (m *OTelMiddleware) Init(_ modular.Application) error { return nil } | ||
|
|
||
| // Process wraps the handler with OpenTelemetry HTTP instrumentation. | ||
| func (m *OTelMiddleware) Process(next http.Handler) http.Handler { | ||
| return otelhttp.NewHandler(next, m.serverName) | ||
| } | ||
|
|
||
| // ProvidesServices returns the services provided by this middleware. | ||
| func (m *OTelMiddleware) ProvidesServices() []modular.ServiceProvider { | ||
| return []modular.ServiceProvider{ | ||
| {Name: m.name, Description: "OpenTelemetry HTTP Tracing Middleware", Instance: m}, | ||
| } | ||
| } | ||
|
|
||
| // RequiresServices returns services required by this middleware. | ||
| func (m *OTelMiddleware) RequiresServices() []modular.ServiceDependency { return nil } | ||
|
|
||
| // Start is a no-op for this middleware. | ||
| func (m *OTelMiddleware) Start(_ context.Context) error { return nil } | ||
|
|
||
| // Stop is a no-op for this middleware. | ||
| func (m *OTelMiddleware) Stop(_ context.Context) error { return nil } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| package module | ||
|
|
||
| import ( | ||
| "context" | ||
| "net/http" | ||
| "net/http/httptest" | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestNewOTelMiddleware(t *testing.T) { | ||
| m := NewOTelMiddleware("otel-mw", "workflow-http") | ||
| if m.Name() != "otel-mw" { | ||
| t.Errorf("expected name 'otel-mw', got %q", m.Name()) | ||
| } | ||
| } | ||
|
|
||
| func TestOTelMiddleware_Init(t *testing.T) { | ||
| app := CreateIsolatedApp(t) | ||
| m := NewOTelMiddleware("otel-mw", "workflow-http") | ||
| if err := m.Init(app); err != nil { | ||
| t.Fatalf("Init failed: %v", err) | ||
| } | ||
| } | ||
|
|
||
| func TestOTelMiddleware_Process_CallsNext(t *testing.T) { | ||
| m := NewOTelMiddleware("otel-mw", "workflow-http") | ||
|
|
||
| nextCalled := false | ||
| handler := m.Process(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| nextCalled = true | ||
| w.WriteHeader(http.StatusOK) | ||
| })) | ||
|
|
||
| req := httptest.NewRequest("GET", "/test", nil) | ||
| rec := httptest.NewRecorder() | ||
| handler.ServeHTTP(rec, req) | ||
|
|
||
| if !nextCalled { | ||
| t.Error("expected next handler to be called") | ||
| } | ||
| if rec.Code != http.StatusOK { | ||
| t.Errorf("expected 200, got %d", rec.Code) | ||
| } | ||
| } | ||
|
|
||
| func TestOTelMiddleware_ProvidesServices(t *testing.T) { | ||
| m := NewOTelMiddleware("otel-mw", "workflow-http") | ||
| svcs := m.ProvidesServices() | ||
| if len(svcs) != 1 { | ||
| t.Fatalf("expected 1 service, got %d", len(svcs)) | ||
| } | ||
| if svcs[0].Name != "otel-mw" { | ||
| t.Errorf("expected service name 'otel-mw', got %q", svcs[0].Name) | ||
| } | ||
| } | ||
|
|
||
| func TestOTelMiddleware_RequiresServices(t *testing.T) { | ||
| m := NewOTelMiddleware("otel-mw", "workflow-http") | ||
| if m.RequiresServices() != nil { | ||
| t.Error("expected nil dependencies") | ||
| } | ||
| } | ||
|
|
||
| func TestOTelMiddleware_Start(t *testing.T) { | ||
| m := NewOTelMiddleware("otel-mw", "workflow-http") | ||
| if err := m.Start(context.TODO()); err != nil { | ||
| t.Fatalf("Start failed: %v", err) | ||
| } | ||
| } | ||
|
|
||
| func TestOTelMiddleware_Stop(t *testing.T) { | ||
| m := NewOTelMiddleware("otel-mw", "workflow-http") | ||
| if err := m.Stop(context.TODO()); err != nil { | ||
| t.Fatalf("Stop failed: %v", err) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -36,6 +36,7 @@ func New() *ObservabilityPlugin { | |||
| "log.collector", | ||||
| "observability.otel", | ||||
| "openapi.generator", | ||||
| "http.middleware.otel", | ||||
|
||||
| "http.middleware.otel", |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -76,5 +76,15 @@ func moduleSchemas() []*schema.ModuleSchema { | |
| }, | ||
| DefaultConfig: map[string]any{"title": "Workflow API", "version": "1.0.0"}, | ||
| }, | ||
| { | ||
| Type: "http.middleware.otel", | ||
| Label: "OTEL HTTP Middleware", | ||
| Category: "observability", | ||
| Description: "Instruments HTTP requests with OpenTelemetry tracing spans", | ||
| ConfigFields: []schema.ConfigFieldDef{ | ||
| {Key: "serverName", Label: "Server Name", Type: schema.FieldTypeString, DefaultValue: "workflow-http", Description: "Server name used as the span operation name", Placeholder: "workflow-http"}, | ||
| }, | ||
| DefaultConfig: map[string]any{"serverName": "workflow-http"}, | ||
| }, | ||
|
Comment on lines
+79
to
+88
|
||
| } | ||
| } | ||
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.
otelMiddlewareFactorydereferencescfgwithout a nil check.ModuleConfig.Configcan be nil when theconfig:section is omitted (and the engine only injects a map when ConfigDir is set), which would panic at runtime. Treat nil as an empty map (or early-return the default) before readingcfg["serverName"].