定位bug花一天?8年程序员竟还不会打日志?!要不要辞退……

程序员鱼皮 2026-01-28 09:42:53
你是小阿巴,刚入职的低级程序员,正在开发一个批量导入数据的程序。

 

没想到,程序刚上线,产品经理就跑过来说:小阿巴,用户反馈你的程序有 Bug,刚导入没多久就报错中断了!

 

你赶紧打开服务器,看着比你发量都少的报错信息:

 

 

你一脸懵逼:只有这点儿信息,我咋知道哪里出了问题啊?!

 

你只能硬着头皮让产品经理找用户要数据,然后一条条测试,看看是哪条数据出了问题……

 

原本大好的摸鱼时光,就这样无了。

 

这时,你的导师鱼皮走了过来,问道:小阿巴,你是持矢了么?脸色这么难看?

 

你无奈地说:皮哥,刚才线上出了个 bug,我花了 8 个小时才定位到问题……

 

鱼皮皱了皱眉:这么久?你没打日志吗?

 

你很是疑惑:谁是日志?为什么要打它?

 

鱼皮叹了口气:唉,难怪你要花这么久…… 来,我教你打日志!

 

一、什么是日志?

 

鱼皮打开电脑,给你看了一段代码:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
@Slf4jpublicclassUserService{    publicvoidbatchImport(List<UserDTO> userList){        log.info("开始批量导入用户,总数:{}", userList.size());                int successCount = 0;        int failCount = 0;                for (UserDTO userDTO : userList) {            try {                log.info("正在导入用户:{}", userDTO.getUsername());                validateUser(userDTO);                saveUser(userDTO);                successCount++;                log.info("用户 {} 导入成功", userDTO.getUsername());            } catch (Exception e) {                failCount++;                log.error("用户 {} 导入失败,原因:{}", userDTO.getUsername(), e.getMessage(), e);            }        }                log.info("批量导入完成,成功:{},失败:{}", successCount, failCount);    }}

 

你看着代码里的 log.info、log.error,疑惑地问:这些 log 是干什么的?

 

鱼皮:这就是打日志。日志用来记录程序运行时的状态和信息,这样当系统出现问题时,我们可以通过日志快速定位问题。

 

 

你若有所思:哦?还可以这样!如果当初我的代码里有这些日志,一眼就定位到问题了…… 那我应该怎么打日志?用什么技术呢?

 

二、怎么打日志?

 

鱼皮:每种编程语言都有很多日志框架和工具库,比如 Java 可以选用 Log4j 2、Logback 等等。咱们公司用的是 Spring Boot,它默认集成了 Logback 日志框架,你直接用就行,不用再引入额外的库了。

 

 

日志框架的使用非常简单,先获取到 Logger 日志对象。

 

 
方法 1:通过 LoggerFactory 手动获取 Logger 日志对象

 

  •  
  •  
  •  
publicclassMyService{    privatestaticfinal Logger logger = LoggerFactory.getLogger(MyService.class);}

 

 
方法 2:使用this.getClass获取当前类的类型,创建Logger对象

 

  •  
  •  
  •  
publicclassMyService{    privatefinal Logger logger = LoggerFactory.getLogger(this.getClass());}

 

然后调用 logger.xxx(比如 logger.info)就能输出日志了。

 

  •  
  •  
  •  
  •  
  •  
  •  
publicclassMyService{    privatefinal Logger logger = LoggerFactory.getLogger(this.getClass());    publicvoiddoSomething(){        logger.info("执行了一些操作");    }}

 

效果如图:

 

 

小阿巴:啊,每个需要打日志的类都要加上这行代码么?

 

鱼皮:还有更简单的方式,使用 Lombok 工具库提供的 @Slf4j 注解,可以自动为当前类生成日志对象,不用手动定义啦。

 

  •  
  •  
  •  
  •  
  •  
  •  
