Что же должен будет уметь наш компонент:
- легко настраиваться
- писать логи в несколько мест одновременно
Давайте создадим базовый класс нашего компонента:
<?php
namespace Logger;
use Psr\Log\AbstractLogger;
use Psr\Log\LoggerInterface;
/**
* Class Logger
*/
class Logger extends AbstractLogger implements LoggerInterface
{
/**
* @inheritdoc
*/
public function log($level, $message, array $context = [])
{
//тут мы будем логировать
}
}
Мы могли бы сделать логирование в файл, базу и пр. прям в методе log(), но нам же нужно гибко настраивать наш компонент. Поэтому для логирования в разные места мы у нас будут использоваться роуты.
Вот так выглядит базовый класс нашего лог-роута:
<?php
namespace Logger;
use Psr\Log\AbstractLogger;
use Psr\Log\LoggerInterface;
/**
* Class Route
*/
abstract class Route extends AbstractLogger implements LoggerInterface
{
/**
* @var bool Включен ли роут
*/
public $isEnable = true;
}
Пока в нём есть только одно свойство $isEnable, но вскоре мы его расширим.
Теперь давайте создадим на его основе роут который будет писать логи в файл:
<?php
namespace Logger\Routes;
use Logger\Route;
/**
* Class FileRoute
*/
class FileRoute extends Route
{
/**
* @var string Путь к файлу
*/
public $filePath;
/**
* @var string Шаблон сообщения
*/
public $template = "{date} {level} {message} {context}";
/**
* @inheritdoc
*/
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
if (!file_exists($this->filePath))
{
touch($this->filePath);
}
}
/**
* @inheritdoc
*/
public function log($level, $message, array $context = [])
{
file_put_contents($this->filePath, trim(strtr($this->template, [
'{date}' => $this->getDate(),
'{level}' => $level,
'{message}' => $message,
'{context}' => $this->contextStringify($context),
])) . PHP_EOL, FILE_APPEND);
}
}
<?php
namespace Logger\Routes;
use PDO;
use Logger\Route;
/**
* Class DatabaseRoute
*
* Создание таблицы:
*
* CREATE TABLE default_log (
* id integer PRIMARY KEY,
* date date,
* level varchar(16),
* message text,
* context text
* );
*/
class DatabaseRoute extends Route
{
/**
* @var string Data Source Name
* @see http://php.net/manual/en/pdo.construct.php
*/
public $dsn;
/**
* @var string Имя пользователя БД
*/
public $username;
/**
* @var string Пароль пользователя БД
*/
public $password;
/**
* @var string Имя таблицы
*/
public $table;
/**
* @var PDO Подключение к БД
*/
private $connection;
/**
* @inheritdoc
*/
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->connection = new PDO($this->dsn, $this->username, $this->password);
}
/**
* @inheritdoc
*/
public function log($level, $message, array $context = [])
{
$statement = $this->connection->prepare(
'INSERT INTO ' . $this->table . ' (date, level, message, context) ' .
'VALUES (:date, :level, :message, :context)'
);
$statement->bindParam(':date', $this->getDate());
$statement->bindParam(':level', $level);
$statement->bindParam(':message', $message);
$statement->bindParam(':context', $this->contextStringify($context));
$statement->execute();
}
}
<?php
namespace Logger\Routes;
use Logger\Route;
use Psr\Log\LogLevel;
/**
* Class SyslogRoute
*/
class SyslogRoute extends Route
{
/**
* @var string Шаблон сообщения
*/
public $template = "{message} {context}";
/**
* @inheritdoc
*/
public function log($level, $message, array $context = [])
{
$level = $this->resolveLevel($level);
if ($level === null)
{
return;
}
syslog($level, trim(strtr($this->template, [
'{message}' => $message,
'{context}' => $this->contextStringify($context),
])));
}
/**
* Преобразование уровня логов в формат подходящий для syslog()
*
* @see http://php.net/manual/en/function.syslog.php
* @param $level
* @return string
*/
private function resolveLevel($level)
{
$map = [
LogLevel::EMERGENCY => LOG_EMERG,
LogLevel::ALERT => LOG_ALERT,
LogLevel::CRITICAL => LOG_CRIT,
LogLevel::ERROR => LOG_ERR,
LogLevel::WARNING => LOG_WARNING,
LogLevel::NOTICE => LOG_NOTICE,
LogLevel::INFO => LOG_INFO,
LogLevel::DEBUG => LOG_DEBUG,
];
return isset($map[$level]) ? $map[$level] : null;
}
}
Для того чтобы во всех наших логах использовался единый формат даты, в базовый класс роута мы добавили метод getDate() и свойство $dateFormat, а так же метод contextStringify() который будет превращать в строку третий параметр метода log():
<?php
namespace Logger;
use DateTime;
use Psr\Log\AbstractLogger;
use Psr\Log\LoggerInterface;
/**
* Class Route
*/
abstract class Route extends AbstractLogger implements LoggerInterface
{
/**
* @var bool Включен ли роут
*/
public $isEnable = true;
/**
* @var string Формат даты логов
*/
public $dateFormat = DateTime::RFC2822;
/**
* Текущая дата
*
* @return string
*/
public function getDate()
{
return (new DateTime())->format($this->dateFormat);
}
/**
* Преобразование $context в строку
*
* @param array $context
* @return string
*/
public function contextStringify(array $context = [])
{
return !empty($context) ? json_encode($context) : null;
}
}
Теперь нам нужно как-то научить наш Logger дружить с роутами:
<?php
namespace Logger;
use SplObjectStorage;
use Psr\Log\AbstractLogger;
use Psr\Log\LoggerInterface;
/**
* Class Logger
*/
class Logger extends AbstractLogger implements LoggerInterface
{
/**
* @var SplObjectStorage Список роутов
*/
public $routes;
/**
* Конструктор
*/
public function __construct()
{
$this->routes = new SplObjectStorage();
}
/**
* @inheritdoc
*/
public function log($level, $message, array $context = [])
{
foreach ($this->routes as $route)
{
if (!$route instanceof Route)
{
continue;
}
if (!$route->isEnable)
{
continue;
}
$route->log($level, $message, $context);
}
}
}
Теперь при вызове метода log() нашего компонента, он пробежится по всем активным роутам и вызовет метод log() у каждого из них. В качестве хранилища наших роутов мы использовали SplObjectStorage из стандартной библиотеки PHP. Теперь для конфигуривания нашего компонента можно писать так:
$logger = new Logger\Logger();
$logger->routes->attach(new Logger\Routes\FileRoute([
'isEnable' => true,
'filePath' => 'data/default.log',
]));
$logger->routes->attach(new Logger\Routes\DatabaseRoute([
'isEnable' => true,
'dsn' => 'sqlite:data/default.sqlite',
'table' => 'default_log',
]));
$logger->routes->attach(new Logger\Routes\SyslogRoute([
'isEnable' => true,
]));
$logger->info("Info message");
$logger->alert("Alert message");
$logger->error("Error message");
$logger->debug("Debug message");
$logger->notice("Notice message");
$logger->warning("Warning message");
$logger->critical("Critical message");
$logger->emergency("Emergency message");
Для конфигурирования роутов при инициализации еще раз дополним класс Route:
<?php
namespace Logger;
use DateTime;
use Psr\Log\AbstractLogger;
use Psr\Log\LoggerInterface;
/**
* Class Route
*/
abstract class Route extends AbstractLogger implements LoggerInterface
{
/**
* @var bool Включен ли роут
*/
public $isEnable = true;
/**
* @var string Формат даты логов
*/
public $dateFormat = DateTime::RFC2822;
/**
* Конструктор
*
* @param array $attributes Атрибуты роута
*/
public function __construct(array $attributes = [])
{
foreach ($attributes as $attribute => $value)
{
if (property_exists($this, $attribute))
{
$this->{$attribute} = $value;
}
}
}
/**
* Текущая дата
*
* @return string
*/
public function getDate()
{
return (new DateTime())->format($this->dateFormat);
}
/**
* Преобразование $context в строку
*
* @param array $context
* @return string
*/
public function contextStringify(array $context = [])
{
return !empty($context) ? json_encode($context) : null;
}
}
Вот и всё, теперь у нас простенькая реализация логера для нашего приложения. Это далеко не предел, ведь можно еще сделать настройку уровней логов которые роут будет обрабатывать, сделать роуты для записи логов в logstash или по ssh на удалённую машину и многое многое другое.
Посмотреть всё в готовом виде можно на github https://github.com/alexmgit/psrlogger
Комментарии (16)
to0n1
09.09.2015 22:58+2Годная статья для начинающих! Внесу пару предложений по коду:
- $isEnable => $isEnabled
- По моему мнению свойства класса Route должны быть приватными
- Во многих фреймворках есть устоявшееся название Chain и я бы его использовал Logger => ChainLogger
- Ну и конечно же скрыть имлементацию списка рутов, зачем клиенту знать что там SplObjectStorage? наружу должен быть доступен метод addLogger
nitso
10.09.2015 20:29SplObjectStorage, вероятно, чтобы можно было удобно detatch'ить роуты. Но совершенно очевидно, что коллекция должна быть приватная.
Напрашивается ответная статья с исправлениями всех ошибок :)alexmat
11.09.2015 06:45-1Методов addLogger, removeLogger и т.п. в Logger нету намеренно, т.к. логер должен логировать. В зону его ответственности не должна входить работа с коллекцией роутов.
Если честно, то Logger и о SplObjectStorage ничего не должен знать, в конструкторе ему тупо должен передаваться Iterator.to0n1
11.09.2015 09:24+1Посмотрите на Monolog, там Route — это Handler, и доступны методы pushHandler, popHandler и setHandlers. Так же там разделена ответственность класса Route на Handler и Processor. Handler — знает куда писать, а Processor — форматирует контекст
Fesor
11.09.2015 11:03вы так заботитесь о разделении ответственности (причем странно, так как если там есть список значит надо) что при этом нарушили инкапсуляцию.
alexmat
11.09.2015 06:53- Тут согласен, моя очепятка
- Предполагалось, что паблик свойствами будут те, которые можно установить при инициализации роута. Просто косяк в установке этих свойств (устанавливаются и паблик и протектед и приват свойства). А вот методы действительно, не должны быть паблик, мой косяк
- Возможно и так)
- Про SplObjectStorage написал выше
kxlab
15.09.2015 15:27Расскажите пожалуйста, почему все создают все новые и новые системы, чем не устраивает syslog()? Там же все есть, и настраивается на логирование в любое место, и соответствует всем принятым нормам. php.net/manual/en/function.syslog.php.
Fesor
15.09.2015 16:24+1Потому что помимо syslog надо еще openlog юзать, а значит надо это все инкапсулировать в какой-то сервис. А значит мы берем стандарт psr-3 регламентирующий ткой интерфейс и радуемся. А еще лучше вообще взять monolog, в котором есть хэндлер для syslog.
А еще есть куча случаев когда syslog не катит. Допустим у меня логи кидаются в аргегатор логов напряму (graylog и другие), кто-то любит файлики, кто-то выплевывает их в stderr/stdout docker-контейнера и т.д. И вот за тем что бы у всех все было одинаково хотя бы в плане интерфейса — ввели psr-3.
vaniaPooh
Скорее статья не для начинающих, а для тех кто любит «поколбасить» своего кода. Товарищи начинающие! Не тратьте время! Берите готовые библиотеки и лучше пишите что-нибудь полезное!
thunderspb
Чтобы узнать как такое вообще писать — статья годная.
alexmat
Конечно, использование готовых библиотек более правильное решение, но новичкам всё же необходимо «колбасить». Ведь единственный способ научиться программировать — сидеть и программировать.
skey
Да-да, берите готовое решение — не нужно понимать как это работает.
thunderspb
Ну да, тогда уж можно вообще сократить — наймите программиста и пусть это будут его проблемы :)