Java日志最佳实践(游戏项目)

Java日志最佳实践(游戏项目)

窥一斑而知全豹(阿里、唯品会)

阿里日志规约

1. 【强制】应用中不可直接使用日志系统 (Log 4 j 、 Logback) 中的 API ,而应依赖使用日志框架
SLF 4 J 中的 API ,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    private static final Logger logger = LoggerFactory.getLogger(Abc.class);
2. 【强制】日志文件推荐至少保存 15 天,因为有些异常具备以“周”为频次发生的特点。
3. 【强制】应用中的扩展日志 ( 如打点、临时监控、访问日志等 ) 命名方式:
    appName _ logType _ logName . log 。 logType :日志类型,推荐分类有
    stats / desc / monitor / visit 等 ;logName :日志描述。这种命名的好处:通过文件名就可知
    道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。
    正例: mppserver 应用中单独监控时区转换异常,如:
mppserver _ monitor _ timeZoneConvert . log
    说明:推荐对日志进行分类,如将错误日志和业务日志分开存放,便于开发人员查看,也便于通过日志对系统进行及时监控。
4. 【强制】对 trace / debug / info 级别的日志输出,必须使用条件输出形式或者使用占位符的方
式。
    说明: logger . debug( " Processing trade with id : " +  id + "  and symbol : " +  symbol);
    如果日志级别是 warn ,上述日志不会打印,但是会执行字符串拼接操作,如果 symbol 是对象,
会执行 toString() 方法,浪费了系统资源,执行了上述操作,最终日志却没有打印。
    正例: ( 条件 )
    if (logger.isDebugEnabled()) {
    logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);
    }
    正例: ( 占位符 )
    logger.debug("Processing trade with id: {} and symbol : {} ", id, symbol);
5. 【强制】避免重复打印日志,浪费磁盘空间,务必在 log 4 j . xml 中设置 additivity = false 。
正例: <logger name="com.taobao.dubbo.config" additivity="false"> 
6. 【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过
关键字 throws 往上抛出。
    正例: logger.error(各类参数或者对象 toString + "_" + e.getMessage(), e);
7. 【推荐】谨慎地记录日志。生产环境禁止输出 debug 日志 ; 有选择地输出 info 日志 ; 如果使
用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请
思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
8. 【参考】可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。注意日志输出的级别, error 级别只记录系统逻辑出错、异常等重要的错误信息。如非必要,请不要在此场景打出 error 级别。

唯品会日志日志规约

Rule 1. 【强制】应用中不可直接使用日志库(Log4j、Logback)中的API,而应使用日志框架SLF4J中的API

使用门面模式的日志框架,有利于维护各个类的日志处理方式统一。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

private static Logger logger = LoggerFactory.getLogger(Foo.class);

Rule 2. 【推荐】对不确定会否输出的日志,采用占位符或条件判断

//WRONG
logger.debug("Processing trade with id: " + id + " symbol: " + symbol);
如果日志级别是info,上述日志不会打印,但是会执行1)字符串拼接操作,2)如果symbol是对象,还会执行toString()方法,浪费了系统资源,最终日志却没有打印。

//RIGHT
logger.debug("Processing trade with id: {} symbol : {} ", id, symbol);
但如果symbol.getMessage()本身是个消耗较大的动作,占位符在此时并没有帮助,须要改为条件判断方式来完全避免它的执行。

//WRONG
logger.debug("Processing trade with id: {} symbol : {} ", id, symbol.getMessage());

//RIGHT
if (logger.isDebugEnabled()) {
  logger.debug("Processing trade with id: " + id + " symbol: " + symbol.getMessage());
}

Rule 3. 【推荐】对确定输出,而且频繁输出的日志,采用直接拼装字符串的方式

如果这是一条WARN,ERROR级别的日志,或者确定输出的INFO级别的业务日志,直接字符串拼接,比使用占位符替换,更加高效。

Slf4j的占位符并没有魔术,每次输出日志都要进行占位符的查找,字符串的切割与重新拼接。

//RIGHT
logger.info("I am a business log with id: " + id + " symbol: " + symbol);

//RIGHT
logger.warn("Processing trade with id: " + id + " symbol: " + symbol);

Rule 4. 【推荐】尽量使用异步日志

低延时的应用,使用异步输出的形式(以AsyncAppender串接真正的Appender),可减少IO造成的停顿。

需要正确配置异步队列长度及队列满的行为,是丢弃还是等待可用,业务上允许丢弃的尽量选丢弃。

Rule 5. 【强制】禁止使用性能很低的System.out()打印日志信息

同理也禁止e.printStackTrace();

例外: 应用启动和关闭时,担心日志框架还未初始化或已关闭。

Sonar-106: Standard outputs should not be used directly to log anything
Sonar-1148: Throwable.printStackTrace(...) should not be called

