FastAPI的日志配置

306 阅读5分钟

使用Logging

编写log.conf配置文件

###############################################
[loggers]
keys=root,fastapi_project

[logger_root]
level=INFO
handlers=stream

[logger_fastapi_project]
level=INFO
handlers=fastapi_project
qualname=fastapi_project
propagate=0

###############################################
[handlers]
keys=stream,fastapi_project

[handler_stream]
class=StreamHandler
level=INFO
formatter=myApp
args=(sys.stderr,)

[handler_fastapi_project]
class=app.utils.custom_logging.EnhancedTimedRotatingFileHandler
level=INFO
formatter=myApp
args=('logs/%(app_name)s.log', 'H', 1, 168)

###############################################
[formatters]
keys=myApp

[formatter_myApp]
format=[%(asctime)s][%(name)s][%(filename)s:%(lineno)d][%(levelname)s] %(message)s
datefmt=%Y-%m-%d %H:%M:%S

编写对应的Handler

# -*- coding: utf-8 -*-
import os
import time
import re
from stat import ST_MTIME

from logging.handlers import BaseRotatingHandler

_MIDNIGHT = 24 * 60 * 60  # number of seconds in a day


class EnhancedTimedRotatingFileHandler(BaseRotatingHandler):

    suffix_extMatch = {
        '%Y': r'\d{4}',
        '%m': r'\d{2}',
        '%d': r'\d{2}',
        '%H': r'\d{2}',
        '%M': r'\d{2}',
        '%S': r'\d{2}',
        '%X': r'\d{2}:\d{2}:\d{2}',
    }
    when_suffix = {
        'S': '%Y-%m-%d_%H-%M-%S',
        'M': '%Y-%m-%d_%H-%M',
        'H': '%Y-%m-%d_%H',
        'D': '%Y-%m-%d',
        'MIDNIGHT': '%Y-%m-%d',
        'W0': '%Y-%m-%d',
        'W1': '%Y-%m-%d',
        'W2': '%Y-%m-%d',
        'W3': '%Y-%m-%d',
        'W4': '%Y-%m-%d',
        'W5': '%Y-%m-%d',
        'W6': '%Y-%m-%d',
    }
    when_interval = {
        'S': 1,  # one second
        'M': 60,  # one minute
        'H': 60 * 60,  # one hour
        'D': 60 * 60 * 24,  # one day
        'MIDNIGHT': 60 * 60 * 24,  # one day
        'W0': 60 * 60 * 24 * 7,  # one week
        'W1': 60 * 60 * 24 * 7,  # one week
        'W2': 60 * 60 * 24 * 7,  # one week
        'W3': 60 * 60 * 24 * 7,  # one week
        'W4': 60 * 60 * 24 * 7,  # one week
        'W5': 60 * 60 * 24 * 7,  # one week
        'W6': 60 * 60 * 24 * 7,  # one week
    }

    def __init__(self, filename, when='h', interval=1, backupCount=0,
                 encoding=None, delay=False, utc=False, atTime=None,
                 suffix=None):
        self.when = when.upper()
        self.backupCount = backupCount
        self.utc = utc
        self.atTime = atTime
        self.baseFilename = os.path.abspath(filename)
        if self.when not in self.when_suffix:
            if self.when.startswith('W'):
                if len(self.when) != 2:
                    raise ValueError(
                        "You must specify a day for weekly rollover"
                        " from 0 to 6 (0 is Monday): %s" % self.when)
                if self.when[1] < '0' or self.when[1] > '6':
                    raise ValueError(
                        "Invalid day specified for weekly "
                        "rollover: %s" % self.when)
            raise ValueError(
                "Invalid rollover interval specified: %s" % self.when)
        if suffix and self.is_suffix_reasonable(self.when, suffix):
            self.suffix = suffix
        else:
            self.suffix = self.when_suffix[self.when]
        self.interval = self.when_interval[self.when]
        self.interval = self.interval * interval  # multiply by units requested
        if self.when.startswith('W'):
            self.dayOfWeek = int(self.when[1])
        if not os.path.exists(os.path.dirname(self.baseFilename)):
            os.makedirs(os.path.dirname(self.baseFilename))
        self.extMatch = self._extMatch_from_suffix(self.suffix)
        self.extMatch = re.compile(self.extMatch)
        if os.path.exists(self.baseFilename):
            self.rename_old_file_to_new_format(self.baseFilename)
        current_filename = self._current_filename()
        if os.path.exists(current_filename):
            t = os.stat(current_filename)[ST_MTIME]
        else:
            t = int(time.time())
        self.rolloverAt = self.computeRollover(t)
        BaseRotatingHandler.__init__(self, filename, 'a', encoding, delay)

    def rename_old_file_to_new_format(self, filename):
        t = os.stat(filename)[ST_MTIME]
        rolloverAt = self.computeRollover(t)
        currentTime = int(time.time())
        dstNow = time.localtime(currentTime)[-1]
        t = rolloverAt - self.interval
        if self.utc:
            timeTuple = time.gmtime(t)
        else:
            timeTuple = time.localtime(t)
            dstThen = timeTuple[-1]
            if dstNow != dstThen:
                if dstNow:
                    addend = 3600
                else:
                    addend = -3600
                timeTuple = time.localtime(t + addend)
        dfn = filename + "." + time.strftime(self.suffix, timeTuple)
        if os.path.exists(dfn):
            os.remove(dfn)
        os.rename(filename, dfn)

    def _extMatch_from_suffix(self, suffix):
        extMatch = suffix
        for _s in self.suffix_extMatch:
            _m = self.suffix_extMatch[_s]
            extMatch = extMatch.replace(_s, _m)
        extMatch = r'^' + extMatch + r'(\.\w+)?$'
        return extMatch

    def is_suffix_reasonable(self, when, suffix):
        if when == 'S':
            return '%S' in suffix or '%X' in suffix
        if when == 'M':
            return '%M' in suffix or '%X' in suffix
        if when == 'H':
            return '%H' in suffix or '%X' in suffix
        if when == 'D' or when == 'MIDNIGHT' or when.startswith('W'):
            return '%d' in suffix
        return False

    def _current_filename(self):
        currentTime = int(time.time())
        dstNow = time.localtime(currentTime)[-1]
        if self.utc:
            timeTuple = time.gmtime(currentTime)
        else:
            timeTuple = time.localtime(currentTime)
            dstThen = timeTuple[-1]
            if dstNow != dstThen:
                if dstNow:  # 开始实行夏令时的时候
                    addend = 3600
                else:  # 退出夏令时的时候
                    addend = -3600
                timeTuple = time.localtime(currentTime + addend)
        filename = self.baseFilename + "." + \
            time.strftime(self.suffix, timeTuple)
        return filename

    def _open(self):
        """
        Open the current base file with the (original) mode and encoding.
        Return the resulting stream.
        """
        # get the time that this sequence started at and make it a TimeTuple
        current_filename = self._current_filename()
        return open(current_filename, self.mode)

    def shouldRollover(self, record):
        """
        Determine if rollover should occur.

        record is not used, as we are just comparing times, but it is needed so
        the method signatures are the same
        """
        t = int(time.time())
        if t >= self.rolloverAt:
            return 1
        return 0

    def getFilesToDelete(self):
        """
        Determine the files to delete when rolling over.

        More specific than the earlier method, which just used glob.glob().
        """
        dirName, baseName = os.path.split(self.baseFilename)
        fileNames = os.listdir(dirName)
        result = []
        prefix = baseName + "."
        plen = len(prefix)
        for fileName in fileNames:
            if fileName[:plen] == prefix:
                suffix = fileName[plen:]
                if self.extMatch.match(suffix):
                    result.append(os.path.join(dirName, fileName))
        result.sort()
        if len(result) < self.backupCount:
            result = []
        else:
            result = result[:len(result) - self.backupCount]
        return result

    def computeRollover(self, currentTime):
        """
        Work out the rollover time based on the specified time.
        """
        result = currentTime + self.interval
        # If we are rolling over at midnight or weekly,
        # then the interval is already known.
        # What we need to figure out is WHEN the next interval is.
        # In other words, if you are rolling over at midnight,
        # then your base interval is 1 day,
        # but you want to start that one day clock at midnight, not now.
        # So, we have to fudge the rolloverAt value in order to trigger
        # the first rollover at the right time.  After that,
        # the regular interval will take care of the rest.
        # Note that this code doesn't care about leap seconds. :)
        if self.when == 'S':
            return result
        else:
            # This could be done with less code, but I wanted it to be clear
            if self.utc:
                t = time.gmtime(currentTime)
            else:
                t = time.localtime(currentTime)
            currentHour = t[3]
            currentMinute = t[4]
            currentSecond = t[5]
            currentDay = t[6]
            # r is the number of seconds left between now and the next rotation
            if self.when == 'M':
                r = 60 - currentSecond
            elif self.when == 'H':
                r = 60 * 60 - (currentMinute * 60 + currentSecond)
            elif self.when == 'MIDNIGHT' or self.when == 'D' or self.when.startswith('W'):
                if self.atTime is None:
                    rotate_ts = _MIDNIGHT
                else:
                    rotate_ts = ((self.atTime.hour * 60 + self.atTime.minute)
                                 * 60 + self.atTime.second)
                r = rotate_ts - ((currentHour * 60 + currentMinute) * 60 +
                                 currentSecond)
                if r < 0:
                    # Rotate time is before the current time (for example when
                    # self.rotateAt is 13:45 and it now 14:15), rotation is
                    # tomorrow.
                    r += _MIDNIGHT
                currentDay = (currentDay + 1) % 7
            result = currentTime + r
            if self.when.startswith('W'):
                day = currentDay  # 0 is Monday
                if day != self.dayOfWeek:
                    if day < self.dayOfWeek:
                        daysToWait = self.dayOfWeek - day
                    else:
                        daysToWait = 6 - day + self.dayOfWeek + 1
                    newRolloverAt = result + (daysToWait * (60 * 60 * 24))
                    if not self.utc:
                        dstNow = t[-1]
                        dstAtRollover = time.localtime(newRolloverAt)[-1]
                        if dstNow != dstAtRollover:
                            if not dstNow:  # DST kicks in before next rollover
                                addend = -3600
                            else:  # DST bows out before next rollover
                                addend = 3600
                            newRolloverAt += addend
                    result = newRolloverAt
        return result

    def doRollover(self):
        """
        do a rollover; in this case, a date/time stamp is appended to the filename
        when the rollover happens.  However, you want the file to be named for the
        start of the interval, not the current time.  If there is a backup count,
        then we have to get a list of matching filenames, sort them and remove
        the one with the oldest suffix.
        """
        if self.stream:
            self.stream.close()
            self.stream = None
        currentTime = int(time.time())
        dstNow = time.localtime(currentTime)[-1]
        if self.backupCount > 0:
            for s in self.getFilesToDelete():
                os.remove(s)
        if not self.delay:
            self.stream = self._open()
        newRolloverAt = self.computeRollover(currentTime)
        while newRolloverAt <= currentTime:
            newRolloverAt = newRolloverAt + self.interval
        # If DST changes and midnight or weekly rollover, adjust for this.
        if (self.when == 'MIDNIGHT' or self.when.startswith('W')) \
                and not self.utc:
            dstAtRollover = time.localtime(newRolloverAt)[-1]
            if dstNow != dstAtRollover:
                if not dstNow:  # DST kicks in before next rollover
                    addend = -3600
                else:  # DST bows out before next rollover
                    addend = 3600
                newRolloverAt += addend
        self.rolloverAt = newRolloverAt

