|
| 1 | +## 前言 |
| 2 | + |
| 3 | +大家好,我是**捡田螺的小男孩**。日志是快速定位问题的好帮手,是**撕逼和甩锅**的利器!打印好日志非常重要。今天我们来聊聊**日志打印**的15个好建议~ |
| 4 | + |
| 5 | +- 公众号:**捡田螺的小男孩** |
| 6 | + |
| 7 | + |
| 8 | +## 1. 选择恰当的日志级别 |
| 9 | + |
| 10 | +常见的日志级别有5种,分别是error、warn、info、debug、trace。日常开发中,我们需要选择恰当的日志级别,不要反手就是打印info哈~ |
| 11 | + |
| 12 | + |
| 13 | + |
| 14 | +- error:错误日志,指比较严重的错误,对正常业务有影响,需要**运维配置监控的**; |
| 15 | +- warn:警告日志,一般的错误,对业务影响不大,但是需要**开发关注**; |
| 16 | +- info:信息日志,记录排查问题的关键信息,如调用时间、出参入参等等; |
| 17 | +- debug:用于开发DEBUG的,关键逻辑里面的运行时数据; |
| 18 | +- trace:最详细的信息,一般这些信息只记录到日志文件中。 |
| 19 | + |
| 20 | + |
| 21 | +## 2. 日志要打印出方法的入参、出参 |
| 22 | + |
| 23 | +我们并不需要打印很多很多日志,只需要打印可以**快速定位问题的有效日志**。有效的日志,是甩锅的利器! |
| 24 | + |
| 25 | + |
| 26 | + |
| 27 | +哪些算得的上**有效关键**的日志呢?比如说,方法进来的时候,打印**入参**。再然后呢,在方法返回的时候,就是**打印出参,返回值**。入参的话,一般就是**userId或者bizSeq这些关键**信息。正例如下: |
| 28 | + |
| 29 | +``` |
| 30 | +public String testLogMethod(Document doc, Mode mode){ |
| 31 | + log.debug(“method enter param:{}”,userId); |
| 32 | + String id = "666"; |
| 33 | + log.debug(“method exit param:{}”,id); |
| 34 | + return id; |
| 35 | +} |
| 36 | +``` |
| 37 | + |
| 38 | + |
| 39 | +## 3. 选择合适的日志格式 |
| 40 | + |
| 41 | +理想的日志格式,应当包括这些最基本的信息:如当**前时间戳**(一般毫秒精确度)、**日志级别**,**线程名字**等等。在logback日志里可以这么配置: |
| 42 | + |
| 43 | +``` |
| 44 | +<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> |
| 45 | + <encoder> |
| 46 | + <pattern>%d{HH:mm:ss.SSS} %-5level [%thread][%logger{0}] %m%n</pattern> |
| 47 | + </encoder> |
| 48 | +</appender> |
| 49 | +``` |
| 50 | + |
| 51 | +如果我们的日志格式,连当前时间都沒有记录,那**连请求的时间点都不知道了**? |
| 52 | + |
| 53 | + |
| 54 | + |
| 55 | + |
| 56 | +## 4. 遇到if...else...等条件时,每个分支首行都尽量打印日志 |
| 57 | + |
| 58 | +当你碰到**if...else...或者switch**这样的条件时,可以在分支的首行就打印日志,这样排查问题时,就可以通过日志,确定进入了哪个分支,代码逻辑更清晰,也更方便排查问题了。 |
| 59 | + |
| 60 | + |
| 61 | + |
| 62 | + |
| 63 | +正例: |
| 64 | +``` |
| 65 | +if(user.isVip()){ |
| 66 | + log.info("该用户是会员,Id:{},开始处理会员逻辑",user,getUserId()); |
| 67 | + //会员逻辑 |
| 68 | +}else{ |
| 69 | + log.info("该用户是非会员,Id:{},开始处理非会员逻辑",user,getUserId()) |
| 70 | + //非会员逻辑 |
| 71 | +} |
| 72 | +``` |
| 73 | + |
| 74 | +## 5.日志级别比较低时,进行日志开关判断 |
| 75 | + |
| 76 | +对于trace/debug这些比较低的日志级别,必须进行日志级别的开关判断。 |
| 77 | + |
| 78 | +正例: |
| 79 | +``` |
| 80 | +User user = new User(666L, "公众号", "捡田螺的小男孩"); |
| 81 | +if (log.isDebugEnabled()) { |
| 82 | + log.debug("userId is: {}", user.getId()); |
| 83 | +} |
| 84 | +``` |
| 85 | + |
| 86 | +因为当前有如下的日志代码: |
| 87 | +``` |
| 88 | +logger.debug("Processing trade with id: " + id + " and symbol: " + symbol); |
| 89 | +``` |
| 90 | + |
| 91 | +如果**配置的日志级别是warn**的话,上述日志不会打印,但是会执行字符串拼接操作,如果```symbol```是对象, |
| 92 | +还会执行```toString()```方法,浪费了系统资源,执行了上述操作,最终日志却没有打印,因此建议**加日志开关判断。** |
| 93 | + |
| 94 | +## 6. 不能直接使用日志系统(Log4j、Logback)中的 API,而是使用日志框架SLF4J中的API。 |
| 95 | + |
| 96 | +SLF4J 是门面模式的日志框架,有利于维护和各个类的日志处理方式统一,并且可以在保证不修改代码的情况下,很方便的实现底层日志框架的更换。 |
| 97 | + |
| 98 | + |
| 99 | + |
| 100 | +正例: |
| 101 | +``` |
| 102 | +import org.slf4j.Logger; |
| 103 | +import org.slf4j.LoggerFactory; |
| 104 | +
|
| 105 | +private static final Logger logger = LoggerFactory.getLogger(TianLuoBoy.class); |
| 106 | +``` |
| 107 | + |
| 108 | +## 7. 建议使用参数占位{},而不是用+拼接。 |
| 109 | + |
| 110 | +反例: |
| 111 | +``` |
| 112 | +logger.info("Processing trade with id: " + id + " and symbol: " + symbol); |
| 113 | +``` |
| 114 | + |
| 115 | +上面的例子中,使用```+```操作符进行字符串的拼接,有一定的**性能损耗**。 |
| 116 | + |
| 117 | +正例如下: |
| 118 | +``` |
| 119 | +logger.info("Processing trade with id: {} and symbol : {} ", id, symbol); |
| 120 | +``` |
| 121 | +我们使用了大括号```{}```来作为日志中的占位符,比于使用```+```操作符,更加优雅简洁。并且,**相对于反例**,使用占位符仅是替换动作,可以有效提升性能。 |
| 122 | + |
| 123 | +## 8. 建议使用异步的方式来输出日志。 |
| 124 | + |
| 125 | +- 日志最终会输出到文件或者其它输出流中的,IO性能会有要求的。如果异步,就可以显著提升IO性能。 |
| 126 | +- 除非有特殊要求,要不然建议使用异步的方式来输出日志。以logback为例吧,要配置异步很简单,使用AsyncAppender就行 |
| 127 | +``` |
| 128 | +<appender name="FILE_ASYNC" class="ch.qos.logback.classic.AsyncAppender"> |
| 129 | + <appender-ref ref="ASYNC"/> |
| 130 | +</appender> |
| 131 | +``` |
| 132 | + |
| 133 | +## 9. 不要使用e.printStackTrace() |
| 134 | + |
| 135 | + |
| 136 | + |
| 137 | + |
| 138 | + |
| 139 | +反例: |
| 140 | +``` |
| 141 | +try{ |
| 142 | + // 业务代码处理 |
| 143 | +}catch(Exception e){ |
| 144 | + e.printStackTrace(); |
| 145 | +} |
| 146 | +``` |
| 147 | +正例: |
| 148 | +``` |
| 149 | +try{ |
| 150 | + // 业务代码处理 |
| 151 | +}catch(Exception e){ |
| 152 | + log.error("你的程序有异常啦",e); |
| 153 | +} |
| 154 | +``` |
| 155 | + |
| 156 | +**理由:** |
| 157 | + |
| 158 | +- e.printStackTrace()打印出的堆栈日志跟业务代码日志是交错混合在一起的,通常排查异常日志不太方便。 |
| 159 | +- e.printStackTrace()语句产生的字符串记录的是堆栈信息,如果信息太长太多,字符串常量池所在的内存块没有空间了,即内存满了,那么,用户的请求就卡住啦~ |
| 160 | + |
| 161 | +## 10. 异常日志不要只打一半,要输出全部错误信息 |
| 162 | + |
| 163 | + |
| 164 | + |
| 165 | +反例1: |
| 166 | + |
| 167 | +``` |
| 168 | +try { |
| 169 | + //业务代码处理 |
| 170 | +} catch (Exception e) { |
| 171 | + // 错误 |
| 172 | + LOG.error('你的程序有异常啦'); |
| 173 | +} |
| 174 | +
|
| 175 | +``` |
| 176 | +- 异常e都没有打印出来,所以压根不知道出了什么类型的异常。 |
| 177 | + |
| 178 | +反例2: |
| 179 | +``` |
| 180 | +try { |
| 181 | + //业务代码处理 |
| 182 | +} catch (Exception e) { |
| 183 | + // 错误 |
| 184 | + LOG.error('你的程序有异常啦', e.getMessage()); |
| 185 | +} |
| 186 | +``` |
| 187 | + |
| 188 | +- ```e.getMessage()```不会记录详细的堆栈异常信息,只会记录错误基本描述信息,不利于排查问题。 |
| 189 | + |
| 190 | +正例: |
| 191 | + |
| 192 | +``` |
| 193 | +try { |
| 194 | + //业务代码处理 |
| 195 | +} catch (Exception e) { |
| 196 | + // 错误 |
| 197 | + LOG.error('你的程序有异常啦', e); |
| 198 | +} |
| 199 | +``` |
| 200 | + |
| 201 | +## 11. 禁止在线上环境开启 debug |
| 202 | + |
| 203 | +禁止在线上环境开启debug,这一点非常重要。 |
| 204 | + |
| 205 | + |
| 206 | +因为一般系统的debug日志会很多,并且各种框架中也大量使用 debug的日志,线上开启debug不久可能会打满磁盘,影响业务系统的正常运行。 |
| 207 | + |
| 208 | +## 12.不要记录了异常,又抛出异常 |
| 209 | + |
| 210 | + |
| 211 | + |
| 212 | + |
| 213 | + |
| 214 | +反例如下: |
| 215 | +``` |
| 216 | +log.error("IO exception", e); |
| 217 | +throw new MyException(e); |
| 218 | +``` |
| 219 | + |
| 220 | +- 这样实现的话,通常会把栈信息打印两次。这是因为捕获了MyException异常的地方,还会再打印一次。 |
| 221 | +- 这样的日志记录,或者包装后再抛出去,不要同时使用!否则你的日志看起来会让人很迷惑。 |
| 222 | + |
| 223 | + |
| 224 | +## 13.避免重复打印日志 |
| 225 | + |
| 226 | +避免重复打印日志,酱紫会浪费磁盘空间。如果你已经有一行日志清楚表达了意思,**避免再冗余打印**,反例如下: |
| 227 | + |
| 228 | +``` |
| 229 | +if(user.isVip()){ |
| 230 | + log.info("该用户是会员,Id:{}",user,getUserId()); |
| 231 | + //冗余,可以跟前面的日志合并一起 |
| 232 | + log.info("开始处理会员逻辑,id:{}",user,getUserId()); |
| 233 | + //会员逻辑 |
| 234 | +}else{ |
| 235 | + //非会员逻辑 |
| 236 | +} |
| 237 | +``` |
| 238 | + |
| 239 | +如果你是使用log4j日志框架,务必在```log4j.xml```中设置 additivity=false,因为可以避免重复打印日志 |
| 240 | + |
| 241 | +正例: |
| 242 | +``` |
| 243 | +<logger name="com.taobao.dubbo.config" additivity="false"> |
| 244 | +``` |
| 245 | + |
| 246 | +## 14.日志文件分离 |
| 247 | + |
| 248 | + |
| 249 | + |
| 250 | + |
| 251 | + |
| 252 | +- 我们可以把不同类型的日志分离出去,比如access.log,或者error级别error.log,都可以单独打印到一个文件里面。 |
| 253 | +- 当然,也可以根据不同的业务模块,打印到不同的日志文件里,这样我们排查问题和做数据统计的时候,都会比较方便啦。 |
| 254 | + |
| 255 | + |
| 256 | +## 15. 核心功能模块,建议打印较完整的日志 |
| 257 | + |
| 258 | + |
| 259 | + |
| 260 | + |
| 261 | + |
| 262 | +- 我们日常开发中,如果核心或者逻辑复杂的代码,建议添加详细的注释,以及较详细的日志。 |
| 263 | +- 日志要多详细呢?脑洞一下,如果你的核心程序哪一步出错了,通过日志可以定位到,那就可以啦。 |
| 264 | + |
| 265 | + |
| 266 | + |
| 267 | + |
| 268 | + |
| 269 | + |
0 commit comments