Логирование - это процесс сохранения информации касающийся работы сервера, приложения, действий пользователя и др. Такая информация помогает отслеживать действия программы и найти причину сбоя в случае, когда что-то пошло не так. Чаще всего сохранение происходит в файл, но не всегда. Файл содержащий такую информацию называют логом.

В Yii2 компонент приложения отвечающий за логирование имеет название 'log' и «из коробки» настроен на сохранение ошибок и предупреждений в файл frontend/runtime/logs/app.log или backend/runtime/logs/app.log в Yii2 версии Advanced и в файл runtime\logs\app.log для базовой версии фреймворка.

Пример из конфигурационного файла config/main.php:
'log' => [
    'traceLevel' => YII_DEBUG ? 3 : 0,
    'targets' => [
        [
            'class' => 'yii\log\FileTarget',
            'levels' => ['error', 'warning'],
        ],
    ],
],

Так же, в данном конфигурационном файле, компонент логирования прописан в свойстве приложения bootstrap:
'bootstrap' => [
    'log'
],
а это значит, что он загружается до запуска самого приложения. Что позволяет, в случае если приложение не запустилось или запустилось с ошибками, просматривать служебные сообщения (логи) с соответствующими ошибками/предупреждениями.

По-умолчанию, для работы логирования используется класс yii\log\FileTarget (из vendor/yiisoft/yii2/log/FileTarget.php) сохраняющий информацию в файлы. Экземпляры таких классов называются целями.
Открыв его код можно посмотреть возможные свойства и методы. Некоторые из свойств можно настроить для своего удобства.
Например:
public $maxLogFiles = 5;
указывает, что запись информации будет производиться поочередно в 5 файлов, максимальный размер которых указывается уже в другом свойстве:
public $maxFileSize = 10240; // in KB

При достижении максимального размера, самый старый файл будет удаляться и вместо него создаваться новый. Для примера, переопределим данное свойство. Конечно это нужно делать не в vendor/yiisoft/yii2/log/FileTarget.php, а в конфигурационном файле config/main.php:
'log' => [
    'traceLevel' => YII_DEBUG ? 3 : 0,
    'targets' => [
        [
            'class' => 'yii\log\FileTarget',
            'levels' => ['error', 'warning'],
            'maxLogFiles' => 10
        ],
    ],
],

Точно так же поступаем с другими свойствами в случае надобности. В самом коде класса есть комментарии к его свойствам и методам, т.е. разобраться не проблема.

Если мы планируем кардинально изменить работу класса отвечающего за логирование, можно переопределить целый класс и уже в нем прописать нужные свойства и методы. Но об этом позже.

Так же класс yii\log\FileTarget наследуется от абстрактного класса yii\log\Target, таким образом наследуя свойства и методы данного класса.



Цели логирования (варианты сохранения данных).
В Yii2 логирование не ограничивается сохранением информации в файлы. Так же есть возможность сохранения данных:
  • в системный файл (класс SyslogTarget). Например в Win-7, после установки данного способа логирования, вы можете посмотреть ваши логи открыв служебную программу «Просмотр событий», далее Журналы Windows - Приложение. Чтобы открыть «Просмотр событий:
Win+R и вводите eventvwr.msc
  • в базу данных (класс DbTarget.php). Не забывайте, что сохранение в БД осуществляется дольше чем в файл.
  • отправка сообщения логов на заранее указанный email (класс EmailTarget). Можно использовать для оперативного получения важной информации.

В конфигурационном файле config/main.php можно указать несколько классов логирования (целей). Один будет сохранять в файл, другой отсылать сообщение на e-mail... Для каждой цели можно настроить свойства levels и categories, которые указывают уровни важности и категории сообщений логов, которые цель должна обрабатывать.


Фильтрация сообщений.
Далее немного теории из документации. В свойстве levels указывается массив, содержащий одно или несколько следующих значений:
error: соответствует сообщениям, сохраненным методом Yii::error().
warning: соответствует сообщениям, сохраненным методом Yii::warning().
info: соответствует сообщениям, сохраненным методом Yii::info().
trace: соответствует сообщениям, сохраненным методом Yii::trace().
profile: соответствует сообщениям, сохраненным методами Yii::beginProfile() и Yii::endProfile(),

Если свойство levels в конфигурации не задано, цель логов будет обрабатывать сообщения всех перечисленных выше уровней.

