Управление лог-файлами в PHP-проектах

Раздел: Администрирование 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)

Работа с лог-файлами в PHP - comments

En
Log файл php (php)