Skip to content

Latest commit

 

History

History
583 lines (429 loc) · 48.5 KB

File metadata and controls

583 lines (429 loc) · 48.5 KB

Логирование

Логирование — это фиксация информации о событиях, происходящих в программной системе, и контексте, в котором эти события происходят, в некий журнал событий (лог).

Разработчики в первую очередь используют логирование для отладки в процессе разработки и для поиска и устранения проблем и ошибок в работе ПО.

Но логирование может быть полезно и для других целей:

  1. Профилирование. По меткам времени в логах можно измерить время между разными событиями, т. е. время выполнения какой-то части программы.
  2. Безопасность. Залогированные события авторизации пользователей, выполнения определённых операций, доступа к какой-то информации можно использовать для отслеживания, предотвращения и расследования инцидентов безопасности.
  3. Аудирование. Из логов можно получать информацию о важных для бизнеса вещах, например, кто последний редактировал текст на сайте или сколько финансовых транзакций произошло за день.
  4. Статистика. По логам можно подсчитывать статистику как технических (как часто вызывается какая-то функция или сколько происходит обращений к базе данных), так и бизнес-показателей (какие кнопки пользователи нажимают чаще всего или какая доля посетителей сайта доходит до корзины).

Лог в стандартном случае — это текстовый файл. В него легко писать, его легко читать, искать по нему, бэкапировать и ротировать. Но также события могут выводиться на консоль или любое другое устройство вывода, записываться в базу данных, посылаться в сокет, ставиться в очередь, отправляться по электронной почте или в мессенджер и так далее. Если вы решите по определённым событиям автоматически получать телефонный звонок — это даже никого не удивит.

Минимальная запись события в логе обычно включает описание события и время, в которое оно произошло:

2023-08-12 17:49:37 User 'johndoe' successfully logged in

В зависимости от конкретной задачи и принятых практик добавляются другие парметры.

Логи должны отвечать на два основных вопроса:

  1. Что произошло?
  2. Где это произошло?

«Где» — широкое понятие, которое может означать не только в какой строчке кода, но и в каком процессе, на каком сервере, при вызове из какой функции или что угодно ещё, важное в контексте конкретной операции.

Чтобы логи успешно отвечали на эти вопросы, стоит придерживаться определённых правил.

Содержимое записи в логе

Осмысленные сообщения

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

Step initiate OK
Transaction failed

Такие сообщения могут казаться понятными в момент их написания, но через некоторое время даже автор не вспомнит, что они значат.

User 'johndoe' successfully logged in
Transaction reverted due to lost connection to DB

Эти сообщения сразу дают какое-то понимание ситуации, в грамматике человеческого языка и понятной терминологии. Для понимания их общей сути не нужно знать, в какой ситуации и в каком месте кода они были залогированы.

Также понимание сообщения не должно основываться на тексте предыдущего сообщения. Например, эти сообщения:

INFO Checking password hash for 'johndoe'
DEBUG Hashes don't match
ERROR Operation failed

выглядят осмысленно, когда идут друг за другом. Но на практике это не обязательно будет так: в многопоточном приложении между ними могут оказаться сообщения из других потоков, или разные уровни логирования окажутся разнесены по разным файлам или вообще отключены. В итоге понять, какая же операция не смогла выполниться, будет невозможно.

! Пишите сообщения на английском. Если не знаете английский — выучите. As simple as that. Это позволит вам избежать любых проблем с кодировками (так как вы будете использовать только ASCII-символы) или лишних хлопот, если ваш проект вдруг станет международным.

Из этого правила есть одно разумное исключение — если ваши логи предназначены не для технических специалистов, а для конечного пользователя. Тогда они должны локализовываться так же, как и вся программа.

При этом нужно понимать, что сообщение:

Call func DB OK

не является примером английского языка, в отличие от:

Connection to database successfully established

Метка времени

Важно знать не только что событие произошло, но и когда это случилось. Исключением могут быть разве что временные сообщения для сиюминутной отладки запускаемого вручную скрипта.

Рекомендуется использовать стандартный формат, соответствующий ISO8601:

2022-10-02 11:41:42,612

