日志记录的 9 项最佳实践

从结构化日志、OpenTelemetry 约定、上下文、日志级别、敏感数据防护、性能、成本、采集流水线到后端选型,系统梳理日志记录的 9 项最佳实践。

作者 译文

我们一直在构建日益复杂的分布式系统,但在排查这些系统问题时,所依赖的工具却往往不过是功能增强版的 printf 语句。尽管日志记录从计算机发展初期就已存在,但仍有太多团队将其视为事后才需考虑的事项。

由此引发的后果大家都不陌生:因未删除调试日志而产生的高额云服务账单、花费整个下午却仍无法理清那些看似包罗万象却毫无实质内容的日志、本应通过工具自动完成的跨服务事件关联,却要手动去完成这一费力不讨好的工作。

本文讨论的 9 项日志记录最佳实践,目标不是让系统输出更多日志,而是让日志成为可查询、可关联、可治理、可用于排障的可观测性信号。

核心摘要

  • 日志本质上是数据,应优先采用 JSON 等结构化格式,而不是只适合人工 grep 的文本块。
  • OpenTelemetry 可以帮助团队统一日志字段、语义约定和采集流水线,避免不同服务各说各话。
  • 高质量日志必须包含平台上下文和事件上下文,才能支持跨服务关联、问题定位和根因分析。
  • 日志级别、采样、异步写入、敏感数据清理和成本控制,都是日志治理的一部分。
  • 最终的可观测性体验取决于链路完整性:应用埋点、收集器、处理策略和后端平台需要使用同一套语义。

如果您有多个告警系统,想要聚合统一到一个平台,做降噪、升级等,可以试试 Flashduty On-call,好用的很,免费注册试用:https://console.flashcat.cloud 转眼创业 4 年了,得到了很多用户的认可,感谢大家一路同行。

9 项日志记录最佳实践总览

实践 核心目标 解决的问题
1. 从结构化日志记录入手 将日志作为可查询数据处理 避免依赖正则和人工文本搜索
2. 通过 OpenTelemetry 确立可观测性约定 统一字段、模型和语义 避免不同服务日志格式混乱
3. 为日志补充充足的上下文信息 让每条日志都能独立说明来源和业务含义 支持跨服务关联和排障
4. 将日志级别用作可执行的信号 区分普通信息、异常和故障 降低告警疲劳,提升响应效率
5. 避免敏感数据进入日志 防止 PII、密码等信息泄露 降低安全和合规风险
6. 将日志记录性能视为核心关注点 控制 CPU、内存和 I/O 开销 避免日志拖慢关键路径
7. 智能管理日志量和成本 减少高数量、低价值日志 控制存储、采集和查询成本
8. 让 OpenTelemetry 收集器成为日志流水线的核心 集中处理、过滤、路由遥测数据 降低供应商耦合和运维复杂度
9. 选择原生支持 OpenTelemetry 的后端系统 充分利用日志、指标、追踪的关联关系 提升查询、分析和调试体验

1. 从结构化日志记录入手

在现代系统中,非结构化、字符串格式的日志是一种不良模式。如果你仍在编写那种仅为方便运维人员在服务器上使用 grep 命令查询的日志,那么不仅是落后于时代,更是在主动构建一个“不可观测”的系统。

日志本质上是数据,从创建之初就应被当作数据对待。这意味着每一条日志条目都必须是结构化的、可被机器解析的对象。JSON 是目前通用的格式。日志中的每一项信息都应成为独立的键值对,以便后续进行索引、查询和聚合操作。

摒弃以下这种文本块形式的日志输出:

[2025-07-24 07:45:10] INFO: Payment processed for user 12345 in 54ms. Request ID: abc-xyz-789

取而代之的,应输出可查询的数据记录:

{
  "level": "info",
  "timestamp": "2025-07-24T06:45:10.123Z",
  "message": "payment processed",
  "service": "billing-api",
  "duration_ms": 54,
  "user_id": "12345",
  "trace_id": "abc-xyz-789"
}

这种方式从根本上改变了你与日志交互的模式,让你摆脱繁琐的基于正则表达式的文本搜索,转而采用更强大的基于查询的分析方式。

借助结构化日志,你可以轻松解答各类问题,例如:

  • “过去一小时内有多少笔支付操作失败?”
  • “显示所有与请求 ID abc-xyz-789 相关的日志。”
  • “展示今天与用户 12345 相关的所有日志。”

这些问题不应依赖临时正则,而应通过字段查询快速完成。如今,大多数现代日志记录框架都支持或默认输出结构化日志,而且编程语言也开始将结构化日志作为核心功能,而非可选附加功能,例如 Go 语言的 log/slog 包。

