ARCHIVE / INITIALIZING000%
正在载入档案界面SYS.07
Interview Prep

Logs 与 Loki

结构化日志、事件模型、Loki标签与Structured Metadata、LogQL、采集可靠性、隐私治理及Agent日志设计。

Logs 与 Loki

日志是带有时间和上下文的事件记录,适合解释具体操作中发生的状态变化和错误。有效日志应采用稳定的结构化事件,而不能只记录缺少上下文的something went wrong。

1. 日志事件结构

{
  "timestamp": "2026-09-02T10:20:31.482Z",
  "severity": "ERROR",
  "service.name": "checkout-api",
  "deployment.environment": "prod",
  "event.name": "db.connection.acquire_failed",
  "trace_id": "8f3c2a9e4b7d4c1e",
  "span_id": "17ab91c4",
  "request_id": "req-8421",
  "db.pool.wait_ms": 1200,
  "error.type": "deadline_exceeded",
  "message": "database connection acquisition timed out"
}

推荐把日志看成Event Schema:

什么时候发生:timestamp
严重程度:severity
谁产生:service / instance / environment
发生什么:event.name / message
关联哪次运行:trace_id / span_id / request_id
业务上下文:tenant / operation / resource
结果怎样:status / error.type / duration

字段名应保持稳定,字段语义应进行版本化。若同一字段在不同版本中分别表示为latency、duration_ms和字符串"slow",LogQL查询与告警规则将无法保持兼容。

2. 日志级别的语义

级别使用场景例子
DEBUG本地诊断或短期开启的细节Retry决策、解析中间状态
INFO正常且有业务价值的生命周期事件任务创建、部署完成
WARN已恢复或可能退化,需要关注第一次重试、使用降级结果
ERROR当前操作失败,需要定位Tool最终超时、事务提交失败

将每次429都记录为ERROR可能形成告警风暴;将所有异常都记录为INFO则会掩盖操作失败。日志级别应反映处理结果和处置需求。

3. Loki的核心模型

Loki把共享同一组Label的日志组织成Log Stream。它主要索引Label,不像全文搜索引擎那样默认索引每个日志字段;查询先由Label缩小Stream,再对其中的日志内容或结构化字段过滤。

{service_name="checkout-api", environment="prod"}

这带来一个关键设计:

放哪字段原因
Index Labelservice、environment、cluster、namespace值域有限,经常用于选Stream
Structured Metadatatrace_id、request_id、user_id高基数,但需要按事件关联
Log Bodymessage、stacktrace、动态参数内容丰富,不适合作为索引维度

trace_id需要查询,不代表它必须是Label。高基数Label会创建大量小Stream并增加索引和对象存储碎片,从而降低查询性能并增加存储成本。

4. Loki日志摄取链路

常见链路:

应用stdout / 文件 / OTLP
→ Grafana Alloy或OpenTelemetry Collector
→ 解析、补充Resource、脱敏、丢弃
→ Loki Distributor
→ Ingester组织Chunk
→ Object Storage + Index
→ Querier / Query Frontend

采集侧应处理:

  • 多行Stack Trace合并;
  • 时间戳和时区;
  • JSON解析失败;
  • Kubernetes元数据补充;
  • Secret与PII脱敏;
  • 重试、缓冲和背压;
  • 超长行截断与标记;
  • 租户路由和访问控制。

应用完成日志写入并不代表Loki已经接收。还需要观察Collector发送失败数、丢弃行数、队列积压量和Loki Ingestion错误。

5. LogQL查询

选择Stream并过滤文本

{service_name="checkout-api", environment="prod"}
  |= "timeout"
  != "client cancelled"

|=包含文本,!=排除文本。先用低基数Label缩小范围,再过滤内容。

解析JSON字段

{service_name="checkout-api"}
  | json
  | severity="ERROR"
  | trace_id="8f3c2a9e4b7d4c1e"

正则过滤

{service_name="checkout-api"}
  |~ "timeout|pool exhausted"

正则查询通常比精确过滤成本更高。应优先使用Label和结构化字段缩小查询范围,再在必要时使用正则过滤。

从日志计算错误速率

sum by (service_name) (
  rate(
    {environment="prod"}
      | json
      | severity="ERROR" [5m]
  )
)

日志指标适合补充缺失埋点或统计低频事件,但稳定的高频SLI应优先直接产生Metrics。若每次Dashboard刷新都扫描大量日志,将显著增加查询延迟和计算成本。

Unwrap数值字段