Он привычен, легко читается и парсится, корректно сортируется (так как все величины расположены в порядке убывания размерности).

Точность до миллисекунд — хорошая отправная точка, но нужно ориентироваться на конкретный случай. Секунд, как правило, недостаточно даже для простых приложений, а для сервиса с нагрузкой 10 000 запросов в секунду понадобятся микросекунды, иначе вы не сможете достоверно определить последовательность событий.

Контекст

Как правило, запись в логе должна содержать и другую информацию о контексте, в котором произошло логируемое событие. Какую именно — зависит от приложения, пишущего в лог, и от конкретного события. Например, в лог обращений к веб-сервису полезно добавить ip-адрес, с которого пришёл запрос, в лог ошибок веб-краулера хост, к которому происходило обращение, и HTTP-код ответа, в лог финансовых транзакций — id транзакции, счета получателя и отправителя.

2023-09-14 14:22:45,329 172.0.0.1 GET /sitemap.xml 200
2023-03-10 16:59:23,556 Request to https://boogle.com failed with 403
2023-02-19 00:19:18,831 Transaction 309841 successful: $ 100500 transferred from 940384 to 473923

Полезно бывает, особенно для ошибок, добавить в лог информацию о файле, классе, функции или даже строчке кода, на которой произошёл сбой. В некоторых случаях для понимания проблемы важно знать аргументы функции, с которыми она не смогла корректно выполниться.

2023-05-15 05:59:46,102 Division by zero at line 18 in function divide_by() with argument 0 in myscript.py

Важный принцип в логировании — не слишком много и не слишком мало. Может возникнуть соблазн добавить в лог все параметры, какие только можно. Но тогда лог будет очень тяжело читать и обрабатывать, он будет занимать много места на диске. Если же добавить слишком мало — информации может оказаться недостаточно для разбора какой-то проблемы. Для оптимального баланса лучше не перегружать лог неочевидными параметрами заранее, а добавлять их со временем, если возникнет конкретная необходимость.

! Не стоит отправлять в логи данные, содержимое которых зависит от ситуации или может меняться со временем, например, дампы объектов или результаты запроса в БД. Там, где вы ожидаете значение одного поля, со временем их может оказаться некотролируемо много, и ваш лог крайне разрастётся или даже логирование совсем перестанет работать из-за превышения размера строки или появления каких-то недопустимых символов.

Читаемость

Логи всегда предназначены для чтения. Как правило — чтения человеком, иногда — чтения машиной.

Лог, предназначенный для ручного разбора и человеческого взгляда, должен иметь не слишком большую длину строки каждой записи, содержать осмысленное сообщение, не быть слишком перегруженными дополнительной информацией:

2023-02-19 00:19:18,831 Transaction 309841 successful: $ 100500 transferred from 940384 to 473923

Если, кроме чтения человеком, лог часто используется для автоматического поиска, парсинга, обработки агрегаторами логов и т. п., может быть полезно с точки зрения простоты и производительности обработки привести его к более строгому формату, сохраняя при этом человекочитаемость:

2023-02-19 00:19:18,831 Transaction successful. transaction_id: 309841, amount: 100500, from: 940384, to: 473923

Если же лог обрабатывается только автоматическими инструментами и точно не предназначен для человеческих глаз, возможно, стоит записывать его в полностью сериализованном виде, например, в JSON, или даже в каком-то бинарном формате:

{"timestamp":1676755158831,"code":"OK","transaction":{"id":309841,"amount":100500,"from":940384,"to":473923}}

Очень частая операция — поиск по логу, будь-то Ctrl + F в текстовом редакторе, grep или функция агрегатора логов. Для упрощения поиска стоит поддерживать одинаковую терминологию. Например, если вы где-то пишете user_id, а где-то client identificator, где-то login, а где-то authorization, то найти все записи, касающиеся авторизации пользователя будет непросто. Одинаковые параметры в разных записях стоит распологать примерно в одинаковой последовательности и виде. Например, если дата/время иногда находится в начале строки, а иногда — в середине, будет сложно отсортировать записи хронологически.

Безопасность