2. 通过 OpenTelemetry 确立可观测性约定

采用结构化日志记录只是第一步。如果没有统一的标准,不同服务间日志的字段名称和语义仍会混乱不一。

某个服务可能将用户 ID 记录为 "user_id": "12345",另一个服务记录为 "userId": "12345",还有一个服务则记录为 "customer": { "id": "12345" }。当这种不一致性扩散到数十个服务中时,可观测性几乎会变得无从谈起,因为每个服务都在使用“不同的语言”。

要解决这个问题,你需要确立一套可观测性约定:为所有服务的遥测数据制定统一且强制执行的模式。而 OpenTelemetry(简称 OTel)正是实现这一目标的基础,它提供了通用的结构,也就是日志数据模型,以及通用的术语表,也就是语义约定。

两条接入路径

好消息是,你无需替换现有的埋点工具。OpenTelemetry 提供了两条清晰的路径,帮助你的日志符合约定要求:

  1. 对于你能掌控的应用程序,可以通过附加器(appender)或导出器(exporter),将 OpenTelemetry 直接与现有的日志库集成。该组件会拦截来自日志库的结构化日志,在内存中将其转换为 OpenTelemetry 模型,然后直接发送到收集器(collector)或后端系统。
  2. 对于无法修改的遗留系统或第三方系统,可允许它们继续向标准输出(stdout)或本地文件写入日志,再由 OpenTelemetry 收集器接收这些日志并将其转换为 OpenTelemetry 模型,之后转发到后端系统。

最后需要注意的是,没有强制执行机制的约定毫无用处。可以使用 OpenTelemetry Weaver 等工具,记录并强制执行核心属性模式,这些核心属性是每个服务都必须包含的。

随后,在 CI/CD(持续集成/持续部署)流水线中通过自动化手段提供支持:如果构建过程中出现未经批准的属性,或遗漏了必需的属性,就自动让构建失败。这样一来,遵守约定就会成为工程标准中与生俱来的一部分。

3. 为日志补充充足的上下文信息

在确立约定之后,你必须确保每条日志都补充了足够的上下文信息,使其能独立发挥作用。没有上下文的日志消息只是无用的“噪音”。每条日志都应能解答关于其来源、范围和目的的关键问题。

日志上下文可以分为两个层面:平台上下文和事件上下文。前者回答“这条日志来自哪里”,后者回答“这条日志描述的业务事件是什么”。

平台上下文

平台上下文是环境和请求级别的上下文,应自动附加到每一条日志中,无需开发人员付出额外努力。它能告诉你日志的来源以及相关联的对象。

这正是平台工程发挥作用的地方,而 OpenTelemetry 提供的工具能让这种自动化变得无缝衔接:

  • OpenTelemetry SDK 会自动注入 trace_idspan_id,将日志与生成它的特定请求关联起来。
  • OpenTelemetry 收集器可以通过 resourcedetectionprocessork8sattributesprocessor 等处理器,自动检测并附加来自主机环境的元数据,例如 cloud.provider(云服务提供商)、cloud.region(云区域)、k8s.pod.name(K8s Pod 名称)和 service.version(服务版本)。

平台上下文是日志的基础。它确保即使是最基础的日志,也能追溯到特定的请求、服务实例,以及运行该实例的特定区域和具体版本。

事件上下文

事件上下文是只有应用程序代码才能知晓的业务级上下文。它是调试中最有价值的信息,也是最难处理得当的部分。例如,实体标识符和其他特定于领域的属性,这些信息能解释尝试执行的操作是什么、为何执行该操作。

你不应仅依赖人工操作来补充事件上下文。相反,应建立一种模式:将支持上下文的日志记录器注入到请求生命周期中。大多数日志框架都提供该能力。

通过这种模式,在请求处理过程中确定相关标识符后,这些标识符会自动传递到所有下游日志行中,无需你付出额外努力。

通过将自动化的平台上下文与系统性注入的事件级上下文相结合,你生成的日志将“天生具备关联性”。它们不再是孤立的消息,而是能反映系统行为的丰富、连贯的叙事。

OWASP(开放式 Web 应用程序安全项目)日志记录指南提供了关于有用事件属性的可靠参考。但请务必在命名这些属性时遵循 OpenTelemetry 的语义约定,以确保日志保持一致性和互操作性。

4. 将日志级别用作可执行的信号

在日志记录领域,很少有话题能像日志严重级别的合理使用一样引发大量争议。

有些人主张简化处理:只使用 INFO(信息)和 ERROR(错误)两个级别,分别表示系统“正常运行”或“出现故障”。

