Представим себе, что мы пишем свой фреймворк, 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();
	}
}


Ну или в 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

Комментарии (16)


  1. vaniaPooh
    08.09.2015 12:04
    -4

    Скорее статья не для начинающих, а для тех кто любит «поколбасить» своего кода. Товарищи начинающие! Не тратьте время! Берите готовые библиотеки и лучше пишите что-нибудь полезное!


    1. thunderspb
      08.09.2015 12:22
      +5

      Чтобы узнать как такое вообще писать — статья годная.


    1. alexmat
      08.09.2015 13:19
      +5

      Конечно, использование готовых библиотек более правильное решение, но новичкам всё же необходимо «колбасить». Ведь единственный способ научиться программировать — сидеть и программировать.


    1. skey
      08.09.2015 14:37
      +5

      Да-да, берите готовое решение — не нужно понимать как это работает.


      1. thunderspb
        08.09.2015 14:51
        +1

        Ну да, тогда уж можно вообще сократить — наймите программиста и пусть это будут его проблемы :)


  1. nitso
    08.09.2015 18:35
    +2

    Статья рассчитана на новичков, tutorial. Где же тесты?
    Можно еще в composer.json добавить требования к php.


    1. Fesor
      09.09.2015 20:22

      Можно предложить вариант этой статьи с применением TDD для начинающих.


  1. Prapor
    08.09.2015 23:33
    -1

    Программирование ради программирования
    IMHO


  1. to0n1
    09.09.2015 22:58
    +2

    Годная статья для начинающих! Внесу пару предложений по коду:

    • $isEnable => $isEnabled
    • По моему мнению свойства класса Route должны быть приватными
    • Во многих фреймворках есть устоявшееся название Chain и я бы его использовал Logger => ChainLogger
    • Ну и конечно же скрыть имлементацию списка рутов, зачем клиенту знать что там SplObjectStorage? наружу должен быть доступен метод addLogger


    1. nitso
      10.09.2015 20:29

      SplObjectStorage, вероятно, чтобы можно было удобно detatch'ить роуты. Но совершенно очевидно, что коллекция должна быть приватная.
      Напрашивается ответная статья с исправлениями всех ошибок :)


      1. alexmat
        11.09.2015 06:45
        -1

        Методов addLogger, removeLogger и т.п. в Logger нету намеренно, т.к. логер должен логировать. В зону его ответственности не должна входить работа с коллекцией роутов.

        Если честно, то Logger и о SplObjectStorage ничего не должен знать, в конструкторе ему тупо должен передаваться Iterator.


        1. to0n1
          11.09.2015 09:24
          +1

          Посмотрите на Monolog, там Route — это Handler, и доступны методы pushHandler, popHandler и setHandlers. Так же там разделена ответственность класса Route на Handler и Processor. Handler — знает куда писать, а Processor — форматирует контекст


        1. Fesor
          11.09.2015 11:03

          вы так заботитесь о разделении ответственности (причем странно, так как если там есть список значит надо) что при этом нарушили инкапсуляцию.


    1. alexmat
      11.09.2015 06:53

      • Тут согласен, моя очепятка
      • Предполагалось, что паблик свойствами будут те, которые можно установить при инициализации роута. Просто косяк в установке этих свойств (устанавливаются и паблик и протектед и приват свойства). А вот методы действительно, не должны быть паблик, мой косяк
      • Возможно и так)
      • Про SplObjectStorage написал выше


  1. kxlab
    15.09.2015 15:27

    Расскажите пожалуйста, почему все создают все новые и новые системы, чем не устраивает syslog()? Там же все есть, и настраивается на логирование в любое место, и соответствует всем принятым нормам. php.net/manual/en/function.syslog.php.


    1. Fesor
      15.09.2015 16:24
      +1

      Потому что помимо syslog надо еще openlog юзать, а значит надо это все инкапсулировать в какой-то сервис. А значит мы берем стандарт psr-3 регламентирующий ткой интерфейс и радуемся. А еще лучше вообще взять monolog, в котором есть хэндлер для syslog.

      А еще есть куча случаев когда syslog не катит. Допустим у меня логи кидаются в аргегатор логов напряму (graylog и другие), кто-то любит файлики, кто-то выплевывает их в stderr/stdout docker-контейнера и т.д. И вот за тем что бы у всех все было одинаково хотя бы в плане интерфейса — ввели psr-3.