五年Java经验,面试仍是说不出日志该怎么写更好?——日志规范与最佳实践篇

本文是一个系列,欢迎关注php

查看上一篇文章能够扫描文章下方的二维码,点击往期回顾-日志系列便可查看全部相关文章html

概览

各位小伙伴你们好,我又回来更新了,上一篇咱们讨论了为何要使用日志框架,此次咱们深刻问题的根源,为何咱们须要日志,日志具体如何使用?java

大多数开发人员会纠结日志该怎么输出,何时输出,输出了会不会有人看等问题,让咱们跳出开发人员的局限来考虑这个问题:谁须要日志?日志有几种?日志都须要输出什么?如何输出日志?git

谁须要日志?

  • 开发者 开发人员在开发过程当中须要输出一些变量方便调试,正确的作法是使用日志来输出(使用 System.out来输出,一不当心发布到线上,会被项目经理痛批);其次线上问题很难重放,用户的表述通常都会失真,何况不少用户发现 bug 就删 app 关网页走人了
  • 运维人员 整个系统大部分时间都是运维人员来维护,日志能够帮助运维人员来了解系统状态(不少运维系统接入的也是日志),运维人员发现日志有异常信息也能够及时通知开发来排查
  • 运营人员 没错,就是运营人员,好比电商的转化率、视频网站的完播率、普通PV数据等均可以经过日志进行统计,随着大数据技术的普及,这部分日志占比也愈来愈高
  • 安全人员 虽然大多数企业不重视安全,可是安全也能够经过日志来进行预警,好比某个用户忽然大额转帐、再好比数据库忽然出现大量无条件分页查库(拖库)等等

日志有几种?

  • 调试日志 用于开发人员开发或者线上回溯问题。
  • 诊断日志 通常用于运维人员监控系统与安全人员分析预警。
  • 埋点日志 通常用于运营决策分析,也有用做微服务调用链路追踪的(运维、调试)。
  • 审计日志 与诊断日志相似,诊断日志偏向运维,审计日志偏向安全。

日志都须要输出什么?

注:日志级别会在下面讲解github

  • 调试日志
    • DEBUG 或者 TRACE 级别,好比方法调用参数,网络链接具体信息,通常是开发者调试程序使用,线上非特殊状况关闭这些日志
    • INFO 级别,通常是比较重要却没有风险的信息,如初始化环境、参数,清理环境,定时任务执行,远程调用第一次链接成功
    • WARN 级别,有可能有风险又不影响系统继续执行的错误,好比系统参数配置不正确,用户请求的参数不正确(要输出具体参数方便排查),或者某些耗性能的场景,好比一次请求执行过久、一条 SQL 执行超过两秒,某些第三方调用失败,不太可能被运行的if分支等
    • ERROR 级别,用于程序出错打印堆栈信息,不该该用于输出程序问题以外的其余信息,须要注意打印了日志异常(Exception)就不该该抛(throw)了
  • 诊断日志 通常输出 INFO 级别,请求响应时间,内存占用等等,线上接入监控系统时打开,建议输出到独立的文件,能够考虑 JSON 格式方便外部工具分析
  • 埋点日志 业务按需定制,好比上文提到的转化率能够在用户付款时输出日志,完播率能够在用户播放完成后请求一次后台输出日志,通常可输出 INFO 级别,建议输出到独立的文件,能够考虑JSON格式方便外部工具分析
  • 审计日志 大多 WARN 级别或者 INFO 级别,通常是敏感操做便可输出,登录、转帐付款、受权消权、删除等等,建议输出到独立的文件,能够考虑JSON格式方便外部工具分析

通常调试日志由开发者自定义输出,其余三种应该根据实际业务需求来定制。sql

日志的其余注意点

  1. 线上日志应尽可能谨慎,要思考:这个位置输出日志能帮助排除问题吗?输出的信息与排查问题相关吗?输出的信息足够排除问题吗?作到很多输出必要信息,很少输出无用信息(拖慢系统,淹没有用信息)
  2. 超级 SessionId 与 RequestId,不管是单体应用仍是微服务架构,应该为每一个用户每次登录生成一个超级 SessionId,方便跟踪区分一个用户;RequestId,每次请求生成一个 RequestId,用于跟踪一次请求,微服务也能够用于链路追踪
  3. 日志要尽可能单行输出,一条日志输出一行,不然不方便阅读以及其余第三方系统或者工具分析
  4. 公司内部应该制定一套通用的日志规范,包括日志的格式,变量名(驼峰、下划线),分隔符(“=”或“:”等),什么时候输出(好比规定调用第三方先后输出INFO日志),公司的日志规范应该不断优化、调整,找到适合公司业务的最佳规范