使用Loguru

按日期滚动

from loguru import logger
logger.add("file.log", rotation="12:00"# 每天12:00会创建一个新的文件
logger.debug("a log!")

按大小滚动

from loguru import logger
logger.add("file.log", rotation="1 MB"# 滚动大日志文件
logger.debug("a log!")

记录Trace

logger.add("out.log", backtrace=True, diagnose=True)
logger.exception("ooooh")

按级别分文件

folder_ = "./log/"
prefix_ = "polaris-"
rotation_ = "10 MB"
retention_ = "30 days"
encoding_ = "utf-8"
backtrace_ = True
diagnose_ = True

# 格式里面添加了process和thread记录,方便查看多进程和线程程序
format_ = '<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> ' \
            '| <magenta>{process}</magenta>:<yellow>{thread}</yellow> ' \
            '| <cyan>{name}</cyan>:<cyan>{function}</cyan>:<yellow>{line}</yellow> - <level>{message}</level>'

# 这里面采用了层次式的日志记录方式,就是低级日志文件会记录比他高的所有级别日志,这样可以做到低等级日志最丰富,高级别日志更少更关键
# debug
logger.add(folder_ + prefix_ + "debug.log", level="DEBUG", backtrace=backtrace_, diagnose=diagnose_,
            format=format_, colorize=False,
            rotation=rotation_, retention=retention_, encoding=encoding_,
            filter=lambda record: record["level"].no >= logger.level("DEBUG").no)

# info
logger.add(folder_ + prefix_ + "info.log", level="INFO", backtrace=backtrace_, diagnose=diagnose_,
            format=format_, colorize=False,
            rotation=rotation_, retention=retention_, encoding=encoding_,
            filter=lambda record: record["level"].no >= logger.level("INFO").no)

# warning
logger.add(folder_ + prefix_ + "warning.log", level="WARNING", backtrace=backtrace_, diagnose=diagnose_,
            format=format_, colorize=False,
            rotation=rotation_, retention=retention_, encoding=encoding_,
            filter=lambda record: record["level"].no >= logger.level("WARNING").no)

# error
logger.add(folder_ + prefix_ + "error.log", level="ERROR", backtrace=backtrace_, diagnose=diagnose_,
            format=format_, colorize=False,
            rotation=rotation_, retention=retention_, encoding=encoding_,
            filter=lambda record: record["level"].no >= logger.level("ERROR").no)

# critical
logger.add(folder_ + prefix_ + "critical.log", level="CRITICAL", backtrace=backtrace_, diagnose=diagnose_,
            format=format_, colorize=False,
            rotation=rotation_, retention=retention_, encoding=encoding_,
            filter=lambda record: record["level"].no >= logger.level("CRITICAL").no)

logger.add(sys.stderr, level="CRITICAL", backtrace=backtrace_, diagnose=diagnose_,
            format=format_, colorize=True,
            filter=lambda record: record["level"].no >= logger.level("CRITICAL").no)