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

В этой статье мы рассмотрим лучшие практики логирования в Python. Следуя им, вы сможете обеспечить информативность, практичность и масштабируемость генерируемых логов. Давайте начнём!

readme

Если вы нашли ошибку, пожалуйста, используйте Ctrl+Enter и я исправлю. Спасибо!

1. Избегайте использования только одного логгера

Корневой логгер - это регистратор по умолчанию в модуле logging. Хотя использование корневого логгера может быть заманчивым для упрощения кода, есть несколько причин, по которым его следует избегать:

  • Отсутствие контроля: При использовании корневого логгера вы имеете ограниченный контроль над тем, как обрабатываются сообщения журнала. Это может привести к проблемам, связанным с отправкой сообщений журнала в неожиданные места или неправильной установкой уровней журнала.

  • Сложность управления регистраторами: При использовании корневого регистратора может возникнуть проблема управления несколькими регистраторами в сложном приложении. Это может привести к проблемам, связанным с дублированием сообщений журнала или неправильной настройкой регистраторов.

  • Невозможность разделения данных журнала: Корневой логгер является общим для всех модулей и компонентов приложения. Это может затруднить разделение данных журнала по модулям или компонентам, что может быть важно при анализе данных журнала.

  • Риски безопасности: Корневой регистратор может быть изменен любым модулем приложения, что может создать риски для безопасности, если злоумышленник сможет изменить настройки журнала.

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

Для создания логгера для каждого модуля в Python можно использовать метод logging.getLogger(), который возвращает объект логгера, который можно использовать для регистрации сообщений для данного модуля. Ниже приведен пример создания логгера для модуля с именем my_module:

logger = logging.getLogger("my_module")

Метод getLogger() принимает аргумент name, который используется для идентификации логгера. Обычно в качестве имени логгера используется имя модуля, чтобы было легко определить, какой модуль генерирует сообщения.

Можно также записать его следующим образом:

logger = logging.getLogger(__name__)

После создания логгера для модуля можно использовать стандартные методы логирования сообщений, такие как debug(), info(), warning(), error() и critical().

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

logger.propagate = False

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

2. Централизация конфигурации

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

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

Ниже приведены некоторые шаги по централизованной настройке протоколирования в Python:

  • Создайте отдельный модуль для настройки логирования: Создайте новый модуль Python, который будет содержать весь код конфигурации. Этот модуль должен импортировать модуль logging и содержать все необходимые конфигурации.

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

  • Импортируйте настройки логирования в приложение: Импортируйте модуль конфигурации логирования в основной код приложения. Это позволит использовать одинаковые настройки во всех модулях приложения.

  • Установите конфигурацию логирования: Установите конфигурацию логирования, вызвав метод logging.config.dictConfig() и передав в него словарь настроек. Этот метод сконфигурирует модуль logging с заданными настройками.

Приведём пример конфигурации централизованного логгирования для Python-проекта, использующего библиотеку python-json-logger для вывода структурированных журналов:

# logging_config.py

import logging.config
from pythonjsonlogger import jsonlogger

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "json": {
            "format": "%(asctime)s %(levelname)s %(message)s %(module)s",
            "class": "pythonjsonlogger.jsonlogger.JsonFormatter",
        }
    },
    "handlers": {
        "stdout": {
            "class": "logging.StreamHandler",
            "stream": "ext://sys.stdout",
            "formatter": "json",
        }
    },
    "loggers": {"": {"handlers": ["stdout"], "level": "DEBUG"}},
}


logging.config.dictConfig(LOGGING)

В приведенном выше примере мы определили словарь LOGGING, содержащий все параметры конфигурации для logging, такие как формат журнала, уровень журнала и место вывода журнала. В logging_config.py используется метод logging.config.dictConfig() для настройки модуля logging с указанными параметрами.

Чтобы использовать эту централизованную конфигурацию logging в своем Python-приложении, достаточно импортировать файл logging_config и вызвать в начале приложения:

# main.py

import logging_config # модуль с конфигурациями
import logging

logger = logging.getLogger(__name__)

