Управление лог-файлами в PHP-проектах
Основные методы логирования в PHP
Логирование в PHP может быть реализовано разными способами. Наиболее эффективное и гибкое решение предлагает библиотека Monolog. Она поддерживает множество обработчиков (файлы, системный журнал, базы данных, удаленные серверы), форматирование сообщений и уровни логирования. Установка Monolog выполняется через Composer: composer require monolog/monolog.
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\JsonFormatter;
$logger = new Logger('app');
$handler = new StreamHandler('/var/log/app.log', Logger::DEBUG);
$handler->setFormatter(new JsonFormatter());
$logger->pushHandler($handler);
$logger->info('Приложение запущено', ['user_id' => 123]);
$logger->error('Ошибка базы данных', ['exception' => $e]);Log файл php (работа с лог-файлами в php)
Этот код создает логгер с именем 'app', добавляет обработчик для записи в файл /var/log/app.log с уровнем DEBUG и устанавливает JSON-формат. Сообщения записываются в структурированном виде, что упрощает машинную обработку и анализ.
Типичные проблемы и их решения:
- Нет прав на запись в файл. Решение: проверить права каталога и файла, установить 0775 для каталога и 0664 для файла, либо использовать обработчик RotatingFileHandler, который создает файлы с правильными правами.
- Блокировка файла при конкурентной записи. Monolog использует flock под капотом, но при высокой нагрузке рекомендуется использовать системный журнал или внешние сервисы.
- Переполнение диска. Решение: настроить ротацию логов (RotatingFileHandler) или установить лимит размера файла.
Как записать сообщение в лог-файл без внешних библиотек?
Для простых проектов подойдет ручное управление файлом с помощью fopen, fwrite и flock. Это позволяет контролировать каждый шаг и избегать зависимостей.
function writeLog(string $message, string $file = '/tmp/app.log'): void
{
$fp = fopen($file, 'a');
if (flock($fp, LOCK_EX)) {
fwrite($fp, date('Y-m-d H:i:s') . ' ' . $message . PHP_EOL);
flock($fp, LOCK_UN);
}
fclose($fp);
}
writeLog('Пользователь зарегистрирован');файл error php (файл ошибок php (error.log))
Функция открывает файл в режиме 'a' (добавление), захватывает исключительную блокировку, записывает строку с датой и снимает блокировку. Это предотвращает конфликты при параллельных запросах.
Проблемы:
- При частой записи блокировки замедляют работу. Решение: использовать бесконфликтные постановки в очередь (например, syslog).
- Отсутствие ротации: файл будет расти бесконечно. Необходимо вручную реализовать архивирование.
- Нет структурирования данных, сложно анализировать.
Как перенаправить все ошибки PHP в отдельный лог-файл?
Директива error_log в php.ini позволяет указать путь к файлу для системных ошибок. Однако можно программно задать обработчик ошибок с собственным логированием.
function customErrorHandler($severity, $message, $file, $line) {
if (!(error_reporting() & $severity)) return;
$logEntry = sprintf("[%s] [%d] %s in %s:%d\n", date('c'), $severity, $message, $file, $line);
file_put_contents('/var/log/php_errors.log', $logEntry, FILE_APPEND | LOCK_EX);
return true;
}
set_error_handler('customErrorHandler');
// Генерируем ошибку
echo $undefinedVar;
Этот перехватчик записывает все ошибки (кроме выключенных уровней) в один файл. Важно вернуть true, чтобы подавить стандартный вывод ошибок PHP.
Ошибки:
- Если в коде вызван trigger_error с пользовательским уровнем, они тоже будут записаны. Нужно фильтровать уровни.
- При использовании фреймворков может быть установлен другой обработчик, конфликт. Решение: использовать регистрацию до начала работы фреймворка.
Как организовать ротацию лог-файлов вручную?
Когда нет возможности использовать Monolog, можно реализовать ротацию по размеру или дате. Пример ротации при превышении размера 10 МБ:
function rotateLog(string $file, int $maxSize = 10485760): void
{
if (file_exists($file) && filesize($file) > $maxSize) {
$backup = $file . '.' . date('YmdHis');
rename($file, $backup);
touch($file);
chmod($file, 0664);
}
}
// Вызывать перед каждой записью
rotateLog('/var/log/custom.log');
// Затем запись...
Функция проверяет размер файла и при превышении переименовывает его с временной меткой, создает новый пустой файл. Недостаток: нет сжатия старых логов и автоматического удаления.
Проблемы:
- При одновременных обращениях возможна потеря данных из-за гонки rename/touch. Решение: использовать блокировку.
- Неограниченное количество архивов заполняет диск. Нужно добавить удаление старых файлов.
Как сделать логи структурированными (JSON) без Monolog?
Можно вручную формировать JSON-строку и записывать ее в файл. Это полезно для последующего импорта в базы данных или системы анализа.
function logJson(string $level, string $message, array $context = []): void
{
$entry = json_encode([
'timestamp' => date('c'),
'level' => $level,
'message' => $message,
'context' => $context
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
file_put_contents('/var/log/structure.log', $entry . PHP_EOL, FILE_APPEND | LOCK_EX);
}
logJson('INFO', 'Запрос обработан', ['url' => '/api/user', 'time' => 0.245]);
Каждая запись - отдельная JSON-строка. Флаг JSON_UNESCAPED_UNICODE сохраняет кириллицу, JSON_UNESCAPED_SLASHES - не экранирует слеши.
Проблемы:
- При большом размере контекста может быть медленное кодирование. Решение: лимитировать контекст.
- Невалидный JSON при спецсимволах в сообщении. Решение: использовать json_encode с проверкой ошибок.
Расширенные примеры работы с лог-файлами
В этом разделе представлены подробные примеры с пояснениями и результатами выполнения.
Пример 1: Настройка Monolog с несколькими обработчиками
Один логгер может отправлять сообщения в разные места в зависимости от уровня:
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\NativeMailerHandler;
use Monolog\Formatter\LineFormatter;
$logger = new Logger('multi');
// Все уровни пишут в файл
$fileHandler = new StreamHandler('/var/log/app.log', Logger::DEBUG);
$logger->pushHandler($fileHandler);
// Ошибки и критические отправляются почтой
$mailHandler = new NativeMailerHandler(
'admin@example.com',
'Критическая ошибка приложения',
'logger@example.com',
Logger::ERROR
);
$mailHandler->setFormatter(new LineFormatter("[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n"));
$logger->pushHandler($mailHandler);
$logger->info('Обычное событие');
$logger->error('Ошибка подключения к БД');
Результат: в файл попадут все сообщения, на почту только ERROR и выше.
Пример 2: Ротация по дате с RotatingFileHandler
Обработчик автоматически создает новый файл каждый день и удаляет старые:
use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;
$logger = new Logger('daily');
$handler = new RotatingFileHandler('/var/log/daily.log', 30, Logger::DEBUG);
// Второй параметр - количество хранимых файлов (30 дней)
$logger->pushHandler($handler);
$logger->info('Новый день начался');
Результат: создаются файлы daily-2025-04-01.log, daily-2025-04-02.log и т.д. Файлы старше 30 дней удаляются автоматически.
Пример 3: Логирование с контекстом и форматом JSON вручную
Создание структурированного лога с контекстом запроса:
$logEntry = [
'date' => gmdate('Y-m-d\TH:i:s\Z'),
'level' => 'WARNING',
'message' => 'Время выполнения превысило порог',
'extra' => [
'duration_ms' => 1500,
'route' => '/api/report'
]
];
$json = json_encode($logEntry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
file_put_contents('/var/log/api_errors.json', $json . PHP_EOL, FILE_APPEND);
Результат (пример содержимого файла):
{
"date": "2025-04-01T12:00:00Z",
"level": "WARNING",
"message": "Время выполнения превысило порог",
"extra": {
"duration_ms": 1500,
"route": "/api/report"
}
}
Пример 4: Логирование в системный журнал (syslog)
Использование встроенной функции syslog() для записи в системный лог (например, /var/log/syslog):
openlog('myapp', LOG_PID | LOG_NDELAY, LOG_USER);
syslog(LOG_INFO, 'Приложение запущено');
syslog(LOG_ERR, 'Критическая ошибка: ' . $e->getMessage());
closelog();
Просмотреть логи можно командой tail -f /var/log/syslog. Результат:
Apr 1 12:00:00 hostname myapp[1234]: Приложение запущено Apr 1 12:01:00 hostname myapp[1234]: Критическая ошибка: connection refused
Пример 5: Кастомный форматтер для Monolog с цветным выводом в консоль
Пример для отладки: вывод логов в консоль с цветом в зависимости от уровня:
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\FormatterInterface;
class ColorConsoleFormatter implements FormatterInterface
{
public function format(array $record): string
{
$colors = [
Logger::DEBUG => "\033[37m", // белый
Logger::INFO => "\033[32m", // зеленый
Logger::WARNING => "\033[33m", // желтый
Logger::ERROR => "\033[31m", // красный
Logger::CRITICAL => "\033[1;31m", // ярко-красный
];
$color = $colors[$record['level']] ?? "\033[0m";
return $color . $record['level_name'] . ": " . $record['message'] . "\033[0m" . PHP_EOL;
}
public function formatBatch(array $records): string
{
return implode('', array_map([$this, 'format'], $records));
}
}
$logger = new Logger('console');
$handler = new StreamHandler('php://stdout', Logger::DEBUG);
$handler->setFormatter(new ColorConsoleFormatter());
$logger->pushHandler($handler);
$logger->info('Зеленое сообщение');
$logger->error('Красное сообщение');
Результат в консоли (если поддерживает ANSI) - цветные строки.
Пример 6: Логирование PDO-запросов с замерами времени
Обертка для PDO, которая логирует каждый запрос и его длительность:
class LoggablePDO extends PDO
{
private $logger;
public function __construct($dsn, $user, $pass, $opts, callable $logger)
{
parent::__construct($dsn, $user, $pass, $opts);
$this->logger = $logger;
}
public function query($statement, ...$fetchModeArgs)
{
$start = microtime(true);
$result = parent::query($statement, ...$fetchModeArgs);
$time = round((microtime(true) - $start) * 1000, 2);
($this->logger)('query', $statement, $time);
return $result;
}
}
$logger = function($level, $message, $time) {
file_put_contents('/var/log/sql.log', sprintf("[%s] %s: %s (%.2fms)\n", date('c'), $level, $message, $time), FILE_APPEND);
};
$pdo = new LoggablePDO('mysql:host=localhost;dbname=test', 'user', 'pass', [], $logger);
$pdo->query('SELECT * FROM users');
Результат в файле /var/log/sql.log:
[2025-04-01T12:00:00+00:00] query: SELECT * FROM users (0.45ms)