Если вы владелец сайта и думаете, что на нём нет ошибок, то вы ошибаетесь. Если вы программист и думаете, что некоторые ошибки можно не обрабатывать, то вы тоже ошибаетесь. В обоих случаях речь идёт о Енотисах (E_NOTICE). Далее вы узнаете, почему им нужно уделять внимание и как сделать удобный инструмент для хранения и обработки ошибок.

Я есть E_NOTICE
Я есть E_NOTICE

В PHP есть несколько уровней ошибок. Все знают что с ошибками уровня E_WARNING и, тем более, E_ERROR нужно бороться, исправлять их. А что вам известно о E_NOTICE? Чаще всего такие ошибки игнорируют, к ним, например, относится обращение к несуществующему элементу массива. Ну нет его, значит и не было, значит результат будет пустым и как-нибудь сам обработается, скажут некоторые.

На самом деле, за E_NOTICE может скрываться более серьёзная ошибка, которая только ожидает своего часа, а точнее полуночи субботы, когда все программисты уже спят и никто её сразу не заметит. Разве что ночной покупатель, увидев ошибку оформления заказа.

Хорошо, давайте обрабатывать E_NOTICE! E_NOTICE могут появляться периодически. Например, когда пользователь заполнил часть полей в форме заказа, а скрипт обратился к элементу, которого нет в $_REQUEST.

Может будем перехватывать все ошибки и отправлять на почту в техподдержку?  Отправлять на E-mail — хорошая идея, только если на одном хите будет сгенерировано не меньше сотни ошибок, почта будет завалена сообщениями, а почтовый сервер решит, что его DDoS-ят.

Поэтому, действовать будем осторожнее. Для начала отсортируем ошибки по категориям. Скорее всего получится так:

  1. Ошибки ядра CMS — теоретически могут появиться, трогать их не будем, ибо ядро.

  2. Чужие ошибки (если работает несколько программистов).

  3. Свои ошибки.

  4. Дубли ошибок — не совсем категория, но их тоже нужно обработать.

Где хранить ошибки, как вести их учёт? Если всё складывать в один лог-файл, то со временем он разрастается. Работать с ним не удобно, так как в результате получаем «портянку» из ошибок, часто с дублями — это хорошо знакомо программистам.

Создаём систему учёта

Поскольку на момент написания статьи по поисковому запросу «php manage errors» вы не найдёте ничего лучше, чем запись в файл или вывод в браузер, то напишем свою систему учёта.

Она должна:

  • отбрасывать дубли, делать ошибки уникальными;

  • позволять работать над ошибками нескольким программистам;

  • группировать ошибки, перемещать между группами.

Цель системы учёта — выделить из общего потока ошибок, те которые нужно обработать вручную. Далее по порядку. Напомню, что перехват ошибок в системе можно настроить в файле /bitrix/.settings.php.

Настройка собственного обработчика в .settings.php
Настройка собственного обработчика в .settings.php

Нужно указать свой класс-обработчик, который должен быть унаследован от ExceptionHandlerLog и реализовывать методы write(), initialize(), а также путь к файлу класса.

При появлении ошибки в обработчике нам доступна информация:

  • текст ошибки c названием файла и строкой;

  • трассировка.

Минимальный набор методов классе-обработчике
Минимальный набор методов классе-обработчике

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

Формирования ID (хеша) ошибки
Формирования ID (хеша) ошибки

О способах хранения ошибок

Стоит ли дублировать сообщения об ошибках на почту и прочие мессенджеры? Все сообщения — не стоит, потому что на одном хите их может быть более сотни. Как насчёт БД? БД — это хороший выбор, до тех пор пока не возникнет ошибка, препятствующая записи ошибки. Риск выглядит теоретическим, но это возможно.

Преимущества

Недостатки

Применение

Файловая система

- запись файла-ошибки - это одна операция (fwrite(), например);

- для чтения ошибок можно использовать текстовый редактор;

- файлы можно пересылать, обмениваться, делать общими;

- нужно написать свой скрипт для вывода списка, управления;

Может использоваться для учёта всех типов ошибок.

База данных

- ошибок может быть много, с подробным описанием;

- поиск, добавление, удаление средствами СУБД;

- можно добавлять дополнительные поля (комментарии, статус и прочие).

- нужно написать свой скрипт для вывода списка, управления;

- есть риск появления ошибок, которые будут препятствовать записи в БД;

- сбор ошибок создаст дополнительную нагрузку на БД сайта;

Может использоваться для учёта всех типов ошибок.

E-mail

- сообщений может быть много;

- сообщения могут содержать подробный текст ошибок.

- при большом потоке сообщений об ошибках с ними становится сложно работать;