Rule 6. 【强制】禁止配置日志框架输出日志打印处的类名,方法名及行号的信息

日志框架在每次打印时,通过主动获得当前线程的StackTrace来获取上述信息的消耗非常大,尽量通过Logger名本身给出足够信息。

Rule 7. 【推荐】谨慎地记录日志,避免大量输出无效日志,信息不全的日志

大量地输出无效日志,不利于系统性能,也不利于快速定位错误点。

记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?

Rule 8. 【推荐】使用warn级别而不是error级别,记录外部输入参数错误的情况

如非必要,请不在此场景打印error级别日志,避免频繁报警。

error级别只记录系统逻辑出错、异常或重要的错误信息。

记录日志的目的和原则

  1. 目的
    • 快速、准确追踪、定位线上问题
    • 统计、监控、报警
  2. 原则
    • 这些日志真的有人看吗?
      • 不要输出无效日志
    • 看到这条日志你能做什么?
      • 日志的可读性要好
    • 能不能给问题排查带来好处?
      • 日志要携带一些必要的上下文信息,准确、全面
    • 不多不少

最佳日志实践(游戏项目)

  1. 统一管理SLF4J#Logger
    • 封装统一的Logs接口,通过此接口获取Logger
    • 使用时通常可结合"import static"
    • 没有必要每一个class内都声明一个Logger
    • 注:当前日志实现是logback
  2. Logger分类
    • system
      • 系统日志、包括系统启停(info)、系统运行监控日志等(debug、每分钟)
      • 通过该日志可以知道系统是否顺利启动、开启debug日志后可以知晓当前系统整体状态
    • persistence
      • 持久化日志,主要是mongo/redis的数据存储、加载日志,较重要
      • 通过该日志可以知道如一个玩家数据的序列化时间,整体一个调度周期的序列化时间,加载一个玩家数据的时间,回档的判断等
    • net
      • 网络层日志,主要包括request/response/notify等(通常这部分是debug日志,在上线前期打开,后期随着版本稳定可以关闭),也包括自定义网络层的一些日志
      • 通常查询线上bug现在业务日志查询,如果需要上下文的request/response等详细信息可去net日志查询
    • business
      • 业务日志,主要包括各业务模块自己的一些日志打印,如方法如参的检查,业务操作的记录等。这个日志是通常排查问题经常用到的
      • 如果有比较大或者比较大的业务模块,日志比较多需要经常查看的话,可以单独出来一个模块日志
    • schedule
      • 定时调度日志,目前主要包括quartz的相关日志。在查询定时任务相关的bug时,此日志比较重要
    • devops
      • 包括游戏与外部运维、运营、三方sdk交互的日志,如gm、推送、global等
    • robot
      • 用于机器人压测的行为录制
    • third
      • 三方依赖库日志,包括revenant框架的日志和部分比较重要的三方库日志
      • 注:1. 需要使用slf4j 2. thrid logger是基于"package name"
    • error
      • 错误日志单独一个文件,主要是一些重要的错误和异常
      • 注:并没有一个单独的这样logger,而是各个模块logger的error除了追到自己的文件外,还要统一汇总到error.log
  3. 日志格式规范
    • 禁用的conversion specifier(logback)
      • C/class、F/file、L/line、M/method、caller
      • its use should be avoided unless execution speed is not an issue
    • logback.xml#推荐pattern
      • 必须字段:d、t、p、m 可选字段:c
      • %d{HH:mm:ss.SSS} [%t] %-5p %c{0} - %m%n
        • 这个输出了logger,主要用于三方库的logger
      • %d{HH:mm:ss.SSS} [%t] %-5p - %m%n
    • msg
      • 必选项2:class.method (用来定位,相当于手动实现了"class.method")
      • 必选项2:如果是玩家相关的日志,最好记录rid(用来定位会话标识)
        • 这有另外一个很重要的作用是可以写过滤器,过滤输出某个指定出错玩家的debug日志
      • 必选项2:精炼内容
        • 记录日志的时候需要考虑当线上出现问题的时候,我记录的日志内容是否能对我排查问题起到作用?是否会出现该记录的没有记录?
        • 需要review是否通顺,别人是否可以看懂,能否提供真正有意义的信息
  4. 日志级别
    • 只使用debug、info、warn、error四个级别
    • error-当系统出现较严重的问题时会经常去错误日志查看
      • 较重要的异常(注:并非所有异常都记录error,有些可能为warn)
      • 较重要的错误信息(注:逻辑参数检查不通过这种不是error)
    • warn
      • 主要用来记录如handler参数校验不通过
      • 用来记录部分不重要的异常、非正常的条件分支(业务流程预期不符)
    • info-通常来说优先根据info日志可初步定位
      • 主要包括如系统状态变化日志
      • 业务流程的核心处理、关键动作
      • 业务流程的状态变化
    • debug
      • 调试信息,如request/response
    • 通常上线前期debug日志会打开,后随着稳定会关闭debug
      • 当出现bug时或者棘手的问题时,需要动态的开启debug
      • 如果只是某个玩家出现问题,那么开启debug日志量会非常大,所以需要有基于rid判断的日志过滤器(RidFilter extends Filter<ILoggingEvent>)
  5. 日志性能
    • 推荐使用AsyncAppender
      • 避免阻塞业务逻辑线程
      • queueSize和discardingThreshold都要设置,即允许丢失日志,否则队列满会造成业务线程阻塞
    • 优先使用{}占位符进行参数格式化,除非参数调用消耗较大需要增加isEnabled条件判断
    • 避免使用代价较大caller data(C/F/L/M)
    • 再向进一步提升性能的话,可考虑log4j2(Asynchronous Loggers based on the LMAX Disruptor library)
  6. 日志细节
    • 个人不建议用一个工具类来再包装一层debug/info/warn/error方法
      • 这种实现通常只能声明一个logger,无法控制多个模块的level
      • 行号不准确,需要通过具体的logger传入'fqcn'实现
    • 记录日志一定不能抛出异常
      • 可能会影响业务逻辑,需要check日志格式化参数调用是否可能抛出异常
    • 记录异常要用规范的格式
      • log.error("filter error:",e) // 这个是正确的方式
      • log.error("filter error:{}",e) // 这个是错误的方式
      • 其实只要看org.slf4j.Logger接口的api就了解了#error(String msg, Throwable t)
    • 建议使用SiftingAppender,避免logback内声明过多的appender
      • 如果需要配合AsyncAppender使用,则外部只需要声明一个该Appender即可,然后其appender-ref指向sift(SiftingAppender admits one and only one nested appender)
    • 线上记录日志避免使用System.out、System.err、e.printStackTrace()
      • 不用担心日志框架没有启动,这个日志框架内部会使用标准输出打印
      • 所以程序启动时刻的标准输出和标准错误不能丢(即不能重定向到/dev/null),另外一个原因是一些三方日志没有使用slf4j,也可能会直接向控制台写日志
    • 如果使用nohup启动程序,建议将标准输出和标准错误都重定向到/dev/null,否则nohup默认的重定向文件nohup.out会越来越大
      • >/dev/null 2>&1
      • 不过当日志框架未初始化时就报错的还是,则还是需要修改启动脚本输出stdout和stderr
      • 所以优先建议使用python启动程序(使用sh库)
    • 一些重要的错误可以和报警系统关联起来
      • 如可以指定通知到钉钉
    • 一些较重要的系统信息建议接入监控系统
      • 如通过grafana这种图表化的形式直接的看到系统信息的变化曲线
    • 线上可根据需要动态的调整各个logger的level
      • 可直接修改logback.xml
      • 可通过'脚本化'的方式获取到对应的logger调用setLevel即可(需要转为对应实际的logger如ch.qos.logback.classic.Logger)
    • 要考虑日志文件的大小
      • 日志太大,则很难grep
      • 所以要通过线上测试酌情考虑根据文件大小滚动文件
      • 日志的maxHistory建议为15天

