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 Label | service、environment、cluster、namespace | 值域有限,经常用于选Stream |
| Structured Metadata | trace_id、request_id、user_id | 高基数,但需要按事件关联 |
| Log Body | message、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. 成本与保留策略
日志成本大致来自:
治理手段包括:
- 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属性关联。