Оповещения об ошибках типа E_ERROR, E_WARNING (т.е. единичные случаи).

Мессенджеры (XMPP)

- мгновенная доставка;

- сообщений может быть много;

- нужна программа-клиент (Psi, например);

- сообщений может быть много, но лучше, если они будут короткими, т.к. большие логи неудобно читать.

Вывод фактов появления ошибок, без подробного описания.

SMS - сообщения

- быстрая доставка;

- не нужен интернет.

- нужно оплачивать;

- сообщения должны быть короткими.

Сообщения о критических ошибках.

Специальная система хранения логов: Kibana+Elasticsearch

- просмотр логов;

- фильтрация логов;

- поиск;

- мониторинг и аналитика.

Это отдельное ПО, которое нужно настраивать, поддерживать и оплачивать вычислительные мощности.

Логи сохраняются структурировано в формате JSON для взаимодействия по Rest API.

Система не приспособлена для добавления новых источников данных, так как тесно связана с Elastic.

Наш выбор — файловая система. Да, не так красиво, но зато наглядно, быстро и надёжно. Плюс к этому, файлами можно обмениваться и это решает задачу совместной работы.

Какие категории ошибок будут в нашей системе? Предлагаю такие:

  • "новая" - тут всё понятно;

  • "обработанная" - устранена программистом или не представляет вреда;

  • "системная" - ошибка ядра, её не исправляем;

  • "пропущена" - не требует обработки, т.к. создана намеренно, в тестовой среде.

Так как решили использовать файлы, то перечисленные категории - это папки. Изменение категории ошибки - это перемещение файла между папками.

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

Папки-категории ошибок и файлы ошибок в категории “Новые”
Папки-категории ошибок и файлы ошибок в категории “Новые”
Пример содержимого файла ошибки
Пример содержимого файла ошибки

Работа над ошибками

Перейдём к алгоритмам. Как будет работать сохранение одной ошибки:

  1. перехват в классе обработчике;

  2. создание хеша (ID);

  3. сохранение подробной информации в файл (в папку "новые").

Работать с массой файлов не удобно, поэтому напишем простой скрипт, который

  • выводит список категорий (выпадающий список);

  • выводит список ошибок выбранной категории (в виде ссылок на файлы);

  • позволяет переносить ошибку в выбранную категорию.

Тогда действия программиста:

  1. выбрать категорию "новые";

  2. кликнуть на ссылку на ошибку;

  3. обработать ошибку, переместить её в одну из категорий.

Список ошибок для обработки
Список ошибок для обработки

Список ошибок для обработки

Может быть ситуация, когда программист обработал ошибку, но она возникла вновь. В этом случае обработчик ошибок перед сохранением файла должен проверять, нет ли файла с таким же названием в папке "обработанные". Если файл ошибки там есть, то выходит, что он не обработан и нужно переместить его обратно в "новые".

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

Заключение

Развивая систему, на одном из проектов мы разработали панель управления, которая программно изменяет настройки .settings, способы оповещения программиста и прочие параметры обработки.

Список ошибок для обработки
Список ошибок для обработки

Конечно, для обработки даже 10% ошибок E_NOTICE на большом сайте потребуется время. Но работа над ошибками превращается из чистки авгиевых конюшен в прозрачный процесс.

