Инструменты логирования в PHP
Основное эффективное решение: библиотека Monolog
Логирование в PHP часто выполняется с помощью библиотеки Monolog, соответствующей стандарту PSR-3. Она позволяет направлять сообщения в различные каналы: файлы, базу данных, системный журнал, Slack и другие.
Как реализовать гибкое логирование с ротацией файлов с помощью Monolog?
Установка через Composer:
composer require monolog/monologJournals php journal (журналы php)
Пример настройки логгера с ротацией по дням:
use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Formatter\LineFormatter;
$log = new Logger('app');
$handler = new RotatingFileHandler('/var/log/app/app.log', 7, Logger::DEBUG);
$handler->setFormatter(new LineFormatter(
"[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n"
));
$log->pushHandler($handler);
$log->info('Приложение запущено');
$log->error('Ошибка соединения', ['db' => 'mysql']);Типичная проблема: отсутствие прав на запись в директорию логов. Решение: создать директорию и установить права 755 или использовать путь внутри проекта.
Ошибка: сообщение не записывается из-за неправильного уровня логирования. Например, если уровень DEBUG не указан, сообщения ниже INFO игнорируются.
Варианты решения
Как просто записывать логи в файл с помощью встроенной функции error_log?
Функция error_log направляет сообщения в журнал ошибок веб-сервера или в указанный файл.
error_log('Текст ошибки', 3, '/tmp/my_errors.log');Параметр 3 означает запись в файл. Если не указать файл, сообщение отправится в журнал сервера.
Проблема: отсутствие контекста и форматирования. Решение: использовать sprintf или json_encode для структурирования.
error_log(json_encode(['time' => date('c'), 'msg' => 'Ошибка', 'level' => 'ERROR']) . "\n", 3, 'log.json');Как создать простой PSR-3 логгер без внешних зависимостей?
Стандарт PSR-3 описывает интерфейс LoggerInterface. Можно реализовать минимальный класс.
class SimpleLogger extends \Psr\Log\AbstractLogger {
public function log($level, $message, array $context = []): void {
$entry = sprintf("[%s] %s: %s %s\n",
date('c'),
strtoupper($level),
$message,
json_encode($context)
);
file_put_contents('app.log', $entry, FILE_APPEND | LOCK_EX);
}
}
$logger = new SimpleLogger();
$logger->warning('Недостаточно памяти', ['memory' => memory_get_usage()]);Ошибка: одновременная запись из нескольких процессов может привести к конфликтам. Использование LOCK_EX частично решает, но для продакшена лучше использовать Monolog с обработчиками, поддерживающими блокировки.
Как вести журнал в базу данных?
Можно использовать PDO для записи логов в таблицу.
$pdo = new PDO('mysql:host=localhost;dbname=logs', 'user', 'pass');
$stmt = $pdo->prepare(
'INSERT INTO log (level, message, context, created_at) VALUES (?, ?, ?, NOW())'
);
$stmt->execute(['ERROR', 'Сбой модуля', '{"module":"auth"}']);Проблема: медленная запись при большом количестве логов. Решение: использовать буферизацию или асинхронную очередь.
Как использовать системный журнал через syslog?
Функция openlog/syslog/closelog отправляет сообщения в системный syslog.
openlog('myapp', LOG_PID, LOG_USER);
syslog(LOG_WARNING, 'Высокая нагрузка');
closelog();Особенность: настройки системного журнала влияют на дальнейшую обработку. Не все серверы поддерживают syslog.
Расширенные примеры логирования
Пример 1: Многоканальное логирование с Monolog (файл + email для критических ошибок)
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\NativeMailerHandler;
$log = new Logger('app');
$log->pushHandler(new StreamHandler('info.log', Logger::INFO));
$log->pushHandler(
(new NativeMailerHandler('admin@example.com', 'Критическая ошибка', 'error@example.com'))
->setLevel(Logger::CRITICAL)
);
$log->info('Обычная операция');
$log->critical('База данных недоступна', ['host' => 'db1']);Результат: в info.log появится запись INFO, а на почту придет письмо с сообщением CRITICAL.
Пример 2: Использование процессоров для добавления контекста (IP, сессия)
use Monolog\Logger;
use Monolog\Processor\IntrospectionProcessor;
use Monolog\Processor\WebProcessor;
$log = new Logger('web');
$log->pushProcessor(new IntrospectionProcessor());
$log->pushProcessor(new WebProcessor());
$log->pushHandler(new StreamHandler('access.log', Logger::INFO));
$log->info('Страница загружена');Результат: в лог добавляются файл, строка, класс, метод, а также IP, URI, метод запроса и т.д.
Пример 3: Логирование в Graylog через GELF
use Monolog\Logger;
use Monolog\Handler\GelfHandler;
use Gelf\Transport\UdpTransport;
$transport = new UdpTransport('graylog.local', 12201);
$handler = new GelfHandler($transport);
$log = new Logger('app');
$log->pushHandler($handler);
$log->error('Соединение потеряно');Результат: сообщение появляется в Graylog с уровнем ERROR и дополнительными полями.
Пример 4: Собственный обработчик для записи в Redis
use Monolog\Logger;
use Monolog\Handler\AbstractProcessingHandler;
use Redis;
class RedisHandler extends AbstractProcessingHandler {
private Redis $redis;
private string $key;
public function __construct(Redis $redis, string $key = 'logs') {
parent::__construct();
$this->redis = $redis;
$this->key = $key;
}
protected function write(\Monolog\LogRecord $record): void {
$this->redis->rPush($this->key, (string)$record->formatted);
}
}
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$log = new Logger('cache');
$log->pushHandler(new RedisHandler($redis));
$log->info('Кеш очищен');Результат: запись в список Redis 'logs' в виде строки.
Пример 5: Использование библиотеки Analog для простых проектов
Analog::handler (Analog\Handler\File::init ('app.log'));
Analog::log ('Сообщение', Analog::WARNING);Результат: в файл app.log добавляется строка с временем и уровнем.
Пример 6: Логирование с условным форматом для разных обработчиков
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\JsonFormatter;
$log = new Logger('json_app');
$handler = new StreamHandler('app.json');
$handler->setFormatter(new JsonFormatter());
$log->pushHandler($handler);
$log->info('Пользователь вошёл', ['user_id' => 42]);Результат: в файл app.json записывается JSON-строка: {"message":"Пользователь вошёл","context":{"user_id":42},"level":200,"channel":"json_app","datetime":"...","extra":[]}