启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第35天,点击查看活动详情
作为一个Web(CRUD)工程师,最近开始琢磨日志,在开发的过程当中,日志并没有太突出的效果,但是到了联调和上线排查的阶段,嗯,真香~(嘿嘿嘿)
日志简述
日志这个东西,在编程过程当中都有逐渐的接触,但是一些基础的知识还是要知道的,无效的日志只是走个过场,有效的日志对于运行数据统计,错误定位都可以起到极大的辅助作用,所以先聊聊下面的点:
1、日志是分等级的,常见的日志等级:info, warning, error, danger,具体的描述可以看下面的表格,当然还可以有更多,这里先说常见的等级
| 日志等级 | 描述 |
|---|---|
| info | 消息,没有报错,就是输出运行内容,一般用来输出执行的流程,运行的结果 |
| warning | 警告,确实有问题,但是不影响目前代码的运行,比如版本升级信息啥的 |
| error | 错误,已经报错导致程序中断了,也是排错需要针对的日志等级 |
| danger | 危险,程序已经中断,并且会对数据,系统造成影响的错误 |
2、日志输出的元素很重要,不同类型的日志,需要输出不同的元素,但是遵循的原则是一个,就是可读性,通过日志是要进行分析的,所以要绝对的可读,参数有价值,常见的元素:
(1)触发日志的身份,比如ip地址,用户id,用户名。
(2)触发日志的时间,这个是定位错误和数据分析绝对需要的参数。
(3)触发日志的代码位置,比如行号,比如具体的代码,这个真的很香,不论是记录执行的流程还是报错,都可以让你很精确清晰的看到日志提示在那行代码触发的。
(4)具体的错误或者数据,这个不必多解释哈。
(5)不同类型的日志还有自己必须的参数,比如http日志的请求方式,ftp请求的文件地址等等,总之日志输出内容一定是服务日志分析的目的的。
基于上面聊到,我们以一次http请求的日志举例:
2022-12-26 18:00:00 127.0.0.0.0 GET “/search?keyword=日志&from=www.baidu.com” httplog:line(45)
当然根据需求可以有更多的设置。
Flask 日志记录思路
flask是Python热门的一个轻量的WEB框架,轻量自然有很多功能是需要自己实现的,日志也是如此,这里首先说一下我们设置日志的目的,然后再设置日志:
1、错误定位,这个是日志的普遍的操作
2、操作数据的流程
3、用户行为记录
日志模块logging
基于上面的我们先来聊聊Python操作日志的模块logging,logging是Python内置的模块,大家可以直接导入使用,基本代码如下:
import logging #导入模块
logging.basicConfig(level = logging.INFO,format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s') #设置日志的等级和日志的格式,格式变量是logging定义好的
logger = logging.getLogger(__name__) #设置日志对象
logger.info("Start print log") #开始编写不同等级的日志
logger.debug("Do something")
logger.warning("Something maybe fail.")
logger.info("Finish")
这里需要解释的是logging的日志等级
| 等级 | 描述 |
|---|---|
| debug | 打印全部的日志( notset 等同于 debug ) |
| info | 打印 info, warning, error, critical 级别的日志 |
| warning | 打印 warning, error, critical 级别的日志 |
| error | 打印 error, critical 级别的日志 |
| critical | 类似上面说的danger级别,打印 critical 级别 |
logging日志常用变量
| 变量 | 描述 |
|---|---|
| asctime | 当前时间,格式类似:2022-12-26 16:14:01,700(年-月-日 时:分:秒,毫秒) |
| evelname | 输出日志级别 |
| filename | 文件名称 |
| lineno | 报错文件行号 |
| message | 输出信息 |
flask结合logging
那么基于上面的代码,我们来把flask和日志模块结合起来:
import logging
from logging.handlers import TimedRotatingFileHandler
from flask import Flask
app = Flask(__name__)
formatter = logging.Formatter(
"[%(asctime)s][%(filename)s:%(lineno)d][%(levelname)s][%(thread)d] - %(message)s") # 设置日志格式
handler = TimedRotatingFileHandler(
"E:\worker\companyGit\dj\junwang2_multi_level_plat\logs\flask.log", when="D", interval=1, backupCount=15,
encoding="UTF-8", delay=False, utc=True) # 定义日志句柄,设置日志的地址,编码,时区,等级
app.logger.addHandler(handler) # flask加载日志
handler.setFormatter(formatter) # 日志句柄加载日志格式
那么这样设置完成后,如何使用呢,flask提供了一个current_app变量(这个变量后面会专门聊),可以在flask请求的上下文当中指代app实例,所以可以借助current_app来调用到刚才设置的日志对象:
from flask import current_app
current_app.logger.info("get 请求:%s"%str(result))
flask logging多日志文件
上面确实实现了flask记录日志,但是又两个问题需要考虑:
1、项目里添加散装代码(不是函数或者类这样的代码块),维护起来会比较麻烦。
2、当前只实现了一个日志文件,但是基于后期考虑,还是把访问日志,用户行为日志,访问日志进行划分放到不同的文件当中比较好。
所以,考虑在一个flask代码当中可以创建多个日志文件,提供给不同的需求使用,一番努力(百度)思考(cv)之后,发现我们可以定义一个创建日志的功能,然后绑定到current_app变量上,先定义日志对象生成函数:
import logging
from logging.handlers import RotatingFileHandler
def setup_log(logger_name,log_file,level = logging.INFO,):
log = logging.getLogger(logger_name)
formatter = logging.Formatter("[%(asctime)s][%(filename)s:%(lineno)d][%(levelname)s][%(thread)d] - %(message)s")
fileHeader = RotatingFileHandler(log_file,maxBytes = 100*1024*1024,backupCount = 10,encoding = "utf-8")
fileHeader.setFormatter(formatter)
streamHandler = logging.StreamHandler()
streamHandler.setFormatter(formatter)
log.setLevel(level)
log.addHandler(fileHeader)
return log
想把这个日志绑定到current_app变量上,自然,配置文件当中加载是个不错的选择。
from loggers import setup_log #刚才定义生成日志函数的文件
# 基于配置文件的获取项目的根目录绝对路径
BASEDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 基于项目根目录拼接日志目录
LOGDIR = os.path.join(BASEDIR,"logs")
class BaseConfig:
#创建出两个日志
RUNLOG = setup_log("runing_log",log_file = os.path.join(LOGDIR,"runing.log"))
POINTLOG = setup_log("point_log",log_file = os.path.join(LOGDIR,"point.log"))
然后flask app加载日志
from flask import Flask
from settings import BaseConfig
app = Flask(__name__)
app.config.from_object(BaseConfig)
然后自然就可以使用了
from flask import current_app
running_log = current_app.config.RUNLOG
running_log.info("日志信息")
结合起来
基于上面的代码,我们来进行日志的设计,以错误定位为例,错误定位,首先要知道,flask本身的报错机制决定了在上线后如果不经过任何处理,只会返回状态吗,如果你的flask接口返回了一个500,啥也没有说,那么这个时候就知道日志的重要性了,要不然在线断点(画面太美了),所以对于错误定位来说:
(1)错误需要进行捕获,不可以让一个错误导致服务宕机。
(2)错误需要有返回,比如对错误的提示。
所以完成的思路就是 捕获异常+日志: Python werkzeug 拥有定义好的Http错误类,HTTPException,可以基于这个类来定义一个错误类来捕获异常。
from flask import request, json
from werkzeug.exceptions import HTTPException
from flask import current_app
running_log = current_app.config.RUNLOG
class APIException(HTTPException):
errno = -5
errmsg = '服务器逻辑错误!'
data = "null"
def __init__(self, errmsg=None, errno=None, data=None,
headers=None):
"""
设置错误可以设定的参数
errmsg 自定义的错误提示信息
errno 错误的编码,可以自己设计,提供给前端进行判断使用
data 错误的数据
headers 错误的句柄,一般用不到
"""
if data:
self.data = data
if errno:
self.errno = errno
if errmsg:
self.errmsg = errmsg
#这里保持父类的init方法
super(APIException, self).__init__(errmsg, None)
def get_body(self, *args, **kwargs):
"""
设置错误的返回数据格式,这里是以字典格式返回json数据
"""
body = dict(
errmsg=self.errmsg,
errno=self.errno,
data=self.data,
request=request.method + ' ' + self.get_url_no_param()
)
text = json.dumps(body)
running_log.info(text) #进行日志记录,返回和记录两不耽误
return text
@staticmethod
def get_url_no_param():
"""
处理请求的地址
"""
full_path = str(request.full_path)
main_path = full_path.split('?')
return main_path[0]
类定义好了之后,然后就是编写一个简单的触发了,flask可以自定义函数绑定到app上,所以可以这么搞一下:
from flask import Flask
from SelfException import APIException
def excetion_handler(app):
@app.errorhandler(Exception) #app加载错误句柄的方法
def _(exc):
"""
exc 捕获错误的内容
"""
return APIException(errmsg = str(exc))
app = Flask(__name__)
excetion_handler(app)
当然,对于记录用户行为的日志来说,flask也定义了生命周期函数,也可以通过before_request方法在每次请求结束后记录日志。
from flask import Flask
from flask import request
from flask import current_app
def point_log_fun():
message = "%s - %s - %s - %s"%(
request.remote_addr,
request.method,
request.cookies.get("username","匿名用户"),
request.path
)
loger = current_app.config.get("POINTLOG")
loger.info(message)
app = Flask(__name__)
app.before_request(point_log_fun)
ok,关于flask日志操作,先聊到这里,由于测试代码使用了蓝图,所以分了功能,视图,路由文件(之前又聊过),所以博客当中的代码进行了重新拼接,欢迎各位大佬多多指点哈。