Решая, какие данные писать в лог, нужно учитывать не только удобство, но и безопасность. Например, если при успешной авторизации пароль в открытом виде записывается в лог, злоумышленнику будет уже всё равно, насколько хитрым алгоритмом он хешируется при записи в БД.

В лог не должны попадать:

  • пароли, ключи безопасности, авторизационные токены;
  • номера кредитных карт или другая платёжная информация;
  • ФИО, паспортные данные, адреса и другая информация, защищаемая законом о персональных данных или GDPR.

Когда, как и что логировать?

Уровни логирования

Ещё один важный инструмент соблюдения правила «не слишком много, не слишком мало»уровни логирования, или уровни критичности (severity levels). Разные события могут иметь разную важность и приоритет с точки зрения работы приложения — какие-то просто дают информацию о поведении программы, а какие-то сообщают о критическом сбое. Для различения таких ситуаций и используют уровни логирования, впервые введённые ещё в 1980 году в стандарте syslog:

Numerical Code Severity
0 Emergency: system is unusable
1 Alert: action must be taken immediately
2 Critical: critical conditions
3 Error: error conditions
4 Warning: warning conditions
5 Notice: normal but significant condition
6 Informational: informational messages
7 Debug: debug-level messages

Разные языки программирования и инструменты работы с логами заимствуют эту классификацию, немного видоизменяя её в каждом случае. Например, в Python такие стандартные уровни логирования:

Уровень Числовое значение Использование
DEBUG 10 Отладочная информация, полезная в процессе разработки или при диагностике.
INFO 20 Информационные сообщения, подтверждающие, что всё работает так, как ожидается.
WARNING 30 Сигнал, что произошло что-то неожиданное, или какая-то проблема может возникнуть в будущем, но приложение всё ещё работает корректно.
ERROR 40 Из-за возникшей проблемы приложение не смогло выполнить какую-то свою функцию, но продолжает работать.
CRITICAL 50 Критическая проблема, из-за которой приложение не сможет продолжить работать или будет работать некорректно.

Можно задать и собственные уровни, имеющие числовые значения в промежутках между стандартными, но, как правило, этого не требуется.

Уровни логирования позволяют управлять тем, что и в каких условиях попадает в логи. Например, сообщения уровня DEBUG, как правило, используются при разработке, а в production-среде по умолчанию отключены, но могут быть включены специально при диагностике какой-то проблемы или релизе сложного нового кода с высокой вероятностью ошибок. Оптимально, если есть возможность их включить не для всего приложения, а только для той подсистемы, в которой происходит диагностика.

Кроме того, разные уровни сообщений могу логироваться по-разному. Например, сообщения INFO могут направляться в отдельный файл, который в можно использовать как готовый отчёт по выполнению какого-то бизнес-процесса. А сообщения ERROR или CRITICAL могут, кроме записи в файл, дублироваться на электронную почту или в мессенджер для более быстрой реакции разработчика или администратора на возникшую проблему.

Точки логирования

События WARNING, ERROR, CRITICAL логируются ровно в том месте, где они происходят, здесь всё просто. Размещение событий DEBUG и INFO определяет разработчик.

Скорее всего нет никакого смысла сопровождать записью в лог каждую операцию в коде. Основные точки интереса — это:

  1. точки принятия решений, в которых поведение программы может измениться (обычно это условные операторы);
  2. результаты выполнения какого-то логически сгруппированного блока инструкций.

Если залогировать нужные параметры в этих точках, как правило, этого будет достаточно, чтобы понять по логу, как вела себя программа.

Для тяжёлых операций (например, сетевых запросов) может быть полезно логировать перед началом и после завершения операции. Это поможет отследить обрывы и зависания программы (если в логе есть только первая запись), или обнаружить проблемы со скоростью выполнения запросов (по разнице между временем первой и второй записи).

Для цикла с большим количеством итераций далеко не всегда нужно логировать внутри каждой итерации, может быть достаточно общего вывода с количеством обработанных записей и результатом после завершения цикла. Если всё же требуется логировать каждую итерацию — как минимум нужно понимать, как будет выглядеть лог и какого размера будет лог-файл.

Инструменты логирования

Готовые инструменты

