@@ -13,17 +13,24 @@ import (
1313 "log"
1414 "log/slog"
1515 "net"
16+ "net/http"
1617 "os"
1718 "os/signal"
1819 "strings"
1920 "syscall"
2021 "time"
2122
2223 extprocv3 "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
24+ "github.com/prometheus/client_golang/prometheus"
25+ "github.com/prometheus/client_golang/prometheus/promhttp"
26+ otelprom "go.opentelemetry.io/otel/exporters/prometheus"
27+ "go.opentelemetry.io/otel/metric"
28+ metricsdk "go.opentelemetry.io/otel/sdk/metric"
2329 "google.golang.org/grpc"
2430 "google.golang.org/grpc/health/grpc_health_v1"
2531
2632 "github.com/envoyproxy/ai-gateway/internal/extproc"
33+ "github.com/envoyproxy/ai-gateway/internal/metrics"
2734 "github.com/envoyproxy/ai-gateway/internal/version"
2835)
2936
@@ -32,6 +39,7 @@ type extProcFlags struct {
3239 configPath string // path to the configuration file.
3340 extProcAddr string // gRPC address for the external processor.
3441 logLevel slog.Level // log level for the external processor.
42+ metricsAddr string // HTTP address for the metrics server.
3543}
3644
3745// parseAndValidateFlags parses and validates the flas passed to the external processor.
@@ -51,13 +59,14 @@ func parseAndValidateFlags(args []string) (extProcFlags, error) {
5159 fs .StringVar (& flags .extProcAddr ,
5260 "extProcAddr" ,
5361 ":1063" ,
54- "gRPC address for the external processor. For example, :1063 or unix:///tmp/ext_proc.sock" ,
62+ "gRPC address for the external processor. For example, :1063 or unix:///tmp/ext_proc.sock. " ,
5563 )
5664 logLevelPtr := fs .String (
5765 "logLevel" ,
5866 "info" ,
5967 "log level for the external processor. One of 'debug', 'info', 'warn', or 'error'." ,
6068 )
69+ fs .StringVar (& flags .metricsAddr , "metricsAddr" , ":9190" , "HTTP address for the metrics server." )
6170
6271 if err := fs .Parse (args ); err != nil {
6372 return extProcFlags {}, fmt .Errorf ("failed to parse extProcFlags: %w" , err )
@@ -102,11 +111,13 @@ func Main() {
102111 log .Fatalf ("failed to listen: %v" , err )
103112 }
104113
114+ metricsServer , meter := startMetricsServer (flags .metricsAddr , l )
115+
105116 server , err := extproc .NewServer (l )
106117 if err != nil {
107118 log .Fatalf ("failed to create external processor server: %v" , err )
108119 }
109- server .Register ("/v1/chat/completions" , extproc .NewChatCompletionProcessor )
120+ server .Register ("/v1/chat/completions" , extproc .ChatCompletionProcessorFactory ( metrics . NewChatCompletion ( meter )) )
110121 server .Register ("/v1/models" , extproc .NewModelsProcessor )
111122
112123 if err := extproc .StartConfigWatcher (ctx , flags .configPath , server , l , time .Second * 5 ); err != nil {
@@ -116,10 +127,18 @@ func Main() {
116127 s := grpc .NewServer ()
117128 extprocv3 .RegisterExternalProcessorServer (s , server )
118129 grpc_health_v1 .RegisterHealthServer (s , server )
130+
119131 go func () {
120132 <- ctx .Done ()
121133 s .GracefulStop ()
134+
135+ shutdownCtx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
136+ defer cancel ()
137+ if err := metricsServer .Shutdown (shutdownCtx ); err != nil {
138+ l .Error ("Failed to shutdown metrics server gracefully" , "error" , err )
139+ }
122140 }()
141+
123142 _ = s .Serve (lis )
124143}
125144
@@ -130,3 +149,46 @@ func listenAddress(addrFlag string) (string, string) {
130149 }
131150 return "tcp" , addrFlag
132151}
152+
153+ // startMetricsServer starts the HTTP server for Prometheus metrics.
154+ func startMetricsServer (addr string , logger * slog.Logger ) (* http.Server , metric.Meter ) {
155+ registry := prometheus .NewRegistry ()
156+ exporter , err := otelprom .New (otelprom .WithRegisterer (registry ))
157+ if err != nil {
158+ log .Fatal ("failed to create metrics exporter" )
159+ }
160+ provider := metricsdk .NewMeterProvider (metricsdk .WithReader (exporter ))
161+ meter := provider .Meter ("envoyproxy/ai-gateway" )
162+
163+ // Create a new HTTP server for metrics.
164+ mux := http .NewServeMux ()
165+
166+ // Register the metrics handler.
167+ mux .Handle ("/metrics" , promhttp .HandlerFor (
168+ registry ,
169+ promhttp.HandlerOpts {
170+ EnableOpenMetrics : true ,
171+ },
172+ ))
173+
174+ // Add a simple health check endpoint.
175+ mux .HandleFunc ("/health" , func (w http.ResponseWriter , _ * http.Request ) {
176+ w .WriteHeader (http .StatusOK )
177+ _ , _ = w .Write ([]byte ("OK" ))
178+ })
179+
180+ server := & http.Server {
181+ Addr : addr ,
182+ Handler : mux ,
183+ ReadHeaderTimeout : 5 * time .Second ,
184+ }
185+
186+ go func () {
187+ logger .Info ("Starting metrics server" , "address" , addr )
188+ if err := server .ListenAndServe (); err != nil && ! errors .Is (err , http .ErrServerClosed ) {
189+ logger .Error ("Metrics server failed" , "error" , err )
190+ }
191+ }()
192+
193+ return server , meter
194+ }
0 commit comments