Files
AIRouter/internal/observability/metrics.go
T
selfrelease da9c8334d8
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled
CI / security-scan (push) Has been cancelled
feat: 十轮网关优化 - 安全加固/可观测性/性能/可靠性
- SSE Keepalive Ping (15s心跳防止代理断连)
- Timing HTTP 头 (X-Timing-Queue/Inference/Total-Ms)
- Adapter Request-ID 传播到后端
- Session 清理日志回调
- Server 安全加固 (ReadHeaderTimeout/MaxHeaderBytes 防 slowloris)
- Usage Tracker 数据保留清理 (retentionDays + 定期清理)
- Config Reload 后 Adapter Registry 更新 (RegisterIfAbsent + RWMutex)
- Rate Limiter 空闲 Bucket 清理 (30分钟过期)
- Shutdown Drain 超时可配置 (ShutdownDrainSeconds)
- Config 模型字段校验增强 (provider/endpoint/actual_model)
- Auth 过期 Key 自动清理 (5分钟扫描)
- Admin API Rate Limiting
- Adapter Health Check 独立超时 (每个 adapter 3s)
- TCP 连接阶段超时 (DialContext 5s + KeepAlive 30s)
- 幂等键缓存、审计日志、Gzip 中间件、CORS Expose Headers
- Backpressure 响应头、熔断器 Prometheus 指标
- 连接池优化、Trace-ID 全链路传播
2026-08-03 15:43:11 +08:00