OK,理论就聊到这里,接下来让咱们回到技术层面。数据库

使用概念

若是要想要学会使用日志框架,先要理解几个简单概念,LoggerAppendersLayout日志级别级别继承(Level Inheritance)apache

Logger(日志实例)

用于输出日志,调用一次org.slf4j.LoggerFactory#getLogger(java.lang.Class<?>)org.slf4j.LoggerFactory#getLogger(java.lang.String)就会产生一个日志实例,相同参数会共用同一个实例。json

Appenders

日志输出器,logback 预约义了输出到控制台、文件、Socket 服务器、MySQL、PostgreSQL、Oracle 和其余数据库、JMS 和 UNIX Syslog 系统调用等实现,经过配置文件配置便可使用,固然咱们经常使用的只有控制台和文件两种。安全

Layout

用于控制日志输出格式,前文所说的”自动输出日志相关信息,如:日期、线程、方法名称等等“就能够用 Layout 来控制,实际使用很简单,写一个 Layout 格式定义表达式(pattern)便可,使用方法相似于Java 的SimpleDateFomat

日志级别

RFC 5424 (page 11)规定了 8 种日志级别,可是SLF4j 只定义了 5 种日志级别,分别是 ERROR、WARN、INFO、DEBUG、TRACE 这五个级别从高到低,配置级别越高日志输出就越少,以下图

日志级别

咱们看到滑动条上五个点正好对应五个级别,滑动指示器能够左右移动,指示器做为分界点,指示器左侧均可以输出,右侧都不能输出,左右调整指示器就能够调整日志的输出,滑倒右侧就能够所有输出,滑倒左侧就能够减小输出,那么是否可以完全关闭输出呢?答案是能够的,配置文件中还能够配置为 ALL 与 OFF,分别对应全部(等价于TRACE)与关闭。

级别继承

理解了日志级别,让咱们来考虑以下场景:

  • 某些重要业务输出 INFO 级别,其余业务输出WARN级别的日志,同时关闭全部库、框架的日志

有需求就会有解决方案,其实很简单,logback 与 log4j 都支持按照日志实例来配置,如今问题解决了,可是新的问题又来了,若是线上全部日志都输出 INFO 级别,难道要一个一个配置吗?这时候就就要请出咱们上面所提到的级别继承,若是 Java 同样,logback 与 log4j 中也都是单根继承模型,Java 中是 Object,日志中是 ROOT,以下图:

级别继承

有了继承机制,咱们只须要将 ROOT 调整到 INFO 级别,再按照需求细化调整咱们业务对应的 logger 实例级别便可知足绝大多数场景。

Codding 实战

问:把大象装冰箱分几步?分三步:一、引入依赖,二、编码输出日志,三、调整配置文件。前文已经讲过步骤一,若是没有看过的读者请移步公众号查看往期回顾,这里直接进入步骤二。

步骤二

若是项目中使用了Lombok,那么能够直接在类上面加@Slf4j注解既可得到日志实例,不然可使用static final org.slf4j.Logger logger = LoggerFactory.getLogger(TestLog.class);来获取日志实例

具体日志输出方法以下:

logger.trace("A TRACE Message");
logger.debug("A DEBUG Message");
logger.info("An INFO Message");
logger.warn("A WARN Message");
logger.error("An ERROR Message");
复制代码

这里有个注意点,尽可能使用参数占位而不要手动拼接字符串,以下

String level = "Trace";
// 反例
logger.trace("A " + level + " Message");
// 正确的作法
logger.trace("A {} Message", level);
复制代码

这样作能够提升效率,若是不输出日志,第一种状况也会拼接字符串形成性能损耗,第二种就不会有此问题(阿里巴巴Java开发手册(华山版)这里表述有问题,占位符效率更高是由于尽可能延迟进行字符串处理,若是不须要输出的日志就不处理了,下一篇原理分析会展开),另外咱们也不须要if (logger.isTraceEnabled())来进行判断了(性能损耗不高,可是代码好看多了)。

步骤三