Все современные языки программирования имеют готовую библиотеку для логирования, или даже несколько. Их и нужно использовать. Не стоит без причин изобретать собственный велосипед или использовать print(). Эти библиотеки имеют разумный интерфейс, адекватные возможности конфигурирования, корректно работают с вводом-выводом, учитывают тонкости реализации и внутренней работы языка. А самое главное — они известны и понятны всем разработчикам.

Кроме собственно записи в лог существует множество других инструментов для ротации и управления логами, аггрегации и поиска по логам, мониторинга ошибок и нотификаций и т. д. Несколько популярных примеров: rsyslogd, logrotate, Sentry, ELK.

Инфраструктура логирования

Существуют разные варианты организации инфраструктуры логирования. Можно писать все логи в один файл, а можно в отдельные файлы для разных типов событий, приложений, подсистем. Можно логировать в локальные файлы на каждом сервере, а можно на общий централизованный лог-сервер. Выбор конкретного подхода зависит от архитектуры системы, серверной платформы, нагрузки, количества логов и записей в них, количества доступного оборудования и компетентности сисадминов. Если в проекте есть какая-то принятая практика — стоит придерживаться её, если нет — скорее всего, лучше начать с самого простого, усложняя по необходимости.

Ротация и бэкапирование

У постоянно работающего приложения со временем растёт объём лог-файла и количество записей в нём. Само собой, это не может продолжаться бесконечно, — рано или поздно файл займёт весь диск, да и работать с ним станет очень трудно.

Предположим, есть файл error.log. Нужно, чтобы файл не разрастался бесконечно, но всегда были доступны логи за последние 24 часа. Первое, что приходит на ум — очищать файл раз в сутки. Но тогда логи за 24 часа у нас будут только перед самой очисткой, а сразу после неё — за 0 часов. Поэтому делают иначе — раз в сутки создают новый файл, в который продолжают писаться логи, а старый переименовывают, допустим, в errors.yesterday.log. Ещё через сутки старый errors.yesterday.log и вся операция повторяется. При таком подходе мы всегда имеем в доступе логи минимум за 24, максимум за 48 часов.

Такая операция — простейший пример ротации логов. Для автоматизации этого процесса существуют специальные утилиты, самая популярная из которых — logrotate. В его конфиге можно настроить регулярную ротацию или по достижению определённого размера файла, хранение ротированных копий, сжание и пр.

/var/log/myapp/error.log {
        rotate 7 # хранить до 7 ротированных файлов
        daily # ротировать ежедневно
        compress # сжимать ротированные файлы
        notifempty # не ротировать, если файл пуст
        create 660 mebossuser mebossuser # устанавливать права на файлы для указанного пользователя
}

Кроме того, логи, как текущие, так и отротированные, могут содержать важную информацию, в том числе необходимую для восстановления после критического сбоя или расследования его причин. Поэтому логи, как и остальные важные файлы, должны вовремя и надёжно бэкапироваться, например, с помощью rsync.

Логирование в Python

Библиотека logging

Простейший способ залогировать сообщение в Python — вывести в консоль с помощью print или записать в файл, открытый с помощью open. Делать так не стоит.

Для логирования в Python используется стандартная библиотека logging. В частности, она содержит функции debug(), info(), warning(), error(), critical(), каждая из которых выводит сообщение с соответствующим её названию уровнем логирования.

Библиотека logging имеет множество настроек. По умолчанию она выводит сообщение на стандартный вывод sys.stderr (как правило это консоль), и имеет уровень логирования WARNING. То есть сообщения с уровнем логирования ниже WARNING (DEBUG или INFO) будут проигнорированы и никуда не выведутся.

Например, при выполнении такого кода:

import logging
logging.info('For your information') # будет проигнорировано, т. к. INFO < WARNING
logging.warning('I warn you!')       # будет выведено в консоль

в консоль будет выведено:

WARNING:root:I warn you!

Первое сообщение будет проигнорировано, так как уровень логирования INFO ниже стандартного WARNING.

Логирование в файл

Чтобы вывести сообщение не на консоль, а в файл, нужно задать конфигурацию:

import logging
logging.basicConfig(filename='example.log', encoding='utf-8', level=logging.DEBUG)
logging.debug('Let\'s see if it works')
logging.info('For your information')
logging.warning('I warn you!')
logging.error('Something bad happened pls help!')

Имя файла задано в параметре filename, указание encoding='utf-8' откроет файл в соответствующей кодировке и позволит записывать в него любые символы Unicode, а параметр level переопределяет уровень логирования по умолчанию. Так как установлен уровень DEBUG, самый низкий, то все сообщения будут выведены в файл.

basicConfig() срабатывает только один раз за время выполнения программы, последующие вызовы ни к чему не приведут. Кроме того, функции логирования debug(), info() и пр. автоматически вызывают внутри себя basicConfig() с параметрами по умолчанию. Поэтому, если вы хотите вызвать её вручную, сделать это нужно до вызова любой из функций логирования.

Форматирование

Используя форматирование, можно интерполировать в логируемое сообщение значение переменной:

import logging
counter = '3rd'
logging.warning('I am warning you for the %s time!', counter)
WARNING:root:I am warning you for the 3rd time!

Можно задать формат не только сообщения, но и всей логируемой записи:

import logging
logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s')
logging.warning('Right about time!')
2023-08-21 13:44:38,417 WARNING Right about time!

Здесь выводится не только уровень логирования levelname и текст сообщения message, но и дата и время логирования asctime. Формат даты-времени можно изменить в basicConfig() (но как правило не стоит).

import logging
logging.basicConfig(format='%(asctime)s %(message)s', datefmt='%d.%m.%Y %H:%M:%S')
logging.warning('is when this event was logged.')
21.08.2023 13:54:51 is when this event was logged.

Полный список атрибутов записи, которые можно использовать форматирования: LogRecord attributes.

Структура библиотеки logging

Базовых функций логирования может быть достаточно для простых приложений. Но для более сложных случаев и глубокого конфигурирования нужно понимать устройство библиотеки.

Библиотека logging — модульная и состоит из нескольких категорий компонентов:

  • логгеры (loggers) предоставляют интерфейс, который код приложения и использует для логирования;
  • обработчики (handlers) отвечают за запись/вывод сообщения;
  • фильтры (filters) позволяют более тонко управлять тем, какие логируемые сообщения будут выведены;
  • форматтеры (formatters) определяют окончательный вид логируемого сообщения.

Каждое логируемое сообщение представляет собой экземпляр класса LogRecords, который передаётся между этими компонентами в соответствии с заданой конфигурацией.

Логгеры (loggers)

Класс Logger предоставляет приложению методы для конфигурирования и собственно логирования сообщения. Полученные сообщения он фильтрует в соответствии с уровнем критичности и заданными фильтрами. И отфильтрованные сообщения отправляет в соответствующие обработчики.

Каждый экземпляр класса Logger имеет имя, эти имена организуются в иерархию используя . как разделитель. Например, file будет родителем для file.error, file.access. В корне иерархии находится логгер с именем root (это тот самый root, который по умолчанию выводится в сообщениях).

Один из принятых вариантов — создавать логгеры на уровне модуля и называть их соответствующим образом:

logger = logging.getLogger(__name__)

В таком случае в сообщениях лога вместо root будет выводиться имя модуля, из которого сообщение записано.

Методы конфигурирования:

  • Logger.setLevel() устанавливает самый низкий уровень критичности сообщений, которые логгер будет обрабатывать;
  • Logger.addHandler(), Logger.removeHandler(), Logger.addFilter(), Logger.removeFilter() добавляют и удаляют соответственно обработчики и фильтры.