参考

  1. 最佳日志实践(v2.0)
  2. 优秀日志实践准则
  3. 唯品会日志规约
  4. 阿里巴巴Java开发手册#日志规约
  5. slf4j faq
  6. logback layouts
  7. logback filters
  8. Apache Log4j 2
  9. log4j日志封装说明
  10. Java日志性能那些事
  11. Java日志的一些细节
  12. Logback SiftAppender
  13. 提升logback的性能
  14. SiftAppender的问题
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容

  • 在应用程序中添加日志记录总的来说基于三个目的:监视代码中变量的变化情况,周期性的记录到文件中供其他应用进行统计分析...
    时待吾阅读 5,008评论 1 13
  • 在项目开发过程中,我们可以通过 debug 查找问题。而在线上环境我们查找问题只能通过打印日志的方式查找问题。因此...
    Java架构阅读 3,470评论 2 41
  • 在应用程序中添加日志记录总的来说基于三个目的:监视代码中变量的变化情况,周期性的记录到文件中供其他应用进行统计分析...
    时待吾阅读 4,975评论 0 6
  • 一、logback的介绍Logback是由log4j创始人设计的另一个开源日志组件,官方网站: http://lo...
    shaolin79阅读 1,020评论 0 0
  • 如果到现场看到佛光山的这尊佛像,是非常震撼心灵的。 不管是佛像的威严和场面布局还是佛学的专业还是星云大师的理念和作...
    华华叶儿阅读 385评论 0 2