Настройка ведения журнала третьей стороны script

У меня есть сторонний скрипт консоли Python, исходный код которого я не хочу изменять.

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

Скрипт использует этот шаблон:

import logging
logger=logging.getLogger(__name__)

Примеры использования:

  • Я хочу, чтобы информационные сообщения файла foo.py игнорировались.
  • Я хочу включить PID в сообщения регистрации.

Как я могу настроить ведение журнала, если я не хочу изменять источники консольного скрипта?

Сценарий вызывается через cron.

Как я могу настроить ведение журнала, если этот скрипт?

Важно

Создание сценария-оболочки, как в этом ответе, не является для меня решением.

Иерархия процессов Linux выглядит следующим образом:

Cron -> third_party_script

Между cron и third_party_script должен быть какой-либо скрипт "glue", "wrapping" или "dirty-hack".

Почему навязчивый/нетпикинг?

Я хочу практиковать "разделение интересов". Я хочу иметь возможность настроить ведение журнала один раз и в одном месте. Эта конфигурация должна использоваться всем кодом Python Virtualenv. Написание обертки было бы обходным решением. Я хочу решение.

Обновление

Несколько месяцев спустя я думаю, что pth файл будет простым ответом.

Ответы

Ответ 1

Я задал этот вопрос несколько месяцев назад. К сожалению, я не получил ответа, который удовлетворил меня.

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

Это мое решение: в нашем контексте мы настраиваем ведение журнала в методе, который вызывается в usercustomize.py.

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

Это почти решило все мои потребности.

До сих пор я не нашел лучшего способа, чем usercustomize.py. Моим идеальным решением я бы назвал virtualenvcustomize.py: Некоторый код инициализации, который запускается, если интерпретатор загружает virtualenv. До сих пор я не мог найти такой крючок. Пожалуйста, дайте мне знать, если у вас есть решение.

Ответ 2

Библиотека не должна настраивать ведение журнала - это до разработчика приложения. Ответ Inbar Rose не совсем прав. Если модуль, на который вы ссылаетесь, называется foo, то ссылка на __name__ в своем вызове getLogger будет передаваться в foo. Поэтому в вашем конфигурационном коде вам нужно будет сделать эквивалент

logging.getLogger('foo').setLevel(logging.WARNING)

Чтобы включить PID в журналы, просто убедитесь, что вы используете соответствующую строку форматирования для вашего Форттера, т.е. тот, который включает %(process)d. Простой пример:

logging.basicConfig(format='%(process)d %(message)s')

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

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

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

if __name__ == '__main__':
    # configure logging here
    # sets the third party logger to do WARNING or greater
    # replace 'foo' with whatever the top-level package name your
    # third party package uses
    logging.getLogger('foo').setLevel(logging.WARNING)
    # set any other loggers to use INFO or greater,
    # unless otherwise configured explicitly
    logging.basicConfig(level=logging.INFO, format='%(process)d %(message)s')
    # now call the main function (or else inline code here)
    main()

Если сторонний код работает через cron, это не код библиотеки - это приложение, и вам, вероятно, не повезло.

Ответ 3

Несколько возможностей:

Упаковочный

Если вы можете отредактировать свою таблицу cron, вы можете создать небольшой script в python, который получит регистратор lib, удалите существующий обработчик журнала и подключите к нему свой собственный обработчик:

# Assumes the lib defines a logger object
from third_party_lib import *

# Note this assumes only one handler was defined by the lib
logger.removeHandler(logger.handlers[0])

# Then we can hook our custom format handler
custom_handler = logging.StreamHandler(sys.stdout)
custom_handler.setFormatter(logging.Formatter(format = '%(asctime)s %(levelname)s %(name)s %(process)d: %(message)s', None))
logger.addHandler(custom_handler)
logger.setLevel(logging.WARNING)

Также помните об этом, предположим, что lib не повторно объявляет регистратор в пути.

Редактирование динамического кода

Если у вас нет возможности изменить вызов cron, вы можете выполнить динамическое редактирование кода, но это эквивалентно редактированию файла вручную (hacky):

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

Ответ 4

Вы можете изменить минимальный уровень журнала этого регистратора.

logging.getLogger(__name__).setLevel(logging.WARNING)

Теперь будет показано только ПРЕДУПРЕЖДЕНИЕ и выше. Нет INFO и DEBUG.

Кроме того, вы также можете изменить формат. %(process)d является PID.

