Представим себе, что мы пишем свой фреймворк, cms или самое обычное приложение и нам, конечно же, понадобится компонент для логирования. Можно было бы взять уже готовое решение , но сегодня мы будем писать свой компонент. И писать мы его будем используя уже готовую реализацию PSR-3 psr/log. Описание самого PSR-3 можно почитать тут.
Что же должен будет уметь наш компонент:
- легко настраиваться
- писать логи в несколько мест одновременно
Давайте создадим базовый класс нашего компонента:
<?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
ссылка на оригинал статьи http://habrahabr.ru/post/266423/
Добавить комментарий