package observability import ( "context" "encoding/json" "fmt" "net" "net/http" "sync" "time" ) // MetricsServer exposes correlation metrics via HTTP. type MetricsServer struct { mu sync.Mutex server *http.Server listener net.Listener metricsFunc func() MetricsSnapshot running bool } // NewMetricsServer creates a new metrics HTTP server. func NewMetricsServer(addr string, metricsFunc func() MetricsSnapshot) (*MetricsServer, error) { if metricsFunc == nil { return nil, fmt.Errorf("metricsFunc cannot be nil") } ms := &MetricsServer{ metricsFunc: metricsFunc, } mux := http.NewServeMux() mux.HandleFunc("/metrics", ms.handleMetrics) mux.HandleFunc("/health", ms.handleHealth) ms.server = &http.Server{ Addr: addr, Handler: mux, ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } return ms, nil } // Start begins listening on the configured address. func (ms *MetricsServer) Start() error { ms.mu.Lock() defer ms.mu.Unlock() if ms.running { return nil } listener, err := net.Listen("tcp", ms.server.Addr) if err != nil { return fmt.Errorf("failed to start metrics server: %w", err) } ms.listener = listener ms.running = true go func() { if err := ms.server.Serve(listener); err != nil && err != http.ErrServerClosed { // Server error or closed } }() return nil } // Stop gracefully stops the metrics server. func (ms *MetricsServer) Stop(ctx context.Context) error { ms.mu.Lock() defer ms.mu.Unlock() if !ms.running { return nil } ms.running = false return ms.server.Shutdown(ctx) } // handleMetrics returns the correlation metrics as JSON. func (ms *MetricsServer) handleMetrics(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } metrics := ms.metricsFunc() w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(metrics); err != nil { http.Error(w, "Failed to encode metrics", http.StatusInternalServerError) return } } // handleHealth returns a simple health check response. func (ms *MetricsServer) handleHealth(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) fmt.Fprintf(w, `{"status":"healthy"}`) } // IsRunning returns true if the server is running. func (ms *MetricsServer) IsRunning() bool { ms.mu.Lock() defer ms.mu.Unlock() return ms.running } // Addr returns the listening address. func (ms *MetricsServer) Addr() string { ms.mu.Lock() defer ms.mu.Unlock() if ms.listener == nil { return "" } return ms.listener.Addr().String() }