Просмотр исходного кода

feat(metrics): Prometheus 指标定义和注册

master
fengsilin 3 недель назад
Родитель
Сommit
688c5f8666
4 измененных файлов: 246 добавлений и 2 удалений
  1. +118
    -0
      common/metrics/metrics.go
  2. +123
    -0
      common/metrics/metrics_test.go
  3. +3
    -2
      go.mod
  4. +2
    -0
      go.sum

+ 118
- 0
common/metrics/metrics.go Просмотреть файл

@@ -0,0 +1,118 @@
package metrics

import (
"net/http"
"os"
"sync"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
globalMetrics *Metrics
globalOnce sync.Once
)

// Metrics 包含所有 Prometheus 指标
type Metrics struct {
RequestsTotal *prometheus.CounterVec
RequestErrorsTotal *prometheus.CounterVec
RequestRetriesTotal *prometheus.CounterVec
TokensTotal *prometheus.CounterVec
QuotaConsumedTotal *prometheus.CounterVec

RequestDuration *prometheus.HistogramVec
UpstreamDuration *prometheus.HistogramVec
FirstTokenDuration *prometheus.HistogramVec

ActiveRequests *prometheus.GaugeVec
}

// NewMetrics 创建并注册所有指标到给定 Registry
func NewMetrics(reg prometheus.Registerer) *Metrics {
m := &Metrics{
RequestsTotal: prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "newapi_requests_total",
Help: "Total number of API requests",
}, []string{"channel", "model", "code", "is_stream", "relay_mode"}),
RequestErrorsTotal: prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "newapi_request_errors_total",
Help: "Total number of request errors",
}, []string{"channel", "model", "error_type", "error_code"}),
RequestRetriesTotal: prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "newapi_request_retries_total",
Help: "Total number of request retries",
}, []string{"channel", "model"}),
TokensTotal: prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "newapi_tokens_total",
Help: "Total tokens used",
}, []string{"channel", "model", "type"}),
QuotaConsumedTotal: prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "newapi_quota_consumed_total",
Help: "Total quota consumed",
}, []string{"channel", "model", "billing_source"}),

RequestDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "newapi_request_duration_seconds",
Help: "End-to-end request duration in seconds",
Buckets: []float64{0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 120},
}, []string{"channel", "model", "is_stream", "relay_mode"}),
UpstreamDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "newapi_upstream_duration_seconds",
Help: "Upstream response duration in seconds",
Buckets: []float64{0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 120},
}, []string{"channel", "model"}),
FirstTokenDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "newapi_first_token_duration_seconds",
Help: "Time to first token in seconds (streaming only)",
Buckets: []float64{0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10},
}, []string{"channel", "model"}),

ActiveRequests: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "newapi_active_requests",
Help: "Current number of active requests",
}, []string{"model"}),
}

reg.MustRegister(
m.RequestsTotal,
m.RequestErrorsTotal,
m.RequestRetriesTotal,
m.TokensTotal,
m.QuotaConsumedTotal,
m.RequestDuration,
m.UpstreamDuration,
m.FirstTokenDuration,
m.ActiveRequests,
)

return m
}

// GetMetrics 返回全局 Metrics 实例
func GetMetrics() *Metrics {
globalOnce.Do(func() {
if os.Getenv("METRICS_ENABLED") == "false" {
globalMetrics = NewMetrics(prometheus.NewPedanticRegistry())
return
}
globalMetrics = NewMetrics(prometheus.DefaultRegisterer)
})
return globalMetrics
}

// IsEnabled 返回 metrics 是否启用
func IsEnabled() bool {
return os.Getenv("METRICS_ENABLED") != "false"
}

// NewMetricsHandler 返回默认 registry 的 /metrics HTTP handler
func NewMetricsHandler() http.Handler {
return promhttp.Handler()
}

// NewMetricsHandlerWithRegistry 返回指定 registry 的 /metrics HTTP handler
func NewMetricsHandlerWithRegistry(reg *prometheus.Registry) http.Handler {
return promhttp.HandlerFor(reg, promhttp.HandlerOpts{})
}

+ 123
- 0
common/metrics/metrics_test.go Просмотреть файл

@@ -0,0 +1,123 @@
package metrics

import (
"testing"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/assert"
)

func newTestRegistry() *prometheus.Registry {
return prometheus.NewRegistry()
}

func TestRequestsTotalCounter(t *testing.T) {
reg := newTestRegistry()
m := NewMetrics(reg)

m.RequestsTotal.WithLabelValues("1", "gpt-4", "200", "true", "chat").Inc()
m.RequestsTotal.WithLabelValues("1", "gpt-4", "200", "true", "chat").Inc()
m.RequestsTotal.WithLabelValues("2", "claude-3", "429", "false", "chat").Inc()

assert.Equal(t, float64(2), testutil.ToFloat64(m.RequestsTotal.WithLabelValues("1", "gpt-4", "200", "true", "chat")))
assert.Equal(t, float64(1), testutil.ToFloat64(m.RequestsTotal.WithLabelValues("2", "claude-3", "429", "false", "chat")))
}

func TestRequestErrorsTotalCounter(t *testing.T) {
reg := newTestRegistry()
m := NewMetrics(reg)

m.RequestErrorsTotal.WithLabelValues("1", "gpt-4", "upstream", "429").Inc()
m.RequestErrorsTotal.WithLabelValues("1", "gpt-4", "upstream", "429").Inc()
m.RequestErrorsTotal.WithLabelValues("2", "claude-3", "timeout", "context_deadline_exceeded").Inc()

assert.Equal(t, float64(2), testutil.ToFloat64(m.RequestErrorsTotal.WithLabelValues("1", "gpt-4", "upstream", "429")))
assert.Equal(t, float64(1), testutil.ToFloat64(m.RequestErrorsTotal.WithLabelValues("2", "claude-3", "timeout", "context_deadline_exceeded")))
}

