[Из песочницы] Пишем свой логер на основе psr/log. Для начинающих
Представим себе, что мы пишем свой фреймворк, cms или самое обычное приложение и нам, конечно же, понадобится компонент для логирования. Можно было бы взять уже готовое решение, но сегодня мы будем писать свой компонент. И писать мы его будем используя уже готовую реализацию PSR-3 psr/log. Описание самого PSR-3 можно почитать тут.
Что же должен будет уметь наш компонент:
- легко настраиваться
- писать логи в несколько мест одновременно
Давайте создадим базовый класс нашего компонента:
Мы могли бы сделать логирование в файл, базу и пр. прям в методе log (), но нам же нужно гибко настраивать наш компонент. Поэтому для логирования в разные места мы у нас будут использоваться роуты.
Вот так выглядит базовый класс нашего лог-роута:
Пока в нём есть только одно свойство $isEnable, но вскоре мы его расширим.
Теперь давайте создадим на его основе роут который будет писать логи в файл:
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);
}
}
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();
}
}
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 ():
format($this->dateFormat);
}
/**
* Преобразование $context в строку
*
* @param array $context
* @return string
*/
public function contextStringify(array $context = [])
{
return !empty($context) ? json_encode($context) : null;
}
}
Теперь нам нужно как-то научить наш Logger дружить с роутами:
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:
$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