feat(telemetry): add Prometheus exporter for embedded metrics scraping

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/claude-agent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Alexander
2026-04-14 21:56:32 +02:00
parent 1bc704a7b2
commit 501e40c53d
3 changed files with 64 additions and 31 deletions
+31 -27
View File
@@ -3,13 +3,16 @@ package telemetry
import (
"context"
"io"
"net/http"
"github.com/fujin/anthropic-proxy/internal/config"
"github.com/fujin/anthropic-proxy/internal/ratelimit"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
promexporter "go.opentelemetry.io/otel/exporters/prometheus"
otellog "go.opentelemetry.io/otel/log/global"
"go.opentelemetry.io/otel/sdk/log"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
@@ -18,46 +21,51 @@ import (
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)
// Setup initializes OpenTelemetry providers. It always creates a MeterProvider
// so metrics can be recorded in-process. When cfg.ExportEnabled(), OTLP gRPC
// exporters are additionally configured to push to the LGTM stack.
// Returns a shutdown function and an optional io.Writer for the log bridge.
func Setup(ctx context.Context, cfg config.TelemetryConfig, tracker *ratelimit.Tracker) (shutdown func(context.Context) error, logWriter io.Writer, err error) {
func Setup(ctx context.Context, cfg config.TelemetryConfig, tracker *ratelimit.Tracker) (shutdown func(context.Context) error, logWriter io.Writer, metricsHandler http.Handler, err error) {
res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceName(cfg.ServiceName),
),
)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
if !cfg.ExportEnabled() {
// No export — set up in-memory meter provider only so metric
// instruments are valid (they just don't export anywhere).
mp := sdkmetric.NewMeterProvider(sdkmetric.WithResource(res))
var readers []sdkmetric.Option
readers = append(readers, sdkmetric.WithResource(res))
var promHandler http.Handler
if cfg.Embedded.Enabled {
exporter, pErr := promexporter.New()
if pErr != nil {
return nil, nil, nil, pErr
}
readers = append(readers, sdkmetric.WithReader(exporter))
promHandler = promhttp.Handler()
}
if !cfg.Export.Enabled() {
mp := sdkmetric.NewMeterProvider(readers...)
otel.SetMeterProvider(mp)
InitMetrics(mp.Meter(cfg.ServiceName), tracker)
return func(ctx context.Context) error { return mp.Shutdown(ctx) }, nil, nil
return func(ctx context.Context) error { return mp.Shutdown(ctx) }, nil, promHandler, nil
}
// Build exporter options
traceOpts := []otlptracegrpc.Option{otlptracegrpc.WithEndpoint(cfg.Endpoint)}
traceOpts := []otlptracegrpc.Option{otlptracegrpc.WithEndpoint(cfg.Export.Endpoint)}
metricOpts := []otlpmetricgrpc.Option{
otlpmetricgrpc.WithEndpoint(cfg.Endpoint),
otlpmetricgrpc.WithEndpoint(cfg.Export.Endpoint),
otlpmetricgrpc.WithTemporalitySelector(sdkmetric.CumulativeTemporalitySelector),
}
logOpts := []otlploggrpc.Option{otlploggrpc.WithEndpoint(cfg.Endpoint)}
if cfg.Insecure {
logOpts := []otlploggrpc.Option{otlploggrpc.WithEndpoint(cfg.Export.Endpoint)}
if cfg.Export.Insecure {
traceOpts = append(traceOpts, otlptracegrpc.WithInsecure())
metricOpts = append(metricOpts, otlpmetricgrpc.WithInsecure())
logOpts = append(logOpts, otlploggrpc.WithInsecure())
}
// Trace exporter
traceExp, err := otlptracegrpc.New(ctx, traceOpts...)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
tp := trace.NewTracerProvider(
trace.WithBatcher(traceExp),
@@ -65,22 +73,18 @@ func Setup(ctx context.Context, cfg config.TelemetryConfig, tracker *ratelimit.T
)
otel.SetTracerProvider(tp)
// Metric exporter
metricExp, err := otlpmetricgrpc.New(ctx, metricOpts...)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
mp := sdkmetric.NewMeterProvider(
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExp)),
sdkmetric.WithResource(res),
)
readers = append(readers, sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExp)))
mp := sdkmetric.NewMeterProvider(readers...)
otel.SetMeterProvider(mp)
InitMetrics(mp.Meter(cfg.ServiceName), tracker)
// Log exporter
logExp, err := otlploggrpc.New(ctx, logOpts...)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
lp := log.NewLoggerProvider(
log.WithProcessor(log.NewBatchProcessor(logExp)),
@@ -104,5 +108,5 @@ func Setup(ctx context.Context, cfg config.TelemetryConfig, tracker *ratelimit.T
return firstErr
}
return shutdownFn, bridge, nil
return shutdownFn, bridge, promHandler, nil
}