228 lines
8.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package observability
import (
"fmt"
"net/http"
"sync"
"sync/atomic"
)
// Metrics holds all Prometheus-compatible metrics for the gateway.
type Metrics struct {
mu sync.RWMutex
// Counters
requestsTotal map[string]int64 // by status
tasksTotal map[string]int64 // by state
tokensInputTotal int64
tokensOutputTotal int64
cancellationsTotal int64
queueTimeoutsTotal int64
firstTokenTimeoutsTotal int64
inferenceTimeoutsTotal int64
degradedRequestsTotal int64
// Gauges
queueLength int64
runningTasks int64
activeSessions int64
backpressureLevel int64
breakerState int64 // 0=closed, 1=open, 2=half_open
// Histograms (simplified as buckets)
gatewayLatencyBuckets map[string]int64
firstTokenLatencyBuckets map[string]int64
}
// NewMetrics creates a new Metrics instance.
func NewMetrics() *Metrics {
return &Metrics{
requestsTotal: make(map[string]int64),
tasksTotal: make(map[string]int64),
gatewayLatencyBuckets: make(map[string]int64),
firstTokenLatencyBuckets: make(map[string]int64),
}
}
// IncRequest increments the request counter by status.
func (m *Metrics) IncRequest(status string) {
key := fmt.Sprintf("status=%s", status)
m.mu.Lock()
m.requestsTotal[key]++
m.mu.Unlock()
}
// IncTask increments the task counter by final state.
func (m *Metrics) IncTask(state string) {
key := fmt.Sprintf("state=%s", state)
m.mu.Lock()
m.tasksTotal[key]++
m.mu.Unlock()
}
// AddTokens adds to the token counters.
func (m *Metrics) AddTokens(input, output int) {
atomic.AddInt64(&m.tokensInputTotal, int64(input))
atomic.AddInt64(&m.tokensOutputTotal, int64(output))
}
// IncCancellation increments the cancellation counter.
func (m *Metrics) IncCancellation() {
atomic.AddInt64(&m.cancellationsTotal, 1)
}
// IncQueueTimeout increments the queue timeout counter.
func (m *Metrics) IncQueueTimeout() {
atomic.AddInt64(&m.queueTimeoutsTotal, 1)
}
// IncFirstTokenTimeout increments the first token timeout counter.
func (m *Metrics) IncFirstTokenTimeout() {
atomic.AddInt64(&m.firstTokenTimeoutsTotal, 1)
}
// IncInferenceTimeout increments the inference timeout counter.
func (m *Metrics) IncInferenceTimeout() {
atomic.AddInt64(&m.inferenceTimeoutsTotal, 1)
}
// IncDegraded increments the degraded request counter.
func (m *Metrics) IncDegraded() {
atomic.AddInt64(&m.degradedRequestsTotal, 1)
}
// SetQueueLength sets the current queue length gauge.
func (m *Metrics) SetQueueLength(n int) {
atomic.StoreInt64(&m.queueLength, int64(n))
}
// SetRunningTasks sets the running tasks gauge.
func (m *Metrics) SetRunningTasks(n int) {
atomic.StoreInt64(&m.runningTasks, int64(n))
}
// SetActiveSessions sets the active sessions gauge.
func (m *Metrics) SetActiveSessions(n int) {
atomic.StoreInt64(&m.activeSessions, int64(n))
}
// SetBackpressureLevel sets the backpressure level gauge.
func (m *Metrics) SetBackpressureLevel(level int) {
atomic.StoreInt64(&m.backpressureLevel, int64(level))
}
// SetBreakerState 设置熔断器状态 gauge0=closed, 1=open, 2=half_open)。
func (m *Metrics) SetBreakerState(state int) {
atomic.StoreInt64(&m.breakerState, int64(state))
}
// ObserveGatewayLatency records gateway latency in a histogram bucket.
func (m *Metrics) ObserveGatewayLatency(ms int64) {
bucket := latencyBucket(ms)
m.mu.Lock()
m.gatewayLatencyBuckets[bucket]++
m.mu.Unlock()
}
// ObserveFirstTokenLatency records first token latency in a histogram bucket.
func (m *Metrics) ObserveFirstTokenLatency(ms int64) {
bucket := latencyBucket(ms)
m.mu.Lock()
m.firstTokenLatencyBuckets[bucket]++
m.mu.Unlock()
}
func latencyBucket(ms int64) string {
buckets := []int64{5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000}
for _, b := range buckets {
if ms <= b {
return fmt.Sprintf("le_%d", b)
}
}
return "le_inf"
}
// Handler returns an http.HandlerFunc that writes Prometheus-format metrics.
func (m *Metrics) Handler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
// Counters
fmt.Fprintf(w, "# HELP edgeai_requests_total Total requests by status\n")
fmt.Fprintf(w, "# TYPE edgeai_requests_total counter\n")
m.mu.RLock()
for key, val := range m.requestsTotal {
fmt.Fprintf(w, "edgeai_requests_total{%s} %d\n", key, val)
}
fmt.Fprintf(w, "# HELP edgeai_tasks_total Total tasks by final state\n")
fmt.Fprintf(w, "# TYPE edgeai_tasks_total counter\n")
for key, val := range m.tasksTotal {
fmt.Fprintf(w, "edgeai_tasks_total{%s} %d\n", key, val)
}
m.mu.RUnlock()
fmt.Fprintf(w, "# HELP edgeai_tokens_input_total Total input tokens processed\n")
fmt.Fprintf(w, "# TYPE edgeai_tokens_input_total counter\n")
fmt.Fprintf(w, "edgeai_tokens_input_total %d\n", atomic.LoadInt64(&m.tokensInputTotal))
fmt.Fprintf(w, "# HELP edgeai_tokens_output_total Total output tokens generated\n")
fmt.Fprintf(w, "# TYPE edgeai_tokens_output_total counter\n")
fmt.Fprintf(w, "edgeai_tokens_output_total %d\n", atomic.LoadInt64(&m.tokensOutputTotal))
fmt.Fprintf(w, "# HELP edgeai_cancellations_total Total cancelled requests\n")
fmt.Fprintf(w, "# TYPE edgeai_cancellations_total counter\n")
fmt.Fprintf(w, "edgeai_cancellations_total %d\n", atomic.LoadInt64(&m.cancellationsTotal))
fmt.Fprintf(w, "# HELP edgeai_queue_timeouts_total Total queue timeouts\n")
fmt.Fprintf(w, "# TYPE edgeai_queue_timeouts_total counter\n")
fmt.Fprintf(w, "edgeai_queue_timeouts_total %d\n", atomic.LoadInt64(&m.queueTimeoutsTotal))
fmt.Fprintf(w, "# HELP edgeai_first_token_timeouts_total Total first token timeouts\n")
fmt.Fprintf(w, "# TYPE edgeai_first_token_timeouts_total counter\n")
fmt.Fprintf(w, "edgeai_first_token_timeouts_total %d\n", atomic.LoadInt64(&m.firstTokenTimeoutsTotal))
fmt.Fprintf(w, "# HELP edgeai_inference_timeouts_total Total inference timeouts\n")
fmt.Fprintf(w, "# TYPE edgeai_inference_timeouts_total counter\n")
fmt.Fprintf(w, "edgeai_inference_timeouts_total %d\n", atomic.LoadInt64(&m.inferenceTimeoutsTotal))
fmt.Fprintf(w, "# HELP edgeai_degraded_requests_total Total requests served with degradation\n")
fmt.Fprintf(w, "# TYPE edgeai_degraded_requests_total counter\n")
fmt.Fprintf(w, "edgeai_degraded_requests_total %d\n", atomic.LoadInt64(&m.degradedRequestsTotal))
// Gauges
fmt.Fprintf(w, "# HELP edgeai_queue_length Current queue length\n")
fmt.Fprintf(w, "# TYPE edgeai_queue_length gauge\n")
fmt.Fprintf(w, "edgeai_queue_length %d\n", atomic.LoadInt64(&m.queueLength))
fmt.Fprintf(w, "# HELP edgeai_running_tasks Current running tasks\n")
fmt.Fprintf(w, "# TYPE edgeai_running_tasks gauge\n")
fmt.Fprintf(w, "edgeai_running_tasks %d\n", atomic.LoadInt64(&m.runningTasks))
fmt.Fprintf(w, "# HELP edgeai_active_sessions Current active sessions\n")
fmt.Fprintf(w, "# TYPE edgeai_active_sessions gauge\n")
fmt.Fprintf(w, "edgeai_active_sessions %d\n", atomic.LoadInt64(&m.activeSessions))
fmt.Fprintf(w, "# HELP edgeai_backpressure_level Current backpressure level (0-3)\n")
fmt.Fprintf(w, "# TYPE edgeai_backpressure_level gauge\n")
fmt.Fprintf(w, "edgeai_backpressure_level %d\n", atomic.LoadInt64(&m.backpressureLevel))
fmt.Fprintf(w, "# HELP edgeai_circuit_breaker_state Circuit breaker state (0=closed, 1=open, 2=half_open)\n")
fmt.Fprintf(w, "# TYPE edgeai_circuit_breaker_state gauge\n")
fmt.Fprintf(w, "edgeai_circuit_breaker_state %d\n", atomic.LoadInt64(&m.breakerState))
// Histograms
fmt.Fprintf(w, "# HELP edgeai_gateway_latency_bucket Gateway latency distribution in milliseconds\n")
fmt.Fprintf(w, "# TYPE edgeai_gateway_latency_bucket histogram\n")
m.mu.RLock()
for bucket, count := range m.gatewayLatencyBuckets {
fmt.Fprintf(w, "edgeai_gateway_latency_bucket{%s} %d\n", bucket, count)
}
fmt.Fprintf(w, "# HELP edgeai_first_token_latency_bucket First token latency distribution in milliseconds\n")
fmt.Fprintf(w, "# TYPE edgeai_first_token_latency_bucket histogram\n")
for bucket, count := range m.firstTokenLatencyBuckets {
fmt.Fprintf(w, "edgeai_first_token_latency_bucket{%s} %d\n", bucket, count)
}
m.mu.RUnlock()
}
}