Логування чого завгодно в Perl

Проблема вибору

Для логування повідомлень Перл пропонує кілька готових рішень. Всі вони, як водиться, розміщені на CPAN'е. За запитом «log» можна знайти купу модулів на всі випадки життя.

Проте, серед усіх цих модулів є один особливий, називається він Log::Any.

Особливість цього модуля для логування полягає в тому, що він не займається, власне, логированием. Модуль Log::Any надає програмі (і програмісту) універсальне API для звернень до інших модулів, які займаються безпосередньо логированием.

Якщо вас мучить проблема вибору способу логування в Перлі — ця стаття для вас.

Проблема
Припустимо, у вас є модуль, який завантажує з мережі якийсь файл. Ви хотіли б знати, в який час почалося завантаження, скільки вона тривала і скільки байт було завантажено. Ви можете вступити найпростішим чином — додати в модуль рядки:

print "$time Починаю завантаження файлу\n";
# тут виконується завантаження
print "$time Завершено завантаження файлу\n";
print "$time Завантажено $size байт\n";

Зверніть увагу — для того, щоб обчислити час $time, потрібні ще якісь додаткові дії, але я навіть не буду загострювати на цьому увагу, тому що це не головна проблема.

Все буде чудово, поки ви запускаєте цей скрипт з цим модулем руками з командного рядка. Повідомлення будуть виводитися вам на консоль, ви їх прочитаєте і дізнаєтеся все, що хотіли.

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

І тоді ви захочете передати висновок лода з консолі куди-небудь ще. У файл, у базу, в іншу програму, в який-небудь веб-сервіс по API, до біса на роги etc. Тут-то і виникає проблема — куди ж саме і як логировать повідомлення?

  • Писати у файл? А раптом ваш модуль буде використовуватися в оточенні, де не можна писати файли?
  • Писати в базу? А раптом модуль для роботи з базою буде не встановлений?
  • Перенаправляти в іншу програму? А раптом користувач вашого скрипта віддає перевагу зовсім не ту програму, яку обрали ви?
Є багато відмінних модулів для логування, але всі вони володіють одним і тим же загальним недоліком — цих модулів більше одного. Вам подобається один спосіб логування, а користувачеві — інший, клієнтам — третій, начальству — четвертий. Фатальний недолік ©.

Рішення
Використовувати модуль Log::Any.

Log::Any вирішує описану вище проблему вибору таким чином, що виявляються задоволеними все — програміст думає тільки про те, він хоче відправити в лог, а споживачі лода самостійно вирішують, куди і як записувати отримані повідомлення.

Ідея цього модуля полягає в тому, щоб розділити процедуру логування на дві окремі частини, які можуть працювати незалежно один від одного. При цьому та частина, яка генерує повідомлення, що не має поняття про те, як і куди ці повідомлення будуть далі записуватися. А частина, яка повідомлення куди записує, не має поняття про те, звідки ці повідомлення взялися і як вони були сформовані.

Ось як це працює
Діаграма роботи Log::Any

Пунктирні лінії на діаграмі означають підключення модулів через use, а суцільні — напрямок руху логируемых повідомлень.

Відправка повідомлень

Для відправки повідомлення ваш код (в модулі або скрипті, не важливо) викликає стандартну функцію, що надається модулем Log::Any:

# Це завантаження модуля Log::Any з одночасним імпортом об'єкта $log, до якого далі потрібно буде звертатися для логування
use Log::Any qw($log);

# А це - вже логування (крім методу error є й інші методи)
$log->error("щось сталося при виконанні якогось завдання");

Зазначені вище два рядки — це все, що потрібно, для того, щоб почати логування. Але відправлене повідомлення поки що не буде нікуди записано, оскільки ми ще не вибрали, куди конкретно писати лог.

Запис повідомлень

Тепер нам потрібно вирішити, куди ми писати лог. Для цього в скрипті потрібно скористатися одним з адаптерів:

# Цей вбудований адаптер буде просто виводити всі повідомлення на екран
use Log::Any::Adapter ('Stdout');

# Цей вбудований адаптер буде записувати всі повідомлення в файл
use Log::Any::Adapter ('File', '/path/to/file.log');

