Недавно возникла задача сделать ротацию логов в веб-сервисе на Tornado. До начала работы казалось, что вопрос должен быть давно изучен и решение будет очевидным.

Но, как это часто случается, если требования несколько отличаются от стандартных, то приходится искать решение самостоятельно. Или читать документацию.

TL;DR

Если необходимо реализовать ротацию логов таким образом, чтобы файлы создавались ежедневно и имя файла соотвествовало формату %Y%m%d.%i.log, где %Y - год, %m - месяц, %d - день, %i - индекс текущего файла лога, то необходимо использовать TimedRotatingFileHandler и настраивать формат имени файла переопределяя свойства namer и suffix этого класса:

def get_filename(filename):
    # Получаем директорию, где расположены логи
    log_directory = os.path.split(filename)[0]

    # Расширением (с точкой) файла является значение suffix (у нас - %Y%m%d) (например .20181231).
    # Но точка нам не нужна, т.к. файл будет называться suffix.log (20181231.log)
    date = os.path.splitext(filename)[1][1:]

    # Сформировали имя нового лог-файла
    filename = os.path.join(log_directory, date)

    if not os.path.exists('{}.log'.format(filename)):
        return '{}.log'.format(filename)

    # Найдём минимальный индекс файла на текущий момент.
    index = 0
    f = '{}.{}.log'.format(filename, index)
    while os.path.exists(f):
        index += 1
        f = '{}.{}.log'.format(filename, index)
    return f

rotation_logging_handler = TimedRotatingFileHandler('./logs/log.log', when='m', interval=1, backupCount=5)
rotation_logging_handler.suffix = '%Y%m%d'
rotation_logging_handler.namer = get_filename

logger.addHandler(rotation_logging_handler)

Чуть более подробно

Постановка задачи

Задача была следующая: необходимо каждый день создавать новый файл журнала. При этом файл должен иметь название годмесяцдень.log и, если приложение перезапускается, то текущий файл не должен удаляться, а должен создаваться новый файл. Например, с именем годмесяцдень.0.log, где 0 обозначал номер лог-файла за указанную дату: если сервис запущен 26 сентября 2018 года и два раза за этот день рестартовал, то в папке с логами должно быть три файла: 20180926.log, 20180926.0.log, 20180926.1.log.

Задача ротации логов в Python решается довольно просто: есть модуль logging и различные Handlerы, которые позволют реализовать ротацию. Например, RotatingFileHandler и TimedRotatingFileHandler. Первый создаёт новый файл при достижении некоторого заданного размера, а второй — по прошествии определённого времени. Первый под заданные требования не подходит и остаётся только TimedRotatingFileHandler.

TimedRotatingFileHandler

Первый подход

Задача казалась максимально простой и всё должно работать “из коробки”:

import logging as logging
from logging.handlers import TimedRotatingFileHandler
from time import sleep

# Комбинация параметров when и interval задаёт период для создания нового файла логов.
# В примере новый файл логов будет создаваться каждую (interval=1) минуту (when='m').
rotation_logging_handler = TimedRotatingFileHandler('./logs/log.log', when='m', interval=1, backupCount=5)

logger = logging.getLogger()
logger.addHandler(rotation_logging_handler)

for i in range(121):
    sleep(1)
    print('current iteration: {}'.format(i))
    logger.error('current iteration: {}'.format(i))

Запускаем, наблюдаем. Видим, что в папке logs создалось три файла: log.log и два файла log.log.дата-время. В простейшем случае этого достаточно, но перфекционизм не даёт мне права оставить это в таком виде. Хочу, чтобы имена файлов были в формате %Y%m%d.%i.log.

Вторая попытка

Окей, попробовали просто - получили не тот результат, которых хотелось. Будем усложнять пример. Судя по документации, у logger есть свойство suffix. Модифицируем приложение, добавив suffix с требуемым форматом %Y%m%d:

import logging as logging
from logging.handlers import TimedRotatingFileHandler
from time import sleep

# Комбинация параметров when и interval задаёт период для создания нового файла логов.
# В примере новый файл логов будет создаваться каждую (interval=1) минуту (when='m').
rotation_logging_handler = TimedRotatingFileHandler('./logs/log.log', when='m', interval=1, backupCount=5)
rotation_logging_handler.suffix = '%Y%m%d'

logger = logging.getLogger()
logger.addHandler(rotation_logging_handler)

for i in range(121):
    sleep(1)
    print('current iteration: {}'.format(i))
    logger.error('current iteration: {}'.format(i))

Следим за ~руками~ логами: в папке logs появился новый файл с именем log.log.дата. Должно было быть два: по истечению первой и второй минут. Похоже, это не работает.

Здесь я ~психанул и~ решил забраться в исходники logger-а и посмотреть как формируется имя файла для лога. Нашёл интересное свойство namer. namer - это функция, которая может определить как именно должен называться файл лога. На самом деле в документации можно было найти эту же информацию.

Третья попытка

Доработаем пример с учётом новых знаний:

import logging as logging
from logging.handlers import TimedRotatingFileHandler
from time import sleep

def get_filename(filename):
    # Получаем директорию, где расположены логи
    log_directory = os.path.split(filename)[0]

    # suffix - это расширение (с точкой) файла. 
    # У нас - %Y%m%d. Например .20181231.
    # Точка нам не нужна, т.к. файл будет называться suffix.log (20181231.log)
    date = os.path.splitext(filename)[1][1:]

    # Сформировали имя нового лог-файла
    filename = os.path.join(log_directory, date)

    if not os.path.exists('{}.log'.format(filename)):
        return '{}.log'.format(filename)

    # Найдём минимальный индекс файла на текущий момент.
    index = 0
    f = '{}.{}.log'.format(filename, index)
    while os.path.exists(f):
        index += 1
        f = '{}.{}.log'.format(filename, index)
    return f


rotation_logging_handler = TimedRotatingFileHandler(path, when=when_mode[when_mode_config], interval=1, backupCount=5)
rotation_logging_handler.suffix = '%Y%m%d'
rotation_logging_handler.namer = get_filename

logger = logging.getLogger()
logger.addHandler(rotation_logging_handler)

for i in range(121):
    sleep(1)
    print('current iteration: {}'.format(i))
    logger.error('current iteration: {}'.format(i))

Если выполнить последний пример, то можно будет наблюдать именно то поведение, что требовалось в самом начале: сценарий выполняется 2 минуты и каждую минуту создаётся новый файл с логами с именем годмесяцдень.индекс.log.