Если нам нужно, чтобы определенные сообщения сохранялись в файл, а остальные отсылались по электронной почте, придется использовать свойство categories для их разделения.
Свойство categories принимает массив, содержащий имена категорий или шаблоны. Цель будет обрабатывать только те сообщения, категория которых совпадает с одним из значений или шаблонов этого массива. Шаблон категории должен состоять из префикса имени категории и звездочки * на конце. Имя категории совпадает с шаблоном, если оно начинается с префикса шаблона. Например, yii\db\Command::execute и yii\db\Command::query используются в качестве имен категорий сообщений, записанных в классе yii\db\Command. Оба они совпадают с шаблоном yii\db\*. В данном случае можно прописать:
'categories' => ['yii\db\*'],

Если свойство categories не задано, цель будет обрабатывать сообщения любой категории.

Теперь пример.
Мне нужно было подключить систему онлайн платежей WebMoney для сайта. API WebMoney предусматривает создание двух страниц, куда пользователь перенаправляется
- в случае успешной оплаты;
- в случае несостоявшейся оплаты.
Я решил сохранять в логи этот процесс, а именно в случае успешной оплаты отсылать лог с определенными данными на e-mail с помощью класса yii\log\EmailTarget, а в случае несостоявшейся оплаты сохранять лог в отдельный от основного файл используя стандартный yii\log\FileTarget.

Т.к. это специфичные логи, касающиеся оплаты, я отделил их использованием категорий payment_success и payment_fail, что бы в них не добавлялось ничего лишнего.
Таким образом в действии контроллера ответственного за вывод страницы несостоявшегося платежа я добавил:
$post = Yii::$app->request->post();

$messageLog = [
 'status' => 'Платеж не прошел.',
 'post' => $post
];

Yii::info($messageLog, 'payment_fail'); //запись в лог

А в действии контроллера выполняемом при удачной оплате:
$post = Yii::$app->request->post();

$messageLog = [
 'status' => 'Получен платеж.',
 'post' => $post
];

Yii::info($messageLog, 'payment_success'); //отправка лога по e-mail

Тут я использовал метод info() для записи лога. В данном случае это не принципиально. В качестве первого параметра можно передать строку или массив. API WebMoney при перенаправлении плательщика передает определенные данные get или post параметром (в моем случае post), его я прикрепил к логу в дополнение к сообщению. Ну а вторым параметром передаю категорию по которой отделяю свои логи от системных.

Согласно данного примера, в конфигурационном файле, настройка логирования будет иметь вид:
'log' => [
    'traceLevel' => YII_DEBUG ? 3 : 0,
    'targets' => [
        [
            'class' => 'yii\log\FileTarget',
            'levels' => ['error', 'warning'],
        ],
        [
            'class' => 'yii\log\FileTarget', //в файл
            'categories' => ['payment_fail'], //категория логов
            'logFile' => '@runtime/logs/pay.log', //куда сохранять
            'logVars' => [] //не добавлять в лог глобальные переменные ($_SERVER, $_SESSION...)
        ],
        [
            'class' => 'yii\log\EmailTarget', //шлет на e-mail
            'categories' => ['payment_success'],
            'mailer' => 'yii\swiftmailer\Mailer',
            'logVars' => [],
            'message' => [
                'from' => ['admin@site.com' => 'НАЗВАНИЕ САЙТА'], //от кого
                'to' => ['mail@gmail.com'], //кому
                'subject' => 'Получен платеж. Лог в теле сообщения.', //тема
            ],
        ],
    ],
],

Т.е. я добавил две дополнительные цели используя стандартные классы логирования но отделенные от других категориями.