从理论上看,这种方式似乎简洁明了,但在实际应用中,它会丢失过多细节。并非所有异常都是故障,也并非所有故障都需要通知相关人员处理。通过减少日志级别的数量,你会模糊重要的差异,无法区分可执行的信号和辅助性的细节。

采用更精细的日志级别,例如 DEBUG(调试)、WARN(警告)或 FATAL(致命错误),会有效得多。这些级别能传递更明确的含义:

  • WARN 通常表示出现了不寻常但需要处理的情况,但情况并不紧急,例如弃用警告。
  • ERROR 标识实际发生的故障。
  • FATAL 表示出现了无法恢复的情况,将导致进程终止。
  • DEBUG 会捕获用于调查的详细信息,但默认不会在生产环境中输出,以免造成干扰。

要合理使用这些日志级别,需遵循以下原则:

  • 为避免“狼来了”的情况,也就是频繁发出警报导致真正重要的警报被忽视,日志级别应能反映问题的严重程度和可执行性。如果无需人员采取行动,就不应将其记录在会触发警报的级别中。
  • 日志记录中最难的部分是将详细程度调整到合适的水平。日志过多会增加成本、拖慢系统速度,并让工程师淹没在无用信息中;日志过少则会导致调试时缺乏有效信息。详细日志虽有其用途,但应限定范围、短期使用,绝不能作为默认设置。
  • 确保无需重新部署,就能实时调整日志的详细程度,无论是针对某个服务、某个模块,甚至是某个特定用户。这有助于你在调查实时事件时,生成恰好所需的详细信息。

日志级别并非日志的核心价值,但如果能统一、合理地使用,它们会成为强大的信号。它们能帮助你区分常规信息和异常情况,仅在最需要时才显示底层细节。将日志级别简化为两个,会毫无必要地丢失这种重要信号。

5. 避免敏感数据进入日志

日志记录中最大的风险之一,就是意外包含个人身份信息(PII)或其他敏感数据。在所有可能出现的日志错误中,这种错误最有可能让公司因负面事件登上新闻头条。

Twitter 和 GitHub 曾发生过将明文密码意外写入内部日志的高调事故,这些案例深刻提醒我们:此类问题不仅后果严重,而且极易发生。

这类数据泄露很少是出于恶意目的。通常是因为开发人员专注于当前任务,没有意识到自己的日志记录选择会带来下游的安全隐患。

有效的防范措施必须是系统性、多层次的。你必须假设会出现人为失误,并设计能在多个环节拦截敏感数据的防护措施。

应用层:不要直接记录完整对象

在应用程序层面,应避免记录可能包含敏感字段的整个对象。相反,应实现支持日志记录的安全表示形式,默认排除或屏蔽敏感数据。这意味着,对象的新属性可能需要先经过允许列表验证,才能被记录到日志中。

// User is a domain object that may contain sensitive fields.
type User struct {
	ID        string
	Email     string
	Password  string
	CreatedAt time.Time
}

func (u User) LogValue() slog.Value {
	// Only allowlist the fields considered safe to log.
	return slog.GroupValue(
		slog.String("id", u.ID),
		slog.Time("created_at", u.CreatedAt),
	)
}

框架和流水线层:屏蔽、清理、允许列表

对于临时构建的结构,大多数日志框架允许你配置“中间件”:在日志写入前,自动屏蔽或清除键名匹配黑名单的已知敏感字段。

最后,在 OpenTelemetry 收集器或类似工具中应用敏感数据清除操作,确保日志离开系统前不会有任何敏感数据遗漏。

# One of the most effective techniques is using an attribute allowlist
processors:
  redaction/allowlist:
    allow_all_keys: false
    allowed_keys:
      - http.method
      - http.url
      - http.status_code

service:
  pipelines:
    logs:
      receivers: [...]
      processors: [attributes/allowlist, ...]
      exporters: [...]

日志中包含敏感数据的错误很容易发生,但通过多层次防护,你可以大幅降低其影响。最有效的解决方案不是追究个人责任,而是培养一种文化:让团队共同对可观测性数据的质量和安全性负责。

6. 将日志记录性能视为核心关注点

人们有时会忽略一个事实:日志记录并非“无成本”操作。每一条日志都会消耗 CPU、内存和 I/O 资源。在小规模系统中,这种消耗几乎无法察觉,但在大规模系统中,它可能会成为真正的性能瓶颈。

要防止日志记录拖慢服务速度,可采用以下做法。

选择高性能的日志库

