Представим себе, что мы пишем свой фреймворк, cms или самое обычное приложение и нам, конечно же, понадобится компонент для логирования. Можно было бы взять уже готовое решение , но сегодня мы будем писать свой компонент. И писать мы его будем используя уже готовую реализацию PSR-3 psr/log. Описание самого PSR-3 можно почитать тут.
Что же должен будет уметь наш компонент:
Давайте создадим базовый класс нашего компонента:
Мы могли бы сделать логирование в файл, базу и пр. прям в методе log(), но нам же нужно гибко настраивать наш компонент. Поэтому для логирования в разные места мы у нас будут использоваться роуты.
Вот так выглядит базовый класс нашего лог-роута:
Пока в нём есть только одно свойство $isEnable, но вскоре мы его расширим.
Теперь давайте создадим на его основе роут который будет писать логи в файл:
Для того чтобы во всех наших логах использовался единый формат даты, в базовый класс роута мы добавили метод getDate() и свойство $dateFormat, а так же метод contextStringify() который будет превращать в строку третий параметр метода log():
Теперь нам нужно как-то научить наш Logger дружить с роутами:
Теперь при вызове метода log() нашего компонента, он пробежится по всем активным роутам и вызовет метод log() у каждого из них. В качестве хранилища наших роутов мы использовали SplObjectStorage из стандартной библиотеки PHP. Теперь для конфигуривания нашего компонента можно писать так:
Для конфигурирования роутов при инициализации еще раз дополним класс Route:
Вот и всё, теперь у нас простенькая реализация логера для нашего приложения. Это далеко не предел, ведь можно еще сделать настройку уровней логов которые роут будет обрабатывать, сделать роуты для записи логов в logstash или по ssh на удалённую машину и многое многое другое.
Посмотреть всё в готовом виде можно на github https://github.com/alexmgit/psrlogger
Что же должен будет уметь наш компонент:
- легко настраиваться
- писать логи в несколько мест одновременно
Давайте создадим базовый класс нашего компонента:
<?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();
}
}
Ну или в syslog
<?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