本指南旨在为您提供基本的见解和实践,以确保您能够更有效地监控和排除服务故障。 在应用程序开发中,日志记录经常被忽视,但它是构建强大且可观察系统的关键组成部分。正确的日志记录实践可以增强应用程序的可见性,加深对其内部工作原理的理解,并改善应用程序的整体健康状况。 默认日志记录 在应用程序的入口点加入默认日志记录机制非常有益。这种自动日志记录可以捕获必要的交互,并可能包括入口点的参数。但是,必须谨慎,因为记录密码等敏感信息可能会带来隐私和安全风险。 常见入口点 :记录有关传入请求和响应的详细信息 API 端点 :记录作业的开始点、执行细节和结果 后台作业 :记录异步事件的处理和相关交互 异步事件 综合记录 应用程序执行的每项重要操作都必须生成日志条目,尤其是那些改变其状态的操作。这种详尽的日志记录方法是快速识别和解决问题的关键,可以透明地查看应用程序的运行状况和功能。如此认真的日志记录可确保更轻松地进行诊断和维护。 选择适当的日志级别 采用适当的日志级别对于管理和解释应用程序生成的大量数据至关重要。通过根据日志的严重性和相关性对其进行分类,您可以确保及时发现和解决关键问题,同时仍可访问不太紧急的信息,而不会让您的监控工作不堪重负。 以下是有效利用日志级别的指南: 等级 描述和示例 接受使用 不接受 ERROR 导致系统停止运行的致命事件。例如,丢失数据库连接 严重系统错误 非严重错误,例如用户登录尝试失败 WARN 存在问题,但系统可以继续执行并完成请求的操作 导致问题的潜在问题 常规状态变化 INFO 了解正常的应用程序功能,例如用户帐户创建或数据写入 状态更改 只读操作,无需更改 DEBUG 详细的诊断信息,例如进程开始/结束 记录进程步骤不会改变系统状态 常规状态变化或高频操作 TRACE 最详细的级别,包括方法入口/出口 了解流程和过程的细节 记录敏感信息 记录哪些 ID - 分层方法 在应用程序中记录操作时,包含直接涉及的实体的 ID 对于将日志信息链接到数据库数据至关重要。分层方法可帮助您通过将项目链接到其父组或类别来快速找到与应用程序特定部分相关的所有日志。 例如,当消息发送失败时,您不应只记录聊天 ID,还应记录聊天室及其所属公司的 ID。这样,您可以获得更多背景信息,并了解问题的更广泛影响。 日志条目示例: Failed to send the message - chat=$roomId, chatRoomId=chatRoomId, company=$companyId 生产日志示例 下面是使用分层方法时生产日志的示例: 一致性和标准化 标准前缀 在所有团队中标准化日志格式可以使您的日志更易于阅读和理解。以下是一些需要考虑的标准化前缀: 做某事 开始 完成某事 未能 某件事 完成了 做某事 跳过 做某事 重试 单独记录变量值 将变量名称和值与日志消息正文分开有几个优点: 更容易过滤和查找特定信息 简化日志搜索和解析: 保持日志消息编写过程简单 简化日志消息创建: 较大的值不会破坏日志消息的可读性 防止消息混乱: 日志格式示例: Log message - valueName=value 使用建议做法的日志示例 理论示例 以下是遵循讨论的最佳实践的结构良好的日志条目示例: 2023-10-05 14:32:01 [INFO] Successful login attempt - userId=24543, teamId=1321312 2023-10-05 14:33:17 [WARN] Failed login attempt - userId=536435, teamId=1321312 这些例子表明: :清晰、一致的前缀(如“成功登录尝试”和“失败登录尝试”)使日志易于理解。 标准化日志前缀 :变量名称和值与日志消息分开,保持清晰度并简化搜索。 分离的变量值 :结构化格式确保日志易于阅读和解析,有助于有效地进行故障排除和监控。 可读性和一致性 生产日志示例 下面是使用建议的实践时生产日志的示例: 轨迹 ID 为了有效地将日志与特定用户操作关联起来,在日志中包含 或也称为 至关重要。该 ID 应在该入口点触发的逻辑生成的所有日志中保持一致,从而清晰地显示事件序列。 traceId correlationId 实现示例 虽然某些监控服务(例如 Datadog)提供了开箱即用的日志分组功能,但也可以手动实现。在使用 Spring 的 Kotlin 应用程序中,您可以使用 HandlerInterceptor 为 REST 请求实现跟踪 ID。 @Component class TraceIdInterceptor : HandlerInterceptor { companion object { private const val TRACE_ID = "traceId" } override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean { val traceId = UUID.randomUUID().toString() MDC.put(TRACE_ID, traceId) return true } override fun afterCompletion(request: HttpServletRequest, response: HttpServletResponse, handler: Any, ex: Exception?) { MDC.remove(TRACE_ID) } } 该拦截器为每个请求生成一个唯一的 ,在请求开始时将其添加到 MDC,并在请求完成后将其删除。 traceId 带有 traceId 的示例日志 实现此类日志聚合将使您能够过滤类似以下示例的日志 在日志中使用 UUID 与长 ID 在许多系统中,实体可能使用 或 ID 作为其主要标识符,而某些系统可能将两种类型的 ID 用于不同的目的。了解每种类型对日志记录的影响对于做出明智的选择至关重要。 UUID Long 以下是需要考虑的事项的细分: ID 更易于阅读,而且明显更短,尤其是当它们不在 范围的高端时。 可读性: Long Long ID 在整个系统中提供唯一性,使您可以使用 ID 搜索日志而不会遇到 ID 冲突的问题。这里的冲突意味着来自不相关数据库表的两个实体有可能具有相同的 ID。 唯一值: UUID Long :在使用长主键作为实体 ID 的系统中,添加随机 ID 通常很简单,但在具有 实体 ID 的分布式系统中,专门用于日志记录的 ID 可能会很有挑战性或成本很高。 系统限制 UUID UUID Long 日志中使用的 ID 类型的一致性至关重要,至少对于每个实体而言都是如此。如果系统已经为某些实体生成日志,而您不打算更改所有实体,则最好坚持使用已用于标识实体的类型。在过渡期间可以考虑记录两个 ID,但永久使用多个 ID 会不必要地使日志变得混乱。 现有日志: 结论 正确的日志记录实践对于有效的服务可观察性至关重要。通过结合全面的日志记录、适当的日志级别、跟踪 ID 和标准化的日志格式,您可以显著增强监控和排除应用程序故障的能力。这些做法可以提高日志的清晰度和一致性,让您更轻松地快速诊断和解决问题。 感谢您花时间阅读这篇文章!