配置文件须要区分 logback 与 log4j2,两种框架在配置文件上有差异但很类似,来看具体配置文件。

logback 配置文件位置

logback 支持 xml 与 groovy 脚本两种配置方式,logback 查找配置文件位置规则以下(后续文章会讲如何修改位置)

  1. logback 尝试在类路径中找到一个名为 logback-test.xml 的文件。
  2. 若是找不到此类文件,则 logback 会尝试在类路径中找到名为 logback.groovy 的文件。
  3. 若是找不到这样的文件,它将在类路径中检查文件 logback.xml。
  4. 若是找不到此类文件,则经过查找文件 META-INF\services\ch.qos.logback.classic.spi.Configurator,若是有这个文件且内容是 com.qos.logback.classic.spi.Configurator 实现类的全类名,直接加载这个实现类。
  5. 若是以上方法均不能成功执行,则 logback 会使用 BasicConfigurator 自动进行自我配置,会将日志输出到控制台。

这段长长的文字其实不用看,咱们就把 logback.xml 放入 Classpath 根目录就能够了。。

logback 配置文件编写规则

logback 配置文件主要分为三类,一个或多个 Appender,用于定义输出位置(不一样文件位置,或者网络又或者数据库);一个或多个 Logger,用于细化配置不一样 logger 的输出级别以及位置;一个 ROOT,是一个特殊的logger,用于配置根 Logger。

日志配置文件格式

咱们一块儿来看下面的配置文件实例

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="false" debug="false">

    <!-- 定义日志文件的存储地 -->
    <property name="LOG_PATH" value="/var/log"/>

    <property name="CONSOLE_LOG_PATTERN" value="%d{HH:mm:ss.SSS} %-5level [%10.10thread] %-30.30logger{29}\(%4L\\) - %msg%n"/>
    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>utf-8</charset>
        </encoder>
    </appender>

    <!-- 文件日志格式(打印日志,不打印行号) -->
    <property name="FILE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%10.10thread] %-30.30logger{29} - %msg%n"/>

    <appender name="FILE_ALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${LOG_PATH}/log.log</file>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- yyyy-MM-dd 按日滚动 -->
            <fileNamePattern>${LOG_PATH}/log-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <!-- 单个文件最大50M -->
            <maxFileSize>50MB</maxFileSize>
            <!-- 最多占用5G磁盘空间,500个文件(总共不能超过该5G) -->
            <maxHistory>500</maxHistory>
            <totalSizeCap>5GB</totalSizeCap>
        </rollingPolicy>
        <!-- 追加方式记录日志 -->
        <append>true</append>
        <!-- 日志文件的格式 -->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${FILE_LOG_PATTERN}</pattern>
            <charset>utf-8</charset>
        </encoder>
    </appender>

    <!-- 日志输出级别 STDOUT:控制台;FILE_ALL:文件 -->
    <root level="warn">
        <appender-ref ref="STDOUT"/>
    </root>
    <logger name="druid.sql" level="warn" additivity="true"/>
    <logger name="druid.sql.ResultSet" level="warn" additivity="true"/>
    <logger name="com.alibaba.druid.pool.DruidDataSource" level="debug" additivity="true">
        <appender-ref ref="FILE_ALL"/>
    </logger>
</configuration>
复制代码

上面配置文件定义了两个 Appender,一个输出控制台,另外一个输出到文件而且自动滚动。需注意的是property标签至关于定义一个变量,可使用${xxx}进行引用,CONSOLE_LOG_PATTERN 与 FILE_LOG_PATTERN 定义了控制台与文件打印格式,具体编写方式相似于 Java 的SimpleDateFomat就不在此展开了,具体能够参考

log4j2 配置文件位置

log4j2 支持 XML、JSON、YAML 或者 properties 格式的配置文件,具体查找方式以下:

  1. 检查“ log4j.configurationFile”系统属性,若是有,尝试使用与文件扩展名匹配的ConfigurationFactory加载配置。
  2. 若是未设置系统属性,则属性 ConfigurationFactory 将在类路径中查找 log4j2-test.properties。
  3. 若是找不到此类文件,则 YAML ConfigurationFactory将在类路径中查找 log4j2-test.yaml或log4j2-test.yml。
  4. 若是找不到此类文件,则 JSON ConfigurationFactory 将在类路径中查找 log4j2-test.json或log4j2-test.jsn。
  5. 若是找不到这样的文件,XML ConfigurationFactory 将在类路径中查找 log4j2-test.xml。
  6. 若是找不到测试文件,则属性 ConfigurationFactory 将在类路径上查找 log4j2.properties。
  7. 若是找不到属性文件,则 YAML ConfigurationFactory 将在类路径上查找 log4j2.yaml或log4j2.yml。
  8. 若是没法找到 YAML 文件,则 JSON ConfigurationFactory 将在类路径上查找 log4j2.json或log4j2.jsn。
  9. 若是没法找到 JSON 文件,则 XML ConfigurationFactory 将尝试在类路径上找到 log4j2.xml。
  10. 若是找不到配置文件,使用 DefaultConfiguration 自动配置,将日志输出到控制台。