不同日志库的性能差异很大。首先应选择现代、高效的日志库,这类库在设计时就以最小化开销为目标。例如,在 Go 语言生态中,内置的 log/slog 包或 Zerolog、Zap 等第三方库,其性能比 Logrus 等旧版库高出多个数量级。

采用异步日志记录方式

不要让应用程序的主线程等待日志写入磁盘或网络。相反,应采用异步日志记录:将日志消息写入快速的内存缓冲区,由单独的后台进程处理速度较慢的 I/O 操作。这样一来,日志记录对请求延迟的影响几乎可以忽略不计。

关注高频代码路径

避免在高频循环或关键代码路径中添加详细的日志语句。如果需要在这些区域记录诊断信息,可采用智能采样或速率限制的方式,在收集必要洞察的同时,避免日志量淹没系统。

延迟执行高开销操作

日志记录中一个更隐蔽的性能问题,在于日志参数的计算方式。例如,在 Python 中,logger.debug(f"Processing {x}") 这类日志语句,即使 DEBUG 级别已被禁用,仍会先计算格式化字符串。

更好的写法是 logger.debug("Processing %s", x):这种方式会延迟字符串格式化操作,直到确认需要输出该日志消息时才执行,从而在关键代码中节省宝贵的计算资源。

总而言之,应像对待业务逻辑一样对待日志记录代码。如果你不会在关键路径中阻塞请求或分配不必要的内存,那么也不应让日志库这样做。

7. 智能管理日志量和成本

放任日志量无限制增长,是快速消耗成本、淹没系统并让工程师无法获取有效信息的常见方式。在大规模系统中,日志的采集和存储成本每年可能高达数百万,而大量无用的日志流会掩盖事件排查中真正需要的信号。

解决办法不是停止记录日志,而是“更聪明地记录日志”。最佳起点是从源头削减“高数量、低价值”的日志。一个典型例子是负载均衡器的成功健康检查日志:这些信息作为指标(metric)远比作为无休止的日志行更有价值,后者几乎无法提供额外信息。

若要进一步控制日志量,某些日志框架允许将日志持续记录到内存环形缓冲区中。在正常情况下,缓冲区会不断覆盖旧数据,因此不会有日志数据离开内存;但当系统出现错误时,缓冲区中的数据会与错误日志一起导出。这样既能在系统正常运行时保持低日志量,又能在出现问题时捕获故障发生前的上下文信息。

另一种有效的方法是日志采样。很少有场景需要保留每一条日志,尤其是对于常规事件。例如,你可以保留所有失败请求的日志,但对于成功请求,每 100 条仅保留 1 条。这样既能获得具有代表性的视图,又不会增加过多负担。

日志采样示例

日志采样在级联故障,也就是一个故障引发多个后续故障的场景中也很有帮助:当某个服务开始重复输出相同错误时,你无需保留一百万条相同的日志条目。几条有代表性的样本就能清晰反映问题,同时不会淹没系统或大幅增加成本。

需要注意的一点是,OpenTelemetry 目前尚未原生支持日志采样,其采样策略仅适用于追踪(trace)数据。因此,你需要通过日志框架或可观测性流水线来实现日志采样功能。

8. 让 OpenTelemetry 收集器成为日志流水线的核心

对于小型系统,直接从应用程序向后端传输遥测数据可能可行,但这种方式无法扩展。随着系统规模增长,它会导致与供应商的强耦合、性能瓶颈、高数据丢失风险,以及其他运维难题。

更好的架构是将 OpenTelemetry 收集器置于可观测性流水线的核心位置。收集器是一个高性能、与供应商无关的服务,它能接收所有遥测数据、对数据进行处理,然后将其路由到多个后端系统。

通过收集器,你可以在统一位置执行以下操作:

  • 强制执行标准;
  • 屏蔽敏感字段;
  • 标准化数据格式;
  • 过滤无用日志流;
  • 自动附加环境元数据。

此外,由于收集器能同时处理日志、指标和追踪数据,它可以完全替代边缘节点上仅用于日志的代理,如 Fluent Bit、Logstash 或 Filebeat,为你提供统一的流水线,而非由多个单一用途工具拼凑而成的复杂体系。

这种架构的优势在于灵活性:

  • 是否希望将安全事件发送到一个后端,将应用程序日志发送到另一个后端?
  • 是否希望在生产环境中丢弃 DEBUG 级日志,但在测试环境中保留它们?
  • 是否希望在无需开发人员参与的情况下,为日志添加新的 Kubernetes 元数据?

只要以收集器为核心,所有这些需求都只需通过配置更改即可实现,无需启动专门的工程项目。

9. 选择原生支持 OpenTelemetry 的后端系统

