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.
This commit is contained in:
@@ -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 (<label>) (rate(x_total{...}[$__rate_interval]))` |
|
||||
| 时延分位(Histogram) | `histogram_quantile(0.95, sum by (le, <label>) (rate(x_bucket{...}[$__rate_interval])))` |
|
||||
| 集群总量(per-replica gauge) | `sum(x{...})` |
|
||||
| 集群唯一值(cluster-wide gauge) | `max(x{...})` 或 `avg(x{...})` |
|
||||
| 按副本拆分调试 | `<agg> by (service_instance_id) (x{...})` |
|
||||
| 错误率 | `100 * sum(rate(x_total{...,status_code=~"5.."}[5m])) / clamp_min(sum(rate(x_total{...}[5m])), 1)` |
|
||||
|
||||
红线:**任何 cumulative counter 都不能直接 `sum()` 不 wrap rate/increase**。Counter 在副本重启时归零,没有 rate() 包裹 Prometheus 会跳变;用 `increase($__range)` 看「时间窗口内总量」,用 `rate([interval])` 看「当前速率」。
|
||||
|
||||
### 「按副本拆分」何时加
|
||||
|
||||
默认 panel 都聚合到集群层面。但以下场景应该加 `by (service_instance_id)` 拆分图:
|
||||
|
||||
- 进程级资源(heap、event loop、DB pool)——一个副本泄漏 / pin CPU 别的副本掩盖不掉
|
||||
- WS 连接 ——可以看出来是不是单个副本不均衡
|
||||
- 自定义的 ObservableGauge 排查
|
||||
|
||||
Dashboard 当前 Infrastructure 行已经是 by instance 的(Heap、Event Loop、DB Pool)。
|
||||
|
||||
## Counter priming 注意事项
|
||||
|
||||
OTel SDK 的 Counter / UpDownCounter 在第一次 `.add()` 之前**完全不出现在 Prometheus 抓取里**。Histogram 同理(要等第一次 `.record()`)。
|
||||
|
||||
后果:低流量 metric 在 dashboard 上看起来像「埋点丢了」,告警里 `absent()` 也无法工作。
|
||||
|
||||
[apps/server/src/libs/otel.ts](/apps/server/src/libs/otel.ts) 的 `primeCounter` 在 SDK 启动后给每个 Counter 调一次 `.add(0)`,把 series 注册出来;`0` 不影响 rate / sum 计算。
|
||||
[apps/server/src/otel/index.ts](/apps/server/src/otel/index.ts) 的 `primeCounter` 在 SDK 启动后给每个 Counter 调一次 `.add(0)`,把 series 注册出来;`0` 不影响 rate / sum 计算。
|
||||
|
||||
加新 Counter 时**记得加进 prime 列表**,否则未触发的指标在 Grafana 里就是空的。
|
||||
|
||||
验证脚本:[apps/server/src/scripts/otel-smoke.mjs](/apps/server/src/scripts/otel-smoke.mjs)
|
||||
验证脚本:[apps/server/src/scripts/otel/smoke.ts](/apps/server/src/scripts/otel/smoke.ts)
|
||||
|
||||
```sh
|
||||
pnpm -F @proj-airi/server exec node --import tsx ./src/scripts/otel-smoke.mjs
|
||||
pnpm -F @proj-airi/server exec node --import tsx ./src/scripts/otel/smoke.ts
|
||||
```
|
||||
|
||||
打印 SDK 启动后立即可见的所有 instrument 名字。
|
||||
|
||||
@@ -14,7 +14,7 @@ OTel SDK 在导出到 Prometheus 时做两件事:
|
||||
- `http_server_request_duration_seconds_bucket`(含 `le` label)
|
||||
- `http_server_request_duration_seconds_count`
|
||||
- `http_server_request_duration_seconds_sum`
|
||||
4. UpDownCounter 不加 `_total`:`ws.connections.active` → `ws_connections_active`
|
||||
4. UpDownCounter / ObservableGauge 不加 `_total`:`ws.connections.active` → `ws_connections_active`、`user.active_sessions` → `user_active_sessions`
|
||||
5. 带单位的 instrument 在 SDK 导出时把单位插进名字:`airi.stripe.revenue`(unit `minor_unit`)→ `airi_stripe_revenue_minor_unit_total`
|
||||
|
||||
> 查询面板若拼名字时不确定后缀,先用 `{__name__=~"airi_billing_flux.*"}` 之类正则探一下。
|
||||
@@ -23,10 +23,14 @@ OTel SDK 在导出到 Prometheus 时做两件事:
|
||||
|
||||
| Metric | 类型 | Unit | 来源 | 关键 attributes |
|
||||
|---|---|---|---|---|
|
||||
| `http.server.request.duration` | Histogram | s | `instrumentation-http`(STABLE semconv) | `http.request.method`、`http.route`、`http.response.status_code` |
|
||||
| `http.server.active_requests` | UpDownCounter | — | [middlewares/otel.ts](../../src/middlewares/otel.ts) `otelMiddleware` | `http.request.method`、`http.route` |
|
||||
| `http.server.request.duration` | Histogram | s | [`@hono/otel`](https://www.npmjs.com/package/@hono/otel) `httpInstrumentationMiddleware` in [app.ts](../../src/app.ts) | `http.request.method`、`http.route`、`http.response.status_code` |
|
||||
| `http.server.active_requests` | UpDownCounter | — | 同上 | `http.request.method` |
|
||||
|
||||
> **STABLE-only**:[instrumentation.mjs:25](../../instrumentation.mjs) 把 `OTEL_SEMCONV_STABILITY_OPT_IN=http` 提前注入。OLD 系列(`http.server.duration` in ms)不再发射。详见 [`observability-conventions.md` 的 SemconvStability 章节](./observability-conventions.md#semconvstability-迁移说明)。
|
||||
> **入站走 @hono/otel,出站走 auto HttpInstrumentation**:auto instrumentation 在 Node http 层抓数据时 Hono 还没匹配路由,`http.route` label 永远为空。`@hono/otel` 在 Hono middleware 链里跑,能拿到匹配后的路由 pattern(`/api/v1/users/:id` 而非具体 URL),所以入站 metric 由它产生。auto HttpInstrumentation 在 [instrumentation.ts](../../instrumentation.ts) 里通过 `ignoreIncomingRequestHook: () => true` 仅保留**出站**(LLM gateway、Stripe、Resend),那部分还是要它来跟踪。
|
||||
>
|
||||
> **STABLE-only**:[instrumentation.ts](../../instrumentation.ts) 把 `OTEL_SEMCONV_STABILITY_OPT_IN=http` 提前注入。OLD 系列(`http.server.duration` in ms)不再发射。详见 [`observability-conventions.md` 的 SemconvStability 章节](./observability-conventions.md#semconvstability-迁移说明)。
|
||||
>
|
||||
> `/health` 路径在 [app.ts](../../src/app.ts) 的 @hono/otel 包装层被显式 skip,Railway 健康检查不进 metric。
|
||||
|
||||
## Auth & Users
|
||||
|
||||
@@ -38,7 +42,11 @@ OTel SDK 在导出到 Prometheus 时做两件事:
|
||||
| `auth.failures` | Counter | `after` hook,`ctx.context.returned` 含 `error` | `auth.method` |
|
||||
| `user.registered` | Counter | `databaseHooks.user.create.after` | — |
|
||||
| `user.login` | Counter | `databaseHooks.session.create.after` | — |
|
||||
| `user.active_sessions` | UpDownCounter | session create / delete | — |
|
||||
| `user.active_sessions` | ObservableGauge | [app.ts](../../src/app.ts) `registerActiveSessionsGauge`,scrape 时查 `SELECT COUNT(*) FROM session WHERE expires_at > NOW()`(10s 内存缓存) | — |
|
||||
|
||||
> **`user.active_sessions` 是 cluster-wide gauge,dashboard 必须用 `max()` / `avg()`,不能用 `sum()`**。所有副本读同一份 DB 报同一个值,sum 会乘以副本数。详见 [observability-conventions.md 的 Multi-Replica 章节](./observability-conventions.md#multi-replica-注意事项)。
|
||||
>
|
||||
> 历史:之前是 UpDownCounter(+1 on login, -1 on logout),但 Better Auth session TTL 过期不会调 delete hook,counter 单实例就漂;多副本下登录在 A、登出在 B 会直接撕裂正负数。所以改成 DB-backed gauge。
|
||||
|
||||
## Engagement
|
||||
|
||||
@@ -48,7 +56,7 @@ OTel SDK 在导出到 Prometheus 时做两件事:
|
||||
| `character.created` | Counter | [services/characters.ts](../../src/services/characters.ts) | — |
|
||||
| `character.deleted` | Counter | 同上 | — |
|
||||
| `character.engagement` | Counter | 同上(like/bookmark) | `action`(`like` / `unlike` / `bookmark` / `unbookmark`) |
|
||||
| `ws.connections.active` | UpDownCounter | [routes/chat-ws/index.ts](../../src/routes/chat-ws/index.ts) | — |
|
||||
| `ws.connections.active` | ObservableGauge | [routes/chat-ws/index.ts](../../src/routes/chat-ws/index.ts) `addCallback` walks `userConnections` Map | — |
|
||||
| `ws.messages.sent` | Counter | 同上 | — |
|
||||
| `ws.messages.received` | Counter | [services/chats.ts](../../src/services/chats.ts) | — |
|
||||
|
||||
@@ -73,12 +81,12 @@ OTel SDK 在导出到 Prometheus 时做两件事:
|
||||
|---|---|---|---|
|
||||
| `airi.billing.flux.consumed` | Counter | [routes/openai/v1/index.ts](../../src/routes/openai/v1/index.ts) `recordMetrics`(chat / tts) | `gen_ai.request.model`、`gen_ai.operation.name`/`airi.gen_ai.operation.kind`、`http.response.status_code` |
|
||||
| `airi.billing.flux.credited` | Counter | [services/billing/billing-service.ts](../../src/services/billing/billing-service.ts) 三条入账路径 | `source`(`stripe.checkout`/`stripe.invoice`/`promo`/`admin_grant`/...)、`type`(`credit`/`promo`) |
|
||||
| `airi.billing.flux.unbilled` | Counter | `routes/openai/v1/index.ts` 流式 debit 失败 catch | `gen_ai.request.model`、`reason`(`debit_failed`)、`stage`(`streaming`) |
|
||||
| `airi.billing.flux.unbilled` | Counter | [routes/openai/v1/index.ts](../../src/routes/openai/v1/index.ts) streaming 路径里 `consumeFluxForLLM` 失败的 catch | `gen_ai.request.model`、`reason`(`debit_failed`)、`stage`(`streaming`) |
|
||||
| `flux.insufficient_balance` | Counter | [services/billing/billing-service.ts](../../src/services/billing/billing-service.ts) `debitFlux` | — |
|
||||
| `airi.billing.tts.chars` | Counter | [services/billing/flux-meter.ts](../../src/services/billing/flux-meter.ts) `accumulate` | `meter`(`tts`)、`model` |
|
||||
| `airi.billing.tts.preflight_rejections` | Counter | `flux-meter.ts` `assertCanAfford` | `meter`、`reason`(`insufficient_balance`) |
|
||||
|
||||
> **`airi.billing.flux.unbilled` 是 P0 告警金线**:任何持续 > 0 都意味着真实收入泄漏,应当 page。语义上等于"流式响应已经发给用户但 DB debit 失败的 Flux 量"。
|
||||
> **`airi.billing.flux.unbilled` 是 P0 告警金线**:流式响应已经发给用户(HTTP 200,token 已经流出),但 post-stream debit 抛错——response 路径不会因此 5xx,DB latency 也只在 catch 那一瞬间显著。HTTP / DB 告警**覆盖不到**这条静默 revenue leak。推荐 alert:`increase(airi_billing_flux_unbilled_total[5m]) > 0` 持续 > 0 立刻 page。
|
||||
|
||||
## GenAI
|
||||
|
||||
@@ -122,36 +130,37 @@ OTel SDK 在导出到 Prometheus 时做两件事:
|
||||
|
||||
## 已落地的 dashboard 行映射
|
||||
|
||||
[airi-server-overview-cloud.json](../../otel/grafana/dashboards/airi-server-overview-cloud.json),从上到下:
|
||||
[airi-server-overview-cloud.json](../../otel/grafana/dashboards/airi-server-overview-cloud.json) 由 [`build.ts`](../../otel/grafana/dashboards/build.ts) 生成(**直接改 JSON 会在下次 regenerate 时被覆盖;改 build.ts**),跑 `pnpm -F @proj-airi/server otel:dashboards` 重新生成。从上到下:
|
||||
|
||||
| Row | 关键 metric |
|
||||
|---|---|
|
||||
| HTTP Overview | `http.server.request.duration`(rate / P95 / by route / 5xx 率) |
|
||||
| Auth & Users | `auth.attempts` / `auth.failures` / `user.{login,registered,active_sessions}` + 失败率 |
|
||||
| Engagement | `ws.connections.active` / `ws.messages.{sent,received}` / `chat.messages` / `character.{created,deleted,engagement}` |
|
||||
| Business Metrics | `airi.billing.flux.consumed` / `flux.insufficient_balance` / `gen_ai.client.token.usage.*` / `stripe.checkout.completed` / `airi.billing.flux.credited` |
|
||||
| Stripe Detail | `stripe.{events,subscription.event,payment.failed}` / checkout funnel / `airi.stripe.revenue` |
|
||||
| Node.js Runtime | runtime instrumentation 那一批 |
|
||||
| LLM Gateway | `gen_ai.client.{operation.count,operation.duration,token.usage.*,first_token.duration}` / `airi.billing.flux.consumed` / `airi.billing.flux.unbilled` / `airi.gen_ai.stream.interrupted` / `airi.billing.tts.chars` |
|
||||
| Reliability | `airi.email.{send,failures}` 失败率 / `airi.rate_limit.blocked` / `airi.billing.tts.preflight_rejections` |
|
||||
| Application Logs | Loki,不是 Prometheus |
|
||||
| Row | viz | 关键 metric |
|
||||
|---|---|---|
|
||||
| Service Health | stat / gauge | `user.active_sessions`(`max()`)、`ws.connections.active`(`sum()`)、`http.server.request.duration_count`(req/s + 5xx%)、`gen_ai.client.operation.count`、`airi.email.{send,failures}` 失败率 |
|
||||
| Distribution (now) | donut | HTTP methods / LLM models / HTTP status codes — `increase([5m])` |
|
||||
| Traffic Trends | timeseries | 同 distribution 的数据 over time |
|
||||
| Latency | timeseries | `http.server.request.duration_bucket`(P95 by route)、`gen_ai.client.first_token.duration_bucket`(P95 by model) |
|
||||
| Errors / Quality | mix | 4xx/5xx stacked area、`airi.gen_ai.stream.interrupted`、`airi.rate_limit.blocked` |
|
||||
| Business | stat / gauge / donut | `airi.stripe.revenue`(by currency)、checkout conversion %、`stripe.events` 分布 |
|
||||
| Infrastructure (collapsed, **by `service_instance_id`**) | timeseries | `db_client_operation_duration` P95(cluster)、`db_client_connection_count`、`v8js_memory_heap_used_bytes` %、`nodejs_eventloop_delay_p99_seconds` |
|
||||
| Logs | logs | Loki,不是 Prometheus |
|
||||
|
||||
> **Multi-replica 聚合方式**:所有 panel 在 `build.ts` 里都已经按 `observability-conventions.md` 的副本安全表选择了正确的 aggregator(Counter 用 `sum(rate)`、cluster-wide gauge 用 `max()`、per-process gauge 用 `sum()`、infra 排查面板用 `by (service_instance_id)`)。加新 panel 时按那张表对照一遍。
|
||||
|
||||
## 验证 metric 是否已注册
|
||||
|
||||
[`src/scripts/otel-smoke.mjs`](../../src/scripts/otel-smoke.mjs) 跑一遍:
|
||||
[`src/scripts/otel/smoke.ts`](../../src/scripts/otel/smoke.ts) 跑一遍:
|
||||
|
||||
```sh
|
||||
pnpm -F @proj-airi/server exec node --import tsx ./src/scripts/otel-smoke.mjs
|
||||
pnpm -F @proj-airi/server exec node --import tsx ./src/scripts/otel/smoke.ts
|
||||
```
|
||||
|
||||
会打印 SDK 启动时立即 export 的所有 instrument 名字。**Counter 通过 `.add(0)` priming**([libs/otel.ts](../../src/libs/otel.ts) `primeCounter`)后会出现在这里 —— Histogram 不会,要等真实 `.record()` 才出现。
|
||||
会打印 SDK 启动时立即 export 的所有 instrument 名字。**Counter 通过 `.add(0)` priming**([otel/index.ts](../../src/otel/index.ts) `primeCounter`)后会出现在这里 —— Histogram 不会,要等真实 `.record()` 才出现。
|
||||
|
||||
## 加新 metric 时的 checklist
|
||||
|
||||
1. 决定命名空间:能映射到 OTel semconv 就用标准名,否则放 `airi.*`(不要造新顶级前缀)
|
||||
2. 在 [utils/observability.ts](../../src/utils/observability.ts) 加常量
|
||||
3. 在 [libs/otel.ts](../../src/libs/otel.ts) 的对应 metric group 接口(`HttpMetrics`/`AuthMetrics`/...)加字段,并在 `initOtel` 里 `meter.create*` 创建
|
||||
3. 在 [otel/index.ts](../../src/otel/index.ts) 的对应 metric group 接口(`HttpMetrics`/`AuthMetrics`/...)加字段,并在 `initOtel` 里 `meter.create*` 创建
|
||||
4. **如果是 Counter,在 `primeCounter` 调用列表里加一行** —— 否则低流量时 panel 看起来"没数据"
|
||||
5. 在 callsite 通过 DI 拿到 metrics 对象后调 `.add()` / `.record()`
|
||||
6. 跑 `pnpm -F @proj-airi/server exec node --import tsx ./src/scripts/otel-smoke.mjs` 确认注册
|
||||
6. 跑 `pnpm -F @proj-airi/server exec node --import tsx ./src/scripts/otel/smoke.ts` 确认注册
|
||||
7. 更新本文档对应章节
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
|
||||
## OpenTelemetry
|
||||
|
||||
初始化在 `src/libs/otel.ts`。
|
||||
初始化在 `instrumentation.ts`(NodeSDK lifecycle)+ `src/otel/index.ts`(metric handles)+ `src/otel/gauges/*.ts`(DB-backed ObservableGauge callbacks,例如 `gauges/active-sessions.ts`)。
|
||||
|
||||
启用条件:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user