第73章:日志框架——SLF4J、Logback、Kotlin-logging、日志最佳实践
日志这东西,说实话,很多项目一开始都不重视。等线上出了bug,大家才急急忙忙翻日志,结果发现要么没打,要么打了一堆没用的。我经历过太多次这种尴尬了。
今天咱们就把Kotlin项目里的日志体系彻底捋一遍。从接口规范到具体实现,再到Kotlin特有的写法,最后聊聊我踩过的坑。
为什么需要SLF4J?
你想想看,Java生态里日志框架一大堆:Log4j、Logback、java.util.logging……如果代码里直接写死某个实现,哪天想换框架怎么办?
SLF4J就是来解决这个问题的。它是个门面(Facade),只定义接口,不负责具体输出。你代码里对着SLF4J编程,底层想用Logback就用Logback,想切到Log4j2也行,改个依赖就行。
核心原则:永远面向SLF4J接口编程,不要直接依赖具体实现。
Logback:我常用的实现
Logback是SLF4J的原生实现,性能好,配置灵活。我个人习惯用它的XML配置,虽然YAML也支持,但XML的schema校验更靠谱。
一个典型的logback.xml长这样:
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
这里有个细节:%logger{36} 里的36是缩写长度。我曾经在生产环境看到过长达200字符的logger名,日志文件里全是包名,根本看不清消息。设个上限,清爽很多。
Kotlin-logging:Kotliner的福音
Java里获取Logger通常要写:
private static final Logger logger = LoggerFactory.getLogger(MyClass.class);
Kotlin里这么写太啰嗦了。kotlin-logging这个库提供了更简洁的方式:
import mu.KotlinLogging
private val logger = KotlinLogging.logger {}
注意那个 {} 是lambda,它会自动推断出当前类的名字。嗯,这里有个小坑——如果你在顶层函数里用,它拿到的logger名可能不是你想要的。我建议还是在class内部使用。
完整用法:
class OrderService {
private val logger = KotlinLogging.logger {}
fun createOrder(userId: String, amount: Double) {
logger.info { "创建订单: userId=$userId, amount=$amount" }
// 业务逻辑...
logger.debug { "订单创建成功: userId=$userId" }
}
}
看到没?info { } 里传的是lambda,不是字符串。这意味着只有当日志级别开启时,lambda才会执行。避免了字符串拼接的性能开销。
小技巧:用lambda传参,既简洁又高效。别再用 logger.info("xxx" + variable) 这种写法了。
日志级别怎么选?
很多团队日志级别用得很随意。我见过有人把所有日志都打成INFO,结果一天几个G的日志,查问题根本翻不过来。
我的建议:
| 级别 | 什么时候用 | 例子 |
|---|---|---|
| ERROR | 系统无法自动恢复的异常 | 数据库连接失败、空指针异常 |
| WARN | 不影响运行但值得关注 | 配置缺失使用默认值、重试3次成功 |
| INFO | 关键业务流程节点 | 订单创建、用户注册、支付回调 |
| DEBUG | 开发调试用,上线后关闭 | 方法入参、中间计算结果 |
| TRACE | 非常细粒度的跟踪 | 循环每次迭代、SQL参数绑定 |
注意:生产环境不要开DEBUG级别,除非你明确知道自己在做什么。我曾经在高峰期误开了DEBUG,结果日志把磁盘打满了,服务直接挂掉。
最佳实践:我总结的几条铁律
这些年经手的项目多了,慢慢总结出一些经验。分享给你:
- 打日志要带上下文。 别只打"保存成功",要打"保存订单成功: orderId=12345"。不然你根本不知道是哪个订单。
- 异常日志要传异常对象。 很多人写
logger.error(e.message),这样堆栈信息就丢了。正确写法:logger.error("操作失败", e)。 - 敏感信息脱敏。 身份证、手机号、密码这些,要么不打,要么打码。我见过有人把用户密码明文打出来的……
- 日志文件要轮转。 用TimeBasedRollingPolicy,按天切分,保留30天。别让日志撑爆磁盘。
- 异步Appender。 高并发场景下,日志I/O可能成为瓶颈。用Logback的AsyncAppender包一层:
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE"/>
<queueSize>512</queueSize>
<discardingThreshold>0</discardingThreshold>
</appender>
这里 discardingThreshold 设为0,表示队列满了也不丢弃日志。虽然会阻塞业务线程,但总比丢日志好。
SVG:日志框架核心结构
下面这张图展示了SLF4J、Logback和Kotlin-logging之间的关系:
说白了,Kotlin-logging帮你省掉了写 LoggerFactory.getLogger 的麻烦,SLF4J让你能随时切换底层实现,Logback则负责真正把日志写出去。三层各司其职。
避坑指南
最后分享几个我踩过的坑:
- 不要用
isDebugEnabled判断。 用lambda传参后,这个判断是多余的。kotlin-logging内部已经帮你做了。 - 小心日志中的换行符。 有些日志系统会把换行符当成一条新日志。我建议日志消息里不要包含
\n,用空格代替。 - MDC(Mapped Diagnostic Context)很好用。 比如在请求入口处设置
MDC.put("traceId", traceId),然后在pattern里用%X{traceId}输出。这样整个请求链路的日志都能关联起来。
嗯,日志这块其实不难,但细节很多。把基础打牢了,线上排查问题能省一半时间。