第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,结果日志把磁盘打满了,服务直接挂掉。

最佳实践:我总结的几条铁律

这些年经手的项目多了,慢慢总结出一些经验。分享给你:

  1. 打日志要带上下文。 别只打"保存成功",要打"保存订单成功: orderId=12345"。不然你根本不知道是哪个订单。
  2. 异常日志要传异常对象。 很多人写 logger.error(e.message),这样堆栈信息就丢了。正确写法:logger.error("操作失败", e)
  3. 敏感信息脱敏。 身份证、手机号、密码这些,要么不打,要么打码。我见过有人把用户密码明文打出来的……
  4. 日志文件要轮转。 用TimeBasedRollingPolicy,按天切分,保留30天。别让日志撑爆磁盘。
  5. 异步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日志框架核心结构 应用代码 Kotlin-logging SLF4J (门面) Logback (实现) 调用 logger.info { } 提供Kotlin友好API 定义日志接口规范 实际输出到控制台/文件

说白了,Kotlin-logging帮你省掉了写 LoggerFactory.getLogger 的麻烦,SLF4J让你能随时切换底层实现,Logback则负责真正把日志写出去。三层各司其职。

避坑指南

最后分享几个我踩过的坑:

  • 不要用 isDebugEnabled 判断。 用lambda传参后,这个判断是多余的。kotlin-logging内部已经帮你做了。
  • 小心日志中的换行符。 有些日志系统会把换行符当成一条新日志。我建议日志消息里不要包含 \n,用空格代替。
  • MDC(Mapped Diagnostic Context)很好用。 比如在请求入口处设置 MDC.put("traceId", traceId),然后在pattern里用 %X{traceId} 输出。这样整个请求链路的日志都能关联起来。

嗯,日志这块其实不难,但细节很多。把基础打牢了,线上排查问题能省一半时间。

公众号:蓝海资料掘金营,微信deep3321