log_format = logging.Formatter('%(asctime)s %(levelname)s %(name)s %(process)d: %(message)s', '%H:%M:%S')
logging.getLogger(__name__).setFormatter(log_format)

Все вместе:

log_format = logging.Formatter('%(asctime)s %(levelname)s %(name)s %(process)d: %(message)s', '%H:%M:%S')
log_handle = logging.getLogger(__name__)
log_handle.setLevel(logging.WARNING)
log_handle.setFormatter(log_format)

Примечание. Вы должны заменить __name__ в вашем коде соответствующим обработчиком журнала.

Ответ 5

ТЛ; др

Короче говоря, мы хотим внедрить код, который выполняется интерпретатором python до того, как будет выполнен наш основной код.

Лучший способ добиться этого - создать virtualenv и добавить sitecustomize.py в пакеты сайта virtualenv.

демонстрация

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

$ cat my_app.py

import logging
logger = logging.getLogger("my_app")

logger.debug("A debug message")
logger.info("An info message")
logger.warning("A warning message")
logger.error("An error message")

Запуск my_app.py должен показывать только те сообщения, уровень серьезности которых> WARNING (это поведение по умолчанию при ведении журнала python).

$ python my_app.py

A warning message
An error message

Теперь давайте создадим виртуальную среду

python3 -m venv my_venv

И давайте добавим sitecustomize.py к пакетам сайта virtualenv.

$ cat my_venv/lib/python3.7/site-packages/sitecustomize.py

import logging

# Setup logging for my_app
# We will only setup a console handler
logger = logging.getLogger("my_app")
logger.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
ch.setFormatter(
    logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
)
logger.addHandler(ch)

Теперь давайте попробуем запустить my_app.py используя virtualenv:

$ ./my_venv/bin/python my_app.py

2019-01-25 16:03:16,815 - my_app - DEBUG - A debug message
2019-01-25 16:03:16,815 - my_app - INFO - An info message
2019-01-25 16:03:16,815 - my_app - WARNING - A warning message
2019-01-25 16:03:16,815 - my_app - ERROR - An error message

И это было все) Мы получили правильную регистрацию без изменения my_app.py или написания оболочки!

Теперь, если вы хотите знать, почему это оптимальный подход, продолжайте читать.

(Действительно) длинный ответ

Прежде чем понять, почему использование virtualenv + sitecustomize.py является правильным подходом к этой проблеме, необходимо сделать не очень краткое введение.

Примечание. Я предполагаю, что вы создаете виртуальные среды с venv модуля venv который использует stdlib site.py Библиотека virtuaelnv использует свой собственный site.py и может делать что-то немного иначе. Тем не менее, прочитав этот ответ, вы сможете проверить, есть ли какие-либо различия между venv и vitualenv и понять, как с ними справиться.

Что такое site-packages

Короткий ответ: site-packages - это место, где python устанавливает сторонний код (как в не -s коде tdlib). Для получения дополнительной информации прочитайте это и предоставленные ссылки.

Как ввести код?

Python позволяет вам настроить интерпретатор python во время его запуска, то есть до того, как будет выполнен наш основной код/​​скрипт/что угодно. Это может быть полезно, например:

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

Способ внедрения - это создание/изменение либо sitecustomize.py либо usercustomize.py. Вы также можете использовать файл "путь конфигурации" (т.е. *.pth) с оператором импорта, но я не буду здесь рассматривать этот случай, так как:

  • Эти заявления об импорте кажутся ужасным взломом
  • Я не уверен, что он дает какие-либо реальные преимущества по сравнению с sitecustomize/usercustomize.
  • Я хотел бы сделать вещи простыми

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

sitecustomize & usercustomize

Итак, sitecustomize и usercustomize - это специальные файлы, которые по умолчанию не существуют, но если мы их создадим, python автоматически импортирует их, прежде чем начнет выполнять наш код. Мы можем создать эти файлы:

  • либо в глобальных пакетах сайта (например, /usr/lib/python3.7/site-packages/)
  • или на сайте пользователя-пакета (например, ~/.local/lib/python3.7/site-packages/)

sitecustomize всегда импортируется перед usercustomize. Если ImportError либо из файлов отсутствует, ImportError игнорируется.

