Выпуск 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
Нас уже 1393. Больше подписчиков — лучше выпуски!

Комментарии к статье