From 28ccfecb41c9a860aac5d46d381edbd2d28732b1 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Mon, 23 Feb 2026 05:57:37 -0500 Subject: [PATCH 1/2] feat: extract HTTP and Observability engine plugins (#82) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move HTTP and Observability module types from core engine into dedicated plugin packages: - plugins/http: http.server, http.router, http.handler, middlewares, proxies, static fileserver, HTTP steps and triggers - plugins/observability: metrics.collector, health.checker, log.collector, observability.otel, openapi.generator Remove duplicate observability wiring hooks from HTTP plugin — health, metrics, log, and OpenAPI endpoint registration now lives exclusively in the observability plugin to avoid double-registration. Both plugins registered in cmd/server/main.go. Module types removed from engine.go core registration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- plugins/http/plugin.go | 4 -- plugins/http/plugin_test.go | 12 ++-- plugins/http/wiring.go | 126 +----------------------------------- 3 files changed, 6 insertions(+), 136 deletions(-) diff --git a/plugins/http/plugin.go b/plugins/http/plugin.go index db6edc76..1ce0e1a0 100644 --- a/plugins/http/plugin.go +++ b/plugins/http/plugin.go @@ -54,10 +54,6 @@ func New() *HTTPPlugin { WiringHooks: []string{ "http-auth-provider-wiring", "http-static-fileserver-registration", - "http-health-endpoint-registration", - "http-metrics-endpoint-registration", - "http-log-endpoint-registration", - "http-openapi-endpoint-registration", }, Capabilities: []plugin.CapabilityDecl{ {Name: "http-server", Role: "provider", Priority: 10}, diff --git a/plugins/http/plugin_test.go b/plugins/http/plugin_test.go index f5fed213..46f60226 100644 --- a/plugins/http/plugin_test.go +++ b/plugins/http/plugin_test.go @@ -152,8 +152,8 @@ func TestModuleSchemas(t *testing.T) { func TestWiringHooks(t *testing.T) { p := New() hooks := p.WiringHooks() - if len(hooks) < 6 { - t.Errorf("WiringHooks() returned %d hooks, want >= 6", len(hooks)) + if len(hooks) != 2 { + t.Errorf("WiringHooks() returned %d hooks, want 2", len(hooks)) } hookNames := make(map[string]bool) @@ -164,10 +164,6 @@ func TestWiringHooks(t *testing.T) { expectedHooks := []string{ "http-auth-provider-wiring", "http-static-fileserver-registration", - "http-health-endpoint-registration", - "http-metrics-endpoint-registration", - "http-log-endpoint-registration", - "http-openapi-endpoint-registration", } for _, name := range expectedHooks { @@ -424,7 +420,7 @@ func TestPluginLoaderIntegration(t *testing.T) { } hooks := loader.WiringHooks() - if len(hooks) < 6 { - t.Errorf("loader has %d wiring hooks, want >= 6", len(hooks)) + if len(hooks) < 2 { + t.Errorf("loader has %d wiring hooks, want >= 2", len(hooks)) } } diff --git a/plugins/http/wiring.go b/plugins/http/wiring.go index 635acf19..0a02c37a 100644 --- a/plugins/http/wiring.go +++ b/plugins/http/wiring.go @@ -1,7 +1,6 @@ package http import ( - "context" "fmt" "github.com/CrisisTextLine/modular" @@ -11,6 +10,8 @@ import ( ) // wiringHooks returns all post-init wiring functions for HTTP-related cross-module integrations. +// Observability endpoint wiring (health, metrics, log, openapi) is handled by +// the observability plugin to avoid duplicate registrations. func wiringHooks() []plugin.WiringHook { return []plugin.WiringHook{ { @@ -23,26 +24,6 @@ func wiringHooks() []plugin.WiringHook { Priority: 50, Hook: wireStaticFileServers, }, - { - Name: "http-health-endpoint-registration", - Priority: 40, - Hook: wireHealthEndpoints, - }, - { - Name: "http-metrics-endpoint-registration", - Priority: 40, - Hook: wireMetricsEndpoint, - }, - { - Name: "http-log-endpoint-registration", - Priority: 40, - Hook: wireLogEndpoint, - }, - { - Name: "http-openapi-endpoint-registration", - Priority: 40, - Hook: wireOpenAPIEndpoints, - }, } } @@ -157,106 +138,3 @@ func wireStaticFileServers(app modular.Application, cfg *config.WorkflowConfig) return nil } - -// wireHealthEndpoints registers health checker endpoints on the first available router. -func wireHealthEndpoints(app modular.Application, _ *config.WorkflowConfig) error { - for _, svc := range app.SvcRegistry() { - hc, ok := svc.(*module.HealthChecker) - if !ok { - continue - } - - // Register persistence health checks if any persistence stores exist - for svcName, innerSvc := range app.SvcRegistry() { - if ps, ok := innerSvc.(*module.PersistenceStore); ok { - checkName := "persistence." + svcName - psRef := ps // capture for closure - hc.RegisterCheck(checkName, func(ctx context.Context) module.HealthCheckResult { - if err := psRef.Ping(ctx); err != nil { - return module.HealthCheckResult{Status: "degraded", Message: "database unreachable: " + err.Error()} - } - return module.HealthCheckResult{Status: "healthy", Message: "database connected"} - }) - } - } - - // Auto-discover any HealthCheckable services - hc.DiscoverHealthCheckables() - - for _, routerSvc := range app.SvcRegistry() { - if router, ok := routerSvc.(*module.StandardHTTPRouter); ok { - healthPath := hc.HealthPath() - readyPath := hc.ReadyPath() - livePath := hc.LivePath() - if !router.HasRoute("GET", healthPath) { - router.AddRoute("GET", healthPath, &module.HealthHTTPHandler{Handler: hc.HealthHandler()}) - router.AddRoute("GET", readyPath, &module.HealthHTTPHandler{Handler: hc.ReadyHandler()}) - router.AddRoute("GET", livePath, &module.HealthHTTPHandler{Handler: hc.LiveHandler()}) - } - break - } - } - } - return nil -} - -// wireMetricsEndpoint registers the metrics collector endpoint on the first available router. -func wireMetricsEndpoint(app modular.Application, _ *config.WorkflowConfig) error { - for _, svc := range app.SvcRegistry() { - mc, ok := svc.(*module.MetricsCollector) - if !ok { - continue - } - metricsPath := mc.MetricsPath() - for _, routerSvc := range app.SvcRegistry() { - if router, ok := routerSvc.(*module.StandardHTTPRouter); ok { - if !router.HasRoute("GET", metricsPath) { - router.AddRoute("GET", metricsPath, &module.MetricsHTTPHandler{Handler: mc.Handler()}) - } - break - } - } - } - return nil -} - -// wireLogEndpoint registers the log collector endpoint on the first available router. -func wireLogEndpoint(app modular.Application, _ *config.WorkflowConfig) error { - for _, svc := range app.SvcRegistry() { - lc, ok := svc.(*module.LogCollector) - if !ok { - continue - } - for _, routerSvc := range app.SvcRegistry() { - if router, ok := routerSvc.(*module.StandardHTTPRouter); ok { - if !router.HasRoute("GET", "/logs") { - router.AddRoute("GET", "/logs", &module.LogHTTPHandler{Handler: lc.LogHandler()}) - } - break - } - } - } - return nil -} - -// wireOpenAPIEndpoints registers OpenAPI spec endpoints on the first available router. -func wireOpenAPIEndpoints(app modular.Application, cfg *config.WorkflowConfig) error { - for _, svc := range app.SvcRegistry() { - gen, ok := svc.(*module.OpenAPIGenerator) - if !ok { - continue - } - gen.BuildSpec(cfg.Workflows) - - for _, routerSvc := range app.SvcRegistry() { - if router, ok := routerSvc.(*module.StandardHTTPRouter); ok { - if !router.HasRoute("GET", "/api/openapi.json") { - router.AddRoute("GET", "/api/openapi.json", &module.OpenAPIHTTPHandler{Handler: gen.ServeJSON}) - router.AddRoute("GET", "/api/openapi.yaml", &module.OpenAPIHTTPHandler{Handler: gen.ServeYAML}) - } - break - } - } - } - return nil -} From da4ba604b2b47634c3a70e53d2a5c1680e601709 Mon Sep 17 00:00:00 2001 From: Jonathan Langevin Date: Mon, 23 Feb 2026 09:09:17 -0500 Subject: [PATCH 2/2] Update plugins/http/plugin_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- plugins/http/plugin_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/http/plugin_test.go b/plugins/http/plugin_test.go index 46f60226..2db3c55c 100644 --- a/plugins/http/plugin_test.go +++ b/plugins/http/plugin_test.go @@ -420,7 +420,7 @@ func TestPluginLoaderIntegration(t *testing.T) { } hooks := loader.WiringHooks() - if len(hooks) < 2 { - t.Errorf("loader has %d wiring hooks, want >= 2", len(hooks)) + if len(hooks) != 2 { + t.Errorf("loader has %d wiring hooks, want 2", len(hooks)) } }