Package: polog
Polog: логгер нового поколенияЛогируйте красиво, удобно и без бойлерплейта. Избавьтесь от "мусора" в коде. Зацените фичи:Продвинутая функцияlog, которую можно использовать "как есть", а можно какконтекстный менеджерили даже как декоратор дляфункций(в том числе корутинных) иклассов.Просто настраивать, причем любые настройки можно менять "на лету" во время работы программы.Записи об ошибкахне дублируются, когда исключение проходит через несколько декораторов логирования.Можноредактироватьлог, записываемый декоратором, изнутри той функции, которую он оборачивает.Можнозапретить логированиеотдельных функций, навесив на них специальныйдекоратор.Ядро Pologподдерживаетмногопоточность, что иногда может заметно ускорить программу. В этом режиме запись лога становитсянеблокирующей операцией.Легко модифицировать. Можно писатьсобственные обработчикиили добавлять собственную логику дляавтоматического заполненияполей лога.У логгеранет внешних зависимостей.Естьболее 580 тестов, которые обеспечивают покрытие более чем на96%. И стремление эти цифры еще увеличить.ОглавлениеБыстрый стартО фреймворкеlog()- одна функция, чтобы править всеми"Ручное" логированиеКонтекстный менеджерДекорируем функцииДедупликация исключенийДекорируем классыПерекрестное декорированиеЗапрет логирования через декоратор@unlogРедактируем автоматические логи из задекорированных функцийУмные ассертыИнтеграция с модулемloggingНастройки логгераУровни логированияДвижки: синхронный и асинхронныйОб объекте логаДобавляем извлекаемые поляОбработчикиПространства имен и иерархия обработчиковВывод в файл или в консольРотация файла с логамиКак включить уведомления по электронной почтеПишем свой обработчикОбщие советы про логированиеБыстрый стартУстановите Polog черезpip:$ pip install pologПрежде, чем вызывать логгер, зарегистрируйтеобработчик:frompologimportconfig,file_writerconfig.add_handlers(file_writer())Теперь наши логи будут выводиться в консоль. При желании, вfile_writerможно передать имя файла и тогда вывод будет происходить туда.Теперь импортируем объектlogи применим его к функции как декоратор:frompologimportlog@logdefsum(a,b):returna+bsum(2,2)Результат:[2022-10-28 16:02:55.988837] |    1    | SUCCESS |  AUTO  | where: __main__.sum() | time of work: 0.00001478 sec. | input variables: 2 (int), 2 (int) | result: 4 (int)Теперь попробуем залогировать ошибку:@logdefdivision(a,b):returna/bdivision(2,0)Результат:[2022-10-29 00:23:20.699334] |    2    |  ERROR  |  AUTO  | where: __main__.division() | time of work: 0.00001097 sec. | input variables: 2 (int), 0 (int) | local variables: a = 2 (int), b = 0 (int) | exception: ZeroDivisionError("division by zero") | traceback: return a / b (".../main.py", line 9, in division)Что, если мы хотим залогировать все методы целого класса? Обязательно ли проходиться ним вручную и на каждый вешать по декоратору? Нет! Классы тоже можнодекорировать:@logclassOneOperation(object):defdivision(self,a,b):returna/bdefoperation(self,a,b):returnself.division(a,b)OneOperation().operation(2,0)Результат:[2022-10-29 00:24:53.348855] |    2    |  ERROR  |  AUTO  | where: __main__.OneOperation.division() | time of work: 0.0000031 sec. | input variables: <__main__.OneOperation object at 0x7fd1805a4100> (OneOperation), 2 (int), 0 (int) | local variables: self = <__main__.OneOperation object at 0x7fd1805a4100> (OneOperation), a = 2 (int), b = 0 (int) | exception: ZeroDivisionError("division by zero") | traceback: return a / b (".../main.py", line 10, in division)Если вам все же не хватило автоматического логирования, вы можете писать логи вручную, вызываяlog()как функциюиз своего кода:log("All right!")log("It's bad.",exception=ValueError("Example of an exception."))В консоли:[2022-10-28 16:11:27.704617] |    1    | UNKNOWN | MANUAL | "All right!" | where: ?
[2022-10-28 16:11:27.704997] |    2    |  ERROR  | MANUAL | "It's bad." | where: ? | exception: ValueError("Example of an exception.") | no tracebackНаконец, блок кода можно обернуть в логирующий контекстный менеджер:withlog("It's the context"):some_function()Результат:[2022-10-28 16:14:21.993514] |    1    | SUCCESS | MANUAL | "It's the context" | where: ? | time of work: 0.00001192 sec.На этом введение закончено. Если вам интересны тонкости настройки логгера и его более мощные функции, можете почитать более подробную документацию.Зачем это нужно? О фреймворкеПроцесс выполнения любой программы состоит из событий: вызываются функции, поднимаются исключения и т. д. Эти события, в свою очередь, состоят из других событий, масштабом поменьше. Задача логгера - записать максимально подробный отчет обо всем этом, чтобы программист в случае сбоя мог быстро найти ошибку и устранить ее. Записать все события до мельчайших деталей невозможно - данных было бы слишком много. Поэтому обычно человек непосредственно указывает места в программном коде, записи из которых его интересуют.Обычно любой логгер, будь то Polog,loggingили какой-то иной, содержит некий базовый набор компонентов:Прежде всего, регистраторы логов. Чаще всего это какие-то функции, которые вызываются посреди основного кода программы. В Polog регистратор один - функцияlog, которую можно использовать одновременно и как обычную функцию, так и в видедекоратора(в том числедля классов) /контекстного менеджера.Далее, поток событий необходимо отфильтровать. Для этого используютсяуровни логирования. На каждое событие ставится метка об уровне его важности (число, к которому может быть привязано отдельное имя), а глобально на уровне программы устанавливается планка, ниже которой события отбрасываются как несущественные.На следующем шаге все оставшиеся события попадают в один компонент -движок. Задача движка - передать их на обработку специальным обработчикам и сделать немного другой, менее существенной работы.Обработчикичто-то делают с полученными сведениями. Типичный обработчик, к примеру,запишетинформацию о событии в файл.Как уже сказано, описанная выше схема является типичной для логгеров, и Polog в этом плане вполне обычен. Так чем же он отличается? Давайте сравним его сlogging, вызовы которого обычно выглядят как-то так:importlogginglogging.debug('Skip this message!')logging.info("Sometimes it's interesting.")logging.warning('This is serious.')logging.error('PANIC')logging.critical("I'm quitting.")Заметили некоторую многословность? Для каждого события вы обязаны указать его уровень, вызвав по имени нужную функцию. Но в реальных программах большинство программистов крайне редко используют больше 2-х уровней: для обычных событий и для ошибок. В Polog вы можетеустановитьдефолтные уровни логирования для этих двух видов событий, и тогда обычный вызов логгера будет выглядеть уже как-то так:log('A normal event.')Это важно не только из соображений красоты. Есть другая причина, экономия ментальных усилий. Вроде кажется не существенным, но знаете, почему большинство людей предпочитает дебажить и логировать черезprint? Потому что это просто, а им есть, о чем еще подумать, пока они пишут свою программу. Так вот, Polog - этодаже проще, чемprint.Вторая важная особенность Polog: здесь отдается предпочтение логированиюцелых блоков кода, а не отдельных "точек" на линии потока событий в программе.Посмотрим опять на пример изlogging:deffunction(arg_1,arg_2):try:logging.info(f"begin of operations with{arg_1}and{arg_2}")operation(arg_1)operation_2(arg_2)logging.info(f"end of operations with{arg}and{arg_2}")exceptExceptionase:logging.error(f"ERROR of operations with{arg_1}and{arg_2},{e}")raiseМы видим здесь функцию, в которой "настоящей" логики всего пара строк. Остальное - логирование. Мы пишем логи когда операция начинается, когда кончается, и отдельно - в случае неудачи, для чего приходится еще городить блок try-except. Итого у нас на 2 строчки "настоящей" логики приходится еще 6 обеспечивающих логирование. Конечно, это утрированный пример и обычно в реальном коде все не так плохо. Чаще всего в функциях все же немного больше логики, обрамляемой логами, в результате чего соотношение строк "настоящего" кода и логирования становится чуть менее драматичным. Однако в целом неудобства все равно налицо:Логирование заставляет писать больше кода, причем шаблонного, в котором легко опечататься. К слову, в примере допущена опечатка, которая ломает логирование, попробуйте ее найти.Оно отвлекает от "настоящей" логики программы. Это происходит потому, что логи перемешаны с "настоящим" кодом, пишутся прямо посреди него. По сути с точки зрения логики программы логи являются не более чем визуальным мусором.Жить стало бы гораздо легче, будь у нас способ вынести логирование куда-то за пределы функции или блока кода, но когда это нужно - иметь возможность что-то подкорректировать и изнутри. Именно такой, гибридный способ логирования, и предоставляет Polog.С Polog пример кода выше превращаетсяв:@log('Some message.')deffunction(arg_1,arg_2):operation(arg_1)operation_2(arg_2)Уровень логирования здесь проставится автоматически, в зависимости от того, случится исключение или нет. Внутри функции нет ничего, кроме собственно логики, а все, что связано с логированием - вынесено за ее пределы.Еще с Polog можно воттак:deffunction(arg_1,arg_2):withlog('Some message.'):operation(arg_1)operation_2(arg_2)Опять же, логирование вынесено за пределы блока кода, выглядит компактно и не отвлекает от основной логики. Кстати, логирование кода блоками - еще и отличный способ улучшить навык декомпозиции. Если есть что-то, что вы хотели бы логировать отдельно, обычно это является блоком кода, который неплохо бы вынести в отдельную функцию.Третье важное отличие Polog - это отделение регистрирующей части от обработчиков. Вloggingрегистраторы логов представляют из себя дерево, у которого есть свойroot. К регистраторам привязаны обработчики, а вы пишете логи в регистраторы. Дерево регистраторов по сути разбросано по разным файлам вашей программы, в результате чего вы вынуждены повсюду создавать экземпляры регистраторов, если хотите иметь возможность специфицировать набор обработчиков для разных участков. В Polog тоже естьдерево, но это деревообработчиков, а не регистраторов. За счет того, что объект регистратора всего один, появляется возможность установитьlogкак built-in функцию, которую не нужно даже импортировать (это можно включить черезнастройки) в разных файлах программы.Помимо перечисленного, Polog отличается от других логгеров еще множеством разных деталей, о них читайте ниже.log()- одна функция, чтобы править всемиОдно из главных преимуществ Polog - фреймворк предоставляет несколько способов собирать логи через одну и ту же супер-функцию. Импортируется она так:frompologimportlogДля удобства черезнастройкивы можете включить режим, при котором эта функция будет доступна по всей программе без дополнительных импортов, какprint:config.set(log_is_built_in=True)logможно использовать для обычногоручногологирования:log('plain text')Или в качествеконтекстного менеджера/ декоратора дляфункций(в том числе корутинных) иклассов:|@log('plain text')|@log('plain text')|@log('plain text')withlog('plain text'):|deffunction():|asyncdeffunction():|classSimpleClass:...|...|...|...Функциясама понимает контекст, в котором она была вызвана! И подстраивает свое поведение соответственно.Во всех примерах выше сообщение лога передается первым неименованным аргументом. Если вам нечего сказать, можно использоватьlogи без скобок:|@log|@log|@logwithlog:|deffunction():|asyncdeffunction():|classSimpleClass:...|...|...|...Имя уровня логирования можно указывать через точку, предварительнозарегистрировавего через настройки. Посмотрим, как это работает, предварительно зарегистрировав новое имя:config.levels(some_level=5)Теперь это имя можно использовать при любом способе логирования:# Обычная функция.log.some_level('plain text')# Контекстный менеджер.withlog.some_level:|withlog.some_level('plain text'):...|...# Декоратор функций.@log.some_level|@log.some_level('plain text')deffunction():|deffunction():...|...# Декоратор корутинных функций.@log.some_level|@log.some_level('plain text')asyncdeffunction():|asyncdeffunction():...|...# Декоратор классов.@log.some_level|@log.some_level('plain text')classSimpleClass:|classSimpleClass:...|...Но можно передать уровень логирования и в качестве аргумента, также будет работать при любом способе сбора логов:log('plain text',level=5)# Будет работать в том числе через декораторы и контекстные менеджеры.log('plain text',level='some_level')Если уровень не указать, по умолчанию для обычных событий будет использоваться тот, что указан внастройкахкакdefault_level, а в случае ошибок -default_error_level.Более подробно о специфических особенностях каждого из перечисленных выше способов регистрации логов читайте далее."Ручное" логированиеВажно: все приведенные здесь примеры ручного логирования могут работать немного странно, если запускать их черезREPL. Это связано с особенностью реализации: при определении контекста, в котором вызывалась функцияlog, используется отслеживание жизненного цикла возвращаемого значения функции. Запись происходит при обнулении счетчика ссылок на этот объект. В REPL же последнее возвращенное значение сохраняется в переменной_, то есть счетчик ссылок обнулится и лог запишется, когда код в следующей строке перезапишет_.Мы начнем с самого привычного способа записывать логи, когда сообщение просто передается в функциюlog:log('Very important message!!!')Строка с сообщением заполняет полеmessageв получившейсязаписи.Уровень логированияможно передать в качестве аргумента, по имени или числом:# Когда псевдонимы для уровней логирования прописаны по стандартной схеме.log('Very important message!!!',level='ERROR')# Ну или просто в виде числа.log('Very important message!!!',level=40)Но есть и другой способ, можно писать названия уровней логирования через точку послеlog:frompologimportconfigconfig.levels(lol=100)# Присваиваем уровню 100 имя "lol".log.lol('kek')# Регистрируем лог уровня "lol".Обратите внимание, в примере выше Polog не знал заранее, что для уровня 100 будет использовано имяlol, резолвинг имен происходит прямо на месте. По сути такой способ записи был бы полностью эквивалентен вот такой записи:log('kek',level='lol')Вы можете передать вlog()функцию, в которой исполняется код:deffoo():log(function=foo)Поляfunctionиmoduleв этом случае заполнятся автоматически.Также вы можете передать вlog()экземпляр исключения (но на самом деле в такой ситуации гораздо удобнее использоватьконтекстный менеджер):try:var=1/0exceptZeroDivisionErrorase:log('I should probably stop dividing by zero.',exception=e)Поляexception_messageиexception_typeтут тоже заполнятся автоматически. Флагsuccessбудет установлен в значение False. Трейсбек и локальные переменные той функции, где произошла ошибка, заполнятся автоматически.При желании, в качестве аргументовfunctionиexceptionможно использовать и обычные строки, но тогда дополнительные поля не заполнятся сами как надо.Еще можно передать в логгер название или объект класса в виде аргументаclass_(с нижним подчеркиванием, поскольку слово "class"зарезервировано):classCar:defwash(self):log('The washing begins.',class_=type(self))...И наконец, вы можете передавать вlog()произвольные переменные, которые считаете нужным залогировать.defbar(a,b,c,other=None):...log(':D',function=bar,other=other)...Контекстный менеджерЕще один удобный способ записывать логи - оборачивать блоки кода в контекстные менеджеры. В Polog эта возможность тоже встроена прямо в универсальную функциюlog()- достаточно лишь использовать ее соответствующим образом:withlog('Go out from the context!'):...Или даже так, это эквивалентно, просто записи будут без сообщений:withlog:...Контекстный менеджер засекает время работы обернутого им блока кода. Кроме того, он автоматически записывает информацию о произошедших внутри него исключениях.В реальных программах очень часто встречаются случаи, когда логгер вызывается внутри блока except (или хуже того, ошибки простоподавляютсябез всяких следов, даже в виде логов). С использованием стандартногоloggingэто обычно выглядит примерно так:try:some_function()exceptSomeException:logging.exception('Error in some operation!')При использовании контекстного менеджера Polog то же самое займет меньше строчек:withlog('Some operation!').suppress(SomeException):some_function()Продемонстрированный здесь метод.suppress()работает очень похоже наsuppressиз стандартной библиотеки Python. Единственное отличие, если сюда не передать никаких аргументов, подавляться будут абсолютно все исключения:withlog('Some exception!').suppress():raiseValueError# Подавлено будет абсолютно любое исключение, включая это.Если данный паттерн подавления исключений вам приходится использовать достаточно часто, вы можете включить подавление исключений в контекстных менеджерах логирования по умолчанию, выставивнастройкуsuppress_by_defaultв значениеTrue:config.set(suppress_by_default=True)Также контекстный менеджер позволяет редактировать создаваемую запись лога прямо изнутри контекста:withlog('The message before change.')ascontext:context('The message after change.',some_variable='some text')В примере выше мы изменили сообщение лога, а также добавили в него новое поле -some_variable.При использованииlogбез скобок это тоже сработает:withlogascontext:context('The message.',some_variable='some text')Изменение лога внутри контекста доступно и при использовании метода.suppress():withlog('The message before change.').suppress()ascontext:context('The message after change.',some_variable='some text')raiseValueErrorДекорируем функцииОбъектlogможет быть использован как декоратор для автоматического логирования вызовов функций. Поддерживает как обычные функции, так икорутинные. Интересная особенность: декоратор по умолчанию не влияет на трейсбек в случае возникновения иключений внутри функции: он выглядит ровно так, будто декоратора там нет.@logможно использовать как со скобками, так и без. Вызов без скобок эквивалентен вызову со скобками, но без аргументов. То есть можно так:@logdeffunction():...... а можно и так, разницы нет:@log()deffunction():...Как вы уже могли прочитать разделом выше, к каждой записи можно добавить произвольный текст, передав его первым неименованным аргументом:@log('This function is very important!!!')defvery_important_function():...Дальше в документации вы можете подробнее узнать, что такоеобработчики. Пока вкратце: обработчик - это функция, которая фактически производит какое-то финальное действие с логом, напримерзаписывает его в файл. Обработчики можно регистрировать глобально, а можно передать в конкретный декоратор списком:defprint_log(log_item):# Пример супер-примитивного обработчика. Не делайте так, придумайте что-то свое.print(log_item)@log(handlers=[print_log])deffunction():...Также далее в документации вы можете прочитать, как работаютизвлекаемые поля. Их тоже можно указать как глобально (и тогда они будут срабатывать для всех регистрируемых логов), так и локально для декоратора:frompologimportfield@log(extra_fields={'kek':field(lambdalog_item:'kek')})deffunction():...А если вам позарез нужно, чтобы использовался как глобальный, так и локальный набор полей, возьмите словарь с полями в квадратные скобки и добавьте троеточие в конце, вот так:@log(extra_fields=[{'kek':field(lambdalog_item:'kek')},...])deffunction():...По умолчанию данные для дополнительных полей извлекаются "на месте", то есть гарантированно в том же потоке, где выполняется основной код. Однако в некоторых случаях вы можете решить вынести извлечение полей внутрь движка, который может быть имногопоточным, для этого заменитеextra_fieldsнаextra_engine_fields:@log(extra_engine_fields=[{'kek':field(lambdalog_item:'kek')},...])deffunction():...Но имейте в виду, нужно это крайне редко.Дедупликация исключенийПри использовании для логированиядекораторов, может возникнуть ситуация, когда одно и то же исключение пройдет через разные декораторы несколько раз:@logdeffunction_3():raiseValueError@logdeffunction_2():returnfunction_3()@logdeffunction_1():returnfunction_2()function_1()Чтобы избежать заполнения хранилища логов мусором, в Polog предусмотрен режим дедупликации исключений при проходе через декораторы. В этом режиме, когда декоратор Polog встречает исключение в первый раз - оно записывается, а дальше - игнорируется. Этот режим не нужно специально включать, он уже работает по умолчанию. Однако если вам это по каким-то причинам не нравится, можно его наоборот отключить, изменивнастройкуdeduplicate_errors:config.set(deduplicate_errors=False)Декорируем классыПомимо функций, объектомlogвы можете декорировать и целые классы. Все работает точно так же: можно указывать или не указывать все те же аргументы, использовать декоратор как со скобками, так и без.При этом игнорируются дандер-методы (это те, чьи названия начинаются и заканчиваются символами "__").Если не хотите логировать все методы класса, можете передать в декоратор список или кортеж с названиями нужных:@log('This class is also very important!!!',methods=('important_method',))classVeryImportantClass:defimportant_method(self):...defnot_important_method(self):......Не забывайте, что при наследовании вы получаете классвместе с навешенным на его родителя логированием, и это логирование не знает, что работает уже не в оригинальном классе, а в наследнике. Если на наследника вы тоже навесите@log, логирование родителя у класса-ребенка заменится собственным. Но если вы этого не сделаете, логироваться он будет как родитель.Перекрестное декорированиеПри наложении на одну функцию нескольких декораторов логирования, срабатывает из них по итогу только один. Это достигается за счет наличия внутреннего реестра задекорированных функций. При каждом новом декорировании оборачивается оригинальная функция, а не ее уже ранее задекорированная версия.Пример:@log(level=6)# Сработает только этот декоратор.@log(level=5)#\@log(level=4)# |@log(level=3)#  > А эти нет. Они знают, что их несколько на одной функции, и уступают место последнему.@log(level=2)# |@log(level=1)#/defsome_function():# При каждом вызове этой функции лог будет записан только 1 раз....Мы наложили на одну функцию 6декораторов, однако реально сработает из них только тот, который выше всех. Это удобно в ситуациях, когда вам нужно временно изменить уровень логирования для какой-то функции. Не редактируйте старый декоратор, просто навесьте новый поверх него, и уберите, когда он перестанет быть нужен.Также вы можете совмещать декорированиеклассаи егоотдельных методов:@log(level=3)classSomeClass:@log(level=10)defsome_method(self):...defalso_some_method(self):......Удекоратора методаприоритет всегда выше, чем удекоратора класса, поэтому в примере some_method() окажется задекорирован только черездекоратор метода, а остальные методы - черездекоратор класса. Используйте это, когда вам нужно залогировать отдельные методы в классе как-то по-особенному.Запрет логирования через декоратор@unlogНекоторые вещи логировать опасно. К примеру, если вы пишете web-сервис, такими вещами могут быть пароли пользователей или токены для доступа к другим сервисам. Для таких случаев в Polog есть специальный декоратор -@unlog. Он делает так, что на защищенной им функции больше не будут срабатыватьлогирующие декораторы. Кроме того, по умолчанию там не будет работать иручное логирование. Еще данный декоратор можно навесить на целый класс, и тогда все перечисленное будет работать на всех его методах. Ниже будут примеры.Импортируется@unlogтак:frompologimportunlogНо если вам не хочется в лишний раз импортировать@unlog, можно воспользоваться уже импортированным объектомlog, подставляя-перед ним:@(-log)# Работает точно так же, как @unlog.defsome_function():...Простейший случай, "отмена" действия логирующего декоратора:@unlog@log(level=5)# Этот декоратор не сработает.defsome_function():..."Отмена" действия ручного логирования:@unlogdefsome_function():log('hello, kitty!')# Эта функция тоже не будет работать.Немного более сложный случай. Мы навешиваем@unlogна функцию, из которой вызывается вторая функция, и вот на той второй функции логирование тоже перестанет работать, когда она вызывается из первой:@log(level=5)# Это не сработает.defsome_function_2():log('hello, kitty!')# И это тоже.@unlogdefsome_function():some_function_2()some_function()Дело в том, что как только выполнение кода проходит через декоратор@unlog, логирование выключается во всем "вложенном" коде выше по стеку вызовов функций. Это крайне полезная возможность, поскольку она позволяет гарантировать, что какая-то область кода, работающая с чувствительными данными, не только не запишет ничего лишнего в области вашей видимости, но и не сделает этого нигде из тех других функций, которые здесь вызываются (а там может быть довольно много кода, который вам иначе придется просмотреть "глазками").Также@unlogможно использовать для методов класса:@logclassVeryImportantClass:defimportant_method(self):...@unlogdefnot_important_method(self):......Иногда это может быть удобнее, чем прописывать "разрешенные" методы в самомдекораторе класса. Например, когда в вашем классе много методов и строка с их перечислением получилась бы слишком огромной.Ну и на классы тоже можно навешивать@unlog:@unlogclassNotImportantClass:defsome_method(self):log('some boring message')# Не будет работать.Выше было сказано, что по умолчанию@unlogотключает на обернутой им функции какдекораторы логирования, так иручныелоги. Но это поведение можно изменить при помощинастройкиfull_unlog. По умолчанию она установлена в значениеTrue, однако если изменить ее наFalse, то@unlogпо-прежнему будет глушить декораторы логирования, но станет пропускать ручные вызовы логгера:frompologimportconfigconfig.set(full_unlog=False)@unlog@log# Этот декоратор по-прежнему не сработает.defsome_function():log('hello, kitty!')# А это сработает!some_function()Кроме того, не обязательно изменять данную настройку глобально.@unlogможно вызвать со скобками, передав туда аргументfull:@unlog(full=False)@log# Это не будет работать.defsome_function():log('hello, kitty!')# А это будет.Локальные аргументы имеют приоритет над выставленной глобально настройкойfull_unlog.Располагая@unlogсреди других декораторов, вам нужно учитывать, что это будет работать при соблюдении хотя бы одного из двух условий:Декоратор@unlogрасположен поверх всех прочих декораторов логирования на данной функции, например вот так:@unlog@log(level=5)# Этот декоратор не сработает.@log(level=4)# И этот.@log(level=3)# И этот.@log(level=2)# И вот этот.@log(level=1)# И даже этот.defsome_function():...Он непосредственно прилегает к одному или нескольким логирующим декораторам, без прослойки в виде каких-то сторонних декораторов (не из Polog):@log(level=5)# Этот декоратор не сработает.@log(level=4)# И этот.@log(level=3)# И этот.@unlog@log(level=2)# И вот этот.@log(level=1)# И даже этот.defsome_function():...Однако! Если@unlogнаходится ниже другого логирующего декоратора и при этом их разделяет какой-то сторонний декоратор, защита может перестать работать:@log(level=2)# Этот декоратор может сработать.@other_decorator# Какой-то сторонний декоратор.@unlog@log(level=1)# Этот декоратор не сработает, т.к. сообщается с @unlog.defsome_function():...Это связано с особенностями реализации@unlog. Он запоминает, какие функции ранее уже попадались декораторам Polog. Если обернуть функцию сторонним декоратором, для Polog она уже воспринимается как "новая", ранее не известная. Поэтому декораторы Polog лучше всего располагать поверх прочих декораторов, которые вы используете. Исключение - регистрирующие декораторы, например роуты во фреймворках вроде Flask. Там синтаксис декораторов используется не для того, чтобы подменить оригинальную функцию, а для регистрации ее где-то. Для корректной работы регистрирующих декораторов, они должны быть размещены поверх всех прочих. То есть иерархия декораторов должны быть по следующей (чем больше номер - тем дальше от определения оригинальной функции): 1. обычные сторонние декораторы, 2. декораторы Polog, 3. регистрирующие декораторы.Редактируем автоматические логи из задекорированных функцийИспользуя декораторы Polog, иногда вы можете столкнуться с необходимостью добавить или изменить какую-то информацию, которая логируется автоматически. В этом вам поможет функцияmessage().Пример работы:frompologimportmessage@log('original message')defsome_function():message('new message')В полученном логе поле 'message' будет заполнено первым аргументом функцииmessage().У объектаlogесть метод, который делает то же самое. Вы можете применять его, чтобы не импортироватьmessage()отдельно:@log('original message')defsome_function():log.message('new message')Также вы можете передавать вmessage()другие именованные аргументы:eилиexception(Exception) - экземпляр исключения, которое вы хотите залогировать. Название и сообщение из него будут извлечены автоматически, однако меткаsuccessзатронута не будет.success(bool) - метка успешности операции.level(str, int) -уровеньлога.Умные ассертыОдной из популярных техник программирования на Python являетсярассеивание инструкцийassertпо всему коду. Пока программа запускается в тестовом режиме, это позволяет отловить часть ошибок и неожиданного поведения, а когда дело доходит до "промышленного" применения - ассерты просто выключаются при помощиспециального флага.В Polog встроена собственная "обертка" над ассертами. На этапе отладки она работает по сути точно так же, как оригинал, а когда ассерты выключены - пишет логи вместо поднятия исключений. Таким образом вы даже в продакшене будете видеть, что в вашем коде пошло не так.Выглядит это так:frompologimportassass(False,"It's bad.")# Эквивалентно: assert False "It's bad."Если хотите, можно включить режим записи логов даже в режиме дебага. Для этого нужно изменить настройки:frompologimportconfigconfig.set(smart_assert_politic='all')Интеграция сloggingPolog - самобытный фреймворк, местами расходящийся сloggingна концептуальном уровне. Однако, поскольку множество легаси-проектов уже используютlogging, взять и начать использовать там Polog может быть не так просто и удобно, как если бы это произошло в самом начале. Кроме того, синтаксисloggingнастолько привычен для большинства Python-разработчиков, что даже сторонние библиотеки часто вынуждены ему подражать. Поэтому в Polog существуют средства, позволяющие в некоторой степени "подружиться" сlogging.Прежде всего, Polog поддерживает прямой перехват событий изlogging, причем это происходит по умолчанию:importloggingfrompologimportconfig,file_writerconfig.add_handlers(file_writer())logging.warning('oops!')Представленный выше код должен вывести что-то вроде:[2015-10-21 18:52:12.856087] |   30    |  ERROR  | MANUAL | "oops!" | where: main.? | line_number: 6 | path_to_code: ".../main.py" | thread: "MainThread (8628192768)" | process: "MainProcess (10281)" | from_logging: TrueКак видите, это не потребовало вообще никаких доработок или настроек на сторонеlogging. Но как это работает? В процессе участвуют две пунктанастроекPolog, которые по умолчанию обе возведены в положениеTrue:integration_with_loggingиlogging_off. В них нет никакой магии. Первая в значенииTrueдобавляет фильтр вroot loggerбиблиотекиlogging, который перерегистрирует содержимое записи уже внутри Polog. То есть фильтр в данном случае используется по сути какколлбек. Вторая - это то значение, которое данный фильтр возвращает:TrueилиFalse.Если вы знакомы с устройствомlogging, вы можете предвидеть, что данный метод перехвата не сработает, если используется неroot logger. То есть если, к примеру, он был создан как-то воттак:logger=logging.getLogger(__name__)В таком случае красиво уже не сделать, придется добавлять каждому логгеру фильтр самостоятельно:frompolog.core.stores.settings.actionsimportfrom_logging_filter_to_pologlogger.addFilter(from_logging_filter_to_polog)# Не нравится, когда тычут в глаза камелкейсом? Переходите на Polog.Еще один аспект синхронизацииloggingи Polog - именауровней логирования. Дело в том, что Polog вам никак не предписывает, какие имена вы должны присваивать разным уровням. Вloggingже имена ужепридуманыза вас:Числовое значениеИмя50CRITICAL40ERROR30WARNING20INFO10DEBUG0NOTSETЕсли вы уже привыкли к таким именам, можно легко добавить их в Polog:frompologimportlog,config,file_writerconfig.add_handlers(file_writer())config.standard_levels()log.error('My dog ate my homework :(')# Это сработает так, как вы ожидаете.Общие настройкиОдно из основных преимуществ Polog - возможность изменить любую глобальную настройкув любой момент. При этом гарантируется, что не будет потеряна ни одна запись лога. Происходит это через классconfig, который импортируется вот так:frompologimportconfigВсе методы от него вызываются через точку, без вызова __init__, например вот так:config.set(pool_size=5)Методы классаconfig:set(): общие настройки логгера.Принимает следующие именованные параметры:pool_size(int) - количество потоков-воркеров. Его можно увеличивать, но при этом стоит помнить, чтобольшое число потоков - это большая ответственностьдополнительные потоки повышают накладные расходы интерпретатора и могут замедлить вашу программу. При значении 0 все логи будут обрабатыватьсябез использования дополнительных потоков.max_queue_size(int) - максимальный размер очереди. Бесконечен в случае значения 0. Данный пункт актуален только в случае не нулевого значенияpool_size.service_name(str) - имя сервиса. По умолчанию не задано, но вы можете передать сюда желаемое имя и оно будет отображаться во всех логах. Если вы используетемикросервисную архитектуру, сюда рекомендуется пробрасывать уникальный идентификатор сервиса, к примеру, через переменные окружения.level(int, str) - общийуровень логирования. События уровнем ниже записываться не будут.default_level(int, str) -уровень логирования, с которым по умолчанию логируются обычные события. По умолчанию он равен 1.default_error_level(int, str) -уровень логирования, с которым по умолчанию логируются ошибки. По умолчанию он равен 2-м.max_delay_before_exit(int, float) - задержка в секундах перед завершением программы для записи оставшихся логов. При завершении работы программы может произойти небольшая пауза, в течение которой будут записаны оставшиеся логи из очереди. Настройка изменяет максимальную продолжительность такой паузы, однако если все логи успели записаться быстрее, лишнего ожидания не произойдет. Также стоит учитывать, что при значенииpool_size, равном нулю, манипуляция данной настройкой становится бессмысленной, поскольку при этом помещение логов в очередь перед записью не происходит.silent_internal_exceptions(bool) - "лояльность" при неправильных вызовахручного логирования. В значенииTrueпри передаче неправильных аргументов или ином некорректном использовании исключения не поднимаются, и по возможности ваши данные все же будут записаны. В значенииFalseпри неправильном использовании лог записываться не будет, а также будет поднято исключение с сообщением об ошибке. При проектировании сервисов с использованием Polog рекомендуется устанавливать данную настройку в значениеFalseна этапе отладки, и переходить на значениеTrueпри реальной эксплуатации.json_module(module) - модуль для обработки данных в форматеjson. Обязательно должен включать 2 функции:loads()иdumps(). Для ускорения работы рекомендуется передать сюда модуль из библиотекиujson.time_quant(int, float) - продолжительность (в секундах) некоторых внутренних операций в Polog. Изменять не рекомендуется.fields_intersection(bool) - поведение при пересечении различныхизвлекаемых полейпод одинаковым именем. Еслиfields_intersectionустановлено в значениеTrue, то внутри движка может происходить перезапись полей, извлеченных ранее. В противном случае - нет.unknown_fields_in_handle_logs(bool) - можно ли передавать при ручном логировании функциейlog()аргументы с неизвестными именами. По умолчанию можно (True).deduplicate_errors(bool) - режим дедупликации автоматических логов об исключениях. По умолчанию -True, то есть включен.log_is_built_in(bool) - устанавливаем функциюlog()в качестве встроенной. По умолчанию -False, то есть данный режим выключен. Если включить,log()можно будет вызывать из любого места программы без дополнительного импорта, по аналогии, к примеру, со встроенной функциейprint().full_unlog(bool) - режим, в котором работает декоратор@unlog. По умолчанию (True) при навешивании данного декоратора любой вид логирования внутри обернутой функции перестает работать, в том числеручной. В значенииFalseперестают работать прилегающие к@unlogдекораторы логирования, однако ручное логирование работать продолжит.suppress_by_default(bool) - режим подавления исключений вконтекстном менеджере. По умолчанию (False) контекстный менеджер не влияет на любые поднимающиеся внутри него исключения, он лишь регистрирует их. Однако в режимеTrueон подавляет все исключения (чем напоминаетsuppressиз стандартной библиотеки), если только иное поведение не будет установлено для конкретного контекста локально.suppress_exception_subclasses(bool) - позволяет немного изменить способ подавления исключений вконтекстном менеджере. При значенииTrue(по умолчанию), при передаче в метод.suppress()одного или нескольких исключений, подавляться будут не только они сами, но и все их подклассы. В режимеFalse- только сами переданные классы.integration_with_logging(bool) -интеграцияс модулемloggingиз стандартной библиотеки. При значенииTrue(по умолчанию) записи логов изloggingкопируются в Polog, а приFalse- нет. Привести данную настройку вFalseможно только при условии, чтоlogging_offтакжеFalse.logging_off(bool) - блокировка вывода всех логов через модульlogging. В значенииTrue(по умолчанию) все записи вloggingигнорируются, не передаются обработчикам. Этот механизм может работать только при условии, что настройкаintegration_with_loggingнаходится также в положенииTrue(сочетание, при которомlogging_offиintegration_with_loggingнаходятся в положениях, соответственно,TrueиFalse, является невозможным). В значенииFalseвсе записи будут игнорироваться вlogging, однако за счет того, что настройкаintegration_with_loggingстоит в положенииTrue, они будут обрабатываться внутри Polog. То есть это работает по сути как чистый перехват всех сообщений. Они не доходят до адресатов (обработчиков) внутри системыlogging, но обрабатываются внутри Polog.traceback_cutting(bool) - обрезание трейсбека при использованиидекоратора для функций. В значенииTrue(по умолчанию) из трейсбека вырезаются фрагменты, отражающие работу самого декоратора. То есть декоратор не должен как-либо воздействовать на трейсбек. Если отключить данный режим, трейсбек станет более длинным и менее информативным для большинства пользователей, однако более "честным".levels(): присвоение именуровням логирования.standard_levels(): присвоение стандартных именуровням логирования.add_handlers(): регистрация новыхобработчиков.get_handlers(): получениедерева обработчиков.delete_handlers(): удалениеобработчиков.add_fields(): регистрация новыхизвлекаемых полей.delete_fields(): удаление ранее зарегистрированныхизвлекаемых полей. Стандартные поля удалить нельзя.add_engine_fields(): регистрация новыхизвлекаемых полей, обрабатываемых внутри движка.delete_engine_fields(): удаление внутридвижковыхизвлекаемых полей.get_in_place_fields(): получение словаря сизвлекаемыми полями, которые отрабатывают до попадания лога внутрь движка. Если не передавать в метод ничего, вернет все такие поля, а если передать одно или несколько имен (в качестве неименованных аргументов), то вернет только их.get_engine_fields(): получение словаря сизвлекаемыми полями, которые отрабатывают внутри движка. По аналогии сget_in_place_fields(), если не передавать в метод ничего, вернет все такие поля, а если передать одно или несколько имен (в качестве неименованных аргументов), то вернет только их.get_all_fields(): получить словарь сизвлекаемыми полямивсех видов (то есть как отрабатывающих внутри движка, так и до). По аналогии сget_in_place_fields()иget_engine_fields(), если не передавать в метод ничего, вернет все такие поля, а если передать одно или несколько имен (в качестве неименованных аргументов), то вернет только их. В случае, если два разных экстрактора для извлечения вне движка и внутри него зарегистрированы под одинаковыми именами, данный метод учитывает значение настройкиУровни логированияУровни логирования - это универсальный и удобный способ разделить все события на группы, и разбить эти группы на две части: одну мы логируем, а другую - нет. "Разделителем" служит глобальный уровень логирования, его мы устанавливаем для всей программы. Каждое событие также имеет пометку об уровне. Если уровень события больше, чем глобальный, или равен ему, оно будет записано, а иначе - проигнорировано. Вы можете не указывать при каждом вызове логгера уровень, которым будут помечаться отслеживаемые им события. В этом случае события будут помечаться уровнями по умолчанию. Для обычных событий уровнем по умолчанию является 1, для ошибок (например, когда в задекорированной логгером функции происходит исключение) - 2. Однако это можно изменить.В декораторе вы можете указать уровень, которым будут помечаться все вызовы обернутой им функции:frompologimportlog@log(level=5)defsum(a,b):returna+bprint(sum(2,2))# Запишется лог с меткой 5 уровня.Это доступно при декорировании как функций, так и классов, работает одинаково.В декораторе вы также можете установить метку уровня, которой будут помечаться только ошибки:@log(level=5,error_level=10)Это может быть вам полезно, поскольку часто ошибки важнее прочих событий в программе, и вы можете сделать так, чтобы только они проходили через "фильтр" общего уровня логирования.Также вы можете установить отдельный уровень логирования по умолчанию для ошибок глобально через настройки:frompologimportconfigconfig.set(default_error_level=50)После этого все ошибки, зарегистрированные декораторами, по умолчанию будут снабжаться соответствующей меткой уровня. Однако в декораторах вы можете продолжать его указывать, поскольку указанный там уровень имеет приоритет над глобальным.Для обычных событий тоже можно установить уровень по умолчанию:config.set(default_level=10)Уровням логирования можно присвоить имена и в дальнейшем использовать их вместо чисел:frompologimportlog,config# Присваиваем уровню 5 имя 'ERROR', а уровню 1 - 'ALL'.config.levels(ERROR=5,ALL=1)# Используем присвоенное имя вместо номера уровня.@log(level='ERROR')defsum(a,b):returna+bprint(sum(2,2))# Запишется лог с меткой 5 уровня.При этом указание уровней числами вам по-прежнему доступно, имена и числа взаимозаменяемы.Также, зарегистрировав имена уровней логирования, вы можете указывать их через точку, причем как при исьзовании объектаlogв качестве декоратора, так и при "ручном" логировании:frompologimportlog,configconfig.levels(halloween_level=13)@log.halloween_leveldefscary_function(a,b):...print(scary_function('friday',13))# Запишется лог 13-го уровня.log.halloween_level('boo!')# Также запишется лог 13-го уровня.Если вы привыкли пользоваться стандартным модулемlogging, вы можете присвоить уровням логированиястандартные именаоттуда:frompologimportlog,config# Имена уровням логирования проставляются автоматически, в соответствии со стандартной схемой.config.standard_levels()@log(level='ERROR')defsum(a,b):returna+bprint(sum(2,2))# Запишется лог с меткой 40 уровня.Общим уровнем логирования вы можете управлять через настройки:frompologimportlog,config# Имена уровням логирования проставляются автоматически, в соответствии со стандартной схемой.config.standard_levels()# Устанавливаем текущий уровень логирования - 'CRITICAL'.config.set(level='CRITICAL')@log(level='ERROR')defsum(a,b):returna+bprint(sum(2,2))# Запись произведена не будет, т. к. уровень сообщения 'ERROR' ниже текущего уровня логирования 'CRITICAL'.Все события уровнем ниже игнорируются.Движки: синхронный и асинхронныйВсе логи в Polog так или иначе проходят через движок. Задача движка - взять лог и передать его во все нужные обработчики по очереди.Движок в Polog является сменяемым компонентом. Вы можете изменить некоторые пунктынастроек, которые подразумевают перезагрузку движка. В этом случае движок перезагрузится автоматически, причем в этом случае гарантируется, что не будет потеряна ни одна запись.Примерами настроек, подразумевающих перезагрузку движка, являютсяpool_sizeиmax_queue_size. Чтобы объяснить, почему при их изменении происходит перезагрузка движка, нужно рассказать, какие вообще бывают движки и как в одном из них (асинхронном) задействуются эти параметры.Синхронный движокпо своей сути очень примитивен. Это обычный объект, у которого есть метод, в который можно передать лог, и он передаст этот лог во все прикрепленные к нему обработчики. Вызов такого метода является блокирующей операцией, то есть ваша программа будет ждать, пока выполнится код всех обработчиков, и только после этого продолжит свое выполнение. За счет своей простоты такой обработчик порождает наименьший оверхед. Кроме того, в случае, к примеру, внезапного отключения питания компьютера, будет потеряна максимум одна запись лога - та, что на момент отключения была в работе. Какие тут минусы? При использовании обработчиков, которые, к примеру, передают логи куда-то по сети, сетевые задержки могут сильно тормозить вашу программу. Особенно это критично, если логгер используется внутри какого-нибудь нагруженного сервиса. Запросы на такой сервис могут приходить не равномерно, и сетевые операции обработчиков могут сильно снизить допустимую пиковую нагрузку.Асинхронный движокв некоторых ситуациях (но далеко не всегда) может ускорить работу программы в целом. Его устройство уже значительно сложнее, чем у синхронного, и включает в себя несколько компонентов: очередь логов; объект, который кладет в нее логи; и несколько потоков с воркерами. Воркер - это некоторая функция, внутри которой запущен бесконечный цикл ожидания лога из очереди. Как только лог там появляется, кто-то из воркеров первым его забирает и далее с ним работает точно так же, как это делал бы обычный синхронный движок. В случае пиковой нагрузки, при которой воркеры не успевают обрабатывать поступающие логи, очередь растет, как бы "размазывая" во времени дополнительную нагрузку на сервер, которую создает сам логгер. Однако за все приходится платить, в данном случае - памятью и вычислительным оверхедом (который еще усугубляется наличиемGIL). Чтобы выяснить, ускорят асинхронный движок и дополнительные потоки ваше приложение, или замедлят, нет другого способа, кроме нагрузочного тестирования, причем желательно проводить его в как можно более приближенных к реальности условиях. Реальное ускорение / замедление программы сильно зависит от характеристик железа, конкретного набора обработчиков, обрабатываемых данных и, возможно, от других факторов. Кроме того, вам нужно учитывать, что при работе асинхронного движка не гарантируется правильный порядок записи логов. Это связано с тем, что, хоть они и поступают в очередь практически в том же порядке, в каком происходили события вашей программы, время работы разных обработчиков может быть разным.Настройкиpool_sizeиmax_queue_sizeвлияют на выбор и характеристики движков. Установкаpool_sizeв значение 0 (по умолчанию) приведет к загрузке синхронного движка, а любое значение больше 0 - асинхронного с соответствующим количеством потоков с воркерами. Пунктmax_queue_size- это лимит числа логов в очереди для асинхронного движка. При значении 0 (то есть по умолчанию) лимит полностью отключается, с чем нужно быть осторожнее, поскольку очередь может стать местом утечки памяти в случае хронической нехватки мощности обработчикам. Если установить сюда любое положительное значение, при попытке положить в очередь новый лог, программа заблокируется до момента, пока кто-то из воркеров не заберет один лог из очереди.При изменении любой из этих настроек происходит следующее:Движок временно блокируется на запись логов. Функции, которые его вызывают, как бы подвиснут до момента, пока перезагрузка завершится.Работа движка прекращается. В случае синхронного движка это означает, что возможно, просто записывается последний лог. Для асинхронного все сложнее. У него логи могут быть в двух разных местах. Во-первых, у него может быть не пустая очередь. То есть нужно дождаться, пока она опустеет. Во-вторых, каждый из воркеров может еще продолжать работу с последним из взятых логов. Поэтому, после опустения очереди, программа передает сигналы каждому воркеру, что после отработки текущего лога нужно завершиться. Воркер после обработки каждого лога проверяет, не был ли ему передан такой сигнал, и, если да - разрывает бесконечный цикл, не приступая к ожиданию следующего лога. В случае же, когда воркер на момент получения сигнала не был занят обработкой лога, а ожидал его из очереди, все немного сложнее. Само по себе ожидание не бесконечно. На самом деле ожидание - это смесь бесконечного цикла и блокировки потока. Поток блокируется на некоторый промежуток времени (в настройках он фигурирует какtime_quant), после чего "просыпается" и проверяет, не поступало ли сообщение о завершении. Если да - выходит, нет - снова засыпает в ожидании лога.Загружается новый движок. Причем решение, какой движок загружать и с какими параметрами, принимается уже на основе новых настроек.Все функции, которые планировали записать лог, разблокируются и продолжают свою работу, даже не заметив, что движок "под капотом" сменился, а логгер, возможно, из синхронного стал асинхронным, или наоборот.Так работает механизм защиты от потери логов при смене настроек.Существует еще один сценарий, при котором происходит что-то похожее - это завершение работы программы. Polog при старте программы регистрирует черезatexitспециальный обработчик выхода. Внутри него должны выполняться шаги 1 и 2 из перечисленных выше, но с лимитом времени. Этот лимит вы можете корректировать, изменяя настройкуmax_delay_before_exit. Рекомендуем при манипуляции данной настройкой учитывать такжеmax_queue_size, иначе возможна ситуация, когда очередь окажется слишком длинной, чтобы успеть "рассосаться" за отведенное время.Об объекте логаНиже вы можете прочитать о том, что такоеобработчики логови как они работают, а также как обогащать логиизвлекаемыми полями, но прежде, чем мы к этому перейдем, нужно коротко объяснить, что такое вообще отдельный лог с точки зрения фреймворка.Задача логгера в принципе - перехватывать поток событий, которые создает программа. На каждое такое событие где-то внутри фреймворка создается специальный объект, в котором заключены извлеченные из события данные. Кроме того, он предоставляет некую базовую логику доступа к этим данным. Этот объект и есть лог.Движок Pologберет каждый такой объект и передает его последовательно в каждый из привязанных к нему обработчиков.Это была общая информация. Конкретно в Polog объект лога по синтаксису очень похож на словарь, с той лишь разницей, что из него можно только читать данные, но нельзя писать.Данные можно получать по ключу:>>>log_item['time']datetime.datetime(2021,10,21,11,24,51,20811)>>>log_item.get('time')datetime.datetime(2021,10,21,11,24,51,20811)Набор полей, записанных для каждого отдельного события, может быть разным. Вот список возможных:level(int, обязательное) - уровень важности лога.auto(bool, обязательное) - метка, автоматический лог или ручной. Проставляется автоматически, вы не можете этим управлять. Автоматическими называются логи, записанные с помощью декораторов.time(datetime.datetime, обязательное) - дата и время начала операции.service_name(str, не обязательное) - название или идентификатор сервиса, из которого пишутся логи. Идея в том, что несколько разных сервисов могут отправлять логи в какое-то одно место, и вы должны иметь возможность их там различить. По умолчанию имя сервиса не задано, но вы можете это изменить черезнастройки.success(str, не обязательное) - метка успешного завершения операции. При автоматическом логировании проставляется в значение True, если в задекорированной функции не произошло исключений. При ручном логировании вы можете проставить метку самостоятельно, либо она заполнится автоматически, если передадите в функциюlog()объект исключения (False).function(str, не обязательное) - название функции, действие в которой мы логируем. При автоматическом логировании (которое происходит через декораторы), название функции извлекается из атрибута __name__ объекта функции. При ручном логировании вы можете передать в логгер как сам объект функции, чтобы из нее автоматически извлекся атрибут __name__, так и строку с названием функции. Рекомендуется предпочесть первый вариант, т.к. это снижает вероятность опечаток.class(str, не обязательное) - название класса, которому принадлежит обернутая декоратором логирования функция.module(str, не обязательное) - название модуля, в котором произошло событие. Автоматически извлекается из атрибута __module__ объекта функции.message(str, не обязательное) - произвольный текст, который вы можете добавить к каждой записи.exception_type(str, не обязательное) - тип исключения. Автоматические логи заполняют эту колонку самостоятельно, вручную - вам нужно передать в логгер объект исключения.exception_message(str, не обязательное) - сообщение, с которым вызывается исключение.traceback(str, не обязательное) - json со списком строк трейсбека. При ручном логировании данное поле заполняется автоматически при передаче в функциюlog()экземпляра пойманного исключения.input_variables(str, не обязательное) - входные аргументы логируемой функции. Автоматически логируются в формате json. Стандартные для json типы данных указываются напрямую, остальные преобразуются в строку. Чтобы вы могли отличить преобразованный в строку объект от собственно строки, к каждой переменной указывается ее оригинальный тип данных из кода python.local_variables(str, не обязательное) - локальные переменные функции. Извлекаются автоматически при логировании через декораторы, либо если вы передадите в функциюlog()экземпляр исключения. Также представлены в виде json с указанием типов данных.result(str, не обязательное) - то, что вернула задекорированная логгером функция.time_of_work(float, не обязательное) - время работы задекорированной логгером функции, в секундах. Проставляется автоматически.Прочие извлекаемые поля, добавленныевручную. Вы можете дать им любые имена, кроме указанных выше.Также у лога работают некоторые базовые методы словарей:log_item.keys()# Коллекция ключей (названий полей) лога.log_item.values()# Коллекция значений.log_item.items()# Коллекция пар ключ-значение.Если лог был создан с помощью декоратора, в нем отдельно, вне обычных полей, содержится ссылки на переменные, переданные в обернутую функцию. Доступ к ним можно получить по атрибутуfunction_input_dataу объекта лога:log_item.function_input_data.argslog_item.function_input_data.kwargslog_item.function_input_data.args- это, соответственно, кортеж неименованных аргументов функции, аlog_item.function_input_data.kwargs- словарь именованных. В случае, если лог зарегистрирован"вручную",log_item.function_input_data.argsиlog_item.function_input_data.kwargsбудут ссылаться на None. При работе с переданными в функцию данными нужно соблюдать осторожность - существует риск, что вы их как-то измените, повлияв таким образом на работу программы. Без особой нужды лучше их вообще никак не использовать.Также каждый лог содержит перечень обработчиков, которым его следует передать. Это необходимо, поскольку разные логи могу передаватьсяразным обработчикам. Получить коллекцию связанных с логом обработчиков можно через метод.get_handlers():log_item.get_handlers()# Вернется дерево обработчиков, привязанное к конкретному логу - по нему можно итерироваться.Ну и последнее, у объекта лога перегружены операторы сравнения. Два таких объекта можно, пользуясь обычным синтаксисом сравнения в Python, сравнить между собой по полю time, где хранятся дата и время события. Это позволяет, к примеру, применять по отношению к коллекциям логов сортировки.Добавляем извлекаемые поляСуществует легкий способ расширить функциональность Polog - добавить в него собственные извлекаемые поля. Извлекаемое поле - это некая именованная сущность, которая выполняет некоторый код на "сырых" данных при каждом логируемом событии и возвращает значение, которое добавляется в лог.Рассмотрим пример добавления поля, в которое будет извлекаться ip-адрес клиента изобработчика запроса Django. Сам обработчик запросов выглядит примерно вот так:importdatetimefromdjango.httpimportHttpResponse@logdefcurrent_datetime(request):now=datetime.datetime.now()html="<html><body>It is now%s.</body></html>"%nowreturnHttpResponse(html)Чтобы ip извлекался из запроса автоматически, необходимо зарегистрировать в Pologextractor- функцию, которая получит на входобъект логас уже успевшими извлечься на момент вызоваextractor'а прочими аргументами. На выходеextractorдолжен дать некий объект, который и будет вставлен в лог в качестве одного из полей. Делается это примерно так:frompologimportconfig,fielddefip_extractor(log_item):request=log_item.function_input_data.args[0]ip=request.META.get('REMOTE_ADDR')returnipconfig.add_fields(ip=field(ip_extractor))Теперь в наборе полей лога, который будет передаваться в каждый обработчик, появится новое - "ip", значением которого будет извлеченный из запроса ip-адрес. Как видите, в данномextractor'е нет никакой обработки ошибок. Их экранирование происходит в самом Polog. Если случится ошибка при извлечении конкретного поля - оно просто не извлечется, на запись прочих полей это никак не повлияет.При необходимости, вы можете также указать функцию, ответственную за форматирование извлеченных данных в строку перед непосредственно записью:defip_converter(ip):"""Делаем так, чтобы ip-адрес указывался через дефис."""returnip.replace('.','-')config.add_fields(ip=field(ip_extractor,converter=ip_converter))В данном примере извлеченныйextractor'ом ip-адрес прежде, чем попасть в лог, проходит через функцию, которую мы указали какconverter.Кроме того, вы можете указывать извлекаемые поля не для всей программы целиком, а для конкретного декоратора. На том же примере с обработчиком запросов Django это будет выглядеть так:@log(extra_fields={'ip':field(ip_extractor)})defcurrent_datetime(request):now=datetime.datetime.now()html="<html><body>It is now%s.</body></html>"%nowreturnHttpResponse(html)В качестве аргументаextra_fieldsможно передать либо словарь как в примере выше, либо список или кортеж таких словарей. В случае, если вы указываете для конкретного декоратора набор извлекаемых полей отдельно, поля, установленные глобально, использоваться не будут. Если вы хотите, чтобы локальный набор полей добавился к глобальному (а в случае совпадения имен - перекрыл), нужно использовать троеточие, вот так:@log(extra_fields=[{'ip':field(ip_extractor)},...])defcurrent_datetime(request):...# Речь не об этом троеточии.Извлекаемые поля из примеров кода выше отрабатывали в том же потоке, в котором происходил вызов логгера. Это базовый режим. Он необходим, к примеру, для случаев, когда вам нужно залогировать идентификатор текущего потока. Однако если вам нужно, например из соображений производительности, чтобы поля извлекались внутри движка (то есть в отдельном потоке, если вы используетемногопоточный движок), этого тоже можно добиться.Используйтеconfig.add_engine_fields()вместоconfig.add_fields():config.add_engine_fields(ip=field(ip_extractor))И параметрextra_engine_fieldsвместоextra_fieldsдля декораторов:@log(extra_engine_fields={'ip':field(ip_extractor)})defcurrent_datetime(request):...ОбработчикиОбработчик логов - это некая функция, которую движок вызывает для каждого логируемого события. Их может быть сколько угодно, причем вы тоже можете написатьсвой обработчики зарегистрировать его в логгере. К примеру, он может слать логи в вашу любимую NoSQL базу данных, писать их в файловую систему, выводить в консоль, или отправлять вам в мессенджерах / соцсетях.Простейшая функция-обработчик может выглядеть примерно так:frompologimportconfigdefprint_function_name(log_item):if'function'inlog_item:print(log_item['function'])else:print('is unknown!')# Передаем наш обработчик в Polog. В метод add_handlers() можно передать несколько функций через запятую.config.add_handlers(print_function_name)# В консоли появится сообщение из вашего обработчика.log('hello!')Каждый обработчик принимает 1 аргумент:объект лога.В данном примере мы зарегистрировали новый обработчик, передав его методуconfig.add_handlers(). Внутри Polog каждый обработчик сохраняется под определенным именем. Либо оно будет сгенерировано автоматически, как в примере выше, либо вы зададите его вручную, передавая свои обработчики в тот же метод в качестве именованных аргументов, вот так:# В данном случае обработчик будет зарегистрирован под именем "handler_name".config.add_handlers(handler_name=handler)Впоследствии вы можете использовать эти имена, чтобы управлять жизненным циклом обработчиков.Получить коллекцию всех зарегистрированных обработчиков можно при помощи методаconfig.get_handlers():all_handlers=config.get_handlers()print(all_handlers)Там вы заодно можете и подглядеть, какие имена были автоматически присвоены обработчикам, которые вы сами не потрудились как-то назвать.Ну и наконец, удаление обработчиков:config.delete_handlers('handler_name_1','handler_name_2')Работает как по названиям, так и прямой передачей объекта обработчика. То есть можно делать как-то так:config.add_handlers(handler)# Обработчик добавлен...config.delete_handlers(handler)# ... и теперь удален.Вы можете писать обработчики для своих нуждсамостоятельно, однако в стандартную поставку Polog некоторые "батарейки" уже включены. Об уже готовых обработчиках Polog, часть из которых включена в стандартную поставку пакета, читайте ниже.Пространства имен и иерархия обработчиковВразделе вышемы увидели, что такое обработчики и как регистрировать их в Polog. Это был базовый сценарий работы с ними, однако есть и более интересные возможности. Допустим, у вас есть два вида обработчиков: пишущие логи в файлы на локальной машине, и отправляющие их на другую машину. Причем вы хотите, чтобы при выполнении одних функций выполнялись обработчики из одной группы, других - из другой, а третьей - из обеих. Это можно устроить.На самом деле все обработчики в Polog хранятся вдереве. Полный путь к конкретному обработчику легко составить, просто соединяя имена нод, подобно тому, как в файловых системах можно получить путь к файлу, составив имена всех папок, в которых он лежит, и добавив в конце имя самого файла. Только, в отличие от большинства файловых систем, где для разделения уровней в дереве используются слэш ("/") или обратный слэш ("\"), в Polog для разделения уровней используется точка. Еще одно отличие заключается в том, что ноды в дереве обработчиков не делятся на типы подобно тому, как в файловых системах есть файлы и директории. Тут одна и та же нода может хранить в себе обработчик, а также быть "родителем" для нод следующего уровня.Если мы хотим добавить обработчик не первого уровня, необходимо делать это с помощью словаря, где желаемый путь к новому обработчику - ключ в словаре, а сам обработчик - значение. Посмотрим на пример (здесь и далее в примерах будем считать, что обработчики определены где-то выше):frompologimportconfigconfig.add_handlers({'lol.kek':handler,})Что здесь произошло?Мы создали пустую ноду с именемlol.Создали пустую ноду с именемkekи добавили ссылку на нее в нодуlol.Положили в нодуlol.kekнаш обработчик.Добавим еще один обработчик:config.add_handlers({'lol.kek.cheburek':handler_2,})Что изменилось? Нодыlolиkekуже существуют. Данной командой мы создали дочернюю к нодеkekнодуcheburekи положили в нее наш обработчик.Теперь используем их:@log(handlers=[handler_3,'lol.kek'])deffunction(a,b):returna*bЧто мы сделали? Мы создали для декоратора конкретной функцииfunction()локальное пространство имен обработчиков и частично спроецировали туда глобальное пространство имен. Так туда попал уникальный обработчикhandler_3, а также обработчики из глобального пространства имен -lol.kekи дочерний емуlol.kek.cheburek, итого 3 обработчика. Локальное пространство имен для конкретной функции переопределяет глобальное. Однако, если вы, уже после определения данной функции, добавите еще один обработчик по путиlol.kek.cheburek.peburek, он также отобразится в ее локальном пространстве, то есть обработчика станет четыре. Таким образом глобальное пространство имен частично проецируется на все локальные пространства, созданные для конкретных функций.Выводим логи в консоль или в файлНаверное, самый популярный способ работы с логами - это их вывод в консоль или запись в файл. Разумеется, Polog так тоже умеет. Для этого необходимо подключить встроенный обработчик, вот так:frompologimportconfig,file_writerconfig.add_handlers(file_writer())В данном примере обработчик был инициализирован без аргументов, поэтому все необходимые настройки проставлены по умолчанию. В частности, способ вывода логов выбран - в консоль (потокstdout). Если вы хотите записывать логи в конкретный файл, передайте его имя первым неименованным аргументом:config.add_handlers(file_writer('file.log'))Вы можете дополнительно настроить файловый обработчик, передав в него следующие именованные аргументы:only_errors(bool) - фильтр записи логов. В режимеFalse(то есть по умолчанию) через него проходят все события. В режимеTrue- только ошибки, т. е., если событие - не ошибка, обработчик срабатывать не будет.filter(function) - дополнительный фильтр на отправку сообщений. По умолчанию он отсутствует, т. е. обработчик срабатывает при любых событиях, прошедших через фильтрonly_errors. Вы можете передать сюда свою функцию, которая должна приниматьобъект лога, и возвращатьbool.Trueиз данной функции будет означать, что обработчик должен сработать, аFalse- что нет.alt(function) - функция, которая будет вызвана в случае, если запись лога запрещена фильтрами, либо его не удалось записать по какой-то еще причине. На вход она принимает тожеобъект лога.Ротация логовРотация- это перенос содержимого файла с логами в какой-то другой файл, а также очистка текущего файла.При создании экземплярафайлового обработчикавы можете включить ротацию. Выглядит это так:frompologimportfile_writerhandler=file_writer('file.log',rotation='200 megabytes >> archive')Выражение, управляющее ротацией, представляет собой строку, состоящую из 2-х частей. Слева от ">>" находится условие, при котором происходит ротация, справа - путь к директории, куда мы перемещаем логи из исходного файла. В этой директории при каждой ротации будет создаваться новый файл с текущими датой и временем в названии. Условий может быть несколько, перечислить их вы можете через запятую или через точку с запятой. Проверка всех условий происходит перед каждой записью новой строки лога. Если хотя бы одно из условий сработало, будет проведена ротация, после чего уже в очищенный файл с логами будет записана новая строка.В настоящее время Polog "из коробки" работает только с одним видом условий:Размер файла с логами. Пример условия вы уже видели выше, это выражения вроде:'200 megabytes''1 gigabyte''5 gb'Левая часть условия - всегда целое число, правая - обозначение размерности. Поддерживается следующий набор размерностей:byte,kilobyte,megabyte,gigabyte,terabyteиpetabyte. Любая из них может быть написана также с буквой "s" на конце, напримерbytes. Также поддерживаются сокращения:b,kb,mb,gb,tbиpb. Кратность шага размерности - 1024. То есть 1kb== 1024b, 1mb== 1024kbи т. д.Операция ротации логов является потенциально опасной при конкурентном выполнении кода. Если 2 разных потока или процесса будут параллельно писать логи в один и тот же файл, а потом один из них решит провести ротацию, второй, который об этом ничего не знает, может записать свой лог между моментами, когда первый уже принял решение удалить файл, и когда он его уже фактически удалил. В результате одна или несколько строк могут потеряться безвозвратно. Такое поведение называетсясостоянием гонки. Чтобы избежать проблем с этим, в Polog доступны 2 типа блокировок:на уровне потокаина уровне файла.Задачаблокировки потока- убедиться, что 2 разных потока (в том числе в рамкаходного движка) не могут одновременно принимать решения о ротации файла и писать туда логи.Блокировка файланужна для того же самого, но защищает уже на уровнепроцессов. Для ее работы автоматически создается еще один файл, название которого образовано из имени оригинального файла с логами + расширения.lockв конце. Файловая блокировка "под капотом" использует специфическийсистемный вызов, который присутствует только в операционных системах семейства *NIX, поэтому, если вы используете Windows или иную специфическую ОС - данная блокировка, возможно, работать не будет.Предпочитаемые типы блокировок вы можете указать в качестве аргументаlock_typeпри создании экземпляра файлового обработчика:handler=file_writer('file.log',rotation='200 megabytes >> archive',lock_type='thread+file')В данном примере мы включили оба доступных типа блокировки.Также можно включить только один:# Только блокировка потока.thread_locked_handler=file_writer('file.log',rotation='200 megabytes >> archive',lock_type='thread')# Только блокировка файла.file_locked_handler=file_writer('file.log',rotation='200 megabytes >> archive',lock_type='file')Нужно учитывать, что любые блокировки сильнозамедляютлюбые параллельные программы и снижают отдачу от использованиямногопоточного движкаPolog. Поэтому, если вы точно знаете, что в конкретный файл пишет ровно один поток, возможно, стоит отключить блокировку в принципе. Для этого нужно передать в качестве аргументаlock_type-None:handler=file_writer('file.log',rotation='200 megabytes >> archive',lock_type=None)По умолчанию в файловом обработчике включен только один вид блокировки - блокировка потока. Это связано с тем, что так не требуется учитывать нюансы доступности системных вызовов под разными ОС, и кроме того, обычный мьютекс просто быстрее.Включаем оповещения по электронной почтеЕще один из встроенныхобработчиковPolog позволяет настроить отправку электронных писем поSMTP-протоколу. Вам это может пригодиться для быстрого реагирования на какие-то особо критичные события в ваших программах.Подключается так:frompologimportconfig,SMTP_sender# Адреса и пароль абсолютно случайны.config.add_handlers(SMTP_sender('from_me42@yandex.com','JHjhhb87TY*Ny08z)','smtp.yandex.ru','to_me@yandex.ru'))SMTP_sender- это вызываемый класс. Обязательных аргументов для его инициализации 4: адрес, с которого мы посылаем письма, пароль от ящика, адрес сервера, к которому мы подключаемся, и адрес, куда мы посылаем письма.Письма, которые будут сыпаться вам на почту, будут выглядеть примерно так:Message from the Polog:

auto = True
module = __main__
function = do
time = 2020-09-22 20:31:45.712366
exception_message = division by zero
exception_type = ZeroDivisionError
success = False
traceback = [" File \"some_path\", line 46, in wrapper\n result = func(*args, **kwargs)\n"," File \"test.py\", line 23, in do\n return x \/ y\n"]
local_variables = {"args":[{"value":55,"type":"int"},{"value":77,"type":"int"}]}
time_of_work = 2.86102294921875e-06
level = 2
input_variables = {"args":[{"value":1,"type":"int"},{"value":0,"type":"int"}]}
service_name = baseПри необходимости, вы можете настроить отправку писем более тонко. Для этого в конструктор класса нужно передать дополнительные именованные параметры. Вот их список:port(int) - номер порта в почтовом сервере, через который происходит отправка почты. По умолчанию 465 (обычно используется для шифрованного соединения).text_assembler(function) - альтернативная функция для генерации текста сообщений. Должна принимать в себя те же аргументы, которые обычно передаются в пользовательские обработчики Polog, и возвращать строковый объект.subject_assembler(function) - по аналогии с аргументом "text_assembler", альтернативная функция для генерации темы письма.is_html(bool) - флаг, является ли отправляемое содержимое HTML-документом. По умолчанию False. Влияет на заголовок письма.Также данный обработчик принимает аргументыonly_errors,filterиaltпо аналогии сфайловым. Делают они здесь то же самое, что и там.Имейте ввиду, что отправка письма - процесс довольно затратный, поэтому имеет смысл это делать только в исключительных ситуациях. Кроме того, если у вас не свой SMTP-сервер, а вы пользуетесь какими-то публичными сервисами, у них часто есть свои ограничения на отправку писем, так что злоупотреблять этим тоже не стоит. В некоторых случаях письма могут просто не отправляться из-за политики используемого вами сервиса.Кроме того, опять же, из-за затратности процесса отправки, некоторые письма могут не успеть отправиться в случае экстренного завершения программы.Пишем свой обработчикВы могли заметить, что часть функциональности разных встроенных обработчиков Polog одинакова. Например, у них у всех есть возможность прописать индивидуальные фильтры, или функции, которые будут запускаться в случае неудачи записи / отправки лога. Это происходит благодаря тому, что все встроенные обработчики отнаследованы от единого базового класса. Вы тоже можете писать собственные обработчики, наследуясь от него.Вот, как импортируется базовый класс:frompolog.handlers.abstract.baseimportBaseHandlerВ самом простом случае, наследуясь от него, вам достаточно переопределить всего 2 метода, чтобы получить полностью рабочий обработчик. Вот названия и сигнатуры этих методов:get_content(log_item)do(content)Методget_content()должен приниматьобъект лога. Его задача - преобразовать лог в некий объект, промежуточное представление, и вернуть его. Чаще всего это будет строка, если речь, к примеру, об обработчике, который пишет логи в файл.Методdo()принимает объект, полученный изget_content(), и непосредственно выполняет действие, которое должно быть произведено с логом. Обычно это либо отправка куда-то лога (на другую машину, или в какой-то сервис, например), либо его запись (в файл, базу данных или куда-то еще).Вот пример суперпростого обработчика, который, однако, будет работать:classStupidHandler(BaseHandler):defget_content(log_item):returnstr(log_item)defdo(self,content):withopen('stupid_file.lol','a')asfile:file.write(content)Все! Весь механизм работы обработчика уже реализован в базовом классе и вам не нужно его повторять.Все немного усложнится, если инициализация вашего обработчика требует каких-то дополнительных аргументов. Тут вам придется переопределить метод__init__()базового класса:classLessStupidHandler(BaseHandler):def__init__(self,some_data,only_errors=False,filter=None,alt=None):# Мы все-таки используем инициализацию объекта из базового класса, чтобы не переписывать часть с валидацией стандартных аргументов.super().__init__(only_errors=only_errors,filter=filter,alt=alt)self.some_data=some_datadefget_content(self,log_item):...defdo(self,content):...В данном примере метод__init__()базового класса сделает со знакомыми ему аргументами все, что необходимо. Например, для функций - проверит их на соответствие сигнатуры. Если вы хотите и для своих аргументов ввести какую-то валидацию, для этого в базовом классе также есть шорткат:classDefendedInputHandler(BaseHandler):# 1. Размещаем в теле класса словарь input_proves.input_proves={'some_data':lambdax:isinstance(x,str),}def__init__(self,some_data,only_errors=False,filter=None,alt=None):super().__init__(only_errors=only_errors,filter=filter,alt=alt)# 2. Вызываем метод .do_input_proves().self.do_input_proves(some_data=some_data)self.some_data=some_data...Как видно на примере, для добавления валидации произвольных аргументов, необходимо сделать 2 вещи: 1. разместить в теле класса словарь под названиемinput_proves, в котором ключи - это названия аргументов, а значения - функции, которые должны принимать эти аргументы и возвращать булевые значения, означающие, что конкретный аргумент прошел / не прошел проверку; 2. вызвать методdo_input_proves(), передав ему в качестве именованных аргументов все переменные, которые необходимо провалидировать.Готово, теперь у вас есть свой обработчик, который умеет валидировать аргументы для своей инициализации, и делает с логами все, что вам угодно.Если вы считаете, что он может быть полезен кому-то еще, опубликуйте его наpypi.org. При этом не забудьте приложить к нему инструкцию, как им пользоваться. При наименовании пакетов рекомендуем соблюдать единый формат:{micro-description}_polog_handler, напримерcolor_console_polog_handler. Часть перед "_polog_handler" должна описывать механизм его работы или место назначения, куда отправляются логи, и ей не стоит быть больше 1-3 слов. Публикуя свой проект на github, вы также можете прописать ему тегpolog, чтобы его можно было увидеть в соответствующемтопике.Общие советы про логированиеЧтобы получить наибольшую пользу от ведения логов, следуйте нескольким небольшим правилам для организации вашего проекта.Заведите для хранения логов отдельную машину. Она может быть одна для нескольких разных проектов или сервисов - главное, чтобы хранение логов физически не могло никак аффектить ваше основное приложение.Следите за конфиденциальностью данных, которые вы логируете. Скажем, если функция принимает в качестве аргумента пароль пользователя, ее не стоит логировать. Polog предоставляет удобные возможности для экранирования функций от логирования, например декоратор@unlog.Избегайте логирования функций, которые вызываются слишком часто. Обычно это функции с низким уровнем абстракции, лежащие в основе вашего проекта. Выберите уровень абстракции, на котором количество логов становится достаточно комфортным. Помните, что, поскольку запись логов в базу делается в отдельном потоке, то, что вы не чувствуете тормозов от записи логов, не означает, что логирование не ведется слишком интенсивно. Вы можете не замечать, пока Polog пишет по несколько гигабайт логов в минуту.Для удобства вы можете разделить граф вызова функций на слои, в зависимости их отдаленности от точки входа при запуске приложения. Каждому из уровней присвоить название, а каждому названию указать уровень логирования, который будет тем меньше, чем дальше соответствующий ему уровень от точки входа. Пока вы тестируете свое приложение, общий уровень логирования можно сделать равным уровню самого дальнего слоя, после чего его можно повысить, оставив логируемыми только 2-3 слоя вокруг точки входа.Как пример, если вы пишете веб-приложение, у вас наверняка там будут какие-то классы или функции-обработчики для отдельных URL. Из них наверняка будут вызываться некие функции с бизнес-логикой, а оттуда - функции для работы с базой данных. Запускаете вы приложение в условной функции main(). В данном случае функции main() можно присвоить уровень 4, обработчикам запросов - 3, слою бизнес-логики - 2, ну и слою работы с БД - 1.Избегайтеизлишнего экранирования ошибок. Если слишком переусердствовать с блоками try-except, можно довести программу до состояния, когда она не работает, а вы об этом даже не узнаете. Если уж очень сильно хочется что-то замести под ковер, используйте встроенные средства подавления ошибок в Polog.