logger.info("An info")
logger.warning("A warning")
Output

{"asctime": "2023-10-08 09:54:52,238", "levelname": "INFO", "message": "An info", "module": "main"}
{"asctime": "2023-10-08 09:54:52,238", "levelname": "WARNING", "message": "A warning", "module": "main"}

3. Использование правильных уровней логгирования

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

Модуль logging поддерживает пять различных уровней, от самого высокого до самого низкого:

  • CRITICAL: На этом уровне отображаются ошибки, которые являются очень серьезными и требуют срочного решения, иначе само приложение может оказаться неспособным продолжать работу.

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

 def connect_to_database():
      try:
          # connect here
      except Exception as e:
          logger.critical(f"Failed to connect to database: {e}", exc_info=True)
          exit(1)
      return conn
  • ERROR: Этот уровень показывает ошибку или невозможность выполнения некоторой задачи или функций. Например, вы можете использовать регистрацию ошибок для отслеживания ошибок базы данных или сбоев HTTP-запросов. Вот пример:

def process_request(request):
    try:
        # Process the request
    except Exception as e:
        logger.error(f'Error processing request: {e}', exc_info=True)
        # Return an error message to the user
  • WARNING: На этом уровне отображается информация, указывающая на то, что произошло нечто непредвиденное или существует вероятность возникновения проблем в будущем, например, "мало места на диске". Это не ошибка, и приложение по-прежнему работает нормально, но требует вашего внимания.

def low_memory_check():
    available_memory = get_available_memory()
    if available_memory < 1024:
        logger.warning('Low memory detected')
        # Send an alert to the you
  • INFO: На этом уровне отображается общая информация о приложении, позволяющая убедиться в том, что оно работает в соответствии с ожиданиями.

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

 def some_function(record_id):
    # Do some processing
    logger.info(f'New record created in the database. ID:{record_id}')
  • DEBUG: На этом уровне отображается подробная информация, обычно представляющая интерес только при диагностике проблем в приложении. Например, вы можете использовать уровень debug для регистрации данных, которые обрабатываются в функции:

def get_user_info(user_id):
    logger.debug(f'Retrieving user info for user with ID: {user_id}')
    # Fetch user info from the database
    user_info = database.get_user_info(user_id)
    logger.debug(f'Retrieved user info: {user_info}')
    return user_info

Установка соответствующего уровня журнала также позволяет контролировать, какие сообщения будут отображаться в журнале. Например, если для уровня журнала установлено значение INFO, то в журнал будут записываться только сообщения с уровнем INFO и выше (т. е. WARNING, ERROR и CRITICAL). Это может быть полезно в производственных средах, где необходимо просматривать только те сообщения, которые указывают на проблему, требующую немедленного решения.

Вот пример того, как можно настроить уровень регистрации на ERROR в Python:

LOGGING = {
    # the rest of your config
    "loggers": {"": {"handlers": ["stdout"], "level": "ERROR"}},
}

logging.config.dictConfig(LOGGING)

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

4. Писать содержательные сообщения журнала

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

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

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

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

  • Будьте ясны и лаконичны: Сообщения журнала должны быть простыми и понятными. Избегайте использования технического жаргона и сложных предложений.

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

  • Будьте последовательны: Используйте единый формат сообщений журнала во всех приложениях. Это облегчает их чтение и понимание, особенно при большом количестве сообщений журнала, как, например, в производственной среде.

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

name = 'Alice'
age = 30
salary = 50000

logger.info(f "Employee name: {name}, age: {age}, salary: {salary}")
output

{"asctime": "2023-04-27 20:31:54,737", "levelname": "INFO", "message": "Employee name: Alice, age: 30, salary: 50000"}

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

data = [1, 2, 3, 4, 5]

if len(data) < 6:
  logging.warning("Данные слишком малы. Рассмотрите возможность сбора большего количества данных, прежде чем приступать к работе.")
else:
  # process data
  pass