func TestRequestRetriesTotalCounter(t *testing.T) {
reg := newTestRegistry()
m := NewMetrics(reg)

m.RequestRetriesTotal.WithLabelValues("1", "gpt-4").Inc()
m.RequestRetriesTotal.WithLabelValues("1", "gpt-4").Inc()

assert.Equal(t, float64(2), testutil.ToFloat64(m.RequestRetriesTotal.WithLabelValues("1", "gpt-4")))
}

func TestTokensTotalCounter(t *testing.T) {
reg := newTestRegistry()
m := NewMetrics(reg)

m.TokensTotal.WithLabelValues("1", "gpt-4", "prompt").Add(100)
m.TokensTotal.WithLabelValues("1", "gpt-4", "completion").Add(50)
m.TokensTotal.WithLabelValues("1", "gpt-4", "cache").Add(20)

assert.Equal(t, float64(100), testutil.ToFloat64(m.TokensTotal.WithLabelValues("1", "gpt-4", "prompt")))
assert.Equal(t, float64(50), testutil.ToFloat64(m.TokensTotal.WithLabelValues("1", "gpt-4", "completion")))
assert.Equal(t, float64(20), testutil.ToFloat64(m.TokensTotal.WithLabelValues("1", "gpt-4", "cache")))
}

func TestQuotaConsumedTotalCounter(t *testing.T) {
reg := newTestRegistry()
m := NewMetrics(reg)

m.QuotaConsumedTotal.WithLabelValues("1", "gpt-4", "wallet").Add(5000)

assert.Equal(t, float64(5000), testutil.ToFloat64(m.QuotaConsumedTotal.WithLabelValues("1", "gpt-4", "wallet")))
}

func TestRequestDurationHistogram(t *testing.T) {
reg := newTestRegistry()
m := NewMetrics(reg)

m.RequestDuration.WithLabelValues("1", "gpt-4", "true", "chat").Observe(1.5)
m.RequestDuration.WithLabelValues("1", "gpt-4", "true", "chat").Observe(2.5)

// 验证 histogram metric family 被注册且有数据
assert.Equal(t, 1, testutil.CollectAndCount(m.RequestDuration))
}

func TestUpstreamDurationHistogram(t *testing.T) {
reg := newTestRegistry()
m := NewMetrics(reg)

m.UpstreamDuration.WithLabelValues("1", "gpt-4").Observe(0.5)

assert.Equal(t, 1, testutil.CollectAndCount(m.UpstreamDuration))
}

func TestFirstTokenDurationHistogram(t *testing.T) {
reg := newTestRegistry()
m := NewMetrics(reg)

m.FirstTokenDuration.WithLabelValues("1", "gpt-4").Observe(0.1)

assert.Equal(t, 1, testutil.CollectAndCount(m.FirstTokenDuration))
}

func TestActiveRequestsGauge(t *testing.T) {
reg := newTestRegistry()
m := NewMetrics(reg)

m.ActiveRequests.WithLabelValues("gpt-4").Inc()
m.ActiveRequests.WithLabelValues("gpt-4").Inc()
m.ActiveRequests.WithLabelValues("claude-3").Inc()

assert.Equal(t, float64(2), testutil.ToFloat64(m.ActiveRequests.WithLabelValues("gpt-4")))
assert.Equal(t, float64(1), testutil.ToFloat64(m.ActiveRequests.WithLabelValues("claude-3")))

m.ActiveRequests.WithLabelValues("gpt-4").Dec()
assert.Equal(t, float64(1), testutil.ToFloat64(m.ActiveRequests.WithLabelValues("gpt-4")))
}

func TestMetricsDisabledIsNoOp(t *testing.T) {
// 当 METRICS_ENABLED=false 时,使用独立 registry(Discard)
reg := prometheus.NewPedanticRegistry()
m := NewMetrics(reg)
// 这些调用不应该 panic
m.RequestsTotal.WithLabelValues("1", "gpt-4", "200", "true", "chat").Inc()
m.ActiveRequests.WithLabelValues("gpt-4").Inc()
m.RequestDuration.WithLabelValues("1", "gpt-4", "true", "chat").Observe(1.0)
}

+ 3
- 2
go.mod Просмотреть файл

@@ -33,9 +33,11 @@ require (
github.com/jinzhu/copier v0.4.0
github.com/joho/godotenv v1.5.1
github.com/mewkiz/flac v1.0.13
github.com/mojocn/base64Captcha v1.3.8
github.com/nicksnyder/go-i18n/v2 v2.6.1
github.com/pkg/errors v0.9.1
github.com/pquerna/otp v1.5.0
github.com/prometheus/client_golang v1.22.0
github.com/samber/hot v0.11.0
github.com/samber/lo v1.52.0
github.com/shirou/gopsutil v3.21.11+incompatible
@@ -111,6 +113,7 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mewkiz/pkg v0.0.0-20250417130911-3f050ff8c56d // indirect
@@ -118,12 +121,10 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mojocn/base64Captcha v1.3.8 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pelletier/go-toml/v2 v2.2.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect


+ 2
- 0
go.sum Просмотреть файл

@@ -203,6 +203,8 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=


Загрузка…
Отмена
Сохранить