Каждая цель может иметь имя:
'log' => [
    'targets' => [
        'file' => [
            'class' => 'yii\log\FileTarget',
            'levels' => ['error', 'warning'],
        ],
на которое можно ссылаться через свойство $target:
Yii::$app->log->targets['file']->enabled = false;
Если имена не даются то ссылаться можно по числовому ключу:
Yii::$app->log->targets[0];

Кроме использования стандартных классов можно написать и свой вариант логирования, наследовав абстрактный класс yii\log\Target и переопределив обязательный метод export(). Кроме того, Target является компонентом приложения, а компоненты наследуются от yii\base\Object и должны содержать метод init() который формирует свойства компонента приложения. Таким образом, при вызове конструктора компонента (при создании объекта класса компонента, а в нашем случае класса отвечающего за логирование) будет автоматически вызван метод init() который инициализирует (присвоит) свойствам компонента значения, если значения свойств не были указаны в классе или в файле конфигурации config/main.php, а так же выполнит нужные действия для подготовки работы компонента.

Так, например, в классе yii\log\FileTarget (файл vendor/yiisoft/yii2/log/FileTarget.php) метод init() устанавливает файл для хранения логов, если он не был указан в конфигурации, проверяет наличие каталога для хранения логов и в случае отсутствия создает с указанными правами. А так же проверяет и, в случае надобности, переопределяет значения свойств maxLogFiles и maxFileSize, если, вдруг, пользователь присвоил им, по-ошибке, нулевые или отрицательные значения.
public function init()
{
    parent::init();
    if ($this->logFile === null) {
        $this->logFile = Yii::$app->getRuntimePath() . '/logs/app.log';
    } else {
        $this->logFile = Yii::getAlias($this->logFile);
    }
    $logPath = dirname($this->logFile);
    if (!is_dir($logPath)) {
        FileHelper::createDirectory($logPath, $this->dirMode, true);
    }
    if ($this->maxLogFiles < 1) {
        $this->maxLogFiles = 1;
    }
    if ($this->maxFileSize < 1) {
        $this->maxFileSize = 1;
    }
}

Т.е. ваш собственный класс для логирования должен включать минимум 2 этих метода – init() для подготовки работы компонента и установки его свойств, и export() для непосредственно реализации сохранения данных.


Для закрепления всего сказанного я создам свой класс для хранения логов. Ничего нового, такого что не было реализовано в Yii2, мне в голову не пришло, поэтому он будет делать все тоже, что и стандартный класс yii\log\FileTarget с небольшими изменениями.

Я использую фреймворк версии advanced и новый класс создам в папке common\components. Таким образом его можно будет использовать и для backend.
Файл common/components/MyFileTarget.php:
<?php

namespace common\components;

use Yii;
use yii\base\InvalidConfigException;
use yii\helpers\FileHelper;
use yii\log\Target;


class MyFileTarget extends Target
{

    public $logFile;

    public $enableRotation = true;

    public $maxFileSize = 1024; // in KB

    public $maxLogFiles = 10;

    public $fileMode;

    public $dirMode = 0775;

    public $rotateByCopy = true;



    public function init()
    {
        parent::init();
        if ($this->logFile === null) {
            $this->logFile = Yii::$app->getBasePath() . '/logs/app.log'; //в папку frontend
        } else {
            $this->logFile = Yii::getAlias($this->logFile);
        }
        $logPath = dirname($this->logFile);
        if (!is_dir($logPath)) {
            FileHelper::createDirectory($logPath, $this->dirMode, true);
        }
        if ($this->maxLogFiles < 1) {
            $this->maxLogFiles = 1;
        }
        if ($this->maxFileSize < 1) {
            $this->maxFileSize = 1;
        }
    }

    public function export()
    {
        $text = implode("\n", array_map([$this, 'formatMessage'], $this->messages)) . "\n";
        if (($fp = @fopen($this->logFile, 'a')) === false) {
            throw new InvalidConfigException("Unable to append to log file: {$this->logFile}");
        }
        @flock($fp, LOCK_EX);
        if ($this->enableRotation) {
            // clear stat cache to ensure getting the real current file size and not a cached one
            // this may result in rotating twice when cached file size is used on subsequent calls
            clearstatcache();
        }
        if ($this->enableRotation && @filesize($this->logFile) > $this->maxFileSize * 1024) {
            $this->rotateFiles();
            @flock($fp, LOCK_UN);
            @fclose($fp);
            @file_put_contents($this->logFile, $text, FILE_APPEND | LOCK_EX);
        } else {
            @fwrite($fp, $text);
            @flock($fp, LOCK_UN);
            @fclose($fp);
        }
        if ($this->fileMode !== null) {
            @chmod($this->logFile, $this->fileMode);
        }
    }

    protected function rotateFiles()
    {
        $file = $this->logFile;
        for ($i = $this->maxLogFiles; $i >= 0; --$i) {
            // $i == 0 is the original log file
            $rotateFile = $file . ($i === 0 ? '' : '.' . $i);
            if (is_file($rotateFile)) {
                // suppress errors because it's possible multiple processes enter into this section
                if ($i === $this->maxLogFiles) {
                    @unlink($rotateFile);
                } else {
                    if ($this->rotateByCopy) {
                        @copy($rotateFile, $file . '.' . ($i + 1));
                        if ($fp = @fopen($rotateFile, 'a')) {
                            @ftruncate($fp, 0);
                            @fclose($fp);
                        }
                        if ($this->fileMode !== null) {
                            @chmod($file . '.' . ($i + 1), $this->fileMode);
                        }
                    } else {
                        @rename($rotateFile, $file . '.' . ($i + 1));
                    }
                }
            }
        }
    }
}

Тут я переопределил всего несколько параметров:
  • свойство $maxFileSize - уменьшил максимально допустимый размер файла, т.к. 10 мб это, обычно, перебор;
  • свойство $maxLogFiles – увеличил кол-во файлов в которых будут храниться логи до 10;
  • в методе init() определил, что по-умолчанию папка логов будет создаваться в каталоге frontend, а не папке runtime:
$this->logFile = Yii::$app->getBasePath() . '/logs/app.log'; //в папку frontend

Конечно это все только для демонстрации создания своего класса. На самом деле, свойства можно определить в конфигурационном файле, а перенос папки с логами в корень приложения вряд ли хорошая идея.
Ну и теперь подключаем свой класс:
'log' => [
    'traceLevel' => YII_DEBUG ? 3 : 0,
    'targets' => [
        [
            'class' => 'common\components\MyFileTarget',
            'levels' => ['error', 'warning', 'info'],
        ],
    ],
],
Тут я так же добавил уровень 'info', чтобы в лог добавлялись сообщения сформированные вызовом Yii::info().


Формат сообщений в логах.
Если вас не устраивает формат в котором записываются логи, а это:
Временная метка [IP адрес][ID пользователя][ID сессии][Уровень важности][Категория] Текст сообщения
например:
2017-11-26 23:14:25 [127.0.0.1][-][2lph7qb0fqviel3t7ck8c5t3vnt7pat1][error][yii\web\HttpException:404] yii\web\NotFoundHttpException: Страница не найдена. in W:\domains\site.loc\vendor\yiisoft\yii2\web\Request.php:195
то у вас есть 2 варианта исправить ситуацию.

Первый это в конфигурационном файле определить свойство prefix, которое должно содержать анонимную функцию возвращающую требуемые значения. Эти данные будут размещены вместо блоков:
[IP адрес][ID пользователя][ID сессии] в логе.
Например вы можете указать id текущего пользователя.
[
    'class' => 'yii\log\FileTarget',
    'prefix' => function ($message) {
        $user = Yii::$app->has('user', true) ? Yii::$app->get('user') : null;
        $userID = $user ? $user->getId(false) : '-';
        return "[$userID]";
    }
]

Второй, более глобальный вариант - это переопределить метод formatMessage() класса yii\log\Target из файла vendor/yiisoft/yii2/log/Target.php. В нем можно сформировать сохраняемое сообщение в любом нужном виде.

Кроме префиксов, в логи так же добавляются значения глобальных PHP переменных: $_GET, $_POST, $_FILES, $_COOKIE, $_SESSION и $_SERVER. Это делает сообщения довольно объемными и иногда имеет смысл отключить их вывод в лог. Я это уже показывал в своем примере:
'logVars' => []

Так же можно оставить что-то из этого набора, например значение $_SERVER:
'logVars' => ['_SERVER'],
Значения данного свойства записываются в массиве в таком формате:
'_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_SERVER'



Выгрузка сообщений.
Логи не сохраняются сразу после формирования их сообщений. Вместо этого они накапливаются в массиве до определенного количества и только потом сохраняются. Сохранение так же происходит при завершении приложения.

Максимальное количество сообщений определяется свойством flushInterval компонента log и по-умолчанию равно 1000. Т.е. как правило, логи сохраняются при завершении работы приложения. Если приложение выполняется долго, например консольное приложение, а логи желаем посмотреть оперативно, то нужно установить меньшее значение свойства flushInterval:
        'log' => [
            'flushInterval' => 1,
            'targets' => [
                [
                    'class' => 'yii\log\FileTarget',
                    'exportInterval' => 1,
                ],
            ],
        ],
В данном случае сообщения не будут накапливаться, а сразу сохраняться. Но учтите, что частая выгрузка сообщений снижает производительность приложения.



Профилирование производительности.

Для замера производительности приложения используется специальный тип сообщений относящийся к логированию. Анализ производительности делается по кол-ву времени затраченному на выполнение операции и по кол-ву используемой памяти.

Для использования профилирования производительности нужно обернуть проверяемый участок кода вызовами методов Yii::beginProfile() и Yii::endProfile(). Данные блоки могут быть вложены друг в друга. Первым параметром нужно передать идентификатор, вторым категорию.

Пример.
Yii::beginProfile('myBenchmark', 'klisl');

for($a=0; $a<500000; $a++){}

Yii::endProfile('myBenchmark', 'klisl');

$result = Yii::getLogger()->getProfiling(['klisl']);

В результате работы, в переменную $result попадет массив с результатами замеров:
logs-profil.jpg

Затем его можно вывести на экран или записать в файл:
Yii::info($result, 'klisl');

Для записи в лог не забываем указать используемые параметры в конфигурации:
[
    'class' => 'yii\log\FileTarget',
    'categories' => ['klisl'],
    'levels' => ['info'],
    'logVars' => []
],