21.08-Metrics指标
要点
- Metrics 是时序数据,用于监控和报警
- Cloudflare Analytics Engine 是专门给 Workers 的时序数据库
- 区分 Counter(累加)、Gauge(当前值)、Histogram(分布)
- 常用指标:请求数、错误率、延迟、token 消耗、缓存命中率
内容
1. Metrics 的三种类型
时序指标分三种类型:
| 类型 | 含义 | 示例 |
|---|---|---|
| Counter | 只增不减的计数器 | 请求总数、错误总数、token 消耗总量 |
| Gauge | 可增可减的当前值 | 当前并发请求数、内存使用量、队列长度 |
| Histogram | 数值分布统计 | 请求延迟分布、响应体大小分布 |
1.1 Counter
Counter 用于累计计数,只增不减:
// 请求计数器
c.env.ANALYTICS.writeDataPoint({
blobs: ['request_count', c.req.method, c.req.path],
doubles: [1], // 每次 +1
indexes: [c.get('userId')],
})查询最近 1 小时的请求数:
SELECT SUM(_sample_interval) as total_requests
FROM metrics
WHERE blob1 = 'request_count'
AND timestamp > NOW() - INTERVAL '1' HOUR1.2 Gauge
Gauge 用于记录当前值,可增可减:
// 并发请求数
const activeRequests = await c.env.ACTIVE_REQUESTS.get('count') || 0
await c.env.ACTIVE_REQUESTS.put('count', activeRequests + 1)
c.env.ANALYTICS.writeDataPoint({
blobs: ['active_requests'],
doubles: [activeRequests + 1],
indexes: [],
})
await next()
// 请求完成后 -1
await c.env.ACTIVE_REQUESTS.put('count', activeRequests)1.3 Histogram
Histogram 用于统计数值分布(P50、P90、P99):
// 请求延迟分布
c.env.ANALYTICS.writeDataPoint({
blobs: ['request_duration', c.req.method],
doubles: [Date.now() - startedAt], // 记录延迟值
indexes: [c.get('userId')],
})查询 P99 延迟:
SELECT APPROX_QUANTILE(0.99, double1) as p99_duration
FROM metrics
WHERE blob1 = 'request_duration'
AND timestamp > NOW() - INTERVAL '1' HOUR2. Analytics Engine 基础
Analytics Engine 是 Cloudflare 提供的时序数据库,专门给 Workers 用。
2.1 配置
// wrangler.jsonc
{
"analytics_engine_datasets": [
{
"binding": "ANALYTICS",
"dataset": "ai_gateway_metrics"
}
]
}2.2 写入
// src/lib/metrics.ts
type Bindings = {
ANALYTICS: AnalyticsEngineDataset
}
export async function recordMetric(
env: Bindings,
name: string,
tags: Record<string, string>,
values: number[]
) {
env.ANALYTICS.writeDataPoint({
blobs: [name, ...Object.values(tags)], // 最多 20 个字符串列
doubles: values, // 最多 20 个数值列
indexes: [tags.userId || ''], // 最多 1 个索引列
})
}注意限制:
blobs:最多 20 个字符串列,用来过滤和 group bydoubles:最多 20 个数值列,用来求和、求平均、计算分位数indexes:最多 1 个索引列,用于高基数场景的采样
2.3 查询
Dashboard 里有 UI,或者用 SQL API:
// src/routes/admin/metrics.ts
app.get('/admin/metrics/query', async (c) => {
const sql = c.req.query('sql')
const response = await c.env.ANALYTICS.sql(sql)
return c.json(response)
})-- 最近 1 小时各模型的请求数和平均延迟
SELECT
blob2 AS model,
SUM(_sample_interval) AS total_requests,
AVG(double1) AS avg_duration_ms,
APPROX_QUANTILE(0.99, double1) AS p99_duration_ms
FROM ai_gateway_metrics
WHERE blob1 = 'llm.call'
AND timestamp > NOW() - INTERVAL '1' HOUR
GROUP BY blob2
ORDER BY total_requests DESC3. AI 网关的核心指标
3.1 请求指标
// src/middleware/metrics.ts
export async function requestMetrics(c: Context, next: Next) {
const startedAt = Date.now()
await next()
const duration = Date.now() - startedAt
// 1. 请求计数
await recordMetric(c.env, 'http.request', {
method: c.req.method,
path: c.req.path,
status: String(c.res.status),
userId: c.get('userId') || 'anonymous',
}, [duration])
// 2. 错误计数
if (c.res.status >= 500) {
await recordMetric(c.env, 'http.error', {
method: c.req.method,
path: c.req.path,
status: String(c.res.status),
}, [1])
}
// 3. 慢请求计数
if (duration > 1000) {
await recordMetric(c.env, 'http.slow', {
method: c.req.method,
path: c.req.path,
}, [duration])
}
}3.2 AI 调用指标
// src/routes/chat.ts
app.post('/v1/chat/completions', async (c) => {
const body = await c.req.json()
const startedAt = Date.now()
try {
const result = await callLLM(body, c.env)
// 1. LLM 调用成功
await recordMetric(c.env, 'llm.call.success', {
model: result.model,
provider: 'openai',
userId: c.get('userId'),
}, [
Date.now() - startedAt, // 延迟
result.usage.total_tokens, // token 总数
result.usage.prompt_tokens, // 输入 token
result.usage.completion_tokens, // 输出 token
])
return c.json(result)
} catch (err) {
// 2. LLM 调用失败
await recordMetric(c.env, 'llm.call.error', {
model: body.model,
provider: 'openai',
error: err.name,
}, [1, Date.now() - startedAt])
throw err
}
})3.3 缓存指标
// src/lib/cache.ts
export async function getWithCache<T>(
env: Env,
key: string,
loader: () => Promise<T>,
ttl: number = 3600
): Promise<T> {
// 1. 尝试从缓存读取
const cached = await env.CACHE.get(key)
if (cached) {
// 缓存命中
await recordMetric(env, 'cache.hit', {
key_prefix: key.split(':')[0],
}, [1])
return JSON.parse(cached)
}
// 2. 缓存未命中
await recordMetric(env, 'cache.miss', {
key_prefix: key.split(':')[0],
}, [1])
// 3. 加载数据
const data = await loader()
// 4. 写入缓存
await env.CACHE.put(key, JSON.stringify(data), {
expirationTtl: ttl,
})
return data
}4. 常用查询示例
4.1 请求量和错误率
-- 最近 1 小时每 5 分钟的请求量和错误率
SELECT
time_slice(timestamp, 5, 'minute') AS time_bucket,
SUM(_sample_interval) AS total_requests,
SUM(CASE WHEN blob3 >= '500' THEN _sample_interval ELSE 0 END) AS error_count,
error_count * 100.0 / total_requests AS error_rate
FROM ai_gateway_metrics
WHERE blob1 = 'http.request'
AND timestamp > NOW() - INTERVAL '1' HOUR
GROUP BY time_bucket
ORDER BY time_bucket4.2 延迟分布
-- 各模型的 P50/P90/P99 延迟
SELECT
blob2 AS model,
APPROX_QUANTILE(0.50, double1) AS p50_ms,
APPROX_QUANTILE(0.90, double1) AS p90_ms,
APPROX_QUANTILE(0.99, double1) AS p99_ms,
AVG(double1) AS avg_ms
FROM ai_gateway_metrics
WHERE blob1 = 'llm.call.success'
AND timestamp > NOW() - INTERVAL '1' HOUR
GROUP BY blob24.3 Token 消耗
-- 各用户今天的 token 消耗
SELECT
blob4 AS user_id,
SUM(double2) AS total_tokens,
SUM(double3) AS prompt_tokens,
SUM(double4) AS completion_tokens,
SUM(double2) * 0.00001 AS estimated_cost -- 假设 $0.01 / 1K tokens
FROM ai_gateway_metrics
WHERE blob1 = 'llm.call.success'
AND timestamp > CURRENT_DATE()
GROUP BY blob4
ORDER BY total_tokens DESC
LIMIT 1004.4 缓存命中率
-- 缓存命中率趋势
SELECT
time_slice(timestamp, 5, 'minute') AS time_bucket,
SUM(CASE WHEN blob1 = 'cache.hit' THEN _sample_interval ELSE 0 END) AS cache_hits,
SUM(CASE WHEN blob1 = 'cache.miss' THEN _sample_interval ELSE 0 END) AS cache_misses,
cache_hits * 100.0 / (cache_hits + cache_misses) AS hit_rate
FROM ai_gateway_metrics
WHERE blob1 IN ('cache.hit', 'cache.miss')
AND timestamp > NOW() - INTERVAL '1' HOUR
GROUP BY time_bucket
ORDER BY time_bucket5. 告警规则
基于 metrics 设置告警:
// src/cron/metrics-alerts.ts
export default {
async scheduled(event, env, ctx) {
// 每 5 分钟执行一次
// 1. 检查错误率
const errorRate = await env.ANALYTICS.sql(`
SELECT
SUM(CASE WHEN blob3 >= '500' THEN _sample_interval ELSE 0 END) * 100.0 /
SUM(_sample_interval) AS error_rate
FROM ai_gateway_metrics
WHERE blob1 = 'http.request'
AND timestamp > NOW() - INTERVAL '5' MINUTE
`)
if (errorRate[0].error_rate > 5) {
await sendAlert(env, {
type: 'high_error_rate',
errorRate: errorRate[0].error_rate,
})
}
// 2. 检查 P99 延迟
const p99 = await env.ANALYTICS.sql(`
SELECT APPROX_QUANTILE(0.99, double1) AS p99_ms
FROM ai_gateway_metrics
WHERE blob1 = 'llm.call.success'
AND timestamp > NOW() - INTERVAL '5' MINUTE
`)
if (p99[0].p99_ms > 10000) {
await sendAlert(env, {
type: 'high_latency',
p99: p99[0].p99_ms,
})
}
// 3. 检查 token 消耗
const tokenUsage = await env.ANALYTICS.sql(`
SELECT SUM(double2) AS total_tokens
FROM ai_gateway_metrics
WHERE blob1 = 'llm.call.success'
AND timestamp > NOW() - INTERVAL '1' HOUR
`)
if (tokenUsage[0].total_tokens > 10000000) {
await sendAlert(env, {
type: 'high_token_usage',
tokens: tokenUsage[0].total_tokens,
})
}
},
}6. Dashboard 可视化
除了 SQL 查询,还可以在 Cloudflare Dashboard 创建自定义图表:
- 进入 Workers → 你的 Worker → Analytics
- 点击「Create Custom Dashboard」
- 添加图表:
- 请求量趋势(Counter)
- 错误率趋势(Counter / Counter)
- P99 延迟趋势(Histogram)
- Token 消耗趋势(Counter)
- 缓存命中率趋势(Counter / Counter)
7. 和其他工具的对比
| 工具 | 适用场景 | 价格 |
|---|---|---|
| Analytics Engine | 中小项目,简单指标 | 免费版每天 10 万写入 |
| Prometheus + Grafana | 自建监控,复杂查询 | 免费(需要自己部署) |
| Datadog | 全功能 APM | 按主机计费,较贵 |
| New Relic | 全功能 APM | 按数据量计费 |
对于 AI 网关,Analytics Engine 足够用了。它和 Workers 深度集成,不需要额外的 SDK 或配置。
8. 实战:完整的 Metrics 中间件
// src/middleware/metrics.ts
import { Context, Next } from 'hono'
import { recordMetric } from '../lib/metrics'
export async function metricsMiddleware(c: Context, next: Next) {
const startedAt = Date.now()
const userId = c.get('userId') || 'anonymous'
await next()
const duration = Date.now() - startedAt
const status = c.res.status
// 1. 请求计数和延迟
await recordMetric(c.env, 'http.request', {
method: c.req.method,
path: c.req.path,
status: String(status),
userId,
}, [duration])
// 2. 错误计数
if (status >= 500) {
await recordMetric(c.env, 'http.error', {
method: c.req.method,
path: c.req.path,
status: String(status),
}, [1])
}
// 3. 慢请求计数
if (duration > 1000) {
await recordMetric(c.env, 'http.slow', {
method: c.req.method,
path: c.req.path,
duration: String(duration),
}, [duration])
}
}
export async function llmMetricsMiddleware(c: Context, next: Next) {
const startedAt = Date.now()
const body = await c.req.json().catch(() => null)
try {
const result = await next()
// 从响应体提取 usage
const responseBody = await result.json()
if (responseBody.usage) {
await recordMetric(c.env, 'llm.call.success', {
model: responseBody.model,
provider: 'openai',
userId: c.get('userId') || 'anonymous',
}, [
Date.now() - startedAt,
responseBody.usage.total_tokens,
responseBody.usage.prompt_tokens,
responseBody.usage.completion_tokens,
])
}
return result
} catch (err) {
await recordMetric(c.env, 'llm.call.error', {
model: body?.model || 'unknown',
provider: 'openai',
error: err.name,
}, [1, Date.now() - startedAt])
throw err
}
}// src/index.ts
app.use('*', metricsMiddleware)
app.post('/v1/chat/completions', llmMetricsMiddleware, async (c) => {
// ...
})9. 小结
Metrics 指标的关键点:
- 三种类型:Counter(计数)、Gauge(当前值)、Histogram(分布)
- Analytics Engine:Workers 原生的时序数据库,免费版每天 10 万写入
- 核心指标:请求量、错误率、延迟、token 消耗、缓存命中率
- 告警规则:基于 metrics 设置阈值告警
- 可视化:Dashboard 自定义图表
Metrics 是监控和告警的基础。下一节讲健康检查接口,看看怎么让监控系统自动检查服务是否可用。