Cloud or Self-Hosted

Grafana Loki 适配器

将宽事件推送到 Grafana Loki——自托管、多租户或 Grafana Cloud——使用低基数标签并支持完整的 JSON 查询。

Loki 适配器通过其推送 API 将宽事件推送到 Grafana Loki。它适用于自托管的单租户实例、多租户部署以及 Grafana Cloud。

每个事件都会作为JSON 日志行推送,并归属于一组小型、低基数的标签。在 Loki 中,这一区别非常重要:标签会按基数建立索引并计费,而日志行则在查询时进行搜索。evlog 默认仅为 serviceenvironmentlevel 添加标签,并将其他所有内容(requestIdpathuser 以及你的自定义字段)保留在日志行中,可通过 | json 进行查询。

添加 Grafana Loki drain 适配器

安装

Loki 适配器已随 evlog 一起提供:

src/index.ts
import { createLokiDrain } from 'evlog/loki'

快速开始

设置 LOKI_ENDPOINT,并将日志排出器接入你的框架。

// server/plugins/evlog.ts
import { createLokiDrain } from 'evlog/loki'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createLokiDrain())
})

配置

环境变量

变量必填描述
LOKI_ENDPOINTLoki 实例的基础 URL,不包含推送路径(也接受 LOKI_URL
LOKI_API_KEYAPI 令牌。设置了 LOKI_USER 时以 Basic 方式发送,否则以 Bearer 方式发送(也接受 GRAFANA_API_KEY
LOKI_USERGrafana Cloud 实例 ID。将身份验证切换为 Basic(也接受 GRAFANA_USER
LOKI_TENANT_ID多租户自托管 Loki 的租户,以 X-Scope-OrgID 发送

优先级

配置按从高到低的优先级解析:

  1. 传递给 createLokiDrain() 的覆盖配置
  2. runtimeConfig.evlog.loki(Nitro)
  3. runtimeConfig.loki(Nitro)
  4. 环境变量

选项

选项类型默认值描述
endpointstring基础 URL,不包含 /loki/api/v1/push
apiKeystringAPI 令牌
userstringGrafana Cloud 实例 ID(启用 Basic 身份验证)
tenantIdstring多租户 Loki 使用的 X-Scope-OrgID
labelFieldsstring[]['service', 'environment', 'level']提升为 Loki 标签的事件字段
labelsRecord<string, string>合并到每个流中的静态标签
timeoutnumber5000请求超时时间,单位为毫秒
retriesnumber2发生瞬时故障时的重试次数

部署

Loki 以这两种方式中的任一种运行,适配器在两种情况下都相同。区别仅在于身份验证。

自托管

单租户实例只需要端点:

server/plugins/evlog.ts
createLokiDrain({ endpoint: 'http://localhost:3100' })
.env
LOKI_ENDPOINT=http://localhost:3100

对于多租户部署,请指定租户名称,该名称会作为 X-Scope-OrgID 发送:

server/plugins/evlog.ts
createLokiDrain({
  endpoint: 'http://loki.internal:3100',
  tenantId: 'team-checkout',
})
.env
LOKI_ENDPOINT=http://loki.internal:3100
LOKI_TENANT_ID=team-checkout

如果你的实例位于需要身份验证的代理之后,则仅发送 apiKey,格式为 Authorization: Bearer

Grafana Cloud

Grafana Cloud 使用你的实例 ID和访问策略令牌进行身份验证,并将二者一起作为 HTTP Basic 发送:

server/plugins/evlog.ts
createLokiDrain({
  endpoint: 'https://logs-prod-eu-west-0.grafana.net',
  user: '123456',
  apiKey: process.env.GRAFANA_API_KEY,
})
.env
LOKI_ENDPOINT=https://logs-prod-eu-west-0.grafana.net
LOKI_USER=123456
LOKI_API_KEY=glc_xxx
user 是 Loki 数据源的数字实例 ID。请在 Grafana Cloud 堆栈中的 Connections → Data sources → Loki 下查找它。它不是你的账户邮箱。使用错误的值通常会导致 401

应用哪种身份验证

userapiKeytenantId发送的请求头
Authorization: Basic base64(user:apiKey)
Authorization: Bearer <apiKey>
X-Scope-OrgID: <tenantId>
无——未进行身份验证的实例

tenantId 独立存在,可以与任一身份验证模式结合使用。

标签与基数

绝不要将高基数字段提升为标签。Loki 会为每种唯一的标签组合创建一个流,因此为 requestIduserId 添加标签会创建数百万个流,并导致你的实例性能下降或无法运行

仅为你会进行筛选且取值集合有界的字段添加标签:

server/plugins/evlog.ts
createLokiDrain({
  // `region` 的取值只有少数几个——安全
  labelFields: ['service', 'environment', 'level', 'region'],
  // 此部署中的所有内容使用的静态标签
  labels: { cluster: 'prod-eu' },
})

其他所有内容都会保留在 JSON 日志行中,并且完全可查询。请参见下文。

Grafana 中的查询

按标签筛选,然后使用 | json 深入查看宽事件:

{service="checkout", environment="production"}
  | json
  | status >= 500
# 某个路由上的慢请求
{service="checkout"} | json | path="/api/orders" | durationMs > 1000
# 端到端的单个请求
{service="checkout"} | json | requestId="4a8ff3a8-..."
# 每个服务的错误率
sum by (service) (rate({environment="production", level="error"}[5m]))

批处理

将 drain 与管道结合使用,以批量推送,而不是逐个请求推送:

server/plugins/evlog.ts
import { createDrainPipeline } from 'evlog/pipeline'
import { createLokiDrain } from 'evlog/loki'
import type { DrainContext } from 'evlog'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 100, intervalMs: 5000 },
})

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', pipeline(createLokiDrain()))
})