В качестве меры безопасности, если существует несоответствие между идентификатором пользователя или группы и эффективным идентификатором, то пользовательские пакеты сайта отключаются (источник). Более того, интерпретатор python имеет аргументы CLI, которые либо полностью отключают пакеты сайта (как системные, так и пользовательские) или отключают пакеты сайта пользователя. Если предположить, что у нас нет несовпадения идентификаторов и что мы не используем какие-либо флаги CLI, то пользовательские пакеты сайтов имеют более высокий приоритет, чем системные пакеты сайтов. Так что, если у нас есть оба:

~/.local/lib/python3.7/site-packages/sitecustomize.py
/usr/lib/python3.7/site-packages/sitecustomize.py

первый - тот, который будет импортирован. На самом деле мы можем проверить приоритет sys.path, выполнив модуль site.py:

$ python3 -msite

sys.path = [
    '/tmp/test',
    '/usr/lib/python37.zip',
    '/usr/lib/python3.7',
    '/usr/lib/python3.7/lib-dynload',
    '/home/username/.local/lib/python3.7/site-packages',      # user site-packages
    '/usr/lib/python3.7/site-packages',                       # system site-packages
]
USER_BASE: '/home/username/.local' (exists)
USER_SITE: '/home/username/.local/lib/python3.7/site-packages' (exists)
ENABLE_USER_SITE: True

Важной информацией здесь является значение ENABLE_USER_SITE. Если это True то пользовательские пакеты сайта включены. Если это False то мы можем использовать только глобальные пакеты сайта. Например, если мы используем python -s:

$ python3 -s -msite

sys.path = [
    '/tmp/test',
    '/usr/lib/python37.zip',
    '/usr/lib/python3.7',
    '/usr/lib/python3.7/lib-dynload',
    '/usr/lib/python3.7/site-packages',
]
USER_BASE: '/home/username/.local' (exists)
USER_SITE: '/home/username/.local/lib/python3.7/site-packages' (exists)
ENABLE_USER_SITE: False

Обратите внимание, что в этом случае ENABLE_USER_SITE имеет значение False.

Просто для полноты позвольте полностью отключить пакеты сайта:

$ python3 -S -msite

sys.path = [
    '/tmp/test',
    '/usr/lib/python37.zip',
    '/usr/lib/python3.7',
    '/usr/lib/python3.7/lib-dynload',
]
USER_BASE: '/home/username/.local' (exists)
USER_SITE: '/home/username/.local/lib/python3.7/site-packages' (exists)
ENABLE_USER_SITE: None

эксперимент

Чтобы лучше это понять, давайте проведем эксперимент. Сначала давайте создадим модули usercustomize sitecustomize как в системных, так и в пользовательских пакетах сайта.

ВНИМАНИЕ: Мы будем создавать файлы в системных пакетах сайта. Это будет мешать вашему дистрибутиву Python. Будьте осторожны и ПОМНИТЕ, чтобы удалить их, когда мы закончим.

# system site packages
echo 'print(f"-> {__file__}")' | sudo tee /usr/lib/python3.7/site-packages/usercustomize.py
echo 'print(f"-> {__file__}")' | sudo tee /usr/lib/python3.7/site-packages/sitecustomize.py

# user site packages
echo 'print(f"-> {__file__}")' | tee ~/.local/lib/python3.7/site-packages/usercustomize.py
echo 'print(f"-> {__file__}")' | tee ~/.local/lib/python3.7/site-packages/sitecustomize.py

Позвольте также создать модуль Python:

echo 'print("Inside foo")' | tee foo.py

Теперь давайте foo.py:

$ python3 foo.py

-> /home/username/.local/lib/python3.7/site-packages/sitecustomize.py
-> /home/username/.local/lib/python3.7/site-packages/usercustomize.py
Inside foo

Как мы можем видеть:

  • sitecustomize как sitecustomize и usercustomize
  • они импортируются из пользовательских пакетов сайта

Что будет, если мы отключим пользовательские пакеты сайтов?

$ python3 -s foo.py

-> /usr/lib/python3.7/site-packages/sitecustomize.py
Inside foo

На этот раз мы видим, что мы:

  • только sitecustomize импортируется. Хотя usercustomize существует в системных пакетах сайта, python не импортирует его! Это важно! Имейте это в виду, когда мы обсуждаем virtualenvs! (СОВЕТ: это связано с ENABLE_USER_SITE; вы помните, какое значение имеет в этом случае?)
  • sitecustomize импортируется из системных пакетов сайта

Наконец, если мы полностью usercustomize пакеты сайтов, очевидно, что usercustomize и sitecustomize будут игнорироваться:

$ python3 -S foo.py

Inside foo

Как насчет virtualenvs?

Хорошо, теперь давайте добавим в игру virtualenv. Есть два типа virtualenvs:

  • нормальные
  • созданные с помощью --system-site-packages.

Пусть создадут virtualenvs обоих типов

python3 -mvenv venv_no_system
python3 -mvenv venv_system

Также sitecustomize.py usercustomize.py модули sitecustomize.py и usercustomize.py в site-пакеты virtualenv:

echo 'print(f"-> {__file__}")' | tee ./venv_no_system/lib/python3.7/site-packages/usercustomize.py
echo 'print(f"-> {__file__}")' | tee ./venv_no_system/lib/python3.7/site-packages/sitecustomize.py

echo 'print(f"-> {__file__}")' | tee ./venv_system/lib/python3.7/site-packages/usercustomize.py
echo 'print(f"-> {__file__}")' | tee ./venv_system/lib/python3.7/site-packages/sitecustomize.py

и давайте посмотрим на различия:

$ ./venv_no_system/bin/python -msite

/tmp/test/venv_no_system/lib/python3.7/site-packages/sitecustomize.py
sys.path = [
    '/tmp/test',
    '/usr/lib/python37.zip',
    '/usr/lib/python3.7',
    '/usr/lib/python3.7/lib-dynload',
    '/tmp/test/venv_no_system/lib/python3.7/site-packages',
]
USER_BASE: '/home/username/.local' (exists)
USER_SITE: '/home/username/.local/lib/python3.7/site-packages' (exists)
ENABLE_USER_SITE: False

Что мы видим здесь? На обычном virtualenv ENABLE_USER_SITE имеет значение False, что означает, что:

  1. пакеты сайта пользователя игнорируются
  2. только sitecustomize импортируется! Т.е. мы не можем использовать usercustomize для внедрения кода !!!

Мы также видим, что вместо наших глобальных пакетов сайтов, virtualenv использует свой собственный (т.е. /tmp/test/venv_no_system/lib/python3.7/site-packages).

Теперь давайте повторим это, но на этот раз с virtualenv, который использует системные пакеты сайта:

$ ./venv_system/bin/python -msite

-> /home/username/.local/lib/python3.7/site-packages/sitecustomize.py
-> /home/username/.local/lib/python3.7/site-packages/usercustomize.py
sys.path = [
    '/tmp/test',
    '/usr/lib/python37.zip',
    '/usr/lib/python3.7',
    '/usr/lib/python3.7/lib-dynload',
    '/tmp/test/venv_system/lib/python3.7/site-packages',
    '/home/username/.local/lib/python3.7/site-packages',
    '/usr/lib/python3.7/site-packages',
]
USER_BASE: '/home/username/.local' (exists)
USER_SITE: '/home/username/.local/lib/python3.7/site-packages' (exists)
ENABLE_USER_SITE: True

В этом случае поведение отличается...

ENABLE_USER_SITE - это True что означает:

  1. что пользовательские пакеты сайта включены
  2. что usercustomize импортируется нормально.

Но есть и еще одно отличие. В этом случае у нас есть 3 каталоги сайтов-пакетов. Virtualenv - это пакет с более высоким приоритетом, за которым следуют пользовательские пакеты сайтов, а системные пакеты сайта - последние.

Так что использовать?

Я думаю, что есть три варианта:

  1. использовать системную установку Python
  2. использовать обычный virtualenv
  3. использовать virtualenv с системными сайтами-пакетами

Я думаю, что в большинстве случаев, например, на обычных серверах/десктопах, изменения системы Python, как правило, следует избегать. По крайней мере, на * nix, слишком многое зависит от Python. Я бы очень не хотел менять свое поведение. Возможными исключениями являются эфемерные или статические "системы" (например, внутри контейнера).

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

Добавить sitecustomize.py в пакеты сайта virtuaelenv.

Убираться

# remove the virtualenvs
rm -rf my_venv
rm -rf venv_system
rm -rf venv_no_system

# remove our usercustomize.py and sitecustomize.py
sudo rm /usr/lib/python3.7/site-packages/sitecustomize.py
sudo rm /usr/lib/python3.7/site-packages/usercustomize.py
rm ~/.local/lib/python3.7/site-packages/sitecustomize.py
rm ~/.local/lib/python3.7/site-packages/usercustomize.py

# remove the modules
rm foo.py
rm my_app.py