Давайте, для понимания, рассмотрим некоторые примеры осмысленных и не очень осмысленных сообщений журнала:

  • Хорошие примеры сообщений:

    • Пользователь с идентификатором 'user-123' успешно прошел аутентификацию

    • Файл успешно загружен на сервер по пути: /home/user/uploads/file.txt

    • Платеж в размере $50 успешно обработан с идентификатором транзакции: 123 456

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

  • Плохие примеры сообщений:

    • Произошла ошибка

    • Что-то пошло не так

    • Никогда такого не было и вот опять

А эти сообщения не информативные и не содержат полезной информаци.

5. % против f-строк для форматирования строк в журналах

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

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

Используйте форматирование %, когда:

  • Необходима совместимость со старыми версиями Python, которые не поддерживают f-строки.

  • Необходимо форматировать более широкий диапазон типов данных, например, в устаревшем коде, который может использовать форматирование % для форматирования сложных типов данных.

  • Необходимо более точно управлять выводом.

  • Форматирование % может быть более производительным, чем f-строки, особенно при работе с большим количеством сообщений журнала.

Используйте f-строки, если:

  • Вы используете Python 3.6 или более позднюю версию и предпочитаете синтаксис и читабельность f-строк.

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

  • Необходимо упростить синтаксис форматирования строк и уменьшить вероятность синтаксических ошибок.

  • Недостатки производительности f-строк не являются существенными для вашего случая использования.

В конечном итоге выбор между %-форматированием и f-строками для форматирования строк в журналах зависит от ваших личных предпочтений, требований вашего приложения и используемой версии Python. Тем не менее, для улучшения читаемости и удобства сопровождения обычно рекомендуется последовательно использовать один из вариантов форматирования.

6. Ведение журнала в формате (JSON)

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

Вот некоторые преимущества использования структурированного JSON-логирования:

  • Улучшенная читаемость и удобство поиска: Структурированные JSON-журналы легче читать и искать по сравнению с традиционными текстовыми форматами. Использование стандартизированного формата JSON позволяет легко анализировать данные журналов с помощью таких инструментов, как Elasticsearch или Kibana.

  • Согласованность между компонентами: Когда различные компоненты приложения используют разные форматы журналов, анализ журналов во всем стеке приложений может быть затруднен. Использование стандартизированного формата JSON гарантирует, что все компоненты будут использовать один и тот же формат, что упрощает анализ журналов во всем приложении.

  • Лучший контекст и метаданные: Структурированный JSON-журнал позволяет добавлять в него дополнительные метаданные, такие как идентификаторы запросов, пользователей или временные метки. Эти метаданные могут обеспечить ценный контекст при устранении неполадок и анализе журнальных данных.

  • Поддержка структурированных данных: JSON - это гибкий формат, поддерживающий структурированные данные, что позволяет легко регистрировать сложные структуры данных, такие как словари или списки. Использование структурированного формата позволяет избежать необходимости разбора текстовых журналов, который может быть сопряжен с ошибками и отнимает много времени.

  • Масштабируемость: По мере роста приложения объем генерируемых им журналов может значительно увеличиваться. Использование структурированного формата JSON позволяет легко масштабировать инфраструктуру протоколирования для работы с большими объемами журнальных данных.

Существует несколько библиотек логгирования на языке Python, поддерживающих структурированный JSON-логи, например python-json-logger, loguru и structlog.

Установив любую из этих библиотек и настроив логгер, можно использовать его для записи журналов в структурированном формате JSON. Для этого можно вызвать метод logger.info() (или любой другой метод логирования) и передать в него словарь пар ключ-значение, представляющих сообщение журнала.

Приведем пример с использованием loguru:

import sys
from loguru import logger

logger.remove(0)
logger.add(
    sys.stdout,
    format="{time:MMMM D, YYYY > HH:mm:ss!UTC} | {level} | {message}",
    serialize=True,
)
logger.info("Incoming API request: GET /api/users/123")

В результате в стандартный вывод будет записано сообщение журнала в формате JSON со следующей структурой:

output
{
  "text": "April 27, 2023 > 19:50:33 | INFO | Incoming API request: GET /api/users/123\n",
  "record": {
    "elapsed": {
      "repr": "0:00:00.017884",
      "seconds": 0.017884
    },
    "exception": null,
    "extra": {},
    "file": {
      "name": "main.py",
      "path": "/home/betterstack/dev/demo/python-logging/main.py"
    },
    "function": "<module>",
    "level": {
      "icon": "ℹ️",
      "name": "INFO",
      "no": 20
    },
    "line": 21,
    "message": "Incoming API request: GET /api/users/123",
    "module": "main",
    "name": "__main__",
    "process": {
      "id": 407115,
      "name": "MainProcess"
    },
    "thread": {
      "id": 140129253443392,
      "name": "MainThread"
    },
    "time": {
      "repr": "2023-04-27 20:50:33.843118+01:00",
      "timestamp": 1682625033.843118
    }
  }
}

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

7. Включение временных меток и обеспечение последовательного форматирования

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

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

Чтобы избежать этого, лучше всего принять стандартный формат временных меток. Одним из таких стандартов является стандарт ISO-8601, который представляет собой международно признанный стандарт обмена данными, связанными с датой и временем.

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

Вот как выглядит временная метка, выраженная в формате ISO-8601:

2022-06-15T04:32:19.955Z

Это базовый пример настройки форматирования для разрешения временных меток ISO-8601:

LOGGING = {
    "formatters": {
        "json": {
            "format": "%(asctime)s %(levelname)s %(message)s",
            "datefmt": "%Y-%m-%dT%H:%M:%SZ",

            "class": "pythonjsonlogger.jsonlogger.JsonFormatter",
        }
    },
}

logging.config.dictConfig(LOGGING)

8. Не допускайте попадания конфиденциальной информации в журналы

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

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

Ниже приведены общие рекомендации по сохранению конфиденциальных данных в журналах и снижению риска их раскрытия:

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

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

Например, если номер кредитной карты имеет вид "1234-5678-9012-3456", его можно замаскировать или отредактировать. Вот как использовать фильтры для реализации редактирования журнала в Python:

# logging_config.py

import logging
import logging.config
from pythonjsonlogger import jsonlogger
import re


class SensitiveDataFilter(logging.Filter):
    pattern = re.compile(r"\d{4}-\d{4}-\d{4}-\d{4}")

    def filter(self, record):
        # Modify the log record to mask sensitive data
        record.msg = self.mask_sensitive_data(record.msg)
        return True

    def mask_sensitive_data(self, message):
        # Implement your logic to mask or modify sensitive data
        # For example, redact credit card numbers like this
        message = self.pattern.sub("[REDACTED]", message)
        return message


LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "filters": {
        "sensitive_data_filter": {
            "()": SensitiveDataFilter,
        }
    },
    "formatters": {
        "json": {
            "format": "%(asctime)s %(levelname)s %(message)s",
            "datefmt": "%Y-%m-%dT%H:%M:%SZ",
            "class": "pythonjsonlogger.jsonlogger.JsonFormatter",
        }
    },
    "handlers": {
        "stdout": {
            "class": "logging.StreamHandler",
            "stream": "ext://sys.stdout",
            "formatter": "json",
            "filters": ["sensitive_data_filter"],
        }
    },
    "loggers": {"": {"handlers": ["stdout"], "level": "INFO"}},
}


logging.config.dictConfig(LOGGING)
# main.py

import logging_config
import logging

logger = logging.getLogger(__name__)

credit_card_number = "1234-5678-9012-3456"
logger.info(f"User made a payment with credit card num: {credit_card_number}")
output

{"asctime": "2023-04-27T21:36:39Z", "levelname": "INFO", "message": "User made a payment with credit card number: [REDACTED]"}

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

  • Ограничение области действия журналов: Можно ограничить объем журналов, записывая в них только то, что необходимо. Это означает, что можно регистрировать только ошибки или критические события, а не все события. Кроме того, можно ограничить объем информации, регистрируемой приложением.

  • Шифрование данных журнала: Для обеспечения безопасности конфиденциальной информации можно зашифровать данные журнала. Это позволит обеспечить доступ к журналам и их чтение только авторизованным сотрудникам.

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