共享同一标签集的事件会被分组到单个 Loki 流中,并按时间戳排序,因为 Loki 会拒绝流内时间顺序不正确的推送。

在本地验证

在 Docker 中启动 Loki 并推送真实事件,无需云账户:

Terminal
docker compose -f packages/evlog/test/e2e/docker-compose.yml up -d
LOKI_ENDPOINT=http://localhost:3100 pnpm run test:e2e
docker compose -f packages/evlog/test/e2e/docker-compose.yml down -v

该测试套件是一次往返测试:它推送事件,通过 Loki 的范围 API 将其查询回来,并断言标签集、JSON 日志行和时间戳均得以保留。如果未设置 LOKI_ENDPOINT,它会显示一个明确的标签并跳过自身,而不是静默通过。

如果想直接查看结果,可以将任意 Grafana 指向 http://localhost:3100,然后运行 {service="evlog-e2e"}

故障排查

Missing endpoint 未设置 LOKI_ENDPOINT,且没有传递 endpoint。drain 会记录一次 [evlog/loki] Missing endpoint,随后不再执行任何操作,因此请求永远不会被阻塞或失败。

Loki API error: 401 在 Grafana Cloud 中,请检查 user 是否为 Loki 数据源的数字实例 ID,而不是你的账户邮箱。

提及时间顺序错误条目的 Loki API error: 400 较旧版本的 Loki 会拒绝早于流中最近条目的条目。evlog 会在每次推送内进行排序;如果仍然遇到此问题,请在 Loki 中启用 unordered_writes,或缩短批处理间隔。

Grafana 中没有显示任何内容。 确认你查询的标签集。先运行 {service=~".+"},查看哪些流已到达。

直接使用 API

绕过 drain,自行推送事件:

scripts/backfill.ts
import { sendBatchToLoki, sendToLoki } from 'evlog/loki'

await sendToLoki(event, { endpoint: 'http://localhost:3100' })
await sendBatchToLoki(events, { endpoint: 'http://localhost:3100' })

buildLokiPayload()toLokiLabels()resolveLokiPushUrl() 也已导出,可用于自定义传输。

后续步骤