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' HOUR

1.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' HOUR

2. 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 by
  • doubles:最多 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 DESC

3. 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_bucket

4.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 blob2

4.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 100

4.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_bucket

5. 告警规则

基于 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 创建自定义图表:

  1. 进入 Workers → 你的 Worker → Analytics
  2. 点击「Create Custom Dashboard」
  3. 添加图表:
    • 请求量趋势(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 指标的关键点:

  1. 三种类型:Counter(计数)、Gauge(当前值)、Histogram(分布)
  2. Analytics Engine:Workers 原生的时序数据库,免费版每天 10 万写入
  3. 核心指标:请求量、错误率、延迟、token 消耗、缓存命中率
  4. 告警规则:基于 metrics 设置阈值告警
  5. 可视化:Dashboard 自定义图表

Metrics 是监控和告警的基础。下一节讲健康检查接口,看看怎么让监控系统自动检查服务是否可用。