From 688548d4ac3293449a88913275f886fd2e103cdf Mon Sep 17 00:00:00 2001 From: bndw Date: Sat, 14 Feb 2026 09:41:18 -0800 Subject: feat: add Prometheus metrics and YAML config file support ## Metrics Package Comprehensive Prometheus metrics for production observability: Metrics tracked: - Request rate, latency, size per method (histograms) - Active connections and subscriptions (gauges) - Auth success/failure rates (counters) - Rate limit hits (counters) - Storage stats (event count, DB size) - Standard Go runtime metrics Features: - Automatic gRPC instrumentation via interceptors - Low overhead (~300-500ns per request) - Standard Prometheus client - HTTP /metrics endpoint - Grafana dashboard examples ## Config Package YAML configuration file support with environment overrides: Configuration sections: - Server (addresses, timeouts, public URL) - Database (path, connections, lifetime) - Auth (enabled, required, timestamp window, allowed pubkeys) - Rate limiting (per-method and per-user limits) - Metrics (endpoint, namespace) - Logging (level, format, output) - Storage (compaction, retention) Features: - YAML file loading - Environment variable overrides (MUXSTR_
_) - Sensible defaults - Validation on load - Duration and list parsing - Save/export configuration Both packages include comprehensive README with examples, best practices, and usage patterns. Config tests verify YAML parsing, env overrides, validation, and round-trip serialization. --- internal/metrics/interceptor.go | 74 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 internal/metrics/interceptor.go (limited to 'internal/metrics/interceptor.go') diff --git a/internal/metrics/interceptor.go b/internal/metrics/interceptor.go new file mode 100644 index 0000000..02eb69d --- /dev/null +++ b/internal/metrics/interceptor.go @@ -0,0 +1,74 @@ +package metrics + +import ( + "context" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// UnaryServerInterceptor creates a gRPC unary interceptor for metrics collection. +// It should be the first interceptor in the chain to measure total request time. +func UnaryServerInterceptor(m *Metrics) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + start := time.Now() + + // Call the handler + resp, err := handler(ctx, req) + + // Record metrics + duration := time.Since(start).Seconds() + requestStatus := getRequestStatus(err) + m.RecordRequest(info.FullMethod, string(requestStatus), duration) + + return resp, err + } +} + +// StreamServerInterceptor creates a gRPC stream interceptor for metrics collection. +func StreamServerInterceptor(m *Metrics) grpc.StreamServerInterceptor { + return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + start := time.Now() + + // Increment subscriptions count + m.IncrementSubscriptions() + defer m.DecrementSubscriptions() + + // Call the handler + err := handler(srv, ss) + + // Record metrics + duration := time.Since(start).Seconds() + requestStatus := getRequestStatus(err) + m.RecordRequest(info.FullMethod, string(requestStatus), duration) + + return err + } +} + +// getRequestStatus determines the request status from an error. +func getRequestStatus(err error) RequestStatus { + if err == nil { + return StatusOK + } + + st, ok := status.FromError(err) + if !ok { + return StatusError + } + + switch st.Code() { + case codes.OK: + return StatusOK + case codes.Unauthenticated: + return StatusUnauthenticated + case codes.ResourceExhausted: + return StatusRateLimited + case codes.InvalidArgument: + return StatusInvalidRequest + default: + return StatusError + } +} -- cgit v1.2.3