zhangkun.logger 使用手册
安装及配置
logging.ini 配置及详解
[loggers]
keys = root, gunicorn.access, gunicorn.error
[handlers]
keys = root, access, access_file, error_file , sentry
[formatters]
keys = common, json
[logger_root]
handlers = root
[logger_gunicorn.access]
handlers = access, access_file
qualname = gunicorn.access
level = INFO
propagate = 0
[logger_gunicorn.error]
handlers = error_file, sentry
qualname = gunicorn.error
level = ERROR
propagate = 0
[handler_root]
class = NullHandler
args = ()
[handler_access]
class = StreamHandler
formatter = common
args = (sys.stdout, )
[handler_sentry]
class = raven.handlers.logging.SentryHandler
level = ERROR
formatter = json
args = ("http://www.baidu.com",)
[handler_access_file]
level = INFO
class = logging.handlers.WatchedFileHandler
formatter = common
args = ('/data/log/web/logger_demo.access.log', 'a')
[handler_error_file]
level = INFO
class = logging.handlers.WatchedFileHandler
formatter = json
args = ('/data/log/web/logger_demo.error.log', 'a')
[formatter_common]
format = [%(levelname)s] [%(asctime)s] %(message)s
datefmt = %Y-%m-%d %H:%M:%S
[formatter_json]
format = [%(levelname)s] [%(asctime)s] %(message)s
datefmt = %Y-%m-%d %H:%M:%S
class = logger_demo.util.logger.JsonLogger
loggers
注册 logger 对象,提供日志接口,供应用代码使用。logger最长用的操作有两类:配置和发送日志消息。可以通过logging.getLogger(name)获取logger对象,如果不指定name则返回root对象,多次使用相同的name调用getLogger方法返回同一个logger对象。
handlers
将日志记录(log record)发送到合适的目的地(destination),比如文件,socket等。一个logger对象可以通过addHandler方法添加0到多个handler,每个handler又可以定义不同日志级别,以实现日志分级过滤显示。
formats
指定日志记录输出的具体格式。formatter的构造方法需要两个参数:消息的格式字符串和日期字符串,这两个参数都是可选的。
formatter_json
- format : 指定日志输出格式,有logging内置变量,主要错误信息是由 %(message)s 抛出
- datefmt: 指定日志时间格式, %Y-%m-%d %H:%M:%S --> 2020-08-25 16:27:29
- Class: 指定日志处理类,这里选择 {py项目}.util.logger 下的 JsonLogger 处理
解释
在 logging.ini 中指定了 root,gunicorn.access,gunicorn.error 三个 Logger 对象,主要分别处理主进程输出,gunicorn 日志输出,其中:
- root: 处理 handler 为 root ,指定类为 Nullhandler,输出为空。
- gunicorn.access:
- 处理 handler 为 access ,指向类为 StreamHandler ,参数为 sys.stdout,输出到系统终端。
- 处理 handler 为 access_file , 指向类为 WatchHandler ,参数为 {access.log 日志路径}, 输出到文件。
- gunicorn.error:
- 处理 handler 为 sentry , 指向类为 SentryHandler ,参数为 { sentry dsn } , 输出到 sentry。
- 处理 handler 为 error_file,指向类为 WatchHandler,参数为 {error.log 日志路径}, 输出到文件。
conf 项目配置
在 devConfig 里设置 GUNICORN_LOGGER_ENABLE=False
在 prodConfig 里设置 GUNICORN_LOGGER_ENABLE=True
如果在开发环境设置 GUNICORN_LOGGER_ENABLE=True ,且使用了 gunicorn 启动项目,则会导致开会环境的异常错误信息输出到项目 Sentry
gunicorn.py 配置
# -*- coding: utf-8 -*-
# 解决 MonkeyPatchWaring 提示
import gevent.monkey
gevent.monkey.patch_all()
import multiprocessing
from pathlib import Path
bind = "0.0.0.0:5000"
workers = multiprocessing.cpu_count() + 1
worker_class = "gevent"
worker_connections = 2000
threads = 8
timeout = 60
graceful_timeout = 5
deploy_dir = Path(__file__).parent.resolve()
chdir = str(deploy_dir.parent)
logconfig = str(deploy_dir / "logging.ini")
loglevel = 'INFO'
# 设置 nginx 代理下真是 remote_ip 地址 及 host 地址
access_log_format = '%({x-forwarded-for}i)s %(l)s ["%({http_host}e)s"] "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
init.py 配置
def configure_logger(app):
if app.config.get("GUNICORN_LOGGER_ENABLE", False):
gunicorn_logger = logging.getLogger('gunicorn.error')
app.logger.handlers = gunicorn_logger.handlers
app.logger.setLevel(gunicorn_logger.level)
使 Flask 的日志处理交由 Gunicorn 进行处理
系统环境变量配置(Docker)
通常交由运维人员统一进行配置,需配置两个环境变量 $DOCKER_HOSTNAME、$DOCKER_HOSTIP
Docker-compose 启动方式
在 docker-compose.yml 的 service 里,设置
sevice:
enviornment:
DOCKER_HOSTNAME: (留空即可)
DOCKER_HOSTIP: (留空即可)
Docker 启动方式(k8s)
docker run -e DOCKER_HOSTNAME=$DOCKER_HOSTNAME DOCKER-HOSTIP=$DOCKER_HOSTIP xxxxxxxxx
安装 zhangkun.logger
直接安装
pip install -e git+https://gitlab+deploy-token-51:kapA9ypL_1wrpQXUzhcY@gitlab-inet.zkyouxi.com/zhouzhenfeng/zhangkun.logger.git@master#egg=zhangkun.logger
requirements.txt
git+https://gitlab+deploy-token-51:kapA9ypL_1wrpQXUzhcY@gitlab-inet.zkyouxi.com/zhouzhenfeng/zhangkun.logger.git@master#egg=zhangkun.logger
概念
元信息
元信息通常指的是 当前环境下的 信息,例如:
ClientRecorder
负责记录在一次请求上下文中(Request Context),记录客户端信息
CvmRecorder
负责记录当前程序运行的虚拟终端信息,eg: 当前主机的 hostname
AppRecorder
负责记录当前程序运行的基础信息,eg: 当前运行程序的名称
Type 大类/ Category 小类
大类(Type)
由于日志系统检索的特性,通常有些时候,我们需要关注某些特定模块的异常,例如整个 Database 错误异常,或者是 Marshmallow 验证表单异常,又或者是 Login 登陆 Action 类错误异常,如果需要查看当前模块错误异常,就需要使用大类来进行区分,在检索时指定 type = login ,即可查看当前 Login 模块所有错误异常。
小类(Category)
小类是用来细分大类下的错误异常。比如: Marshmallow 类下的 ParamError 参数错误,UndefindError 参数未定义, ValueError 参数值错误等 DataBase 类下的 ConnError 链接错误, NotFoundError 404错误等 Login 类可定义为 SignError 签名失败,Permission 权限错误, 甚至于可以定义为xx接口错误异常, 例如 login_by_account 为小类。
消息(Msg)
消息是用来补充实际错误信息使用的,用以明确当前错误发生的原因。例如:
Login 类 SignError 错误下就有: msg = "密钥过期"、“签名错误”
Login 类 PermissionError 错误下就有 : Msg = "用户不存在"、“用户权限不足"
快速使用
util.logger
在项目 util 目录下新建 logger.py ,内容如下:
# coding:utf8
from zhangkun.logger import JsonLogger, LoggerRecorder
# 增加元信息记录
JsonLogger.register_recorders(["cvm", "app", "client"])
class LoginException(LoggerRecorder):
logType = "login" # logType 大类
@classmethod
def SigntureErr(cls, msg):
return cls(msg, category="signture") # category 小类
@classmethod
def PermissionErr(cls, msg):
return cls(msg, category="permission")
在项目 view.py 下对 util.logger 进行调用
# coding:utf8
from flask import current_app
from logger_demo.util.logger import LoginException
# 第一种方式: 直接抛出 PermissionErr, 捕获处理
def foo(*args, **kwargs):
try:
raise LoginException.PermissionErr("用户不存在")
except LoginException as e:
current_app.logger.error(e, exc_info=True) # exc_info=True, 回溯错误异常
# 第二种方式: 生成 LoginException 对象
def foo(*args, **kwargs):
userIds = ["user_123"]
try:
assert 'user_123' not in userIds, "该用户已封禁"
except AssertionError as e:
err = LoginException(category="permission", msg=e)
current_app.logger.error(err, exc_info=False)
# 第三种方式: 直接记录错误
def foo(*args, **kwargs):
userIds = ["user_123"]
try:
assert 'user_123' not in userIds, "该用户已封禁"
except AssertionError as e:
current_app.logger.error(LoginException.PermissionErr(e))
定义元信息
在日常使用中,通常会有自定义元信息需求,在 zhangkun.logger 里能处理的元信息通常为,错误本身信息及 Flask 里的 Request 上下文,由于 register_recorders 是定义全局调用,所以避免引入外界变量导致异常。
元信息定义如下:
# coding:utf8
from flask import request, has_request_context
from zhangkun.logger import JsonLogger, BaseRecorder
# 定义客户端抬头元信息
class ClientHeadersRecorder(BaseRecorder):
@classmethod
def handler(cls, record):
if has_request_context():
record.clientHeader = {
"referrer": request.referrer
}
# 注册到JsonLogger, CvmRecorder等内置元信息可采用字符串形式引入
# {"cvm": CvmRecorder, "app": AppRecorder, "client": ClientRecorder}
JsonLogger.register_recorders(["cvm", "app", "client", ClientHeadersRecorder])
# 如果只想调用 CvmRecord, 就只引入 CvmRecord
JsonLogger.register_recorders(["cvm"])
目前内置元信息有( CvmRecorder、AppRecorder、ClientRecorder ),可直接采用 cvm 等字符串注册
特定元信息 ( 重要 )
如果有额外需求,不希望采用 msg 输出特定的元信息,比如 SRV 服务中,请求参数( srvParam )及响应结果 ( srvResponse ),请定义个 SrvException ,在 hanlder 函数里面进行处理。
# coding:utf8
from zhangkun.logger import JsonLogger, LoggerRecorder
# 增加元信息记录
JsonLogger.register_recorders(["cvm", "app", "client"])
class SrvException(LoggerRecorder):
logType = "srv" # logType 大类
@classmethod
def handler(cls, record):
record.srv = {}
if "srvParam" in record.__dict__: # 先判断srvParam存不存在
srvParam = record.__dict__.pop("srvParam") # 删除srvParam
record.srv.update({"srvParam": srvParam})
if "srvResponse" in record.__dict__:
srvResponse = record.__dict__.pop("srvResponse")
record.srv.update({"srvResponse": srvResponse})
然后在 views.py 抛出日志时 加上 extra={"srvParam": srv, "xxx":xxx}
# coding:utf8
from flask import current_app
from logger_demo.util.logger import SrvException
def foo(*args, **kwargs):
...
params = {"sign": "xxxx", "userId": "xxx"}
err = SrvException(category="请求 xxx SRV", msg=e)
current_app.logger.error(err, extra={"srvParam": params})
⚠ ️ 注意 !!!
Handler 里面一定要 先判断 元信息 srvParam 存不存在,且一定要 POP 抛出, 这是因为避免 多个 Logger 对象在处理时,会遇到这个 srvParam 参数 同时, 请放到统一的 元信息-抬头 srv, 这样显示出来会是 srv : { "srvParam": srvParam, "srvResponse" : srvResponse}