@Slf4jpublicclassMyService{    publicvoiddoSomething(){        log.info("执行了一些操作");    }}

 

上面的代码等同于“自动为当前类生成日志对象”:

 

  •  
  •  
privatestaticfinal org.slf4j.Logger log =     org.slf4j.LoggerFactory.getLogger(MyService.class);

 

你咧嘴一笑:这个好,爽爽爽!

 

等等,不对,我直接用 Java 自带的 System.out.println 不也能输出信息么?何必多此一举?

 

  •  
System.out.println("开始导入用户" + user.getUsername());

 

鱼皮摇了摇头:千万别这么干!

 

首先,System.out.println 是一个同步方法,每次调用都会导致耗时的 I/O 操作,频繁调用会影响程序的性能。

 

 

而且它只能输出信息到控制台,不能灵活控制输出位置、输出格式、输出时机等等。比如你现在想看三天前的日志,System.out.println 的输出早就被刷没了,你还得浪费时间找半天。

 

 

你恍然大悟:原来如此!那使用日志框架就能解决这些问题吗?

 

鱼皮点点头:没错,日志框架提供了丰富的打日志方法,还可以通过修改日志配置文件来随心所欲地调教日志,比如把日志同时输出到控制台和文件中、设置日志格式、控制日志级别等等。

 

 

在下苦心研究日志多年,沉淀了打日志的 8 大邪修秘法,先传授你 2 招最基础的吧。

 

三、打日志的8大最佳实践

 

 
1、合理选择日志级别

 

第一招,日志分级。

 

你好奇道:日志还有级别?苹果日志、安卓日志?

 

鱼皮给了你一巴掌:可不要乱说,日志的级别是按照重要程度进行划分的。

 

 

其中 DEBUG、INFO、WARN 和 ERROR 用得最多。

 

  • 调试用的详细信息用 DEBUG

     

  • 正常的业务流程用 INFO

     

  • 可能有问题但不影响主流程的用 WARN

     

  • 出现异常或错误的用 ERROR

 

  •  
  •  
  •  
  •  
log.debug("用户对象的详细信息:{}", userDTO);  // 调试信息log.info("用户 {} 开始导入", username);  // 正常流程信息log.warn("用户 {} 的邮箱格式可疑,但仍然导入", username);  // 警告信息log.error("用户 {} 导入失败", username, e);  // 错误信息

 

你挠了挠头:俺直接全用 DEBUG 不行么?

 

鱼皮摇了摇头:如果所有信息都用同一级别,那出了问题时,你怎么快速找到错误信息?

 

 

在生产环境,我们通常会把日志级别调高(比如 INFO 或 WARN),这样 DEBUG 级别的日志就不会输出了,防止重要信息被无用日志淹没。

 

 

你点点头:俺明白了,不同的场景用不同的级别!

 

 
2、正确记录日志信息

 

鱼皮:没错,下面教你第二招。你注意到我刚才写的日志里有一对大括号 {} 吗?

 

  •  
log.info("用户 {} 开始导入", username);

 

你回忆了一下:对哦,那是啥啊?

 

鱼皮:这叫参数化日志。{} 是一个占位符,日志框架会在运行时自动把后面的参数值替换进去。

 

你挠了挠头:我直接用字符串拼接不行吗?

 

  •  
log.info("用户 " + username + " 开始导入");

 

鱼皮摇摇头:不推荐。因为字符串拼接是在调用 log 方法之前就执行的,即使这条日志最终不被输出,字符串拼接操作还是会执行,白白浪费性能。

 

 

你点点头:确实,而且参数化日志比字符串拼接看起来舒服~

 

 

鱼皮:没错。而且当你要输出异常信息时,也可以使用参数化日志:

 

  •  
  •  
  •  
  •  
  •  