# А цей зовнішній адаптер буде надсилати всі повідомлення в окремий просунутий модуль для логування Log::Dispatch
use Log::Dispatch;
my $log = Log::Dispatch->new(outputs => [[ ... ]]);
Log::Any::Adapter->set( { category => 'Foo::Baz' }, 'Dispatch', dispatcher => $log );

У комплекті з Log::Any поставляється кілька простих вбудованих адаптерів — File, Stdout і Stderr. Як можна здогадатися з назви, перша з них записує повідомлення в файл, а два інших відправляють повідомлення на стандартні висновки.

Крім вбудованих адаптерів на CPAN'е можна знайти зовнішні, такі як Log4perl або Syslog. Зовнішні адаптери дозволяють писати логи куди завгодно — хоч в Твіттер.

А якщо треба в Фейсбук? Теж не проблема. Ви можете без особливих зусиль написати свій власний адаптер до чого завгодно. Створення свого адаптера описано в документації модуля або ось тут російською мовою.

Я не буду тут вдаватися в деталі написання адаптера, оскільки за посиланнями вище ця задача добре описана. Замість цього я розгляну іншу завдання — впровадження логування у вже існуючий код.

Логування вже наявних дій

Коннектори

Якщо ви пишете код з нуля, ви можете відразу додати логування в потрібні місця. Але якщо у вас вже є якийсь обсяг коду, то додавання функцій логування може вимагати великого обсягу роботи з перекапыванию і модифікації коду. Замість цього є спосіб додати логування, нічого (майже) для цього не роблячи.

Для цього потрібно скористатись коннектором — модулем, що підключається до вже наявного коду і сам додає в нього функції логування.

Ремарка — термін «коннектор» я вигадав сам, можливо, є якесь інше загальноприйнята назва.

Модулі-коннектори зазвичай розміщуються в просторі імен Log::Any::For. Є кілька готових конекторів, наприклад, Log::Any::For::DBI або Log::Any::For::LWP.

Написання своїх власних коннекторів не формалізовано, так як сильно залежить від того, до чого, власне, пишеться коннектор. У загальному і цілому, коннектор працює так:

  • Перехоплюється подія, яке потрібно логировать. Для цього можуть бути використані різні засоби, типу моков або tie.
  • Повідомлення про подію відправляється в лог з допомогою стандартного виклику $log->method('повідомлення').
Використання коннектора відбувається так (на прикладі коннектора LWP):

# Підключаємо LWP
use LWP::Simple

# Підключаємо коннектор до LWP
use Log::Any::For::LWP;

# Завантажуємо і сторінку з мережі за допомогою функції з LWP
get "http://www.google.com/";

У цьому коді не зроблено ніяких явних дії по відправці повідомлень в лог, крім підключення коннектора. Але, проте, функція get чарівним чином отримає логування і почне виводити багато всяких корисних повідомлень.

Логування попереджень та вилучень

На жаль, час від часу в програмі трапляються події, які не передбачені і — по хорошому — не повинні траплятися. Наприклад, раптово виявилося, що в якійсь функції відбувається звернення до неинициализированной змінної, або відвалюється коннект до бази, або взагалі відбувається крах програми. У таких випадках інтерпретатор Перла викине попередження або виключення.

Якщо така подія сталося в простому скрипті, який ви запускали з консолі, то відповідне повідомлення з'явиться у вас перед очима і ви одразу побачите (хоча теж не факт). Якщо програма запускається з Крона або працює під управлінням веб-сервера, або ще що-небудь в цьому дусі, то зауважити таке повідомлення буде не так-то просто.

Правильне рішення — записати таке повідомлення в лог. Але як? Адже в лог пишуться повідомлення, які були відправлені туди програмістом через об'єкт $log, а варнинги і виключення викидає інтерпретатор, який нічого про наше чудове логування не знає і валить всі свої повідомлення просто на STDERR.

Значить, треба все, що йде на STDERR, примусово перенаправити в лог.

Для вирішення цього завдання я не знайшов підходящого коннектора на CPAN'е, тому написав свій — Log::Any::For::Std. Цей конектор відправляє в лог всі можливі повідомлення интепретатора, на будь-якому етапі виконання програми.

Для перехоплення STDERR використовується функція tie:

tie *STDERR, __PACKAGE__;

