polog 0.0.12
Логирование должно быть красивым

# Polog - самый простой и удобный фреймворк для логирования


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

- **Упрощенный синтаксис**. Polog гораздо минималистичнее и удобнее как модуля logging из стандартной библиотеки, так и большинства сторонних решений. При этом близкий к традиционному синтаксис логирования вам здесь также доступен.
- **Упор на декораторы**, которые здесь гораздо функциональнее, чем у большинства подобных библиотек.
- **Универсальный логгер** - одна и та же универсальная функция может использоваться как для [ручного логирования](#ручное-логирование), так и для декорирования [функций](#декорируем-функции) или даже [классов](#декорируем-классы).
- **Защита от дублирования записей об ошибках** при проходе исключения через стек вызовов, если вы используете декоратор логирования.
- **Система разрешения конфликтов** между декораторами. При наложении на одну функцию двух декораторов логирования, она не будет [логироваться дважды](#перекрестное-декорирование).
- **Возможность редактировать лог, записываемый декоратором, изнутри той функции, которую он оборачивает**. [Она](#редактируем-автоматические-логи-из-задекорированных-функций) делает логирование через декоратор таким же мощным, как обычное, и в то же время - гораздо более минималистичным.
- **Высокая производительность**. Ядро Polog [поддерживает многопоточность](#движки-синхронный-и-асинхронный), что в некоторых ситуациях может заметно повысить производительность программы.
- **Поддержка асинхронности**. Декоратор для автоматического логирования работает как на обычных функциях, так и на корутинах. Также запись логов может быть неблокирующей операцией, если вы используете многопоточный движок для Polog.
- **Гибкая система уровней**. Вы можете создавать собственные [уровни логирования](#уровни-логирования), давать им произвольные названия и пользоваться ими так, будто они были встроены изначально.
- **Простор для модификации**. Вы можете [писать собственные](#пишем-свой-обработчик) **обработчики** или пользоваться уже существующими. К примеру, можно настроить [отправку](#включаем-оповещения-по-электронной-почте) уведомлений об ошибках по электронной почте. Также возможно писать собственную логику для автоматического заполнения [**полей лога**](#добавляем-собственные-поля) через декораторы.
- **Отсутствие внешних зависимостей** в базовой поставке. Это защищает логгер от возможных неучтенных изменений и багов в сторонних пакетах, а также делает его собственный пакет меньше, что может иметь значение при запуске на машинах с ограниченным ресурсом. Также это защищает ваш проект от конфликтов версий зависимостей.
- **Высокий уровень покрытия тестами**. В данный момент для логгера написано более 300 тестов, обеспечивающих покрытие более чем на 90%, и есть стремление эти цифры еще увеличить. При этом покрытие тестами функционального ядра близко к 100%. В случае обнаружения вами багов, они будут пофикшены в приоритетном порядке, если вы предоставите тест-кейсы, где они проявляются.
- **Гарантия безопасности данных**. Некоторые данные не должны попадать в логи из соображений безопасности. Если для вас это актуально - вы можете [защитить от этого отдельные функции](#запрет-логирования-через-декоратор-logging_is_forbidden).


## Оглавление

- [**Быстрый старт**](#быстрый-старт)
- [**О фреймворке**](#зачем-это-нужно-о-фреймворке)
- [**Уровни логирования**](#уровни-логирования)
- [**Настройки логгера**](#общие-настройки)
- [**Движки: синхронный и асинхронный**](#движки-синхронный-и-асинхронный)
- [**```log()``` - одна функция, чтобы править всеми**](#log---одна-функция-чтобы-править-всеми)
- [**Декорируем функции**](#декорируем-функции)
- [**Декорируем классы**](#декорируем-классы)
- [**Перекрестное декорирование**](#перекрестное-декорирование)
- [**Запрет логирования через декоратор ```@logging_is_forbidden```**](#запрет-логирования-через-декоратор-logging_is_forbidden)
- [**Редактируем автоматические логи из задекорированных функций**](#редактируем-автоматические-логи-из-задекорированных-функций)
- [**"Ручное" логирование**](#ручное-логирование)
- [**Об объекте лога**](#об-объекте-лога)
- [**Добавляем собственные поля**](#добавляем-собственные-поля)
- [**Обработчики**](#обработчики)
- [**Пространства имен и иерархия обработчиков**](#пространства-имен-и-иерархия-обработчиков)
- [**Вывод в файл или в консоль, ротация логов**](#выводим-логи-в-консоль-или-в-файл-включаем-ротацию)
- [**Как включить уведомления по электронной почте**](#включаем-оповещения-по-электронной-почте)
- [**Пишем свой обработчик**](#пишем-свой-обработчик)
- [**Общие советы про логирование**](#общие-советы-про-логирование)


## Быстрый старт

Установите Polog через [pip](https://pypi.org/project/polog/):

```
$ pip install polog
```

Прежде, чем вызывать логгер, необходимо зарегистрировать [обработчик](#обработчики) для записей:

```python
from polog import config, file_writer


config.add_handlers(file_writer())
```

Теперь наши логи будут выводиться в консоль. При желании, в [```file_writer```](#выводим-логи-в-консоль-или-в-файл-включаем-ротацию) можно передать имя файла и тогда вывод будет происходить туда.

Теперь импортируем объект [```log```](#log---одна-функция-чтобы-править-всеми) и применим его к функции как декоратор:

```python
from polog import log


@log
def sum(a, b):
  return a + b

print(sum(2, 2))
```

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

Теперь попробуем залогировать ошибку:

```python
@log
def division(a, b):
  return a / b

print(division(2, 0))
```

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

Еще небольшой пример кода:

```python
@log
def division(a, b):
  return a / b

@log
def operation(a, b):
  return division(a, b)

print(operation(2, 0))
```

Чего в нем примечательного? В данном случае ошибка происходит в функции division(), а затем, поднимаясь по стеку вызовов, она проходит через функцию operation(). Однако логгер записал сообщение об ошибке только один раз! Встретив исключение в первый раз, он его записывает и подменяет другим, специальным, которое игнорирует в дальнейшем. В результате ваше хранилище логов не засоряется бесконечным дублированием информации об ошибках.

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

Что, если мы хотим залогировать все методы целого класса? Обязательно ли проходиться ним вручную и на каждый вешать по декоратору? Нет! Классы тоже можно [декорировать](#декорируем-классы):

```python
@log
class OneOperation(object):
  def division(self, a, b):
    return a / b

  def operation(self, a, b):
    return self.division(a, b)

print(OneOperation().operation(2, 0))
```

Если вам все же не хватило автоматического логирования, вы можете писать логи вручную, вызывая ```log()``` [как функцию](#ручное-логирование) из своего кода:

```python
log("All right!")
log("It's bad.", exception=ValueError("Example of an exception."))
```

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


## Зачем это нужно? О фреймворке

Процесс выполнения любой программы состоит из событий: вызываются функции, поднимаются исключения и т. д. Эти события, в свою очередь, состоят из других событий, масштабом поменьше. Задача логгера - записать максимально подробный отчет обо всем этом, чтобы программист в случае сбоя мог быстро найти ошибку и устранить ее. Записать все события до мельчайших деталей невозможно - данных было бы слишком много. Поэтому обычно человек непосредственно указывает места в программном коде, записи из которых его интересуют. На этом принципе построен [модуль логирования](https://docs.python.org/3/library/logging.html) из стандартной библиотеки, и на нем же основан Polog.

Внутреннее устройство Polog отображает его предназначение:

1. Прежде всего, поток событий из программы нужно как-то ловить. Для этого существует специальная функция - [log](#log---одна-функция-чтобы-править-всеми). Вы можете ее использовать в качестве [декоратора](#декорируем-функции) (в том числе [для классов](#декорируем-классы)), а можете напрямую писать [сообщения в нее](#ручное-логирование). Фактически вам нужен всего один импорт в любом модуле вашей программы, чтобы логировать любые события любым из доступных способов.
2. Далее, поток событий необходимо отфильтровать. Для этого используется система на основе [уровней логирования](#уровни-логирования). По сути фильтрация устроена очень просто. Каждое событие снабжается меткой об уровне его важности - числом, а глобально на уровне программы устанавливается уровень, ниже которого события отбрасываются логгером как несущественные.
3. На следующем шаге все оставшиеся события попадают в один компонент - [движок](#движки-синхронный-и-асинхронный). Задача движка - передать их на обработку специальным функциям - обработчикам.
4. [Обработчики](#обработчики) производят на основе каждого события какие-то действия внутри или за пределами программы. Типичный обработчик, к примеру, [запишет](#выводим-логи-в-консоль-или-в-файл-включаем-ротацию) информацию о событии в файл.

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

Обычно вызовы стандартного логгера в коде выглядят как-то так:

```python
import logging


logging.debug('Skip this message!')
logging.info("Sometimes it's interesting.")
logging.warning('This is serious.')
logging.error('PANIC')
logging.critical("I'm quitting.")
```

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

```python
import logging


def function(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_1} and {arg_2}")
  except Exception as e:
    logging.error(f"ERROR of operations with {arg_1} and {arg_2}, {e}")
    raise
```

На данном примере мы видим функцию, в которой "настоящей" логики всего пара строк. Остальное - логирование. Мы пишем логи когда операция начинается, когда кончается, и отдельно - в случае неудачи, для чего приходится еще городить блок try-except. Итого у нас на 2 строчки "настоящей" логики приходится еще 6 обеспечивающих логирование. Еще одна особенность представленного примера - по сути, у нас чаще всего есть всего 2 уровня логирования: для успешных операций и для неуспешных, причем большинство обычно успешные. Но мы вынуждены каждый раз для каждого события вручную указывать его уровень - в примере это ```info``` и ```error```.

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

1. Логирование заставляет писать сильно больше кода, причем этот код весьма шаблонен и несколько базовых паттернов (таких, как отдельные сообщения на начало и конец каждой операции, или обрамление логами блоков try-except) переиспользуются повсюду. То есть логи отнимают самое главное - время программиста.
2. Логи отвлекают от "настоящей" логики программы при чтении ее кода. Это происходит потому, что они перемешаны с "настоящим" кодом, пишутся прямо посреди него. По сути с точки зрения логики логи являются не более чем визуальным мусором.

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

С Polog пример кода выше превращается в:

```python
from polog import log


@log
def function(arg_1, arg_2):
  operation(arg_1)
  operation_2(arg_2)
```

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

При этом, несмотря на то, что логирование описывается за пределами функции, доступно также и создание / изменение логов изнутри:

```python
@log
def function(arg_1, arg_2):
  operation(arg_1)
  operation_2(arg_2)
  log.message('Success!')
```

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

```python
def function(arg_1, arg_2):
  operation(arg_1)
  operation_2(arg_2)
  log('Success!')
```

А здесь - создали запись лога непосредственно внутри функции. Обратите внимание, мы не импортировали ничего нового, везде фигурирует один и тот же универсальный объект [```log```](#log---одна-функция-чтобы-править-всеми).

Да, вы можете заметить, что и здесь можно воспроизвести паттерн, описанный выше в отношении логгера из стандартной библиотеки. Однако теперь это находится под контролем программиста и делать __именно__ так становится вовсе не обязательно. Более того, есть надежда, что широкое использование объекта [```log```](#log---одна-функция-чтобы-править-всеми) именно как декоратора может улучшить декомпозицию задач и сделает навык разбивать крупные задачи на небольшие функции более часто применяемым. Ведь по сути, то, что чаще всего хочет программист от логгера - это всего лишь знать, зашла ли его программа в какой-то блок кода, и если да, то с каким результатом. То есть, скорее всего, то, что вы хотели бы логгировать отдельно - уже является логическим блоком, который неплохо бы вынести в отдельную функцию.


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

Уровни логирования - это универсальный и удобный способ разделить все события на группы, и разбить эти группы на две части: одну мы логируем, а другую - нет. "Разделителем" служит глобальный уровень логирования, его мы устанавливаем для всей программы. Каждое событие также имеет пометку об уровне. Если уровень события больше, чем глобальный, или равен ему, оно будет записано, а иначе - проигнорировано. Вы можете не указывать при каждом вызове логгера уровень, которым будут помечаться отслеживаемые им события. В этом случае события будут помечаться уровнями по умолчанию. Для обычных событий уровнем по умолчанию является 1, для ошибок (например, когда в задекорированной логгером функции происходит исключение) - 2. Однако это можно изменить.

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

```python
from polog import log


@log(level=5)
def sum(a, b):
  return a + b

print(sum(2, 2))
# Запишется лог с меткой 5 уровня.
```

Это доступно при декорировании как функций, так и классов, работает одинаково.

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

```python
@log(level=5, error_level=10)
```

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

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

```python
from polog import config


config.set(error_level=50)
```

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

Уровням логирования можно присвоить имена и в дальнейшем использовать их вместо чисел:

```python
from polog import log, config


# Присваиваем уровню 5 имя 'ERROR', а уровню 1 - 'ALL'.
config.levels(ERROR=5, ALL=1)

# Используем присвоенное имя вместо номера уровня.
@log(level='ERROR')
def sum(a, b):
  return a + b

print(sum(2, 2))
# Запишется лог с меткой 5 уровня.
```

При этом указание уровней числами вам по-прежнему доступно, имена и числа взаимозаменяемы.

Также, зарегистрировав имена уровней логирования, вы можете указывать их через точку, причем как при использовании объекта ```log``` в качестве декоратора, так и при "ручном" логировании:

```python
from polog import log, config


config.levels(halloween_level=13)

@log.halloween_level
def scary_function(a, b):
  ...

print(scary_function('friday', 13))
# Запишется лог 13-го уровня.
log.halloween_level('boo!')
# Также запишется лог 13-го уровня.
```

Если вы привыкли пользоваться стандартным модулем [logging](https://docs.python.org/3.8/library/logging.html), вы можете присвоить уровням логирования [стандартные имена](https://docs.python.org/3.8/library/logging.html#logging-levels) оттуда:

```python
from polog import log, config


# Имена уровням логирования проставляются автоматически, в соответствии со стандартной схемой.
config.standard_levels()

@log(level='ERROR')
def sum(a, b):
  return a + b

print(sum(2, 2))
# Запишется лог с меткой 40 уровня.
```

Общим уровнем логирования вы можете управлять через настройки:

```python
from polog import log, config


# Имена уровням логирования проставляются автоматически, в соответствии со стандартной схемой.
config.standard_levels()

# Устанавливаем текущий уровень логирования - 'CRITICAL'.
config.set(level='CRITICAL')

@log(level='ERROR')
def sum(a, b):
  return a + b

print(sum(2, 2))
# Запись произведена не будет, т. к. уровень сообщения 'ERROR' ниже текущего уровня логирования 'CRITICAL'.
```

Все события уровнем ниже игнорируются.


### Общие настройки

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

```python
from polog import config
```

Класс ```config``` предоставляет несколько методов. Все они работают непосредственно от класса, без вызова \_\_init\_\_, например вот так:

```python
config.set(pool_size=5)
```

Методы класса ```config```:

- **```set()```**: общие настройки логгера.

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

  Метод ```set()``` принимает следующие именованные параметры:

    **pool_size** (int) - количество потоков-воркеров, по умолчанию равное 2-м. Вы можете увеличить или уменьшить это число, если ваша программа пишет логи достаточно интенсивно. Но помните, что ~~большое число потоков - это большая ответственность~~ дополнительные потоки повышают накладные расходы интерпретатора и могут замедлить вашу программу. При значении 0 все логи будут обрабатываться [без использования дополнительных потоков](#движки-синхронный-и-асинхронный).

    **max_queue_size** (int) - максимальный размер очереди. Бесконечен в случае значения 0. Данный пункт актуален только в случае не нулевого значения **pool_size**.

    **service_name** (str) - имя сервиса. Указывается в каждой записи. По умолчанию 'base'.

    **level** (int, str) - общий [уровень логирования](#уровни-логирования). События уровнем ниже записываться не будут.

    **errors_level** (int, str) - [уровень логирования](#уровни-логирования) для ошибок. По умолчанию он равен 2-м.

    **original_exceptions** (bool) - режим оригинальных исключений. По умолчанию False. True означает, что все исключения остаются как были и никак не видоизменяются логгером. Это может приводить к дублированию информации об одной ошибке в записях, т. к. исключение, поднимаясь по стеку вызовов функций, может пройти через несколько задекорированных логгером функций. В режиме False все исключения логируются 1 раз, после чего оригинальное исключение подменяется на ```LoggedError```, которое не логируется никогда.

    **max_delay_before_exit** (int, float) - задержка в секундах перед завершением программы для записи оставшихся логов. При завершении работы программы может произойти небольшая пауза, в течение которой будут записаны оставшиеся логи из очереди. Максимальная продолжительность такой паузы указывается в данной настройке.

    **silent_internal_exceptions** (bool) - "лояльность" при неправильных вызовах [ручного логирования](#ручное-логирование). В значении True при передаче неправильных аргументов или ином некорректном использовании исключения не поднимаются, и по возможности ваши данные все же будут записаны. В значении False при неправильном использовании лог записываться не будет, а также будет поднято исключение с сообщением об ошибке. При проектировании сервисов с использованием Polog рекомендуется устанавливать данную настройку в значение False на этапе отладки, и переходить на значение True при реальной эксплуатации.

    **json_module** (module) - модуль для обработки данных в формате [json](https://en.wikipedia.org/wiki/JSON). Обязательно должен включать 2 функции: ```loads()``` и ```dumps()```. Для ускорения работы рекомендуется передать сюда модуль из библиотеки [ujson](https://pypi.org/project/ujson/).

    **time_quant** (int, float) - продолжительность (в секундах) некоторых внутренних операций в Polog.

- **```levels()```**: присвоение имен [уровням логирования](#уровни-логирования).

- **```standard_levels()```**: присвоение стандартных имен [уровням логирования](#уровни-логирования).

- **```add_handlers()```**: регистрация новых [обработчиков](#обработчики).

- **```get_handlers()```**: получение [дерева обработчиков](#пространства-имен-и-иерархия-обработчиков).

- **```delete_handlers()```**: удаление [обработчиков](#обработчики).

- **```add_fields()```**: регистрация новых [полей лога](#добавляем-собственные-поля).

- **```delete_fields()```**: удаление ранее зарегистрированных [полей лога](#добавляем-собственные-поля). Стандартные поля удалить нельзя.


### Движки: синхронный и асинхронный

Все логи в Polog так или иначе проходят через движок. Задача движка - взять лог и передать его во все нужные обработчики по очереди.

Движок в Polog является сменяемым компонентом. Вы можете изменить некоторые пункты [настроек](#общие-настройки), которые подразумевают перезагрузку движка. В этом случае движок перезагрузится автоматически, причем в этом случае гарантируется, что не будет потеряна ни одна запись лога.

Примерами настроек, подразумевающих перезагрузку движка, являются **pool_size** и **max_queue_size**. Чтобы объяснить, почему при их изменении происходит перезагрузка движка, нужно рассказать, какие вообще бывают движки и как в одном из них (асинхронном) задействуются эти параметры.

**Синхронный движок** по своей сути очень примитивен. Это обычный объект, у которого есть метод, в который можно передать лог, и он передаст этот лог во все прикрепленные к нему обработчики. Вызов такого метода является блокирующей операцией, то есть ваша программа будет ждать, пока выполнится код всех обработчиков, и только после этого продолжит свое выполнение. За счет своей простоты такой обработчик порождает наименьший оверхед. Кроме того, в случае, к примеру, внезапного отключения питания компьютера, будет потеряна максимум одна запись лога - та, что на момент отключения была в работе. Какие тут минусы? При использовании обработчиков, которые, к примеру, передают логи куда-то по сети, сетевые задержки могут сильно тормозить вашу программу. Особенно это критично, если логгер используется внутри какого-нибудь нагруженного сервиса. Запросы на такой сервис могут приходить не равномерно, и сетевые запросы обработчиков могут сильно снизить допустимую пиковую нагрузку.

**Асинхронный движок** в некоторых ситуациях (но далеко не всегда) может ускорить работу программы в целом. Его устройство уже значительно сложнее, чем у синхронного, и включает в себя несколько компонентов: очередь логов; объект, который кладет в нее логи; и несколько потоков с воркерами. Воркер - это некоторая функция, внутри которой запущен бесконечный цикл ожидания лога из очереди. Как только лог там появляется, кто-то из воркеров первым его забирает и далее с ним работает точно так же, как это делал бы обычный синхронный движок. В случае пиковой нагрузки, при которой воркеры не успевают обрабатывать поступающие логи, очередь растет, как бы "размазывая" во времени дополнительную нагрузку на сервер, которую создает сам логгер. Однако за все приходится платить, в данном случае - памятью и вычислительным оверхедом (который еще усугубляется наличием [GIL](https://en.wikipedia.org/wiki/Global_interpreter_lock)). Чтобы выяснить, ускорят асинхронный движок и дополнительные потоки ваше приложение, или замедлят, нет другого способа, кроме нагрузочного тестирования, причем желательно проводить его в как можно более приближенных к реальности условиях. Реальное ускорение / замедление программы сильно зависит от характеристик железа, конкретного набора обработчиков, обрабатываемых данных и, возможно, от других факторов. Кроме того, вам нужно учитывать, что при работе асинхронного движка не гарантируется правильный порядок записи логов. Это связано с тем, что, хоть они и поступают в очередь практически в том же порядке, в каком происходили события вашей программы, время работы разных обработчиков может быть разным.

Настройки **pool_size** и **max_queue_size** влияют на выбор и характеристики движков. Установка **pool_size** в значение 0 (по умолчанию - 2) приведет к загрузке синхронного движка, а любое значение больше 0 - асинхронного с соответствующим количеством потоков с воркерами. Пункт **max_queue_size** - это лимит числа логов в очереди для асинхронного движка. При значении 0 (то есть по умолчанию) лимит полностью отключается, с чем нужно быть осторожнее, поскольку очередь может стать местом утечки памяти в случае хронической нехватки мощности обработчикам. Если установить сюда любое положительное значение, при попытке положить в очеред новый лог, программа заблокируется до момента, пока кто-то из воркеров не заберет один лог из очереди.

При изменении любой из этих настроек происходит следующее:

1. Движок временно блокируется на запись логов. Функции, которые его вызывают, как бы подвиснут до момента, пока перезагрузка завершится.
2. Работа движка прекращается. В случае синхронного движка это означает, что возможно, просто записывается последний лог. Для асинхронного все сложнее. У него логи могут быть в двух разных местах. Во-первых, у него может быть не пустая очередь. То есть нужно дождаться, пока она опустеет. Во-вторых, каждый из воркеров может еще продолжать работу с последним из взятых логов. Поэтому, после опустения очереди, программа передает сигналы каждому воркеру, что после отработки текущего лога нужно завершиться. Воркер после обработки каждого лога проверяет, не был ли ему передан такой сигнал, и, если да - разрывает бесконечный цикл, не приступая к ожиданию следующего лога. На случай же, если воркер на момент получения сигнала не был занят обработкой лога, а ожидал его из очереди, все немного сложнее. Само по себе ожидание не бесконечно. На самом деле ожидание - это смесь бесконечного цикла и блокировки потока. Поток блокируется на некоторый промежуток времени (в настройках он фигурирует как **time_quant**), после чего "просыпается" и проверяет, не поступало ли сообщение о завершении. Если да - выходит, нет - снова засыпает в ожидании лога.
3. Загружается новый движок. Причем решение, какой движок загружать и с какими параметрами, принимается уже на основе новых настроек.
4. Все функции, которые планировали записать лог, разблокируются и продолжают свою работу, даже не заметив, что движок "под капотом" сменился, а логгер, возможно, из синхронного стал асинхронным, или наоборот.

Так работает механизм защиты от потери логов при смене настроек.

Существует еще один сценарий, при котором происходит что-то похожее - это завершение работы программы. Polog при старте программы регистрирует через [atexit](https://docs.python.org/3/library/atexit.html) специальный обработчик выхода. Внутри него должны выполняться шаги 1 и 2 из перечисленных выше, но с лимитом времени. Этот лимит вы можете корректировать, изменяя настройку **max_delay_before_exit**. Рекомендуем при манипуляции данной настройкой учитывать также **max_queue_size**, иначе возможна ситуация образования очереди, слишком длинной, чтобы она успевала "рассасываться" в пределах лимита.


### ```log()``` - одна функция, чтобы править всеми

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

```python
from polog import log
```

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


### Декорируем функции

Объект [```log```](#log---одна-функция-чтобы-править-всеми) может быть использован как декоратор для автоматического логирования вызовов функций. Поддерживает как обычные функции, так и [корутинные](https://docs.python.org/3/library/asyncio-task.html).

```@log``` можно использовать как со скобками, так и без. Вызов без скобок эквивалентен вызову со скобками, но без аргументов.

Параметр ```message``` можно использовать для добавления произвольного текста к каждому логу.

```python
from polog import log


@log(message='This function is very important!!!')
def very_important_function():
  ...
```

Про управление уровнями логирования через аргументы к данному декоратору читайте в разделе "[уровни логирования](#уровни-логирования)".

Если в задекорированной функции возникло необработанное исключение, по умолчанию ```@log``` записывает его, после чего подменяет внутренним исключением ```LoggedError```. В дальнейшем, если логгеру встречается это исключение, оно игнорируется. Отловить его вы  можете следующим образом:

```python
from polog import log, LoggedError


@log
def error():
  return 4 / 0

try:
  error()
except LoggedError as e:
  # Поймал - выброси.
  pass
```

Будьте осторожны с этим! Если ваш стиль кодирования подразумевает проброс исключений на несколько уровней и их ловлю с указанием типа, данное поведение следует отключить. Делается это через [настройки](#общие-настройки), вот так:

```python
from polog import config


config.set(original_exceptions=True)
```


### Декорируем классы

Помимо функций, объектом [```log```](#log---одна-функция-чтобы-править-всеми) вы можете декорировать также и целые классы. Все работает точно так же: можно указывать или не указывать все те же аргументы, использовать декоратор как со скобками, так и без.

При этом игнорируются дандер-методы класса (это те, чьи названия начинаются и заканчиваются символами "\_\_").

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

```python
@log(methods=('important_method',), message='This class is also very important!!!')
class VeryImportantClass:
  def important_method(self):
    ...
  def not_important_method(self):
    ...
  ...
```

Не забывайте, что при наследовании вы получаете класс __вместе с навешенным на его родителя логированием__, и это логирование не знает, что работает уже не в оригинальном классе, а в наследнике. Если на наследника вы тоже навесите ```@log```, логирование родителя у класса-ребенка заменится собственным. Но если вы этого не сделаете, логироваться он будет как родитель.


### Перекрестное декорирование

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

Пример:

```python
@log(level=6) # Сработает только этот декоратор.
@log(level=5) #\
@log(level=4) # |
@log(level=3) #  > А эти нет. Они знают, что их несколько на одной функции, и уступают место последнему.
@log(level=2) # |
@log(level=1) #/
def some_function(): # При каждом вызове этой функции лог будет записан только 1 раз.
  ...
```

Мы наложили на одну функцию 6 [декораторов](#декорируем-функции), однако реально сработает из них только тот, который выше всех. Это удобно в ситуациях, когда вам нужно временно изменить уровень логирования для какой-то функции. Не редактируйте старый декоратор, просто навесьте новый поверх него, и уберите, когда он перестанет быть нужен.

Также вы можете совмещать декорирование [класса](#декорируем-классы) и его [отдельных методов](#декорируем-функции):

```python
@log(level=3)
class SomeClass:
  @log(level=10)
  def some_method(self):
    ...

  def also_some_method(self):
    ...
  ...
```

У [декоратора метода](#декорируем-функции) приоритет всегда выше, чем у [декоратора класса](#декорируем-классы), поэтому в примере some_method() окажется задекорирован только через [декоратор метода](#декорируем-функции), а остальные методы - через [декоратор класса](#декорируем-классы). Используйте это, когда вам нужно залогировать отдельные методы в классе как-то по-особенному.


### Запрет логирования через декоратор ```@logging_is_forbidden```

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

Импортируется ```@logging_is_forbidden``` так:

```python
from polog import logging_is_forbidden
```

```@logging_is_forbidden``` сработает при любом расположении среди декораторов логирования:

```python
@log(level=5) # Этот декоратор не сработает.
@log(level=4) # И этот.
@log(level=3) # И этот.
@logging_is_forbidden
@log(level=2) # И вот этот.
@log(level=1) # И даже этот.
def some_function():
  ...
```

Также ```@logging_is_forbidden``` можно использовать для методов класса:

```python
@log
class VeryImportantClass:
  def important_method(self):
    ...

  @logging_is_forbidden
  def not_important_method(self):
    ...
  ...
```

Иногда это может быть удобнее, чем прописывать "разрешенные" методы в самом [декораторе класса](#декорируем-классы). Например, когда в вашем классе много методов и строка с их перечислением получилась бы слишком огромной.

Имейте ввиду, что ```@logging_is_forbidden``` "узнает" функции по их id. Это значит, что, если вы задекорируете функцию каким-то сторонним декоратором после того, как она помечена в качестве нелогируемой, декораторы Polog будут относиться к ней как к незнакомой:

```python
@log(level=2) # Этот декоратор сработает, так как не знает, что some_function() запрещено логировать, поскольку функция, вокруг которой он обернут, имеет другой id.
@other_decorator # Какой-то сторонний декоратор. Из-за него изменится первоначальный id функции some_function() и теперь для декораторов Polog это совершенно новая функция.
@logging_is_forbidden
@log(level=1) # Этот декоратор не сработает, т.к. сообщается с @logging_is_forbidden.
def some_function():
  ...
```

Поэтому декораторы Polog лучше всего располагать поверх прочих декораторов, которые вы используете. Исключение - регистрирующие декораторы, например роуты во фреймворках вроде Flask. Там синтаксис декораторов используется не для того, чтобы подменить оригинальную функцию, а для регистрации ее где-то. Для корректной работы регистрирующих декораторов, они должны быть размещены поверх всех прочих. То есть иерархия декораторов должны быть по следующей (чем больше номер - тем дальше от определения оригинальной функции): 1. обычные сторонние декораторы, 2. декораторы Polog, 3. регистрирующие декораторы.


### Редактируем автоматические логи из задекорированных функций

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

Пример работы:

```python
from polog import log, message


@log(message='original message')
def some_function():
  message('new message')
```

В полученном логе поле 'message' будет заполнено первым аргументом функции ```message()```.

У объекта [```log```](#log---одна-функция-чтобы-править-всеми) есть метод, который делает то же самое. Вы можете применять его, чтобы не импортировать ```message()``` отдельно:

```python
@log(message='original message')
def some_function():
  log.message('new message')
```

Также вы можете передавать в ```message()``` другие именованные аргументы:

- ```e``` или ```exception``` (Exception) - экземпляр исключения, которое вы хотите залогировать. Название и сообщение из него будут извлечены автоматически, однако метка ```success``` затронута не будет.
- ```success``` (bool) - метка успешности операции.
- ```level``` (str, int) - [уровень](#уровни-логирования) лога.
- ```local_variables``` (str) - ожидается json с локальными переменными.


### "Ручное" логирование

Отдельные важные события можно регистрировать вручную. Для этого нужно использовать объект [```log```](#log---одна-функция-чтобы-править-всеми) как обычную функцию. Отличие от метода [```message()```](#редактируем-автоматические-логи-из-задекорированных-функций) в данном случае в том, что мы не редактируем автоматически созданную запись, а создаем новую при каждом вызове объекта ```log``` как функции.

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

```python
from polog import log


log('Very important message!!!')
```

Обратите внимание, что первым аргументом всегда идет строка. Ею заполняется поле ```message``` в получившейся записи.

Уровень логирования указывается так же, как при использовании объекта ```log``` в виде [декоратора](#декорируем-функции):

```python
# Когда псевдонимы для уровней логирования прописаны по стандартной схеме.
log('Very important message!!!', level='ERROR')
# Ну или просто в виде числа.
log('Very important message!!!', level=40)
```

При желании, вы можете вызывать от объекта ```log``` методы, соответствующие названиям зарегистрированных ранее уровней логирования:

```python
from polog import config, log
# Присваиваем уровню 100 имя "lol".
config.levels(lol=100)
# Регистрируем лог уровня "lol".
log.lol('kek')
```

Запись лога через метод "lol" в примере выше полностью идентична прямому вызову ```log()``` как функции, с указанием соответствующего уровня, то есть вот так:

```python
log('kek', level='lol')
```

Впрочем, это работает в том числе и при использовании объекта ```log``` как декоратора, и вам уже, вероятно, знакомо.

Вы можете передать в ```log()``` функцию, в которой исполняется код:

```python
def foo():
  log(function=foo)
```

Колонки **function** и **module** в этом случае заполнятся автоматически.

Также вы можете передать в ```log()``` экземпляр исключения:

```python
try:
  var = 1 / 0
except ZeroDivisionError as e:
  log('I should probably stop dividing by zero.', exception=e)
```

Колонки **exception_message** и **exception_type** тогда тоже заполнятся автоматически. Флаг ```success``` будет установлен в значение False. Трейсбек и локальные переменные той функции, где произошла ошибка, заполнятся автоматически.

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

Также вы можете передавать в ```log()``` произвольные переменные, которые считаете нужным залогировать. Для этого нужно использовать функцию ```json_vars()```, которая принимает любые аргументы и переводит их в стандартный json-формат:

```python
from polog import log, json_vars


def bar(a, b, c, other=None):
  ...
  log(':D', function=bar, vars=json_vars(a, b, c, other=other))
  ...
```

Также вы можете автоматически получить все переменные в функции при помощи [```locals()```](https://docs.python.org/3/library/functions.html#locals):

```python
def bar(a, b, c, other=None):
  ...
  log(':D', function=bar, vars=json_vars(**locals()))
  ...
```


### Об объекте лога

Ниже вы можете прочитать о том, что такое [обработчики логов](#обработчики) и как они работают, а также как обогащать логи [собственными полями](#добавляем-собственные-поля), но прежде, чем мы к этому перейдем, нужно коротко объяснить, что такое вообще отдельный лог с точки зрения фреймворка.

Задача логгера в принципе - перехватывать поток событий, которые создает программа. На каждое такое событие где-то внутри фреймворка создается специальный объект, в котором заключены извлеченные из события данные. Кроме того, он предоставляет некую базовую логику доступа к этим данным. Этот объект и есть лог. [Движок Polog](#движки-синхронный-и-асинхронный) берет каждый такой объект и передает его последовательно в каждый из привязанных к нему обработчиков.

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

Данные можно получать по ключу:

```python
>>> log['time']
datetime.datetime(2021, 10, 21, 11, 24, 51, 20811)
>>> log.get('time')
datetime.datetime(2021, 10, 21, 11, 24, 51, 20811)
```

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

- **level** (int, обязательное) - уровень важности лога.
- **auto** (bool, обязательное) - метка, автоматический лог или ручной. Проставляется автоматически, вы не можете этим управлять.
- **time** (datetime.datetime, обязательное) - дата и время начала операции.
- **service** (str, обязательное) - название или идентификатор сервиса, из которого пишутся логи. Идея в том, что несколько разных сервисов могут отправлять логи в какое-то одно место, и вы должны иметь возможность их там различить. Имя сервиса по умолчанию - 'base'. Изменить его вы можете через [настройки](#общие-настройки).
- **success** (str, не обязательное) - метка успешного завершения операции. При автоматическом логировании проставляется в значение True, если в задекорированной функции не произошло исключений. При ручном логировании вы можете проставить метку самостоятельно, либо она заполнится автоматически, если передадите в функцию ```log()``` объект исключения (False).
- **function** (str, не обязательное) - название функции, действие в которой мы логируем. При автоматическом логировании (которое происходит через декораторы), название функции извлекается из атрибута \_\_name\_\_ объекта функции. При ручном логировании вы можете передать в логгер как сам объект функции, чтобы из нее автоматически извлекся атрибут \_\_name\_\_, так и строку с названием функции. Рекомендуется предпочесть первый вариант, т.к. это снижает вероятность опечаток.
- **module** (str, не обязательное) - название модуля, в котором произошло событие. Автоматически извлекается из атрибута \_\_module\_\_ объекта функции.
- **message** (str, не обязательное) - произвольный текст, который вы можете добавить к каждой записи.
- **exception_type** (str, не обязательное) - тип исключения. Автоматические логи заполняют эту колонку самостоятельно, вручную - вам нужно передать в логгер объект исключения.
- **exception_message** (str, не обязательное) - сообщение, с которым вызывается исключение.
- **traceback** (str, не обязательное) - json со списком строк трейсбека. При ручном логировании данное поле заполняется автоматически при передаче в функцию ```log()``` экземпляра исключения.
- **input_variables** (str, не обязательное) - входные аргументы логируемой функции. Автоматически логируются в формате json. Стандартные для json типы данных указываются напрямую, остальные преобразуются в строку. Чтобы вы могли отличить преобразованный в строку объект от собственно строки, к каждой переменной указывается ее оригинальный тип данных из кода python. Для генерации подобных json'ов при ручном логировании рекомендуется использовать функцию ```json_vars()```, куда можно передавать любые аргументы (позиционные и именные) и получать в результате стандартно оформленный json.
- **local_variables** (str, не обязательное) - локальные переменные функции. Извлекаются автоматически при логировании через декораторы, либо если вы передадите в функцию ```log()``` экземпляр исключения. Также представлены в виде json с указанием типов данных.
- **result** (str, не обязательное) - то, что вернула задекорированная логгером функция. Вы не можете заполнить это поле при ручном логировании.
- **time_of_work** (float, не обязательное) - время работы задекорированной логгером функции, в секундах. Проставляется автоматически. При ручном логировании вы не можете указать этот параметр.
- Прочие именованные поля, добавленные [вручную](#добавляем-собственные-поля). Вы можете дать им любые имена, кроме указанных выше.

Также у лога работают некоторые базовые методы словарей:

```python
log.keys() # Коллекция ключей (названий полей) лога.
log.values() # Коллекция значений.
log.items() # Коллекция пар ключ-значение.
```

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

```python
log.function_input_data.args
log.function_input_data.kwargs
```

```log.function_input_data.args``` - это, соответственно, кортеж неименованных аргументов функции, а ```log.function_input_data.kwargs``` - словарь именованных. В случае, если лог зарегистрирован ["вручную"](#ручное-логирование), ```log.function_input_data.args``` и ```log.function_input_data.kwargs``` будут ссылаться на None. При работе с переданными в функцию данными нужно соблюдать осторожность - существует риск, что вы как-то измените поступающие туда данные, повлияв таким образом на работу программы. Без особой нужды лучше их вообще никак не использовать.

Также каждый лог содержит перечень обработчиков, которым его следует передать. Это необходимо, поскольку разные логи могу передаваться [разным обработчикам](#пространства-имен-и-иерархия-обработчиков). Получить коллекцию связанных с логом обработчиков можно через метод ```.get_handlers()```:

```python
log.get_handlers()
# Вернется дерево обработчиков, привязанное к коенкретному логу - по нему можно итерироваться.
```

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


### Добавляем собственные поля

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

Рассмотрим пример добавления поля, в которое будет извлекаться ip-адрес клиента из [обработчика запроса Django](https://django.readthedocs.io/en/stable/topics/http/views.html). Сам обработчик запросов выглядит примерно вот так:

```python
import datetime
from django.http import HttpResponse
from polog import log


@log
def current_datetime(request):
    now = datetime.datetime.now()
    html = "<html><body>It is now %s.</body></html>" % now
    return HttpResponse(html)
```

Чтобы ip извлекался из запроса автоматически, необходимо зарегистрировать в Polog ```extractor``` - функцию, которая получит на вход [объект лога](#об-объекте-лога) с уже успевшими извлечься на момент вызова ```extractor```'а прочими аргументами. На выходе ```extractor``` должен дать некий объект, который и будет вставлен в лог в качестве одного из полей. Делается это примерно так:

```python
from polog import config, field


def ip_extractor(log_item):
  request = log_item.function_input_data.args[0]
  ip = request.META.get('REMOTE_ADDR')
  return ip

config.add_fields(ip=field(ip_extractor))
```

Теперь в наборе полей лога, который будет передаваться в каждый обработчик, появится новое - "ip", значением которого будет извлеченный из запроса ip-адрес. Как видите, в данном ```extractor```'е нет никакой обработки ошибок. Их экранирование происходит в самом Polog. Если случится ошибка при извлечении конкретного поля - оно просто не извлечется, на запись прочих полей это никак не повлияет.

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

```python
def ip_converter(ip):
  """Делаем так, чтобы ip-адрес указывался через дефис."""
  return ip.replace('.', '-')

config.add_fields(ip=field(ip_extractor, converter=ip_converter))
```

В данном примере извлеченный ```extractor```'ом ip-адрес прежде, чем попасть в лог, проходит через функцию, которую мы указали как ```converter```.


### Обработчики

Обработчик логов - это некая функция, которую движок вызывает для каждого логируемого события. Их может быть сколько угодно, причем вы тоже можете написать [свой обработчик](#пишем-свой-обработчик) и зарегистрировать его в логгере. К примеру, он может слать логи в вашу любимую NoSQL базу данных, писать их в файловую систему, выводить в консоль, или отправлять вам в мессенджерах / соцсетях.

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

```python
from polog import config, log


def print_function_name(log_item):
  if 'function' in log_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 каждый обработчик сохраняется под определенным именем. Либо оно будет сгенерировано автоматически, как в примере выше, либо вы зададите его вручную, передавая свои обработчики в тот же метод в качестве именованных аргументов, вот так:

```python
# В данном случае обработчик будет зарегистрирован под именем "handler_name".
config.add_handlers(handler_name=handler)
```

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

Получить коллекцию со всеми зарегистрированными обработчиками можно при помощи метода ```config.get_handlers()```:

```python
all_handlers = config.get_handlers()
print(all_handlers)
```

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

Ну и наконец, удаление обработчиков:

```python
config.delete_handlers('handler_name_1', 'handler_name_2')
```

Работает как по названиям, так и прямой передачей объекта обработчика. То есть можно делать как-то так:

```python
config.add_handlers(handler)
# Обработчик добавлен...
config.delete_handlers(handler)
# ... и теперь удален.
```

Вы можете писать обработчики для своих нужд [самостоятельно](#пишем-свой-обработчик), однако в стандартную поставку Polog некоторые "батарейки" уже включены. Об уже готовых обработчиках Polog, часть из которых включена в стандартную поставку пакета, вы можете прочитать ниже.


### Пространства имен и иерархия обработчиков

В [разделе выше](#обработчики) мы увидели, что такое обработчики и как регистрировать их в Polog. Это был базовый сценарий работы с ними, однако есть и более интересные возможности. Допустим, у вас есть два вида обработчиков: пишущие логи в файлы на локальной машине, и отправляющие их на другую машину. Причем вы хотите, чтобы при выполнении одних функций выполнялись обработчики из одной группы, других - из другой, а третьей - из обеих. Это можно устроить.

На самом деле все обработчики в Polog хранятся в дереве. Полный путь к конкретному обработчику в дереве легко составить, просто соединяя имена нод, подобно тому, как в файловых системах можно получить путь к файлу, составив имена всех папок, в которых он лежит, и добавив в конце имя самого файла. Только, в отличие от большинства файловых систем, где для разделения уровней в дереве используются слэш ("/") или обратный слэш ("\\"), в Polog для разделения уровней используется точка. Еще одно отличие заключается в том, что ноды в дереве обработчиков не делятся на типы подобно тому, как в файловых системах есть файлы и директории. Тут одна и та же нода может хранить в себе обработчик, а также быть "родителем" для нод следующего уровня.

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

```python
from polog import config


config.add_handlers(
  {
    'lol.kek': handler,
  }
)
```

Что здесь произошло?

1. Мы создали пустую ноду с именем ```lol```.
2. Создали пустую ноду с именем ```kek``` и добавили ссылку на нее в ноду ```lol```.
3. Положили в ноду ```lol.kek``` наш обработчик.

Добавим еще один обработчик:

```python
config.add_handlers(
  {
    'lol.kek.cheburek': handler_2,
  }
)
```

Что изменилось? Ноды ```lol``` и ```kek``` уже существуют. Данной командой мы создали дочернюю к ноде ```kek``` ноду ```cheburek``` и положили в нее наш обработчик.

Теперь используем их:

```python
from polog import log


@log(handlers=[handler_3, 'lol.kek'])
def function(a, b):
  return a * b
```

Что мы сделали? Мы создали для декоратора конкретной функции ```function()``` локальное пространство имен обработчиков и частично спроецировали туда глобальное пространство имен. Так туда попал уникальный обработчик ```handler_3```, а также обработчики из глобального пространства имен - ```lol.kek``` и дочерний ему ```lol.kek.cheburek```, итого 3 обработчика. Локальное пространство имен для конкретной функции переопределяет глобальное. Однако, если вы, уже после определения данной функции, добавите еще один обработчик по пути ```lol.kek.cheburek.peburek```, он также отобразится в ее локальном пространстве, то есть обработчика станет 4. Таким образом, глобальное пространство имен частично проецируется на все локальные пространства, созданные для конкретных функций.


### Выводим логи в консоль или в файл, включаем ротацию

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

```python
from polog import config, file_writer


config.add_handlers(file_writer())
```

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

```python
config.add_handlers(file_writer('file.log'))
```

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

```python
config.add_handlers(file_writer('file.log', rotation='200 megabytes >> archive'))
```

[Ротация](https://ru.wikipedia.org/wiki/%D0%A4%D0%B0%D0%B9%D0%BB_%D1%80%D0%B5%D0%B3%D0%B8%D1%81%D1%82%D1%80%D0%B0%D1%86%D0%B8%D0%B8#%D0%A0%D0%BE%D1%82%D0%B0%D1%86%D0%B8%D1%8F_%D0%B6%D1%83%D1%80%D0%BD%D0%B0%D0%BB%D0%BE%D0%B2) - это перенос содержимого файла с логами в какой-то другой файл, а также очистка текущего файла. Выражение, управляющее ротацией, состоит из 2-х частей. Слева от ">>" находится условие, при котором происходит ротация, справа - директория, куда мы перемещаем логи из исходного файла. В этой директории при каждой ротации будет создаваться новый файл с текущими датой и временем в названии. Условий может быть несколько, перечислить их вы можете через запятую. Проверка всех условий происходит перед каждой записью новой строки лога. Если хотя бы одно из условий сработало, будет проведена ротация, после чего уже в очищенный файл с логами будет записана новая строка.

В настоящее время Polog "из коробки" работает только с одним видом условий:

- Размер файла с логами. Пример условия вы уже видели выше, это выражения вроде:

```python
'200 megabytes'
'5 megabytes'
'5 gb'
```

  Левая часть условия - всегда целое число, правая - обозначение размерности. Поддерживается следующий набор размерностей: ```byte```, ```kilobyte```, ```megabyte```, ```gigabyte```, ```terabyte``` и ```petabyte```. Любая из них может быть написана также с буквой "s" на конце, например ```bytes```. Также поддерживаются сокращения: ```b```, ```kb```, ```mb```, ```gb```, ```tb``` и ```pb```. Кратность шага размерности - 1024. То есть 1 ```kb``` == 1024 ```b```, 1 ```mb``` == 1024 ```kb``` и т. д.


### Включаем оповещения по электронной почте

Еще один встроенных [обработчиков](#обработчики) Polog позволяет настроить отправку электронных писем по [SMTP](https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol)-протоколу. Вам это может пригодиться для быстрого реагирования на какие-то особо критичные события в вашем сервисе.

Подключается так:

```python
from polog import config, 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", альтернативная функция для генерации темы письма.
- ```only_errors``` (bool) - фильтр на отправку писем. В режиме False (то есть по умолчанию) через него проходят все события. В режиме True - только ошибки, т. е., если это не ошибка, письмо гарантированно отправлено не будет.
- ```filter``` (function) - дополнительный фильтр на отправку сообщений. По умолчанию он отсутствует, т. е. отправляются сообщения обо всех событиях, прошедших через фильтр "only_errors". Вы можете передать сюда свою функцию, которая должна принимать стандартный для расширений Polog набор аргументов, и возвращать bool. Возвращенное значение True из данной функции будет означать, что сообщение нужно отправлять, а False - что нет.
- ```alt``` (function) - функция, которая будет вызвана в случае, если отправка сообщения не удалась или запрещена фильтрами. Набор принимаемых аргументов, опять же, стандартный для обработчиков.
- ```is_html``` (bool) - флаг, является ли отправляемое содержимое HTML-документом. По умолчанию False. Влияет на заголовок письма.

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

Кроме того, опять же, из-за затратности процесса отправки, некоторые письма могут не успеть отправиться в случае экстренного завершения программы.


### Пишем свой обработчик

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

Вот, как импортируется базовый класс:

```python
from polog.handlers.abstract.base import BaseHandler
```

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

```python
get_content(log_item)
do(content)
```

Метод ```get_content()``` должен принимать [объект лога](#об-объекте-лога). Его задача - преобразовать лог в некий объект, промежуточное представление, и вернуть его. Чаще всего это будет строка, если речь, к примеру, об обработчике, который пишет логи в файл.

Метод ```do()``` принимает объект, полученный из ```get_content()```, и непосредственно выполняет действие, которое должно быть произведено с логом. Обычно это либо отправка куда-то лога (на другую машину, или в какой-то сервис, например), либо его запись (в файл, базу данных или куда-то еще).

Вот пример суперпростого обработчика, который, однако, будет работать:

```python
class StupidHandler(BaseHandler):
  def get_content(log_item):
    return str(log_item)

  def do(self, content):
    with open('stupid_file.lol', 'a') as file:
      file.write(content)
```

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

Все немного усложнится, если инициализация вашего обработчика требует каких-то дополнительных аргументов. Тут вам придется переопределить метод ```__init__()``` базового класса:

```python
class LessStupidHandler(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_data

  def get_content(self, log_item):
    ...

  def do(self, content):
    ...
```

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

```python
class DefendedInputHandler(BaseHandler):
  # 1. Размещаем в теле класса словарь input_proves.
  input_proves = {
      'some_data': lambda x: 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```, в котором ключи - это названия аргументов, а значения - функции, которые должны принимать эти аргументы и возвращать bool'еаны, означающие, что конкретный аргумент прошел / не прошел проверку; 2. вызвать метод ```do_input_proves()```, передав ему в качестве именованных аргументов все переменные, которые необходимо провалидировать.

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

Если вы считаете, что он может быть полезен кому-то еще, опубликуйте его на [pypi.org](https://pypi.org/)! При этом не забудьте приложить к нему инструкцию, как им пользоваться. При наименовании пакетов рекомендуем соблюдать единый формат: ```{micro-description}_polog_handler```, например ```color_console_polog_handler```. Часть перед "_polog_handler" должна описывать механизм его работы или место назначения, куда отправляются логи, и ей не стоит быть больше 1-3 слов. Публикуя свой проект на github, вы также можете прописать ему тег [```polog```](https://github.com/topics/polog), чтобы его можно было увидеть в соответствующем [топике](https://github.com/topics/polog).


## Общие советы про логирование

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

- Заведите для хранения логов отдельную машину. Она может быть одна для нескольких разных проектов или сервисов - главное, чтобы хранение логов физически не могло никак аффектить ваше основное приложение.
- Держите каждый класс в отдельном файле. Не держите "отдельно стоящих" функций в одном файле с классом. Помимо очевидного, что это делает вашу работу с проектом удобнее, это также устраняет возможность конфликта имен. Polog записывает название функции и модуля. Но если в модуле присутствуют 2 функции с одинаковыми названиями (например, в составе разных классов), вы не сможете их отличить, когда будете читать логи, и можете принять за одну функцию, которая почему-то ведет себя по-разному.
- Следите за конфиденциальностью данных, которые вы логируете. Скажем, если функция принимает в качестве аргумента пароль пользователя, ее не стоит логировать. Polog предоставляет удобные возможности для экранирования функций от логирования, например декоратор ```@logging_is_forbidden```.
- Избегайте логирования функций, которые вызываются слишком часто. Обычно это функции с низким уровнем абстракции, лежащие в основе вашего проекта. Выберите уровень абстракции, на котором количество логов становится достаточно комфортным. Помните, что, поскольку запись логов в базу делается в отдельном потоке, то, что вы не чувствуете тормозов от записи логов, не означает, что логирование не ведется слишком интенсивно. Вы можете не замечать, пока Polog пишет по несколько гигабайт логов в минуту.

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

  Как пример, если вы пишете веб-приложение, у вас наверняка там будут какие-то классы или функции-обработчики для отдельных URL. Из них наверняка будут вызываться некие функции с бизнес-логикой, а оттуда - функции для работы с базой данных. Запускаете вы приложение в условной функции main(). В данном случае функции main() можно присвоить уровень 4, обработчикам запросов - 3, слою бизнес-логики - 2, ну и слою работы с БД - 1.
- Избегайте [излишнего экранирования ошибок](https://en.wikipedia.org/wiki/Error_hiding). Когда вы ловите исключения блоками try-except, в логирующие декораторы они могут не попасть. Поэтому полезно взять за правило каждое использование инструкции except сопровождать [ручным логированием](#ручное-логирование-через-log) образовавшегося исключения.