Python 如何优雅的记录线上服务日志?基于标准库 logging

5,724 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第22天,点击查看活动详情

背景

我开发过《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋象棋等游戏。

它是已经在线上稳定运行了半年之久的服务,每当用户找我反馈Bug时,我都能通过日志快速定位到问题。时间已经证明了我的日志系统的有效性。

那么,我是如何记录线上服务的日志呢?本文给出答案。

日志系统介绍

选(或搭建)一套日志系统,只需要解决2个问题:

  • 如何记录日志?
  • 如何检索日志?

如果能够方便、快速的记录和检索日志,而不影响线上服务的性能,那么这就是一套好的日志系统。

本文解决的是「记录日志」的问题。

日志系统通常对实时性要求不高,因为它是帮助事后排查问题的,允许有分钟级别的延迟。当然,如果你想做「基于日志系统」的监控,或者做一套系统同时支持「日志」+「监控」,那么你可能需要考虑其它问题,尤其是「实时性」,这时候你除了往日志数据库或日志文件写入,还需要把监控相关的写入监控相关的内存数据库,从而让监控实时更新。本文不考虑这种情况。

Python 标准库 logging

Python 原生已经提供了 logging 标准库,可以用于程序记录日志。官网介绍如下图:

image.png

我的诉求

  • 我有3种日志类型,要存到3个名字的日志文件。一个用于记录程序异常,一个用于记录用户行为,一个用于记录用户的环境信息。
  • 我期望每个日志文件最大占4M,最多存100份日志文件(超过100份的自动删除),都存储到文件data/log_name.log。如果超出大小了,就改名为data/log_name.log.1备份存储起来,新的日志继续写到data/log_name.log。如果又超出大小了,每一个旧日志的后缀都+1,并且把data/log_name.log改为data/log_name.log.1,新的日志继续写到data/log_name.log

实践代码

封装了一个logger.py

import logging
from logging import handlers

formatter = logging.Formatter('%(asctime)s\t%(levelname)s\t%(message)s')
server_name = '你的服务的英文名,一定要避免跟标准库的模块重名'


def get_logger(log_name):
    # 每个日志 4M
    handler = handlers.RotatingFileHandler(f'data/{log_name}.log', maxBytes=4194304, backupCount=100, encoding='utf8')
    handler.setLevel(logging.INFO)
    handler.setFormatter(formatter)
    logger = logging.getLogger(f'{server_name}.{log_name}')
    logger.propagate = False
    logger.setLevel(logging.INFO)
    logger.addHandler(handler)
    return logger


user_environment_logger = get_logger('user_environment')
user_behavior_logger = get_logger('user_behavior')
exception_logger = get_logger('exception')


def log_user_environment(user_environment, room_id):
    user_environment_logger.info(f'room_id={room_id}\tenv={user_environment}')


def log_user_behavior(action):
    user_behavior_logger.info(f'...')


def log_exception(exception_name):
    user_behavior_logger.info(f'...')

我封装了函数get_logger用于生成一个logger,当然,这个函数不是给其它业务代码调用的,这个函数只在本文将调用,用于获得user_environment_logger等这3个我需要的logger。

业务代码只需要调用log_user_environment log_user_behavior log_exception,按照需求传入参数即可,这里会按一定格式写入日志。 我选用的格式,需要结合\t=做正则匹配来提取结构化数据,当然你也可以使用json序列化日志数据。只要后续解决「检索日志」这个问题时,你有办法解析和提取日志内容即可。

另外formatter = logging.Formatter('%(asctime)s\t%(levelname)s\t%(message)s')是日志的格式,参考官方文档Formatter

写在最后

我是HullQin,公众号线下聚会游戏的作者(欢迎关注公众号,联系我,交个朋友),转发本文前需获得作者HullQin授权。我独立开发了《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋象棋等游戏,不收费无广告。还独立开发了《合成大西瓜重制版》。还开发了《Dice Crush》参加Game Jam 2022。喜欢可以关注我噢~我有空了会分享做游戏的相关技术,会在这2个专栏里分享:《教你做小游戏》《极致用户体验》