Выпуск 2. Апрель 2013
← Преобразование XML в Perl-структуры с помощью XML::Simple | Содержание | Debug-fu в стиле Perl →Удобное логирование с Log::Any
Log::Any — исключительно полезная библиотека, которая скрывает внутренности механизма логирования от кода, которому это логирование нужно.
Какую проблему решает?
Существует огромное количество разных библиотек на CPAN, помогающих организовать логирование в своём проекте. Выбор есть на любой вкус: простые и сложные, быстрые и не очень. К сожалению, такое разнообразие и вездесущий TIMTOWTDI имеет и негативную сторону.
Допустим, вам понадобилось добавить логирование к вашему модулю управления кофеваркой. Используется он в веб-интерфейсе удалённого управления домашней кофеваркой, а также в скрипте для варки кофе не вылезая из консоли. Представим, что первый написан на Mojolicious, второй — собран по-быстрому на коленке на чистом Perl, без всяких фреймворков. Ах да: а ещё есть неограниченное количество благодарных программистов, которые интегрировали кофеварочный модуль в свои проекты.
При этом у всех потребителей вашего модуля есть собственные соображения по поводу логирования в их проекте: один пишет в STDERR
, второй ограничивается Mojo::Log, третьего устраивает Log::Dispatch, а четвёртый прикрутил Log::Log4perl с развесистой конфигурацией.
Какой механизм логирования выбрать в сложившейся ситуации? Так, чтобы логирование всё-же появилось, и не представляло собой нестандартную реализацию в виде накопления лога в какой-нибудь last_log_messages()
?
Log::Any
решает именно эту проблему: унификация интерфейса логирующих вызовов. Идейно он напоминает известные AnyEvent, CHI и DBI.
Как работает?
Довольно просто, на самом деле. Необходимо только разделить логирующий механизм на две составляющие:
- создание данных (log production); включает в себя весь внешний интерфейс: методы передачи данных (
debug()
,warning()
и прочие) и флаги проверки включенного уровня логирования (is_debug()
и так далее); в каком-либо виде это присутствует во всех логирующих библиотеках; - потребление переданных данных и их обработка (log consumption); фактически, это конфигурирование (куда направлять вывод, как его форматировать, фильтровать и так далее) и непосредственный код реализации; выбор реализации — это зона ответственности приложения и её разработчик волен включать любой подходящий механизм.
Внешний интерфейс и предоставляет Log::Any
. Он достаточно простой, обобщённый и легковесный. А связывание интерфейса с логгером выполняется через базовый модуль Log::Any::Adapter.
Внешний интерфейс
Доступные методы упрощены до предела и делятся на два типа:
- loggingmethods_ и их псевдонимы:
trace
,debug
,notice
,warning
,error
,alert
и прочие; - detectionmethods_ и их псевдонимы:
is_trace
,is_debug
и так далее printf
-версии логирующих методов, умеющие показывать сложные структуры:debugf
,errorf
и остальные;- установка нужного адаптера через
set_adapter()
; по умолчанию, включаетсяNull
-адаптер, который просто игнорирует все получаемые данные.
Именно на этот унифицированный интерфейс и нужно закладываться в своём модуле. Теперь эта задача делегируется разработчику приложения, использующего модуль. Вот всё, что нужно сделать:
use Log::Any '$log';
$log->debug("что-то произошло");
То есть, импортировать логгер как объект $log
для вызова его интерфейсных методов. И не забыть прописать Log::Any
в зависимостях модуля, проекта или приложения.
Следует сделать ремарку о глобальных переменных, которые, как известно, ни к чему хорошему не приводят. Здесь как раз тот случай, где использование глобальной переменной на уровне класса оправдано. Особенно это чувствуется в большом проекте, с большим количеством своего кода, модулей, классов, каждый из которых что-то логирует в процессе своей работы. Передавать объект логгера через accessor’ы и поля классов при их инициализации — неудобно: нужен он абсолютно всем и каждому. Замучаешься. Кстати, в том же Python такой подход с его import logging
— повсеместная практика.
Адаптер
Весь задекларированный интерфейс должен реализовываться через специальный адаптер. На CPAN уже имеется множество их вариантов под самые известные логгеры, например, Log::Any::Adapter::Log4perl или Log::Any::Adapter::Mojo.
Для подключения нужного адаптера в приложении достаточно примерно такого кода в инициализирующей части:
use Log::Any::Adapter;
Log::Any::Adapter->set('Log4perl');
И, опять же, не забыть прописать выбранный адаптер в зависимостях.
В создании своего адаптера нет ничего сложного, достаточно написать реализацию тех самых методов, соответсвующих интерфейсу Log::Any
. Распространённая практика — создавать методы с помощью шаблонов кода, так как чаще всего они сильно похожи друг на друга и отличаются только именами вызываемых методов логгера. Здесь пригодятся списки Log::Any->logging_methods
и Log::Any->detection_methods
, а также вспомогательная функция Log::Any::Adapter::Util::make_method()
. Подробности можно найти в Log::Any::Adapter::Development. А пример написания своего собственного простого адаптера будет дальше.
Пример использования
Возьмём всё тот же пример с кофеварочным модулем и одного из его потребителей — консольный скрипт. Скрипт совсем простой и, по сути, является обвязкой над кофеварочным модулем, который очень хочется выложить на CPAN. Только проблема в том, что он использует корпоративный модуль для логирования, который нельзя публиковать.
Вот здесь нам и пригодится Log::Any.
Допустим, так выглядит наш корпоративный логгер (пример схематичный — настоящий модуль может быть намного сложнее):
package Logger;
use strict;
use warnings;
use v5.10;
use Term::ANSIColor ':constants';
sub Debug { say STDERR shift }
sub Info { say STDERR GREEN, shift, RESET }
sub Warn { say STDERR YELLOW, shift, RESET }
sub Error { say STDERR RED, shift, RESET }
1;
От него нам предстоит отвязаться.
Так выглядит консольный скрипт-обвязка:
#!/usr/bin/env perl
use strict;
use warnings;
use CoffeeMaker;
CoffeeMaker->make(80);
А вот и сам кофеварочный модуль. Для наглядности и краткости с одной единственной функцией, которая не делает ничего полезного, кроме гипотетического логирования:
package CoffeeMaker;
use strict;
use warnings;
use utf8;
use Logger;
sub make {
my $self = shift;
my ($temperature) = @_;
Logger::Debug("лоток загружен, вода есть");
Logger::Info("варю кофе, температура $temperature");
Logger::Warn("осторожно, горячо!")
if $temperature > 70;
Logger::Error("непредвиденная проблема");
Logger::Info("готово");
return;
}
1;
В нём во весь рост видна проблема сильной связности и зависимости от реализации механизма логирования.
Устраняется она легко:
package CoffeeMaker;
use strict;
use warnings;
use utf8;
use Log::Any '$log';
sub make {
my $self = shift;
my ($temperature) = @_;
$log->debug("лоток загружен, вода есть");
$log->info("варю кофе, температура $temperature");
$log->warn("осторожно, горячо!")
if $temperature > 70;
$log->error("непредвиденная проблема");
$log->info("готово");
return;
}
1;
Что мы сделали:
- заменили
use Logger
наuse Log::Any '$log'
, то есть импортировали объект$log
, который предоставляет унифицированный интерфейс логирования и скрывает выбранный потребителем модуля механизм логирования; - заменили все вызовы
Logger::*
на$log->*
.
После того, как CoffeeMaker
перешел на Log::Any
, при вызове make()
из скрипта никаких логов мы не увидим. Это сделано специально, так как на данный момент Log::Any
не знает, какую реализацию логирования мы хотим использовать, поэтому по умолчанию включает Null
-адаптер. Теперь у нас развязаны руки и мы можем использовать любой понравившийся логгер (всё с того же CPAN). Или, наоборот, ничего не добавлять и не зависеть от дополнительного модуля, если логи нам не интересны.
Свой адаптер
А теперь попробуем написать свой адаптер для Log::Any
, который будет проксировать логирующие методы в наш корпоративный логгер.
package Adapter;
use strict;
use warnings;
use Log::Any::Adapter::Util 'make_method';
use Logger;
use base 'Log::Any::Adapter::Base';
my %pairs = (
Debug => [qw/debug/],
Info => [qw/info inform/],
Warn => [qw/notice warn warning/],
Error => [qw/err error fatal crit critical alert emergency/],
);
while (my ($function, $methods) = each %pairs) {
my $code = <<EOC;
sub {
shift;
\@_ = (join '', \@_);
\&Logger:\:$function;
}
EOC
my $sub = eval $code;
for my $method (@$methods) {
make_method($method, $sub);
}
}
for my $method (Log::Any->detection_methods) {
make_method($method, sub { 1 });
}
1;
Разберём по-порядку. Есть инициализация:
- импортируем
make_method
из утилит базового адаптера; - делаем класс зависимым от базового адаптера, именно его интерфейс необходимо реализовать;
- задаём хеш с маппингом «функция
Logger
а» <-> «интерфейсные функции адаптера»; интерфейс у адаптера содержит самые распространённые названия логирующих функций, которые могут обозначать одно и то же: в нашем случае, например, реализацияwarn()
иwarning()
будет использовать одну и ту же функциюLogger::Warn()
.
И генерация нужных функций:
- для каждой функции
Logger
генерируем код с помощью шаблона; - создаём через
make_method()
все связанные с этой функцией методы (по списку); - а также реализуем все
detection_methods()
, которые всегда возвращаютtrue
: делаем вид, что логируется любой уровень важности.
Реальный адаптер, конечно, может быть намного сложнее. Для более глубокого погружения имеет смысл изучить реализации уже существующих адаптеров. А при написании тестов воспользоваться удобным Log::Any::Test.
Теперь подключаем адаптер к скрипту и возвращаем назад «раскрашенные» логи от Logger
:
#!/usr/bin/env perl
use strict;
use warnings;
use Log::Any::Adapter;
Log::Any::Adapter->set('+Adapter');
use CoffeeMaker;
CoffeeMaker->make(80);
Чуть больше сахара
Для ещё большего упрощения логирования в повседневных скриптах, есть интересный модуль Log::Any::App. Фактически, это заранее сконфигурированный адаптер к Log4perl
, который умеет адаптироваться под разные контексты (one-liner выводит лог на экран, скрипт — в файл, демон — в syslog
). Дефолтная конфигурация продумана заранее за вас и подходит в большинстве ситуаций. Есть и возможность точечной настройки.
То есть, вместо:
use Log::Any '$log';
use Log::Any::Adapter;
use Log::Log4perl;
my $log4perl_config = '
some
long
multiline
config...';
Log::Log4perl->init(\$log4perl_config);
Log::Any::Adapter->set('Log4perl');
делаем так:
use Log::Any::App '$log';
и логгер готов к работе.
Заключение
Разделение интерфейса логгера и его реализации помогает упростить зависимости модуля и отдать право выбора логгера непосредственному потребителю. Log::Any
удобен в использовании и для авторов, выкладывающих свои работы на CPAN, и для интеграции в корпоративные проекты с большим количеством собственных модулей и классов. Также упрощается тестирование отдельно взятых модулей, так как устраняется необходимость инициализировать и поднимать всю существующую экосистему логгера только для того, чтобы модуль вообще мог работать — это достигается включенным по умолчанию Null
-адаптером.
← Преобразование XML в Perl-структуры с помощью XML::Simple | Содержание | Debug-fu в стиле Perl →