Функции логгирования:

  • Logger.debug(), Logger.info(), Logger.warning(), Logger.error(), и Logger.critical() логируют сообщение с соответствующим уровнем.
  • Logger.exception() логирует сообщение аналогично Logger.error(), но добавляет стектрейс. Этот метод стоит вызвать только из обработки исключений.
  • Logger.log() принимает уровень логирования в виде явно заданного аргумента. Он используется, как правило, для нестандартных уровней логирования.
  • getLogger() возвращает ссылку на экземпляр логгера с указанным именем, или root, если оно не задано. Многократные вызовы `getLogger()`` с одним и тем же именем будут возвращать ссылку на один и тот же экземпляр.

Если для логгера не указан явно уровень логирования, он наследует его от своих родителей, вплоть до root, для которого уровень всегда задан (по умолчанию WARNING).

Связи с обработчиками тоже по умолчанию наследуются от родителей, поэтому нет необходимости переопределять их для каждого логгера, достаточно установить на верхнем уровне.

Обработчики (handlers)

Обрбаботчики отвечают за отправку или сохранение переданного сообщения по соответствующему назначению. Библиотека включает много стандартных обработчиков для разных форматов, например, FileHandler для записи в файл, SMTPHandler для отправки по электронной почте. При необходимости можно написать свой обработчик, унаследовав от Handler. Напрямую экземпляры Handler не используются.

Методы конфигурирования:

  • setLevel(), как и у логгеров, устанавливает уровень логирования (нужно иметь ввиду, что уровень сообщений, которые логгер передаёт в обработчик, и уровень, который обработчик считает нужным обрабатывать, могут отличаться);
  • setFormatter() устанавливает форматтер;
  • addFilter(), removeFilter() добавляют или удаляют фильтры.

Форматтеры (formatters)

Форматтеры определяют конечный вид записи в логе.

logging.Formatter.__init__(fmt=None, datefmt=None, style='%')

Конструктор форматтера принимает три аргумента:

  • формат сообщения fmt;
  • формат даты datefmt;
  • стиль оператора форматирования style для интерполяции атрибутов LogRecord: '%' для %-форматирования (по умолчанию), '{' для str.format() или '$' для `string.Templates.

Пример использования компонентов

import logging

# создание логгера
logger = logging.getLogger('exampleLogger')
logger.setLevel(logging.INFO)

# создание обработчика для вывода в консоль
console = logging.StreamHandler()
console.setLevel(logging.INFO)

# создание форматтера
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# добавление форматтера к хендлеру
console.setFormatter(formatter)

# добавление хендлера к логгеру
logger.addHandler(console)

# использование (будут выведены все сообщения, кроме DEBUG)
logger.debug('debug message')
logger.info('info message')
logger.warning('warn message')
logger.error('error message')
logger.critical('critical message')

Конфигурирование

Помимо явного создания и конфигурирования логгеров, обработчиков и форматтеров в коде, можно загружить конфигурацию из файла с помощью функции fileConfig() или передавать её в виде словаря в функцию dictConfig().

Использование конфигурационного файла предпочтительно, так как позволяет отделить код от конфигурации, легко переключаться между разными конфигурациями при необходимости, упростить поддержку и модификацию конфигурации даже для пользователей или администраторов, не знакомых с синтаксисом Python.

import logging
import logging.config

# считывание конфига
logging.config.fileConfig('logging.conf')

# создание логгера
logger = logging.getLogger('exampleLogger')

# использование
logger.debug('debug message')
logger.info('info message')
logger.warning('warn message')
logger.error('error message')
logger.critical('critical message')
[loggers]
keys=root,exampleLogger

[handlers]
keys=consoleHandler

[formatters]
keys=simpleFormatter

[logger_root]
level=DEBUG
handlers=consoleHandler

[logger_simpleExample]
level=DEBUG
handlers=consoleHandler
qualname=simpleExample
propagate=0

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)