Ця конструкція загортає абсолютно все, що відправляється на STDERR, в потрібний нам пакет, а в ньому вже не становить праці перенаправити повідомлення в лог з допомогою Log::Any.

При бажанні ви можете реалізувати будь-який інший коннектор для перехоплення попереджень та вилучень (або взагалі їх не перехоплювати).

Фільтрація повідомлень

Обставини можуть скластися так, що наявний код виводить якісь повідомлення. Наприклад, у мене є великий проект з легасі-кодом, у якому всі логування зроблено так:

print STDERR "$time --- $login --- $pid --- якесь повідомлення\n";

Як можна бачити, тут все повідомлення відправляються на STDERR, а в тексті повідомлень є всякі змінні, роздільники, та плюс ще переклад рядка. До того ж, хоч це і не видно на око, що всі повідомлення, написані без використання utf8.

Пересилання повідомлень з STDERR в лог легко вирішується за допомогою коннектора Log::Any::For::Std, а от зайвий сміття з тексту повідомлення доведеться прибирати окремо. Для цього при підключенні модуля Log::Any потрібно включити фільтрацію.

Робиться це приблизно так:

use Log::Any '$log', filter => sub { my $msg = $_[2]; utf8::decode($msg); return $msg };

Кожне повідомлення, надіслане в лог з допомогою $log->method(), буде пропускатися через функцію, зазначену в аргументі filter. Змінна $_[2] у цій функції містить надісланого повідомлення. Якщо ви хочете щось зробити з повідомленням, то ви повинні взяти його з цієї змінної, модифікувати і повернути. Обчислене значення буде записано в лог.

Наприклад, у наведеному вище коді текст повідомлення наводиться до utf8.

Приклад скрипта
Давайте зберемо все це разом в скрипті test.pl:

#!/usr/bin/perl

use strict;
use warnings;

use Log::Any '$log', filter => sub { my $msg = $_[2]; $msg =~ s/привіт/прЕвеД/; return $msg };
use Log::Any::Adapter ('Stdout');
use Log::Any::For::Std;

print "Це просто повідомлення\n";

$log->info("А це повідомлення в лог");

$log->info("Тут на слові привіт спрацює фільтрація");

Module::func();

warn "Попередження теж опиниться в балці";

die "І навіть виняток можна загорнути в лог";

# Модуль
package Module;

use Log::Any '$log';

sub func {
$log->info("Повідомлення з модуля виявиться в балці");
}

Запускаємо і бачимо наступне:

$ ./test.pl 
Це просто повідомлення
А це повідомлення в лог
Тут на слові прЕвеД спрацює фільтрація
Повідомлення з модуля виявиться в балці
Попередження теж опиниться в балці at ./test.pl line 18.
І навіть виняток можна загорнути в лог at ./test.pl line 20.

Всі повідомлення будуть виведені на консоль, але не вірте очам своїм — лише перший рядок є звичайним висновком, все інше — це лог. Просто цей лог виведений на консоль.

А що, якщо тепер нам раптом захочеться відправити лог не на консоль, а в файл? З модулем Log::Any немає нічого простіше:

# Досить змінити адаптер з цього
# use Log::Any::Adapter ('Stdout');

# На цей
use Log::Any::Adapter ('File', 'file.log');

Запускаємо і бачимо:

$ ./test.pl 
Це просто повідомлення

Як і очікувалося, на консоль буде виведений тільки print.

А ось куди поділося все інше:

$ cat file.log 
[Fri Jun 19 17:25:44 2015] А це повідомлення в лог
[Fri Jun 19 17:25:44 2015] Тут на слові прЕвеД спрацює фільтрація
[Fri Jun 19 17:25:44 2015] Повідомлення з модуля виявиться в балці
[Fri Jun 19 17:25:44 2015] Попередження теж опиниться в балці at ./test.pl line 18.
[Fri Jun 19 17:25:44 2015] І навіть виняток можна загорнути в лог at ./test.pl line 20.

Резюме
  • Модуль Log::Any дозволяє додати у ваші програми та модулі гнучке логування, яке потім не потрібно буде переробляти при зміні способу збереження лода
  • Адаптери Log::Any::Adapter дозволяють адаптувати вашу програму до будь-якого способу збереження лода
  • Коннектори Log::Any::For дозволяють підключити логування до будь-якого джерела повідомлень

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.