这段更长的文字固然也不用看,咱们就把 log4j2.xml 放入 Classpath 根目录就能够了

log4j2 配置文件编写

log4j 也是 Logger 与 Appender 配置项,也有一个ROOT的特殊 Logger,Appender 比logback支持更多输出位置,如kafka、Cassandra、Flume等。

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="debug" strict="true">
    <!-- 定义变量,能够被${xxx}引用 -->
    <Properties>
        <Property name="baseDir">logs</Property>
    </Properties>

    <!-- 定义 Appenders 用来指定输出位置 -->
    <Appenders>
        <!-- 日志滚动 $${date:yyyy-MM}:按月滚动文件夹 按小时、文件序号滚动,每次滚动都使用gz压缩 -->
        <RollingFile name="RollingFile" fileName="${baseDir}/log.log" filePattern="${baseDir}/$${date:yyyy-MM}/log-%d{yyyy-MM-dd-HH}-%i.log.gz">
            <!-- 日志格式 -->
            <PatternLayout pattern="%d %p %c{1.} [%t] %m%n"/>
            <Policies>
                <!-- 时间滚动(按月滚动目录,按小时滚动文件) -->
                <TimeBasedTriggeringPolicy/>
                <!-- 文件大小滚动(1小时内超过250M,强制滚动一次) -->
                <SizeBasedTriggeringPolicy size="250 MB"/>
            </Policies>
            <!-- 天天最多100个文件 -->
            <DefaultRolloverStrategy max="100">
                <!-- 删除策略,超过三十天删除,若是总文件小于100G,文件数量小于10个,则不会被删除 -->
                <Delete basePath="${baseDir}" maxDepth="2">
                    <IfFileName glob="*/app-*.log.gz">
                        <IfLastModified age="30d">
                            <IfAny>
                                <IfAccumulatedFileSize exceeds="100 GB"/>
                                <IfAccumulatedFileCount exceeds="10"/>
                            </IfAny>
                        </IfLastModified>
                    </IfFileName>
                </Delete>
            </DefaultRolloverStrategy>
        </RollingFile>
    </Appenders>

    <Loggers>
        <!-- 多个logger -->
        <Logger name="org.apache.logging.log4j.test2" level="debug" additivity="false">
            <AppenderRef ref="RollingFile"/>
        </Logger>

        <!-- 一个ROOT -->
        <Root level="trace">
            <AppenderRef ref="STDOUT"/>
        </Root>
    </Loggers>
</Configuration>
复制代码

能够看得出 log4j2 与 logback 配置文件书写大同小异,甚至一样须要注意additivity="true"时致使的日志重复输出问题,毕竟 log4j1 与 logback 都是 Ceki大神都做品。

总结

得益于 Ceki 大佬的努力,日志使用几乎没有有差别(Logback 与 Log4j2,Google 于 2018年4月开源了流式(fluent)日志框架 Flogger,Slf4j 也将在 2.0 版本支持,而 log4j2 再次落后,不过笔者认为 log4j2 有不少优势,更多内容请关注后续文章)。关于日志如何输出本人也是经验之谈,免不了纰漏,欢迎补充指正,另外每一个公司都有不一样的应用场景,具体应该遵照公司统一规范。

本篇更多倾向基础使用,接下来的文章将展开对比、原理以及扩展日志框架,敬请各位小伙伴们期待。

本文相关代码

若是以为写的不错,求关注、求点赞、求转发,若是有问题或者文中有错误,欢迎留言讨论。

扫描关注公众号,第一时间得到更新

扫码关注

参考

logging.apache.org/log4j/2.x/m…

logback.qos.ch/manual/conf…

相关文章
相关标签/搜索