[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s

Вариант с передачей конфига в виде словаря ещё удобнее. Содержимое словаря тоже можно считать из файла, причём не только стандартного синтаксиса, но и любого, принятого в конкретном проекте (например, JSON или YAML). Но содержимое словаря можно получить и из любого другого источника, сформировать в коде, модицицировать при необходимости.

Пример модуля логирования в многоклассовом приложении

config.json

{
  "version": 1,
  "disable_existing_loggers": false,
  "formatters": {
    "simple": {
      "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
    },
    "extra": {
      "format": "%(asctime)s %(name)s %(filename)s %(lineno)s %(funcName)s %(levelname)s %(message)s"
    }
  },

  "handlers": {
    "console_handler": {
      "class": "logging.StreamHandler",
      "level": "DEBUG",
      "formatter": "simple",
      "stream": "ext://sys.stdout"
    },

    "info_file_handler": {
      "class": "logging.FileHandler",
      "level": "INFO",
      "formatter": "simple",
      "filename": "info.log",
      "encoding": "utf8"
    },

    "error_file_handler": {
      "class": "logging.FileHandler",
      "level": "ERROR",
      "formatter": "extra",
      "filename": "errors.log",
      "encoding": "utf8"
    }
  },

  "loggers": {
    "MyApp": {
      "level": "WARNING",
      "handlers": ["console_handler"],
      "propagate": false
    },
    "MyApp.MyClass1": {
      "level": "DEBUG",
      "handlers": ["console_handler", "info_file_handler", "error_file_handler"],
      "propagate": false
    },
    "MyApp.MyClass2": {
      "level": "ERROR",
      "handlers": ["error_file_handler"],
      "propagate": false
    }
  },

  "root": {
    "level": "WARNING",
    "handlers": ["console_handler"]
  }
}

Код

import json
import logging
import logging.config


class MyAppLogger:
    def __init__(self):
        dict = json.loads(config)
        logging.config.dictConfig(dict)
        self.logger = logging.getLogger('MyApp') # логгер по умолчанию

    def getLogger(self, name):
        """Генерация логгера в иерархии"""
        return logging.getLogger('MyApp.' + name)

class MyClass1:
    def __init__(self):
        self.logger = MyAppLogger().getLogger(self.__class__.__name__)

class MyClass2:
    def __init__(self):
        self.logger = MyAppLogger().getLogger(self.__class__.__name__)

def test(object):
    object.logger.debug('debug message')
    object.logger.info('info message')
    object.logger.warning('warn message')
    object.logger.error('error message')
    object.logger.critical('critical message')

x0 = MyAppLogger()
test(x0)

x1 = MyClass1()
test(x1)

x2 = MyClass2()
test(x2)

Консоль

2023-08-21 17:09:40,562 - MyApp - WARNING - warn message
2023-08-21 17:09:40,563 - MyApp - ERROR - error message
2023-08-21 17:09:40,563 - MyApp - CRITICAL - critical message
2023-08-21 17:09:40,564 - MyApp.MyClass1 - DEBUG - debug message
2023-08-21 17:09:40,565 - MyApp.MyClass1 - INFO - info message
2023-08-21 17:09:40,565 - MyApp.MyClass1 - WARNING - warn message
2023-08-21 17:09:40,566 - MyApp.MyClass1 - ERROR - error message
2023-08-21 17:09:40,566 - MyApp.MyClass1 - CRITICAL - critical message

info.log

2023-08-21 17:16:19,753 - MyApp.MyClass1 - INFO - info message
2023-08-21 17:16:19,756 - MyApp.MyClass1 - WARNING - warn message
2023-08-21 17:16:19,756 - MyApp.MyClass1 - ERROR - error message
2023-08-21 17:16:19,756 - MyApp.MyClass1 - CRITICAL - critical message

errors.log

2023-08-21 17:17:18,172 MyApp.MyClass1 main.py 93 test ERROR error message
2023-08-21 17:17:18,172 MyApp.MyClass1 main.py 94 test CRITICAL critical message
2023-08-21 17:17:18,173 MyApp.MyClass2 main.py 93 test ERROR error message
2023-08-21 17:17:18,173 MyApp.MyClass2 main.py 94 test CRITICAL critical message

Логгер MyApp (в классе MyAppLogger) имеет только обработчик console_handler, выводящий сообщения на консоль. console_handler имет уровень логирования DEBUG, но сам логгер MyAppWARNING, поэтому на консоль будут выведены только три сообщения — WARNING, ERROR и CRITICAL.

Логгер MyApp.MyClass1 (в классе MyClass1) имеет три обработчика: console_handler, info_file_handler, error_file_handler. В консоль выводятся все сообщения, так как и логгер, хендлер имеют уровень DEBUG. В info.log выводятся сообщения INFO и выше в соответствии с уровнем info_file_handler, а в errors.logERROR и CRITICAL.

Логгер MyApp.MyClass2 (в классе MyClass2) логирует только ERROR и CRITICAL в errors.log.

Ссылки

© Timur Nozadze