Комментарии (16)


  1. CoLdFS
    28.05.2022 17:20
    +4

    Чем вам https://sentry.io/ не угодил? Там есть все что вам нужно и даже больше. Подключается и настраивается быстро и легко


    1. Medic84
      28.05.2022 23:00
      +6

      Битриксоиды любят создавать проблемы из ничего и решать их, придумывая костыли.


    1. Areso
      28.05.2022 23:08
      -1

      Нет обёртки для чистого PHP, поддерживаются фреймворки Laravel и Symfony. Yii2, Zend - так же в пролёте.


      1. Okspen
        28.05.2022 23:14
        +2

        Есть PHP SDK от Сентри. В ваш метод write можно добавить вызов сентри и готово :)


        1. Areso
          28.05.2022 23:53

          Век живи, век учись. Я глянул лишь по главной странице проекта, и на том остановился.


  1. kirik
    28.05.2022 18:23
    +4

    Описаный подход будет работать на небольших проектах с малым количеством ошибок. А если так, то и старый-добрый php-errors log можно поразбирать раз в день :)

    Немного комментов по теме:

    • много отдельных маленьких файлов - плохо, тем более в одной директории. Что если использовать SQLite? Не потеряв в функционале и стабильности вы получите более удобную работу с данными и возможность расширения функционала в будущем;

    • мне, как разработчику, не хватает информации: как часто ошибка возникала и когда (время);

    • получать айди ошибки из текста+файла+строки, не самая лучшая идея. Например текст ошибки может содержать в себе переменную составляющую (типа Notice: Undefined offset: 102 <<), то же самое и со строкой - она может меняться при изменении вышестоящего кода;

    • при возникновении ошибки необходимо всё-таки её проверять во всех директориях (ignore, processed, system) - в статье мы проверяем только processed;

    • не вешайте на хэндлер ошибок слишком много (в статье вы допускаете отправку почты на возникновение ошибки). В идеале хэндлер должен содержать в себе какой-то один условный сисколл (типа запись в файл) и не заниматься поиском файликов в разных директориях, итп.


  1. FanatPHP
    29.05.2022 12:36
    +2

    "Шел в комнату, попал в другую" © Горе от ума

    Довольно странное построение статьи, которая начинается про забавных енотисов, затем идёт весьма спорное утверждение про то что их все игнорируют (видимо, это в порядке вещей для экосистемы битрикс, как и работа со сваленными в кучу входными данными через $_REQUEST), и дальше объясняется про обращение к несуществующему элементу массива, которое в современном РНР внезапно никакой не "енотис", а полноценный E_WARNING.


    И тут выясняется, что на самом деле статья представляет собой несколько бессвязные абстрактные размышления о написании самопального трекера ошибок для битрикса. А про "енотисов" уже никто до самого конца и не вспоминает.


    Хотя от статьи с таким заголовком ожидаешь хоть каких-то рекомендаций по борьбе с этими самыми "енотисами". Хотя бы пояснения — а как, собственно, отключить игнорирование ошибок уровня E_NOTICE. А так же стандартного предупреждения о том что бездумное подавление этой ошибки через null coalescing operator не слишком будет отличаться от игнорирования. И что надо в первую очередь разделить переменные на создаваемые в скрипте и получаемые извне.


    И для первых ввести жесткое правило, что внутренние переменные всегда должны быть инициализированы (при этом для комплексных структур отлично подойдут DTO).


    Внешние же переменные в свою очередь разделить на обязательные и необязательные. И проверку на существование делать только для первых. А для обязательных определиться со стратегией, которая с одной стороны, не будет являться тем же игнорированием, а с другой — не будет заваливать логи фальшивыми срабатываниями, когда очередной скрипт-кидди решит попарсить ваш сайт, забыв заполнить половину полей формы. Но опять же, эту проблему надо решать только по мере её появления, а по умолчанию использовать нотисы для внешних переменных строго по назначению — разбираться с тем, почему обязательная переменная не была передана.


  1. 0x131315
    29.05.2022 17:38
    +1

    Хз, действительно велосипед. Таких самодельных логов уже насмотрелся. Нужно использовать готовое.

    Что касается обработки исключений - считаю что исключения обрабатывать в большинстве случаев не нужно, как минимум потому, что правильно это делать умеют единицы, у остальных вся обработка сводится к подавлению, что приносит только вред: попробуй потом найди, почему код не работает как нужно.

    Исключения нужно обрабатывать в действительно исключительных ситуациях, и только те, что ожидаешь: если исключение для алгоритма естественно, является нормальным сценарием. Например как при работе с апи исключение при попытке соединения - нормальная ситуация, и требует просто отключить логику, которая зависит от апи, падать тут смысла нет, если сценарий работы без апи предусмотрен. И опять же - тут обрабатываем только ошибки коннекта, остальные случаи обрабатывать обычно не стоит: в случае неожиданной ошибки апи гораздо ценнее если код полностью упадет, заставив обратить внимание на действительно исключительную ситуацию - очевидно что апи поменялось, нужно изучить вопрос и адаптировать код под новые условия, а не подавлять ошибку.

    Что касается логирования - по мне требуется использовать как минимум три подхода: отдельные каналы с группировкой по сущности (например заказу, чтобы все, относящееся к заказу, было доступно в отдельном канале), отдельные каналы на каждую из ключевых подсистем (чтобы исследовать работу подсистемы в проблемных ситуациях - можно поднять логи конкретной системы и посмотреть что было на входе, на выходе, в ключевых точках, отладка с такими логами - минутное дело), и отдельные каналы на общие исключения, которые стоит перехватывать через отдельные неблокирующие обработчики исключений (что кстати редко наблюдаю) - это те самые проблемы, на которые нужно обращать внимание в первую очередь, такие логи показывают жёсткие падения, что явно ненормальная ситуация.

    Все эти логи, особенно по подсистемам, весят очень много - их хранение отдельная большая тема. И файловые логи тут - наихудшее решение: они огромные, нет удобных инструментов для работы/поиска/анализа. Гораздо удобнее использовать специальные брокеры/бд для логов, типа эластика, где можно партицировать/удалять/фильтровать отдельные логи по каналам/времени/содержимому, и сами записи из множества каналов/логов можно сшивать в единый поток или анализировать отдельно - это гораздо удобнее, чем возиться с файлами или собирать данные по нескольким файлам. К тому же размер логов - больная тема, файлы менеджерить не особо удобно. А с такими бд можно всегда устаревшие логи отправить в архив, и поднять обратно, если всплывёт что-то, что требует исторического анализа.

    Нотисы же тут мало интересны - это не особо критичные проблемы в коде, больше относящиеся к стилю, их лучше отлавливать через дебаг-панели, там просто висит счётчик нотисов, есть время/желание - бери и рефактори, убирай устаревший код, но конкретно на cms это неблагодарная работа - сами cms это источник тысяч нотисов.


  1. mSnus
    29.05.2022 18:52

    А почему, собственно, решили писать велосипед вместо того же Elastic? Он же как раз заточен изначально под сбор информации из логов и её анализ


    1. kirik
      30.05.2022 05:51

      Позволю себе ответить за автора цитатой из статьи: "Это отдельное ПО, которое нужно настраивать, поддерживать и оплачивать вычислительные мощности."

      Если у меня простой L(A)MP (а то и шаред - так исторически сложилось) и CMS с тремя пользователями в день и один разраб - куда здесь эластик.. :) Оверинжениринг - лютое зло.


      1. FanatPHP
        30.05.2022 09:05

        Я извиняюсь, а вот этот вот самопал из статьи на таком сетапе будет не оверинжениринг? :)


        Позволю себе ответить цитатой из другого комментатора: если у вас CMS с тремя пользователями в день и один разраб, "то и старый-добрый php-errors log можно поразбирать раз в день :)".


        У нас вот не CMS, а небольшой магазинчик с тремя (сотнями тысяч) пользователей в день, и мы прекрасно справляемся с обычными логами. Потому что ошибка — это не поразбирать, а форс-мажор, это значит что что-то поломалось — или в инфраструктуре, или в новом коде косяк, который не получилось отловить при стандартном тестировании — и надо чинить, а не "складировать".


        Сейчас конечно внедряется всякая кибана, но мне как-то надёжнее со своим простым сторожевым пёсиком, который начинает гавкать в почту и телеграмм, если ошибки в лог идут потоком, и сразу притаскивает пример ошибки.


        1. kirik
          30.05.2022 17:37

          а вот этот вот самопал из статьи на таком сетапе будет не оверинжениринг? :)

          отнюдь. Это какая-никакая степень удобства по разбору ошибок вместо tail.

          надо чинить, а не "складировать"

          бесспорно, но речь в статье про "енотисы", которые возникают постоянно и не всегда в подконтрольном коде.

          Сейчас конечно внедряется всякая кибана, но мне как-то надёжнее со своим простым сторожевым пёсиком

          если есть ресурсы (и необходимость) на поддержку ELK - очень здорово, "сырые" логи перестают работать тогда, когда в инфре становится >=2 скриптовых машин и появляется необходимость в централизованной обработке логов.


          1. FanatPHP
            31.05.2022 07:18

            Не, ну вы уж определитесь :)
            Или речь про "CMS с тремя пользователями в день" — и тогда всё, что сложнее tail, здесь будет overkill.
            Или про систему где нотисы льются потоком, и все равно пилить какую-то систему сортировки. Но тогда непонятно, зачем конкретно этот самопал, и почему нельзя делать сразу нормально.


            Если бы автор хотя бы приложил к статье код, то тогда, возможно, для каких-то частных случаев, можно было бы говорить о его применении. А так — я реально не вижу, зачем кто-то будет их применять вместо того чтобы делать сразу нормально.


            Хотя возможно, конечно, это специфическое решение для какого-то адова легаси типа битрикса. Тут я не копенгаген, не сталкивался.


            1. kirik
              31.05.2022 07:41

              Определиться с чем? :)

              "сразу нормально" это как? обычно приходится выбирать: или сразу, или нормально))

              Тут и без кода понятно какую проблему автор решал для себя, и, в целом, единственная "нормальная" альтернатива - это sentry, но есть нюансы в виде непостоянства нашего интернета (да и мира в целом), а если разворачивать on premise - то выйдет дороже ELK.

              Ну и да, описан специфичный кейс, который вполне может кому-нибудь пригодиться как идея. Доработать немного и будет легковесный менеджер ошибок.


  1. zorn_v
    30.05.2022 17:50

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

    Ну а для не нормальных set_error_handler.


    1. FanatPHP
      31.05.2022 07:20
      +1

      Ну вообще-то автор именно про это и пишет. Что у него не "нормальный фрейворк", а битрикс. И про то, как использовать set_error_handler в этом случае :)