9. Ротация файлов журнала

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

Существует несколько стратегий ротации файлов журнала, в том числе:

  • Ротация по времени: Создание нового файла журнала через фиксированные промежутки времени (например, ежедневно или еженедельно) и архивирование или удаление старых файлов журнала.

  • Ротация по размеру: Создание нового файла журнала при достижении текущим файлом журнала определенного размера (например, 10 МБ) и архивирование или удаление старых файлов журнала.

  • Гибридная ротация: Комбинирование стратегий ротации на основе времени и размера для создания новых файлов журнала через фиксированные промежутки времени и архивирования или удаления старых файлов журнала на основе ограничений по размеру.

В Python ротацию лог-файлов можно выполнять с помощью встроенного модуля logging. Модуль logging предоставляет класс RotatingFileHandler, который позволяет создавать файлы журнала, ротируемые в зависимости от заданного размера или временного интервала.

Приведем пример использования класса RotatingFileHandler для ротации лог-файлов по размеру:

# Create a logger
logger = logging.getLogger('my_logger')
logger.setLevel(logging.DEBUG)

# Create a rotating file handler
handler = logging.handlers.RotatingFileHandler(
    'my_log.log', maxBytes=1000000, backupCount=5)

# Set the formatter for the handler
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)

# Add the handler to the logger
logger.addHandler(handler)

# Test the logger
logger.debug('Debug message')

В этом примере мы создаем регистратор с именем my_logger и устанавливаем уровень регистрации DEBUG. Затем мы создаем RotatingFileHandler с максимальным размером файла 1 МБ и количеством резервных копий 5.

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

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

Это лишь простой пример того, как ротировать лог-файлы с помощью модуля logging в Python. Обычно мы рекомендуем доверить ротацию журналов внешнему инструменту, например logrotate, который поможет обеспечить согласованность политик ротации журналов для нескольких приложений или служб, работающих на одной машине.

10. Централизация журналов в одном месте

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

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

Централизация журналов в одном месте имеет ряд преимуществ, среди которых можно выделить следующие:

  • Улучшение процесса поиска и устранения неисправностей: Централизация журналов облегчает поиск и устранение неисправностей, поскольку обеспечивает единый источник истины для данных журналов. Это позволяет коррелировать события в различных системах и быстрее выявлять первопричину проблем.

  • Повышение уровня безопасности: Централизация журналов позволяет повысить уровень безопасности за счет централизованного мониторинга и обнаружения угроз безопасности. Анализируя журналы из нескольких систем, можно выявить закономерности и аномалии, которые могут свидетельствовать о нарушении безопасности.

  • Повышение масштабируемости: Централизация журналов позволяет повысить масштабируемость за счет централизованного сбора и хранения больших объемов журнальных данных. Это облегчает масштабирование инфраструктуры журналов по мере роста системы.

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

При выборе облачного решения для ведения журналов необходимо учитывать несколько факторов:

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

  • Масштабируемость: Убедитесь, что решение способно работать с текущим объемом журналов и масштабироваться по мере его роста.

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

  • Безопасность: Ищите решение, обеспечивающее надежные средства защиты, такие как шифрование, контроль доступа и политики хранения данных.

  • Стоимость: Рассмотрите стоимость решения, включая авансовые платежи, текущие расходы на подписку и дополнительные расходы на такие функции, как хранение или обработка данных.

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

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

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

Здесь была реклама Logtail

Заключение

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

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

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

Спасибо за прочтение, Happy Logging!

Дополнительные материалы по теме логирования в Python:

Комментарии (19)


  1. Suharkov
    09.10.2023 11:34
    +7

    Например, если номер кредитной карты имеет вид "1234-5678-9012-3456", его можно замаскировать или отредактировать.

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


  1. freeg0r
    09.10.2023 11:34
    +2

    Для конфигурации лучше использовать файл, а не код. Например:

    logging.config.fileConfig(Path(__file__), "logging.ini")


    1. Gadd
      09.10.2023 11:34

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


      1. freeg0r
        09.10.2023 11:34
        +2

        Например, читабельность, сравните конфиг из статьи и такой:

        [loggers]
        keys=root
        
        [handlers]
        keys=consoleHandler
        
        [formatters]
        keys=json
        
        [logger_root]
        level=INFO
        handlers=consoleHandler
        
        [handler_consoleHandler]
        class=StreamHandler
        level=INFO
        formatter=json
        args=(sys.stdout,)
        
        [formatter_json]
        class=pythonjsonlogger.jsonlogger.JsonFormatter
        format=%(asctime)s %(process)d %(thread)d %(levelname)s %(name)s %(funcName)s %(message)s


        1. Gadd
          09.10.2023 11:34

          Если сравнивать с конфигом из статьи, то конечно, это вариант смотрится гораздо лучше. Но я имел в виду задание конфигурации стандартными методами/функциями из модуля logging - .basicConfig(), создание хендлеров, форматтеров и прочее. Зачем вместо этого использовать голый dict - для меня загадка.

          Прошу прощения, нужно было это момент сразу уточнить.


      1. NAI
        09.10.2023 11:34
        +1

         не вижу особой разницы: использовать файл-конфиг или использовать отдельный метод/функцию/модуль

        Как только у вас появляются разные среды (dev1, dev2, test, prod), в которых должны быть разные настройки, то хранение конфигов в коде превратится в боль и страдание. Вы сразу же перейдете или к переменным окружения (env'ам), или к конфиг-файлам. Особенно быстро это произойдет если добавить CI\CD.


        1. Gadd
          09.10.2023 11:34

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


  1. dyens
    09.10.2023 11:34
    +4

    При логировании лучше использовать % а не f строки. В этом случае подстановка аргументов в строку будет происходить в последний момент. Например если у вас много debug логов, то в warning режиме методы строки не будут создаваться, + не будут вызываться методы приведения к строке у аргументов.


    1. Hungryee
      09.10.2023 11:34

      Вы хотите сказать, что если у меня будет выражение

      f'Result = {heavyOperation()}'

      И эту строку я пихаю в logging.debug(), и при этом включаю логи уровня warning+, то heavyOperation() все равно будет выполняться?

      Если да, то чем будет отличаться поведение с %?

      Если нет, то тогда не понял смысл комментария

      UPD: почитал из интереса - действительно выражение считается в рантайме для % и в компайлтайме для f-строк, прошу прощения

      Думал разница только в удобстве и версии питона


      1. Gadd
        09.10.2023 11:34
        +2

        Думаю, с примером сразу станет всё ясно.
        тут строка-результат формируется до вызова debug (точнее в debug передается уже сформированная строка-результат)

        logger.debug(f"User {user} logged in")
        

        А тут - строка-результат формируется внутри вызова debug, потому что сам вызов содержит только набор аргументов, соответственно, при уровне INFO новая строка формироваться не будет.

        logger.debug("User %s logged in", user)
        


        1. Tangeman
          09.10.2023 11:34

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

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

          Единственный надёжный способ с этим бороться - это использовать конструкции типа:

          if __debug__:
            logger.debug(f"User {user} logged in")

          В этом случае, если приложение запускается не в отладочом режиме (python -O) то всё что внутри if __debug__ вообще будет вырезано и не будет выполняться.

          К сожалению, эта магия работает только с __debug__ и только если нет других условий - удобной условной компиляции методов как в C# у Python нет, увы.


          1. Gadd
            09.10.2023 11:34

            Прятать сложные вычисления внутри вызова .debug() - вообще идея не очень, в таком случае лучше воспользоваться чем-то вроде https://docs.python.org/3/library/logging.html#logging.Logger.isEnabledFor или https://docs.python.org/3/library/logging.html#logging.Logger.getEffectiveLevel, которые работают для любого уровня логирования.

            Форматирование строк - операция всё же довольно затратная, если .debug() вызовов много, то лучше использовать С-style форматирование. Даже если аргументы типа user уже известны на момент вызова (а это происходит в 99% случаев).


        1. hVostt
          09.10.2023 11:34

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

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

          _logger.LogInfomation("User {UserName} is logged in", userName);
          

          Даёт следующее:

          1. Строка "User {UserName} is logged in" является константой и не приводит к выделению памяти.

          2. Так как журнал полностью асинхронный, т.е. запись журнала в файл/эластик/oltp идёт из буфера, и не блокирует код, отсутствие лишних выделений памяти чувствительно

          3. Самое главное, в журнале будет следующее:

          { 
             "@timestamp": "2023-10-11T17:44:27Z",
             "message": "User \"Vasya Pupkin\" is logged in",
             "UserName": "Vasya Pupkin",
             ...
          }
          

          Т.е. UserName запишется отдельным полем в журнал. И ничего специально для этого не надо делать. А это даёт просто колоссальные преимущества для эффективной работы с журналом.

          На наших проектах на Python этого здорово не хватает.


          1. Gadd
            09.10.2023 11:34

            Возможно, вот это будет для вас полезно?
            https://docs.python.org/3/howto/logging-cookbook.html#adding-contextual-information-to-your-logging-output
            Как-то на одном из проектов автоматически добавляли к логам query_id, но похоже, что и в вашем случае может быть полезно (но если честно, то я особо не вникал).

            Плюс pythonjsonlogger.jsonlogger.JsonFormatter, как было сказано в статье, если нужны логи в формате JSON.


            1. hVostt
              09.10.2023 11:34

              Добавление кастомных полей, через контекст это не совсем то. Подходит для насыщения журнала дополнительными полями из контекста. А речь буквально о каждом сообщении в журнал. Это называется структурный лог. Формат лога, JSON/XML/или ещё что-то, это вопрос форматирования и вывода, суть не в этом.


      1. Gadd
        09.10.2023 11:34

        UPD: почитал из интереса - действительно выражение считается в рантайме для % и в компайлтайме для f-строк, прошу прощения

        f-строки и С-style строки с % не могут считаться во время "компиляции", потому что значения аргументов становятся известны как правило только во время выполнения. Потому что нет смысла использовать заранее известные значения для форматирования строк, проще сразу использовать строковые литералы.


        1. Pavel1114
          09.10.2023 11:34
          +1

          смысл есть.
          При использовании записи вида

          logger.debug("hello, %s", user)

          из инстанса Record можно будет легко достать этот аргумент. Использовать это можно, например, в фильтрах или при форматировании.
          если же использовать

          logger.debug("hello, %s" % user)

          то, как тут уже написали, строка будет формироваться ещё до создания Record и, соответственно, Record.args будет пустым


    1. FireHawk
      09.10.2023 11:34
      +1

      Мы много обсуждали это на работе, но в итоге свичнулись на f-strings. Разница в производительности даже на debug-коде мизерная, а сложные строки с выводом 3-4 переменных выглядят куда понятнее в виде f-strings, чем в виде форматированного через % кода.


  1. Pavel1114
    09.10.2023 11:34

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

    Кто нибудь может объяснить этот момент? Есть ли в python возможность защитить какой то модуль от модификации при компрометации другого модуля? Или это тут для красного словца.

    logger.propagate = False

    propagate не спроста по умолчанию установлено в True. Как правило, логгер вообще не должен заниматься решением того кто будет, а кто не будет читать его записи. Его задача - формировать эти записи, а дальше там разберутся

    Про f-string выше уже написали. Хотя для меня важнее не само то что они вычисляются в момент формирования записи, а то что далее из этой записи невозможно будет достать эти аргументы для фильтрации и/или форматирования. Поэтому, несмотря на мою любовь к f-string, именно в логгерах использовать желательно % синтаксис.

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

    В целом статья очень слабая. Очевидно что автор (авторка?) не имеет достаточного опыта чтобы называть свои статьи "10 Best Practices ..." . Вроде и не хочется шеймить и, возможно, какая польза есть. Но есть ощущение, что подобные статьи являются информационным шумом, который мешает людям добраться, наконец, до документации python где всё подробно написано.