[Из песочницы] Пишем свой логер на основе 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();
        }
}



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

© Habrahabr.ru