Add telemetry
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
otellog "go.opentelemetry.io/otel/log"
|
||||
sdklog "go.opentelemetry.io/otel/sdk/log"
|
||||
)
|
||||
|
||||
// LogBridge implements io.Writer and forwards zerolog JSON lines to the
|
||||
// OTel LoggerProvider. It is used as an extra writer in zerolog's MultiWriter
|
||||
// so that logs go to both file and OTLP.
|
||||
type LogBridge struct {
|
||||
provider *sdklog.LoggerProvider
|
||||
}
|
||||
|
||||
func (b *LogBridge) Write(p []byte) (n int, err error) {
|
||||
var entry map[string]interface{}
|
||||
if err := json.Unmarshal(p, &entry); err != nil {
|
||||
return len(p), nil // skip malformed lines
|
||||
}
|
||||
|
||||
logger := b.provider.Logger("zerolog")
|
||||
|
||||
var rec otellog.Record
|
||||
rec.SetTimestamp(time.Now())
|
||||
|
||||
if msg, ok := entry["message"].(string); ok {
|
||||
rec.SetBody(otellog.StringValue(msg))
|
||||
}
|
||||
|
||||
if lvl, ok := entry["level"].(string); ok {
|
||||
rec.SetSeverity(mapSeverity(lvl))
|
||||
}
|
||||
|
||||
// Forward all fields as attributes
|
||||
attrs := make([]otellog.KeyValue, 0, len(entry))
|
||||
for k, v := range entry {
|
||||
if k == "message" || k == "level" || k == "time" {
|
||||
continue
|
||||
}
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
attrs = append(attrs, otellog.String(k, val))
|
||||
case float64:
|
||||
attrs = append(attrs, otellog.Float64(k, val))
|
||||
case bool:
|
||||
attrs = append(attrs, otellog.Bool(k, val))
|
||||
default:
|
||||
b, _ := json.Marshal(val)
|
||||
attrs = append(attrs, otellog.String(k, string(b)))
|
||||
}
|
||||
}
|
||||
rec.AddAttributes(attrs...)
|
||||
|
||||
logger.Emit(context.Background(), rec)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func mapSeverity(level string) otellog.Severity {
|
||||
switch level {
|
||||
case "trace":
|
||||
return otellog.SeverityTrace
|
||||
case "debug":
|
||||
return otellog.SeverityDebug
|
||||
case "info":
|
||||
return otellog.SeverityInfo
|
||||
case "warn", "warning":
|
||||
return otellog.SeverityWarn
|
||||
case "error":
|
||||
return otellog.SeverityError
|
||||
case "fatal":
|
||||
return otellog.SeverityFatal
|
||||
case "panic":
|
||||
return otellog.SeverityFatal2
|
||||
default:
|
||||
return otellog.SeverityInfo
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
)
|
||||
|
||||
var (
|
||||
RequestCounter metric.Int64Counter
|
||||
RequestDuration metric.Float64Histogram
|
||||
RequestBodySize metric.Int64Histogram
|
||||
UpstreamErrors metric.Int64Counter
|
||||
TokensInput metric.Int64Counter
|
||||
TokensOutput metric.Int64Counter
|
||||
CredentialCooldowns metric.Int64Counter
|
||||
ActiveCredentials metric.Int64UpDownCounter
|
||||
StreamRequests metric.Int64Counter
|
||||
)
|
||||
|
||||
// InitMetrics creates all metric instruments from the given meter.
|
||||
func InitMetrics(meter metric.Meter) {
|
||||
RequestCounter, _ = meter.Int64Counter("proxy.request.count",
|
||||
metric.WithDescription("Total proxy requests"),
|
||||
)
|
||||
RequestDuration, _ = meter.Float64Histogram("proxy.request.duration_ms",
|
||||
metric.WithDescription("Request latency in milliseconds"),
|
||||
metric.WithUnit("ms"),
|
||||
)
|
||||
RequestBodySize, _ = meter.Int64Histogram("proxy.request.body_size_bytes",
|
||||
metric.WithDescription("Request body size in bytes"),
|
||||
metric.WithUnit("By"),
|
||||
)
|
||||
UpstreamErrors, _ = meter.Int64Counter("proxy.upstream.errors",
|
||||
metric.WithDescription("Upstream error count"),
|
||||
)
|
||||
TokensInput, _ = meter.Int64Counter("proxy.tokens.input",
|
||||
metric.WithDescription("Input tokens consumed"),
|
||||
)
|
||||
TokensOutput, _ = meter.Int64Counter("proxy.tokens.output",
|
||||
metric.WithDescription("Output tokens consumed"),
|
||||
)
|
||||
CredentialCooldowns, _ = meter.Int64Counter("proxy.credential.cooldowns",
|
||||
metric.WithDescription("Credential cooldown activations"),
|
||||
)
|
||||
ActiveCredentials, _ = meter.Int64UpDownCounter("proxy.credential.active",
|
||||
metric.WithDescription("Currently active (non-cooldown) credentials"),
|
||||
)
|
||||
StreamRequests, _ = meter.Int64Counter("proxy.stream.requests",
|
||||
metric.WithDescription("Streaming request count"),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/fujin/anthropic-proxy/internal/config"
|
||||
"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"
|
||||
otellog "go.opentelemetry.io/otel/log/global"
|
||||
"go.opentelemetry.io/otel/sdk/log"
|
||||
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
"go.opentelemetry.io/otel/sdk/trace"
|
||||
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) (shutdown func(context.Context) error, logWriter io.Writer, err error) {
|
||||
res, err := resource.New(ctx,
|
||||
resource.WithAttributes(
|
||||
semconv.ServiceName(cfg.ServiceName),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return 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))
|
||||
otel.SetMeterProvider(mp)
|
||||
InitMetrics(mp.Meter(cfg.ServiceName))
|
||||
return func(ctx context.Context) error { return mp.Shutdown(ctx) }, nil, nil
|
||||
}
|
||||
|
||||
// Build exporter options
|
||||
traceOpts := []otlptracegrpc.Option{otlptracegrpc.WithEndpoint(cfg.Endpoint)}
|
||||
metricOpts := []otlpmetricgrpc.Option{
|
||||
otlpmetricgrpc.WithEndpoint(cfg.Endpoint),
|
||||
otlpmetricgrpc.WithTemporalitySelector(sdkmetric.CumulativeTemporalitySelector),
|
||||
}
|
||||
logOpts := []otlploggrpc.Option{otlploggrpc.WithEndpoint(cfg.Endpoint)}
|
||||
if cfg.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
|
||||
}
|
||||
tp := trace.NewTracerProvider(
|
||||
trace.WithBatcher(traceExp),
|
||||
trace.WithResource(res),
|
||||
)
|
||||
otel.SetTracerProvider(tp)
|
||||
|
||||
// Metric exporter
|
||||
metricExp, err := otlpmetricgrpc.New(ctx, metricOpts...)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
mp := sdkmetric.NewMeterProvider(
|
||||
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExp)),
|
||||
sdkmetric.WithResource(res),
|
||||
)
|
||||
otel.SetMeterProvider(mp)
|
||||
InitMetrics(mp.Meter(cfg.ServiceName))
|
||||
|
||||
// Log exporter
|
||||
logExp, err := otlploggrpc.New(ctx, logOpts...)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
lp := log.NewLoggerProvider(
|
||||
log.WithProcessor(log.NewBatchProcessor(logExp)),
|
||||
log.WithResource(res),
|
||||
)
|
||||
otellog.SetLoggerProvider(lp)
|
||||
|
||||
bridge := &LogBridge{provider: lp}
|
||||
|
||||
shutdownFn := func(ctx context.Context) error {
|
||||
var firstErr error
|
||||
if e := tp.Shutdown(ctx); e != nil && firstErr == nil {
|
||||
firstErr = e
|
||||
}
|
||||
if e := mp.Shutdown(ctx); e != nil && firstErr == nil {
|
||||
firstErr = e
|
||||
}
|
||||
if e := lp.Shutdown(ctx); e != nil && firstErr == nil {
|
||||
firstErr = e
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
|
||||
return shutdownFn, bridge, nil
|
||||
}
|
||||
Reference in New Issue
Block a user