quantile_over_time(
  0.99,
  {service_name="agent-runtime"}
    | json
    | unwrap tool_duration_ms [5m]
) by (tool_name)

unwrap把日志字段转为数值样本,再执行区间聚合。生产查询需处理解析错误和单位一致性。

6. 用trace_id关联Trace

OpenTelemetry Context进入应用后,应把当前trace_id和span_id自动注入结构化日志。排障时:

Grafana Trace View选中失败Span
→ Trace to Logs带入service、时间窗、trace_id
→ Loki返回这次请求相关日志
→ 查看Span前后事件和错误上下文

关联失败通常由以下某一层配置或上下文不一致导致:

  • 上下文没有跨HTTP、gRPC或队列传播;
  • 异步消费者没有恢复Trace Context;
  • 日志字段名和Data Source配置不一致;
  • 时钟漂移导致查询时间窗错开;
  • Trace被采样保留,但相关日志被过滤或丢弃;
  • 租户和环境标签没有映射到同一范围。

7. Agent日志字段设计

Agent日志应记录可解释的运行事件,而不是泄露隐藏思维链:

task.created / task.cancelled / task.completed
planner.decision          结构化Action与理由摘要
tool.call.started         Tool名称、参数摘要、审批状态
tool.call.completed       状态、耗时、结果大小、错误类型
retrieval.completed       索引版本、TopK、过滤条件、Evidence ID
policy.denied             规则ID、动作类型、身份范围
synthesis.completed       Citation数量、Schema状态、Token

不要默认记录:

  • 完整System Prompt;
  • 未脱敏用户输入;
  • API Key、Cookie、Authorization Header;
  • 完整Tool返回的大段敏感数据;
  • 模型隐藏思维链;
  • 可以直接重放危险动作的凭据和参数。

可以保存Prompt Hash、模板版本、参数Digest、Evidence ID和受控的决策摘要。可解释性不要求记录全部原始输入、输出和中间状态。

8. Audit Log与普通Log不同

普通运行日志用于排障,可能采样、降级、按期删除;审计日志要证明谁在何时以什么权限做了什么,通常要求:

  • 不可抵赖或防篡改;
  • 明确Actor、Action、Resource和Result;
  • 审批人与策略版本;
  • 更严格的访问控制与保留期;
  • 独立存储或归档;
  • 删除和导出本身也被审计。

危险Tool的运行日志能够在Loki中查询,并不等同于满足审计合规要求。Loki是可观测性后端,审计系统仍需实现完整性、访问控制和保留策略。

9. 成本与保留策略

日志成本大致来自:

C≈Events/s×Bytes/Event×RetentionC\approx\mathrm{Events/s}\times\mathrm{Bytes/Event}\times\mathrm{Retention}

治理手段包括:

  • DEBUG默认关闭或动态限时开启;
  • 对高频成功事件采样,对错误与安全事件保留;
  • 删除无查询价值字段;
  • 对Stack Trace去重或指纹化;
  • 热数据短期保留,冷数据归档;
  • 按租户、环境和敏感等级设置策略;
  • 查询配额、超时、分片和公平调度。

成本优化不能简单丢弃错误日志,而应依据排障价值、风险等级和保留要求进行分层采样与存储。

10. 测试日志体系

日志也需要测试:

  • 单测Event Schema和脱敏器;
  • 集成测试Collector到Loki的投递;
  • 注入多行、超长、非法JSON和Unicode;
  • 验证trace_id能从Trace跳到Log;
  • 模拟Loki 429、磁盘满、网络断连和缓冲溢出;
  • 检查Secret扫描与Retention;
  • 用固定日志Fixture测试LogQL告警规则。

如果可观测性链路从未接受故障注入验证,它可能在业务故障期间同时失效,使团队失去关键诊断信号。

11. 高频追问

Loki和Elasticsearch最大的模型差异是什么?

Loki主要索引Label并按Stream组织日志,日志正文通常不做完整倒排索引;Elasticsearch更偏向对文档字段建立索引。Loki在日志场景成本可控,但Label设计和查询范围非常关键。

为什么不把错误消息做Label?

错误消息包含动态参数,值域可能无界,会造成高基数。使用稳定的error.type或error_code作为Label,完整消息留在日志正文。

日志越详细越好吗?

不是。详细度要服从排障价值、隐私、吞吐和成本。稳定结构、关联ID和关键状态通常比复制整个对象更有用。

Agent日志和Trace怎样分工?

Trace表示一次任务的调用树与耗时关系,日志记录Span内部具体事件和错误上下文。两者通过trace_id、span_id和Resource属性关联。

参考资料