你已经完成了艰巨的工作:实现了结构化日志记录、通过 OpenTelemetry 确立了日志模式、为日志补充了丰富的上下文信息,还构建了以收集器为核心的稳健流水线。最后一步,是确保你的可观测性平台能充分利用这些投入。

此时,选择原生支持 OpenTelemetry 的平台就变得至关重要。原生支持 OpenTelemetry 的平台不仅是能接收 OTLP(OpenTelemetry 协议)数据的后端,其整个数据模型都围绕 OpenTelemetry 标准构建。

这意味着它天生就能理解不同信号,也就是日志、指标、追踪之间的内在关联,并将语义约定视为核心要素,而非普通的标签集合。

例如,它知道 db.query.text 不只是一个字符串,而是数据库查询语句。它能解析该语句以识别操作类型、标记缓慢查询,并提供数据库性能的预制仪表板。Dash0 正是基于这些原则构建的平台。

通过将高质量的日志埋点与使用相同“原生语言”的平台相结合,你不仅能获得更优质的数据,还能实现更快速的调试、更清晰的洞察,以及更可靠的系统。这正是你所有努力应得的回报。

日志治理落地清单

如果要把这 9 项实践落到工程流程中,可以按以下顺序推进:

  1. 先把日志统一改为结构化输出,并确认关键字段可查询。
  2. 再用 OpenTelemetry 约定统一服务名、资源属性、Trace 关联字段和业务字段命名。
  3. 为平台上下文建立自动注入机制,为事件上下文建立请求生命周期内的传递机制。
  4. 明确日志级别和告警触发规则,避免把不可行动的信息推到高严重级别。
  5. 在应用、日志框架和收集器多层处理敏感数据,优先采用允许列表。
  6. 针对关键路径检查日志性能,避免同步 I/O、高频详细日志和无意义的字符串格式化。
  7. 用过滤、采样、环形缓冲区和指标替代低价值日志,控制日志量和成本。
  8. 将 OpenTelemetry 收集器作为统一入口,集中做标准化、清理、过滤和路由。
  9. 选择能理解 OpenTelemetry 数据模型的后端,让日志、指标和追踪真正关联起来。

FAQ

结构化日志为什么比普通文本日志更适合排障?

结构化日志把时间、级别、服务名、用户 ID、请求 ID、耗时等信息拆成独立字段。这样日志后端可以直接索引、查询和聚合字段,而不是依赖正则从文本中提取信息。对于分布式系统排障来说,这能显著降低查询复杂度。

OpenTelemetry 在日志记录中解决的核心问题是什么?

OpenTelemetry 解决的不是“如何打印一行日志”,而是“不同服务如何用一致的模型描述遥测数据”。它通过日志数据模型、语义约定、SDK 和收集器,帮助团队统一字段含义、关联日志与追踪,并把日志送入后端系统。

日志上下文应该包含哪些信息?

日志上下文至少应包含两类信息:平台上下文和事件上下文。平台上下文包括 trace_idspan_id、服务版本、云区域、Kubernetes Pod 等环境信息;事件上下文包括用户 ID、订单 ID、支付 ID、业务状态等只有应用代码才知道的信息。

日志级别越少越好吗?

不一定。只保留 INFOERROR 看似简单,但会丢失可行动性差异。WARNERRORFATALDEBUG 分别表达不同语义,合理使用可以区分异常、故障、致命问题和调查细节,减少误报和告警疲劳。

如何避免日志成本失控?

日志成本治理应从源头开始:减少高数量、低价值日志,把健康检查等重复信息转成指标;对成功请求做采样;对重复错误做限流;在必要时使用环形缓冲区保存故障前上下文。关键不是不记录日志,而是只保留对排障和分析有价值的日志。

结语

如果日志记录工作做得好,它将从“昂贵的负担”转变为“提升效率的推动力”。它能缩短事件响应时间、减少运维开销,并提高系统的可靠性。

日志治理的关键不是某一个工具,而是一条完整链路:结构化日志让数据可查询,OpenTelemetry 让语义可统一,上下文让事件可关联,收集器让流水线可治理,后端平台让排障体验可落地。

下次生产环境出现故障时,故障总会发生,日志的质量将决定你是在黑暗中盲目猜测,还是能胸有成竹地排查问题。

作者:Ayooluwa Isaiah
原文:https://www.dash0.com/guides/logging-best-practices

延伸路径

继续看解决方案和产品对比

如果你正在做监控、可观测性或故障定位相关选型,建议从解决方案和产品对比继续往下看。

标签 Logging
快猫星云 联系方式 快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云