From 272cdae03b7f7c56732dfd02ba7ce8e471215aef Mon Sep 17 00:00:00 2001 From: RainbowBird Date: Tue, 12 May 2026 23:09:39 +0800 Subject: [PATCH] feat(server/otel): restructure observability metrics and add active sessions gauge - Moved RateLimitMetrics import path to a more centralized location. - Introduced a new file for active sessions gauge to track user sessions in the database. - Updated index.ts to include new metrics and ensure proper initialization of observability metrics. - Modified various routes and services to utilize the new observability structure. - Added smoke tests for HTTP and WebSocket metrics to ensure proper metric registration and functionality. - Enhanced error handling for metrics reading failures to improve observability. --- .../ai-context/observability-conventions.md | 88 ++++- .../docs/ai-context/observability-metrics.md | 59 +-- .../docs/ai-context/workers-and-runtime.md | 2 +- ...instrumentation.mjs => instrumentation.ts} | 31 +- .../airi-server-overview-cloud.json | 370 +++++++++++++----- .../dashboards/{build.mjs => build.ts} | 160 ++++++-- apps/server/package.json | 9 +- apps/server/src/app.ts | 15 +- apps/server/src/libs/auth.ts | 8 +- apps/server/src/libs/db.ts | 2 +- apps/server/src/middlewares/rate-limit.ts | 2 +- .../server/src/otel/gauges/active-sessions.ts | 102 +++++ .../src/{libs/otel.ts => otel/index.ts} | 78 +++- apps/server/src/routes/auth/index.ts | 2 +- apps/server/src/routes/chat-ws/index.ts | 2 +- apps/server/src/routes/openai/v1/index.ts | 15 +- apps/server/src/routes/stripe/index.ts | 2 +- .../otel/{http-smoke.mjs => http-smoke.ts} | 14 +- .../src/scripts/otel/{smoke.mjs => smoke.ts} | 8 +- .../otel/{ws-smoke.mjs => ws-smoke.ts} | 35 +- .../src/services/billing/billing-service.ts | 2 +- .../server/src/services/billing/flux-meter.ts | 2 +- apps/server/src/services/characters.ts | 2 +- apps/server/src/services/chats.ts | 2 +- apps/server/src/services/email.ts | 2 +- apps/server/src/utils/observability.ts | 7 + apps/server/tsconfig.json | 3 +- 27 files changed, 785 insertions(+), 239 deletions(-) rename apps/server/{instrumentation.mjs => instrumentation.ts} (80%) rename apps/server/otel/grafana/dashboards/{build.mjs => build.ts} (80%) create mode 100644 apps/server/src/otel/gauges/active-sessions.ts rename apps/server/src/{libs/otel.ts => otel/index.ts} (79%) rename apps/server/src/scripts/otel/{http-smoke.mjs => http-smoke.ts} (93%) rename apps/server/src/scripts/otel/{smoke.mjs => smoke.ts} (92%) rename apps/server/src/scripts/otel/{ws-smoke.mjs => ws-smoke.ts} (84%) diff --git a/apps/server/docs/ai-context/observability-conventions.md b/apps/server/docs/ai-context/observability-conventions.md index 34d81ddf3..39eb12aa3 100644 --- a/apps/server/docs/ai-context/observability-conventions.md +++ b/apps/server/docs/ai-context/observability-conventions.md @@ -76,21 +76,13 @@ Redis 相关优先复用 instrumentation 自动产生的标准属性,不要重 ## Metric Name 策略 -当前 `apps/server` 仍保留以下 metric name: +`apps/server` 的 LLM gateway metric 现在全部用标准 `gen_ai.client.*` semconv 名 + AIRI `airi.billing.*` 计费名。旧的 `llm.request.*` / `llm.tokens.*` / `flux.consumed` 字面名都已经迁移完,请**不要在新代码或 reviewer 建议里复活**它们 —— 代码里 const 命名(如 `METRIC_FLUX_CONSUMED`)保留是历史 identifier,对应的字面值已经是 `airi.billing.flux.consumed`,以字面值为准。 -- `llm.request.duration` -- `llm.request.count` -- `llm.tokens.prompt` -- `llm.tokens.completion` -- `flux.consumed` +新增或重命名 metric 时遵守: -这是有意为之,不是遗漏。 - -原因: - -- metric name 改动比 attribute 改动更容易破坏现有 Prometheus 查询、Grafana 面板和告警。 -- 目前更高价值的是先统一 metric attributes,使查询维度稳定。 -- 如需迁移 metric name,应该走兼容迁移方案,而不是在普通功能改动里直接重命名。 +- metric name 改动比 attribute 改动更容易破坏现有 Prometheus 查询、Grafana 面板和告警。**先确认 dashboard / alerts 是否在跑这条 series**,再决定是否重命名。 +- 重命名一定要走兼容迁移:先双发新旧两条 series,留出窗口给消费方切换,再删旧的;不要在普通功能改动里直接重命名。 +- 完整 metric 清单(含 Prometheus 系列名)维护在 [`observability-metrics.md`](./observability-metrics.md)。新增任何 metric 都要同步更新那份文档。 ## Grafana / Prometheus 查询策略 @@ -162,13 +154,13 @@ span name 目前允许保留业务可读格式,例如: - [packages/server-shared/src/observability.ts](/packages/server-shared/src/observability.ts) - [apps/server/src/routes/v1completions.ts](/apps/server/src/routes/v1completions.ts) -- [apps/server/src/libs/otel.ts](/apps/server/src/libs/otel.ts) +- [apps/server/src/otel/index.ts](/apps/server/src/otel/index.ts) - [services/telegram-bot/src/llm/actions.ts](/services/telegram-bot/src/llm/actions.ts) - [services/telegram-bot/src/bots/telegram/agent/actions/read-message.ts](/services/telegram-bot/src/bots/telegram/agent/actions/read-message.ts) ## SemconvStability 迁移说明 -`@opentelemetry/instrumentation-http` 0.215+ 默认 OLD semconv(`http.server.duration` in ms),不是 STABLE 名。AIRI 在 [apps/server/instrumentation.mjs](/apps/server/instrumentation.mjs) 顶部强制 `OTEL_SEMCONV_STABILITY_OPT_IN=http`(仅 STABLE)。 +`@opentelemetry/instrumentation-http` 0.215+ 默认 OLD semconv(`http.server.duration` in ms),不是 STABLE 名。AIRI 在 [apps/server/instrumentation.ts](/apps/server/instrumentation.ts) 顶部强制 `OTEL_SEMCONV_STABILITY_OPT_IN=http`(仅 STABLE)。 | Semconv 模式 | 发哪些 series | 我们用 | |---|---|---| @@ -185,20 +177,80 @@ span name 目前允许保留业务可读格式,例如: **何时切回 `dup`**:将来如果有别的 service 主动 scrape 本 server 的 OLD-name 系列,临时切几周完成迁移即可。 +## Multi-Replica 注意事项 + +服务跑在 Railway 上有 ≥2 个副本(见 [workers-and-runtime.md](./workers-and-runtime.md)),所有 metric 设计必须显式考虑跨副本聚合。 + +### `service.instance.id` 必须设 + +[apps/server/instrumentation.ts](/apps/server/instrumentation.ts) 在 resource 上注入 `service.instance.id`,按优先级取 `RAILWAY_REPLICA_ID` → `SERVER_INSTANCE_ID` → `randomUUID()`(带 warn 日志,提示 ops 系列会随重启 churn)。`HOSTNAME` 曾经在 fallback 链里但 Railway 没文档化它是否 per-replica 唯一,所以踢出去了;需要跨重启稳定时显式设 `SERVER_INSTANCE_ID`。 + +**没设的后果**:两个副本的所有 metric series label tuple 完全一致(`service_name` + `deployment_environment` 一样),Prometheus 收到时按规则丢一条 / collapse 系列,结果一个副本完全消失。 + +加新 metric 时不用做任何事——只要从 `meter` 创建出来,instance id 自动随 resource 一起带上。 + +### 按 instrument 类型的副本安全表 + +| 类型 | 副本安全? | 聚合方式 | 备注 | +|---|---|---|---| +| `Counter` | ✅ | `sum(rate(x[5m]))` | 每副本本地累加,`rate()` 自动处理重启 | +| `Histogram` | ✅ | `histogram_quantile(0.95, sum by (le, ...) (rate(x_bucket[5m])))` | 每副本本地 bucket,`sum by (le)` 合并 | +| `ObservableGauge`(**per-replica 状态**,如 `ws.connections.active`) | ✅ | `sum(x)` | callback 读本地 registry,所有副本求和 = 集群总量 | +| `ObservableGauge`(**cluster-wide 状态**,如 `user.active_sessions`) | ⚠️ | `max(x)` 或 `avg(x)` | 所有副本读同一份外部状态(DB),sum 会乘以副本数 | +| `UpDownCounter` | ⚠️ | 看场景 | 必须保证 `+1` 和 `-1` 在**同一副本**触发;否则单副本永久 +N 另一副本永久 -N | + +### `UpDownCounter` 红线 + +只在以下情况用: +- `+1` 和对应的 `-1` 都在**同一请求生命周期**或**同一进程的局部状态机**里发生(典型:`http.server.active_requests` —— 请求开始 +1,结束 -1,必在同一副本) +- 不依赖任何外部 TTL / GC / 异步过期 + +如果存在「TTL 自然过期」「跨实例资源转移」「依赖 webhook 异步触发 -1」之类的情况,**不要用 UpDownCounter**。改用: +- `ObservableGauge` 从权威存储(DB / Redis)按 callback 读真实值,dashboard 用 `max()` / `avg()` 聚合 +- 或者只保留对应的 `Counter`("created" + "deleted"),让 dashboard 自己算差值 + +历史教训:`user.active_sessions` 最早是 UpDownCounter,登录 +1 / 登出 -1。但 Better Auth 的 session TTL 过期不会调 delete hook,counter 单实例就漂;多副本登录在 A、登出在 B 直接撕裂。改成 `ObservableGauge` 后由 [apps/server/src/app.ts](/apps/server/src/app.ts) 的 `registerActiveSessionsGauge` 通过 `SELECT COUNT(*) FROM session WHERE expires_at > NOW()` 在 scrape 时按需查 DB,带 10s 内存缓存避免 hammer。 + +### Dashboard 查询模板 + +加新 panel 时按这个清单核对: + +| 数据语义 | PromQL 模板 | +|---|---| +| 业务事件速率(Counter) | `sum(rate(x_total{...}[$__rate_interval]))` | +| 按 label 切分速率 | `sum by (