try {    // 业务逻辑catch (Exception e) {    log.error("用户 {} 导入失败", username, e);  // 注意这个 e}

 

这样日志框架会同时记录上下文信息和完整的异常堆栈信息,便于排查问题。

 

 

你抱拳:学会了,我这就去打日志!

 

 
3、把控时机和内容

 

很快,你给批量导入程序的代码加上了日志:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
@Slf4jpublicclassUserService{    public BatchImportResult batchImport(List<UserDTO> userList){        log.info("开始批量导入用户,总数:{}", userList.size());        int successCount = 0;        int failCount = 0;        for (UserDTO userDTO : userList) {            try {                log.info("正在导入用户:{}", userDTO.getUsername());                   // 校验用户名                if (StringUtils.isBlank(userDTO.getUsername())) {                    thrownew BusinessException("用户名不能为空");                }                // 保存用户                saveUser(userDTO);                successCount++;                log.info("用户 {} 导入成功", userDTO.getUsername());            } catch (Exception e) {                failCount++;                log.error("用户 {} 导入失败,原因:{}", userDTO.getUsername(), e.getMessage(), e);            }        }        log.info("批量导入完成,成功:{},失败:{}", successCount, failCount);        returnnew BatchImportResult(successCount, failCount);    }}

 

光做这点还不够,你还翻出了之前的屎山代码,想给每个文件都打打日志。

 

 

但打着打着,你就不耐烦了:每段代码都要打日志,好累啊!但是不打日志又怕出问题,怎么办才好?

 

鱼皮笑道:好问题,这就是我要教你的第三招 —— 把握打日志的时机。

 

对于重要的业务功能,我建议采用防御性编程,先多多打日志。比如在方法代码的入口和出口记录参数和返回值、在每个关键步骤记录执行状态,而不是等出了问题无法排查的时候才追悔莫及。之后可以再慢慢移除掉不需要的日志。

 

 

你叹了口气:这我知道,但每个方法都打日志,工作量太大,都影响我摸鱼了!

 

鱼皮:别担心,你可以利用 AOP 切面编程,自动给每个业务方法的执行前后添加日志,这样就不会错过任何一次调用信息了。

 

 

你双眼放光:这个好,爽爽爽!

 

鱼皮:不过这样做也有一个缺点,注意不要在日志中记录了敏感信息,比如用户密码。万一你的日志不小心泄露出去,就相当于泄露了大量用户的信息。

 

 

你拍拍胸脯:必须的!

 

 
4、控制日志输出量

 

一个星期后,产品经理又来找你了:小阿巴,你的批量导入功能又报错啦!而且怎么感觉程序变慢了?

 

你完全不慌,淡定地打开服务器的日志文件。结果瞬间呆住了……

 

好家伙,满屏都是密密麻麻的日志,这可怎么看啊?!

 

 

鱼皮看了看你的代码,摇了摇头:你现在每导入一条数据都要打一些日志,如果用户导入 10 万条数据,那就是几十万条日志!不仅刷屏,还会影响性能。

 

你有点委屈:不是你让我多打日志的么?那我应该怎么办?

 

鱼皮:你需要控制日志的输出量。

 

1)可以添加条件来控制,比如每处理 100 条数据时才记录一次:

 

  •  
  •  
  •  
if ((i + 1) % 100 == 0) {    log.info("批量导入进度:{}/{}", i + 1, userList.size());}

 

2)或者在循环中利用 StringBuilder 进行字符串拼接,循环结束后统一输出:

 

  •  
  •  
  •  
  •  
  •  
  •  
StringBuilder logBuilder = new StringBuilder("处理结果:");for (UserDTO userDTO : userList) {    processUser(userDTO);    logBuilder.append(String.format("成功[ID=%s], ", userDTO.getId()));}log.info(logBuilder.toString());

 

3)还可以通过修改日志配置文件,过滤掉特定级别的日志,防止日志刷屏:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
<appendername="FILE"class="ch.qos.logback.core.FileAppender">    <file>logs/app.log</file>    <!-- 只允许 INFO 级别及以上的日志通过 -->    <filterclass="ch.qos.logback.classic.filter.ThresholdFilter">        <level>INFO</level>    </filter></appender>

 

 
5、统一日志格式

 

你开心了:好耶,这样就不会刷屏了!但是感觉有时候日志很杂很乱,尤其是我想看某一个请求相关的日志时,总是被其他的日志干扰,怎么办?

 

鱼皮:好问题,可以在日志配置文件中定义统一的日志格式,包含时间戳、线程名称、日志级别、类名、方法名、具体内容等关键信息。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
<!-- 控制台日志输出 --><appendername="CONSOLE"class="ch.qos.logback.core.ConsoleAppender">    <encoder>        <!-- 日志格式 -->        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>    </encoder></appender>

 

这样输出的日志更整齐易读:

 

 

此外,你还可以通过 MDC(Mapped Diagnostic Context)给日志添加额外的上下文信息,比如请求 ID、用户 ID 等,方便追踪。

 

 

在 Java 代码中,可以为 MDC 设置属性值:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
@PostMapping("/user/import")public Result importUsers(@RequestBody UserImportRequest request){    // 1. 设置 MDC 上下文信息    MDC.put("requestId"generateRequestId());    MDC.put("userId"String.valueOf(request.getUserId()));    try {        log.info("用户请求处理完成");              // 执行具体业务逻辑        userService.batchImport(request.getUserList());             return Result.success();    } finally {        // 2. 及时清理MDC(重要!)        MDC.clear();    }}

 

然后在日志配置文件中就可以使用这些值了:

 

  •  
  •  
  •  
  •  
  •  
  •  
<appendername="FILE"class="ch.qos.logback.core.rolling.RollingFileAppender">    <encoder>        <!-- 包含 MDC 信息 -->        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{requestId}] [%X{userId}] %msg%n</pattern>    </encoder></appender>

 

这样,每个请求、每个用户的操作一目了然。

 

 

 
6、使用异步日志

 

你又开心了:这样打出来的日志,确实舒服,爽爽爽!但是我打日志越多,是不是程序就会更慢呢?有没有办法能优化一下?

 

鱼皮:当然有,可以使用异步日志

 

正常情况下,你调用 log.info() 打日志时,程序会立刻把日志写入文件,这个过程是同步的,会阻塞当前线程。而异步日志会把写日志的操作放到另一个线程里去做,不会阻塞主线程,性能更好。

 

你眼睛一亮:这么厉害?怎么开启?

 

鱼皮:很简单,只需要修改一下配置文件:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
<appendername="ASYNC"class="ch.qos.logback.classic.AsyncAppender">    <queueSize>512</queueSize>  <!-- 队列大小 -->    <discardingThreshold>0</discardingThreshold>  <!-- 丢弃阈值,0 表示不丢弃 -->    <neverBlock>false</neverBlock>  <!-- 队列满时是否阻塞,false 表示会阻塞 -->    <appender-refref="FILE" />  <!-- 引用实际的日志输出目标 --></appender><rootlevel="INFO">    <appender-refref="ASYNC" /></root>

 

不过异步日志也有缺点,如果程序突然崩溃,缓冲区中还没来得及写入文件的日志可能会丢失。

 

 

所以要权衡一下,看你的系统更注重性能还是日志的完整性。

 

你想了想:我们的程序对性能要求比较高,偶尔丢几条日志问题不大,那我就用异步日志吧。

 

 
7、日志管理

 

接下来的很长一段时间,你混得很舒服,有 Bug 都能很快发现。

 

你甚至觉得 Bug 太少、工作没什么激情,所以没事儿就跟新来的实习生阿坤吹吹牛皮:你知道日志么?我可会打它了!

 

直到有一天,运维小哥突然跑过来:阿巴阿巴,服务器挂了!你快去看看!

 

你连忙登录服务器,发现服务器的硬盘爆满了,没法写入新数据。

 

你查了一下,发现日志文件竟然占了 200GB 的空间!

 

 

你汗流浃背了,正在考虑怎么甩锅,结果阿坤突然鸡叫起来:阿巴 giegie,你的日志文件是不是从来没清理过?

 

你尴尬地倒了个立,这样眼泪就不会留下来。

 

鱼皮叹了口气:这就是我要教你的下一招 —— 日志管理。

 

你好奇道:怎么管理?我每天登服务器删掉一些历史文件?

 

鱼皮:人工操作也太麻烦了,我们可以通过修改日志配置文件,让框架帮忙管理日志。

 

首先设置日志的滚动策略,可以根据文件大小和日期,自动对日志文件进行切分。

 

  •  
  •  
  •  
  •  
  •  
<rollingPolicyclass="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">    <fileNamePattern>logs/app-%d{yyyy-MM-dd}.%i.log</fileNamePattern>    <maxFileSize>10MB</maxFileSize>    <maxHistory>30</maxHistory></rollingPolicy>

 

这样配置后,每天会创建一个新的日志文件(比如 app-2025-10-23.0.log),如果日志文件大小超过 10MB 就再创建一个(比如 app-2025-10-23.1.log),并且只保留最近 30 天的日志。

 

 

还可以开启日志压缩功能,进一步节省磁盘空间:

 

  •  
  •  
  •  
  •  
<rollingPolicyclass="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">    <!-- .gz 后缀会自动压缩 -->    <fileNamePattern>logs/app-%d{yyyy-MM-dd}.log.gz</fileNamePattern></rollingPolicy>

 

 

你有些激动:吼吼,这样我们就可以按照天数更快地查看日志,服务器硬盘也有救啦!

 

 
8、集成日志收集系统

 

两年后,你负责的项目已经发展成了一个大型的分布式系统,有好几十个微服务。

 

如今,每次排查问题你都要登录到不同的服务器上查看日志,非常麻烦。而且有些请求的调用链路很长,你得登录好几台服务器、看好几个服务的日志,才能追踪到一个请求的完整调用过程。

 

 

你简直要疯了!

 

于是你找到鱼皮求助:现在查日志太麻烦了,当年你还有一招没有教我,现在是不是……

 

鱼皮点点头:嗯,对于分布式系统,就必须要用专业的日志收集系统了,比如很流行的 ELK。

 

你好奇:ELK 是啥?

 

阿坤抢答道:我知道,就是 Elasticsearch + Logstash + Kibana 这套组合。

 

简单来说,Logstash 负责收集各个服务的日志,然后发送给 Elasticsearch 存储和索引,最后通过 Kibana 提供一个可视化的界面。

 

 

这样一来,我们可以方便地集中搜索、查看、分析日志。

 

 

你惊讶了:原来日志还能这么玩,以后我所有的项目都要用 ELK!

 

鱼皮摆摆手:不过 ELK 的搭建和运维成本比较高,对于小团队来说可能有点重,还是要按需采用啊。

 

四、结局

 

至此,你已经掌握了打日志的核心秘法。

 

 

只是你很疑惑,为何那阿坤竟对日志系统如此熟悉?

 

阿坤苦笑道:我本来就是日志管理大师,可惜我上家公司的同事从来不打日志,所以我把他们暴打了一顿后跑路了。

 

阿巴 giegie 你要记住,日志不是写给机器看的,是写给未来的你和你的队友看的!

 

你要是以后不打日志,我就打你!

 

作者丨程序员鱼皮
来源丨公众号:程序员鱼皮(ID:coder_yupi
dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn
最新评论
访客 2024年04月08日

如果字段的最大可能长度超过255字节,那么长度值可能…

访客 2024年03月04日

只能说作者太用心了,优秀

访客 2024年02月23日

感谢详解

访客 2024年02月20日

一般干个7-8年(即30岁左右),能做到年入40w-50w;有…

访客 2023年08月20日

230721

活动预告