Часть 1. Логирование событий в Oracle PL/SQL

рис. Старый "дедовский" debug кода
рис. Старый "дедовский" debug кода

Добрый день! Работая разработчиком Oracle PL/SQL, часто ли вам приходилось видеть в коде dbms_output.put_line в качестве средства debug-а? Стоит признать, что к сожалению, большинство (по моему личному мнению и опыту) разработчиков Oracle PL/SQL не уделяет должного внимания логированию как к «спасательному кругу» в случае возникновения ошибок. Более того, большая часть разработчиков не совсем понимает зачем нужно логировать информацию об ошибках и самое главное, не совсем понимают что делать и как использовать эту информацию в будущем.

Предисловие

Данным постом хотел бы начать цикл статей посвященных «Логированию ошибок» в Oracle PL/SQL. В первую очередь донести мысль до многих разработчиков, о том как можно построить функционал фиксации, хранения логов в БД. На своем опыте продемонстрировать поэтапный процесс создания полноценного логирования в БД. Рассказать как нам удалось создать логирование ошибок, разработать единую нумерацию событий для их дальнейшей идентификации, как поверх логирования «натянуть» мониторинг событий, создать функционал позволяющий увидеть все текущие ошибки в БД в виде таблиц (с указанием частоты возникновения ошибок и кол-ва и т.д.), графиков (отразить динамику роста кол-ва ошибок) и правильно распределить ресурсы для устранения тех или иных ошибок.

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

Введение

В этой и следующих статьях будет показано как реализованный функционал «логирования» позволяет фиксировать факт возникновения не только ошибок (сообщение с типом «Error»), но и сообщений с предупреждением (тип «Warning»), информативных сообщений (с типом «Info») и т.д., поэтому, в рамках данных статей введём термин - «Модель логирования событий» (далее по тексту - "модель") или коротко «Логирование событий», где под "событием" подразумевается некоторое ожидаемое действие, возникшее в ходе штатной/внештатной работы алгоритма.

Модель логирования позволяет реализовать:

  1. Единый подход в обработке и хранении событий

  2. Собственную нумерацию и идентификацию событий происходящих в БД (статья)

  3. Единый мониторинг событий (статья в разработке)

  4. Анализ событий происходящих в БД (статья в разработке)

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

Единый подход в обработке и хранении событий

Основной идеей "Единого подхода в обработке и хранении событий" заключается в создании одного одновременно простого и в тоже время очень сложного правила: "Все объекты базы данных (функции, процедуры) в обязательном порядке должны завершаться блоком обработки исключений с последующим логированием события". Простота заключается в том, что легко, в команде разработчиков, на словах договориться об исполнении данного правила. Сложность же заключается в том, что данное правило должно быть установлено на ранних этапах создания вашей БД и выполняться обязательно на протяжении всего жизненного цикла. Внедрить функционал логирования в уже существующие и действующие БД очень сложно (практически не возможно).

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

Наверное сейчас кто-то из читателей может возразить: "Зачем в обязательном порядке?". А всё очень просто, если вы разработчик PL/SQL и вы не согласны с этим правилом, то вот вам пример. Посмотрите на свой текущий проект более внимательно. Скорее всего вы найдете какое-нибудь логирование событий реализованное кем-то, когда-то. Вспомните сколько раз вы обращались к этому логированию при решении багов. Именно в таких ситуациях, когда есть срочность по времени в исправлении бага, вы или ваши коллеги начинают использовать dbms_output.put_line в качестве экспресс-дебага (быстрый способ получения значений переменных используемых в коде). Согласитесь, что для исправления бага мало знать в какой процедуре, в каком запросе и на какой строке возникла ошибка, необходимо знать параметры запроса на которых возникает ошибка. И вот тут нам на помощь приходит "Логирование событий", потому что помимо места возникновения ошибки мы узнаем параметры вызова процедуры, в которой возникает ошибка и это очень упрощает исправление бага.

Первая статья посвящена базовому функционалу «Логирования событий». В простейшей реализации это одна общая таблица и пакет процедур для работы с ней. Для создания и демонстрации логирования, нам необходимо реализовать следующие объекты БД (весь список объектов с их исходными кодами представлен в Git):

  1. Таблица messagelog - единая таблица логов. Именно в данной таблице будет храниться информация о дате и времени события, об объекте где происходит событие, типе события с указанием кода, текста и параметров. В нашем примере, столбец backtrace вынесен в отдельную таблицу messagelog_backtrace для удобства.

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

    Также, учитывайте пожалуйста, что создание партиции требует как минимум Oracle EE. Создание партиции вне указанной версии Oracle приведет к нарушению лицензионного соглашения.

    Структура таблицы

    Название столбца

    Тип данных

    Комментарий

    id

    number primary key

    Первичный ключ таблицы

    sessionid

    number(38)

    Идентификатор сессии для объединения группы ошибок

    objname

    varchar2(60)

    Содержит наименование объекта из которого было инициировано событие. Чаще всего это комбинация имя_пакета.имя_процедуры

    msgtype

    varchar2(3)

    Тип события, например: ERR - сообщение об ошибке; INF - информационное сообщение; WRN - сообщение с предупреждением и т.д.

    insertdate

    date

    Дата и время создания записи о событии

    msgcode

    varchar2(10)

    Код ошибки, чаще всего это SQLCODE, либо код ошибки из справочника ошибок (об этом будет отдельная статья)

    msgtext

    varchar2(500)

    Текст ошибки, чаще всего это SQLERRM, либо текст ошибки из справочника ошибок

    paramvalue

    varchar2(500)

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

    backtrace

    varchar2(4000)

    Записывается результат функции dbms_utility.format_error_backtrace, либо иная дополнительная информация

    Исходный код таблицы
    create table messagelog(id         number(38)      not null,
                            sessionid  number(38)      not null,
                            msgtype    varchar2(3)     not null,
                            objname    varchar2(60)    default null,
                            insertdate date            default sysdate,
                            msgcode    varchar2(10)    default null,
                            msgtext    varchar2(4000)  default null,
                            paramvalue varchar2(4000)  default null,
                            constraint pk_messagelog_id primary key (id))
    partition by range (insertdate)
      interval (numtoyminterval(3, 'MONTH'))
        (partition p1 values less than (to_date('01.01.2020', 'DD.MM.YYYY'))); 

    *Исходный код других используемых объектов смотрите в Git

    В данной статье не будем подробно описывать способы индексирования и партицирования таблицы, всё зависит от целей и задач логирования. Самый предпочтительный вид хранения таблицы это партицирование по столбцу insertdate с интервалом 3 месяца.

    Отдельно стоит упомянуть про столбцы msgtext, paramvalue и backtrace. По своему опыту скажу, что указанные столбцы в 60% - 70% случаев будут пустыми и это очень сильно не нравится архитекторам и/или руководителям отделов разработки. Поэтому иногда есть смысл вынести указанные столбцы в отдельную таблицу с привязкой по id записи. В нашем примере столбец backtrace вынесен в отдельную таблицу messagelog_backtrace (см. исходный код в git).

  2. Пакет pkg_msglog - содержит набор процедур необходимых для логирования событий. В нашем примере будут представлены одна "корневая" и две процедуры для логирования событий: "ERR" - событие об ошибке; "WRN" - предупреждающее событие. Вызов корневой процедуры выполняется в автономной транзакции и делается это для того, чтобы была возможность выполнить сохранение данных об ошибке с откатом всех предыдущих изменений.

    Исходный код процедур пакета
    -- процедура логирования ошибок
    procedure p_log_err(p_objname    in varchar2,
                        p_msgcode    in varchar2,
                        p_msgtext    in varchar2 default null,
                        p_paramvalue in varchar2 default null,
                        p_backtrace  in varchar2 default null)
      is
      begin
        p_insert_log(p_msgtype_    => 'ERR',
                     p_sessionid_  => v_sid,
                     p_objname_    => p_objname,
                     p_insertdate_ => sysdate,
                     p_msgcode_    => p_msgcode,
                     p_msgtext_    => p_msgtext,
                     p_paramvalue_ => p_paramvalue,
                     p_backtrace_  => p_backtrace);
      end p_log_err;
      
      -- корневая процедура логирования
      procedure p_insert_log(p_msgtype_    in varchar2,
                             p_sessionid_  in number,
                             p_objname_    in varchar2,
                             p_insertdate_ in date,
                             p_msgcode_    in varchar2,
                             p_msgtext_    in varchar2 default null,
                             p_paramvalue_ in varchar2 default null,
                             p_backtrace_  in varchar2 default null)
      is
        v_id   messagelog.id%type;
        pragma autonomous_transaction;
      begin
        insert into messagelog(msgtype,
                               sessionid,
                               objname,
                               insertdate,
                               msgcode,
                               msgtext,
                               paramvalue)
            values(p_msgtype_,
                   p_sessionid_,
                   p_objname_,
                   p_insertdate_,
                   p_msgcode_,
                   p_msgtext_,
                   p_paramvalue_)
        return id
          into v_id;
        if trim(p_backtrace_) is not null then
          insert into messagelog_backtrace(id,
                                           backtrace)
          values(v_id,
                 trim(p_backtrace_));
        end if;
    
        commit;
      end p_insert_log;

    *Исходный код других используемых объектов смотрите в Git

  3. Тестовый пакет pkg_clients - пакет с демонстрационными процедурами поиска и создания пользователей. Обратите внимание, что процедура p_insert_user выполняет функции аудита события - "создание нового пользователя".

    Исходный код процедур поиска и создания пользователей
    create or replace package body pkg_clients 
    as
      procedure p_insert_user(p_login_     in varchar2,
                              p_firstname_ in varchar2,
                              p_lastname_  in varchar2,
                              p_id_        out number)
      is
        v_id clients.id%type;
      begin
        insert into clients(login,
                            firstname,
                            lastname)
            values(upper(p_login_),
                   p_firstname_,
                   p_lastname_)
        return id
          into v_id;
        if v_id > 0 then
          -- аудит события - создание нового пользователя
          pkg_msglog.p_log_wrn(p_objname    => 'pkg_clients.p_insert_user',
                               p_msgcode    => '101',
                               p_msgtext    => 'Создан новый пользователь с id = '||v_id,
                               p_paramvalue => 'p_login = '||p_login_
                                                 ||', p_firstname = '||p_firstname_
                                                 ||', p_lastname = '||p_lastname_);
        end if;
        commit;
      exception
        when others then
          pkg_msglog.p_log_err(p_objname    => 'pkg_clients.p_insert_user',
                               p_msgcode    => SQLCODE,
                               p_msgtext    => SQLERRM,
                               p_paramvalue => 'p_login_ = '||p_login_
                                                 ||', p_firstname_ = '||p_firstname_
                                                 ||', p_lastname_ = '||p_lastname_,
                               p_backtrace  => dbms_utility.format_error_backtrace);
          raise;
      end p_insert_user;
      
      procedure p_create_user(p_login     in varchar2,
                              p_firstname in varchar2,
                              p_lastname  in varchar2,
                              p_id        out number)
      is
        v_id clients.id%type;
      begin
        begin
          select id
            into v_id
            from clients
           where login = upper(p_login);
        exception
          when no_data_found then
            p_insert_user(p_login_     => p_login,
                          p_firstname_ => p_firstname,
                          p_lastname_  => p_lastname,
                          p_id_        => v_id);
        end;   
        p_id := v_id;
      exception
        when others then
          pkg_msglog.p_log_err(p_objname    => 'pkg_clients.p_create_user',
                               p_msgcode    => SQLCODE,
                               p_msgtext    => SQLERRM,
                               p_paramvalue => 'p_login = '||p_login
                                                 ||', p_firstname = '||p_firstname
                                                 ||', p_lastname = '||p_lastname,
                               p_backtrace  => dbms_utility.format_error_backtrace);
          raise;
      end p_create_user;
    end pkg_clients;

    *Исходный код других используемых объектов смотрите в Git

Демонстрация логирования событий

Для демонстрации работы выполним три стандартных кейса с помощью созданного ранее пакета pkg_clients.

  1. (Ошибочная ситуация) Создание пользователя с длиной имени превышающей допустимое значение в таблице

    Пример 1
    рис. пример запуска процедуры
    рис. пример запуска процедуры

    Результат

    рис. описание ошибки
    рис. описание ошибки
  2. (Ошибочная ситуация) Создание пользователя с пустым значением имени или фамилии (предположим, что есть такое требование)

    Пример 2
    рис. пример запуска процедуры
    рис. пример запуска процедуры

    Результат

    рис. описание ошибки
    рис. описание ошибки
  3. (Успешная ситуация) Создание нового пользователя с аудитом события

    Пример 3
    рис. пример запуска процедуры
    рис. пример запуска процедуры

    Результат

    рис. описание ошибки
    рис. описание ошибки

    Обратите внимание, что все демонстрационные процедуры завершаются блоком exception when others then с дальнейшим вызовом процедуры логирования события вне зависимости от "важности" процедуры.

Заключение

В заключении я повторюсь, что гораздо проще и правильнее добавлять логирование событий в обработку исключений при первом написании процедуры (функции) т.к. это не занимает много времени. Гораздо сложнее в будущем, внести правки в большое количество объектов БД и это займет гораздо больше времени на написание кода, тестирование и т.д.

По своему опыту отмечу, что пакет pkg_msglog (со всеми дополнительными объектами логирования) должен быть создан самым первым в проектируемой БД и в дальнейшем другие объекты создаются с логированием событий. Также, часто в командах появляются разработчики, которые говорят: "Зачем логировать все процедуры (функции)? Давайте вести логирование только важных и нужных процедур (функций)!". Вот здесь скрывается самая главная и распространенная ошибка. У всех нас разное понимание что есть "важная и нужная" процедура (функция). По сути, вы создаете логирование ошибок, которое "как бы и есть, но как бы его нет" т.е. все в команде знают, что в БД есть лог ошибок, но в нужный момент (например, при возникновении "блокера" на продакшн) вы обнаружите что в логах нет никакой информации об ошибке. Именно в такие моменты и возникает ошибочное мнение, что логировать события бесполезно и бессмысленно, а ведь это не так.

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

Спасибо за внимание.

Upd. 15.04.2021. Добавлен столбец sessionid для объединения группы ошибок возникших в рамках одной сессии.

Upd. 04.05.2021. Слияние, создание перекрестных ссылок со второй статьей.

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 45

    +1
    Аж олдскулы свело
      0
      Извиняюсь, не совсем понял комментарий, что не так?
        0
        Все так! Очень приятно, что разработка на PL/SQL не умерла!
        От себя, я бы подумал использовать systimestamp вместо sysdate.
        Штука полезная, благодаря ей в свое время благодаря этому находили странные ошибки
          0
          Все так! Очень приятно, что разработка на PL/SQL не умерла!

          Думаете это хорошо? Я пол жизни хотел поработать с ораклом, поработал, не понравилось, в том числе изза огромного легаси которое по репозиторий даже не слышало. Классный способ выстрелить себе в ногу.

          Кроме того видел очень много ораклистов, которые так заигрались в pl/sql что даже вещи которые можно сделать проще (например check) заворачивают тудаже.

          А уж в какую прелесть превращается переиспользование кода, мммм… Функции вызываемые построчно, функции в условии, каскад процедур функий, ням ням.
            +1
            Я долго занимался разработкой на pl/sql. Все выше это не проблемы языка. Можно все то же самое сказать про любой язык программирования. Это проблема организации процесса разработки.

            И код пере использовали и делали апи для вызова заказчиками. Много чего было сделано и работает очень хорошо.
              0
              Сложность интеграции с гит это именно проблемы языка. Остальное да, культура разработки.
                +2
                Ну как бы и Java не умеет работать с гитом :)
                А так, нет там сложности с интеграцией.
                PL/SQL Developer и Oracle SQL Developer умеют работать с git. В конце концов можно и файлы сохранять.
                Для дистрибутива лучше использовать ликвид.
                Не, норм все. Весь вопрос в процессе
                  0
                  Ну вот как раз яву в гит сунуть проблем нет, а вокруг SQL танцы с бубном.
                  Ликвид конечно вариант, но весьма костыльный.
                    0
                    Бесплатно, сохраняем в виде файликов и пихаем в гит.

                    Дорого: www.gitora.com
                      +1

                      Скажу по секрету что гитора взламывается на раз два(потому что анвраппер). И при этом можно активировать типа платные вип-фичи для понтового просмотрщика репозитория. Но только тсс))) И там еще и свои болячки тоже есть у него принципиальные, по которому оно для прода не готово вообще. Побаловаться в тестовых средах еще может быть...

      +2
      Не рассматривали log4plsql.sourceforge.net?
        +1
        Спасибо за подсказку, если честно даже и не знал, что есть такой готовый продукт, но этот продукт не рассматривал. Конечно можно внедрить уже готовый проект, я же хотел показать о способе создания «своего» лога на этапе построения БД. Более того, сейчас чуть внимательнее посмотрел на реализацию «log4plsql» и в принципе есть схожести. Но если я смогу дописать весь цикл статей, то наверное моё логирование буде содержать функционал которого нет в «log4plsql».
          0

          Используем plog для не очень важных/нагруженых мест и где не так сильно много логов надо писать.
          Потому что логи в таблицу — это фу.

            0
            Используем plog для не очень важных/нагруженых мест и где не так сильно много логов

            Я об этой ситуации написал в статье:
            Также, часто в командах появляются разработчики, которые говорят: «Зачем логировать все процедуры (функции)? Давайте вести логирование только важных и нужных процедур (функций)!».

            У всех разработчиков разное понимание «не очень важных/нагруженых мест» и если использовать такой критерий логирования, то тогда наверное такое логирование будет бесполезным.

            Используем plog ...

            Это все хорошо до тех пор, пока вы не попадете в крупную ИТ-компанию, например Сбер, ВТБ. На коммерческий продукт для логирования ошибок в Oracle в проекте никто никогда не закладывает бюджет, а использовать оперсоурсные по, библиотеки и прочее вам скорее всего не дадут «безопасники».

            Потому что логи в таблицу — это фу

            По своему опыту использования таблиц для логов, могу сказать, что в среднем в партицию за один квартал прилетает примерно 5-6 тыс строк. В период активного внедрения нового функционала кол-во ошибок возрастает, но со временем цифра усредняется. Зачем изобретать велосипед и если не в Oracle, то тогда где вести лог ошибок? В файлах на удаленном сервере? И ведь такой «лог» ошибок практически бесполезен, я не говорю о том, что рано или поздно файлы лога забьют свободное место на дисках. Контролировать наполнение таблицы лога вы всегда можете своими силами (силами отдела разработки или сопровождения), а вот файловый сервер это как правило зона ответственности администраторов. И по своему опыту в таком банке могу сказать, что такой лог ошибок часто «ронял» сервер.
          0
          partition by range (insertdate)

          Это подразумевает не только использование Enterprise Edition, но ещё и «Extra cost option». Значит, без нарушения лицензионного соглашения, Ваш метод не подойдёт для других редакций.
            0
            В текущей статье способ партицирования выбран для примера. Я специально не стал подробно описывать способы индексирования т.к. в разных компаниях, в разных командах разработчиков свои взгляды. Большей проблемой при создании логирования в компании, является сам факт создания и поддержание логирования в актуальном состоянии (о чем я пытался довести в статье).
              0
              Мне кажется, об этом аспекте стоило упомянуть. А то скопипастят же не глядя.
                0
                Спасибо за совет. Добавил пояснение в статье.
            0
            Не хватает иерархиии (ссылки сообщения на родительское). В сложных системах когда вызов какого-то действия порождает сотни-тысячи записей в лог, при этом система сильно параллельна, то лог превращается в жуткую кашу, которую не разобрать. Иерархия позволяет вытаскивать только лог нужного вызова, а также удобно смотреть весь стек вызова.
              0
              Вы говорите про существенное усложнение модели логирования, которая сложна как разработке так и поддержании в актуальном состоянии. В статье приведен пример простейшего логирования и зачастую в большинстве компаниях вообще нет и такого логирования ошибок (событий). В дальнейших статьях постараюсь описать способ выявления «критичных» ошибок из общего пулла записей в таблице лога.
                0
                Нет особого усложнения, один раз пишется иерархия, а при использовании вообще почти никакой разницы. Зато можно с прома получать нормальные логи, которые позволяют разобраться.
                  0
                  Спасибо, наверное стоит подумать. Сначала попробую на своей базе реализовать и чуть позже опишу в дополнении к действующему функционалу.
              0
              А ротация записей? А обслуживание роста таблицы? А быстрый доступ к конкретным сообщениям, типам сообщений? Через 3-4 года работы (когда записей может стать десятки миллионов на нагруженной системе) этот вопрос станет ребром.
                0
                Это уже тонкости реализации. Обычно все гораздо проще: при наступлении нового квартала партиция с данными предыдущего квартала остается, все что старее удаляется таким образом мы храним в истории события предыдущего квартала. Локальные индексы внутри партиции можно создать, но это уже тема отдельного обсуждения. Повторюсь, что всё очень индивидуально в разных компаниях разные цели и задачи от функционала логирования событий.
                  0
                  За весь кровавый не скажу. Но обычно. Глубины около месяца хватает. Самое важное это после релиза. Плюс уровень логирования то же должен меняться динамически
                  0

                  Во первых, я бы рекомендовал захинтовать вставку при помощи APPEND NOLOGGING, чтоб ускорить и не генерить redo для этих данных. А ещё покурите асинхронный коммит, например https://www.orafaq.com/node/93. Я вижу у вас партиции, следовательно это решение предназначено для прода а не для разработки.


                  А не лучше ли в таком случае вместо хранения логов в базе слать их во внешнюю систему? Можно написать простейший Java source пакетик (или или готовый байткод загрузить в базу), и через него стримить наши логи в Apache Kafka или в любую другую шину? Оттуда направлять это куда душе угодно, в любую time-series database, или в старый добрый Prometheus, потом их можно будет видеть в системе мониторинга вкупе с остальными метриками и делать выводы, например в Grafana Loki. Заодно и retention policy для логов проще управлять. И можно алерты настроить чтоб по мейлу или прям в телегу получать важные оповещения.

                    0
                    Спасибо за комментарий. Все commit выполняются в автономной транзакции. Хинт «APPEND» можно использовать только (желательно) при insert в рамках одной сессии. Если выполнять insert из разных сессий с хинтом «APPEND», то текущий insert будет блокировать таблицу для других сессий.
                    И в целом, мне кажется вы чуть-чуть преувеличиваете важность таблицы логирования событий. Чтоб вы понимали, за весь вчерашний день 31.03.2021 в таблице лога на прод сервере у нас появилось 124 строчки (3 с типом Err и 121 с типом Msg).
                    По поводу хранения логов во внешней среде, то я уже видел подобные реализации. Во-первых, основная проблема это в дальнейшем использовании этих логов (неудобно анализировать их, искать конкретную ошибку и т.д.). Во-вторых, файлы периодически заполняют все свободное место.
                    В дальнейших статьях я постараюсь описать как мы настроили мониторинг событий в самом Oracle. Мы в текущей компании используем qlikview для визуализации событий.

                    Я вижу у вас партиции, следовательно это решение предназначено для прода а не для разработки.

                    Вот здесь если честно я не понял о чем речь. У нас на всех стендах от развернуто подобное логирование. На данный момент могу сказать, что им пользуются разработчики на dev-стенде, тестировщики на своем (прелайв) стенде и пользователи используют лог на прод-сервере.
                      0
                      Ничего не имею против предложенного решения.
                      Но текущая реальность несколько иная.

                      У нагруженных систем, логов обычно не 100 или 1000 в день, а от десятков миллионов.
                      В свете этого всякие ES, CH и прочие, не зря свой хлеб едят.
                      Традиционные БД итак, обычно, узкое место, еще и логи писать потоком.

                      При этом и выбор в plsql не очень большой. Либо в табличку писать, либо по сети через java pkg. Из табличек, понятно, все равно потом нужно данные переливать в ES, CH. Можно, конечно, работу с табличками оптимизировать — batch, partition, no index, multiple tables, etc и дожать до 4-10 тыс с сек на хорошем железе. Но если будет больше, придется съезжать на другой стек.
                        0
                        нагруженных систем, логов обычно не 100 или 1000 в день, а от десятков миллионов.

                        Согласен, наверное в случае миллионов логов в день, то лучше использовать промышленное решение. В одной компании (в которой я работал раньше и где уже был реализовано подобное логирование) с течением времени перешли на коммерческое решение для логирования всего массива событий. Такой качественный переход был осуществлен только тогда, когда появилось понимание, что вышеописанного логирования недостаточно чтобы покрыть нужды компании.
                        Поэтому я в описании статьи делаю упор на то, что подобное логирование нужно создавать именно на «начальных» этапах построения БД.
                    0

                    Обработка when others без raise внутри — отличный способ выстрелить себе в ногу. Вызванная процедура отработала вхолостую, вызывающая об этом ничего не знает и продолжает как ни в чем не бывало.
                    Архитекторы у вас боятся пустых полей, зато не боятся лишнего insert. Очень странные архитекторы.
                    А ещё rollback без savepoint, который откатывает весь dml, сделанный в вызывающей процедуре. Такая мина заложена, что когда она взорвется, уйдет не один час на то, чтобы понять, почему данные не сохраняются в базу.
                    Вероятность 50/50, что программист при добавлении параметра в процедуру забудет добавить его в строку p_paramvalue и в самый неподходящий момент выяснится, что его значение не залогировано, а оно-то как раз и необходимо.
                    Хороший набор антипаттернов для включения в каждую процедуру!

                      0
                      Приведенные примеры показывают как использовать логирование ошибок в момент возникновения самой ошибки (exception), то как будет вести алгоритм далее после передачи управления во внешний блок это уже совсем другая история. Описанные вами примеры использования raise и savepoint необходимо использовать в конкретных ситуациях, описание которых это отдельная тема для разговора.

                      Вероятность 50/50, что программист при добавлении параметра в процедуру забудет добавить его в строку p_paramvalue и в самый неподходящий момент выяснится, что его значение не залогировано, а оно-то как раз и необходимо.

                      Да, есть такая вероятность и даже сам с этим сталкивался. А какая может быть альтернатива? Пока что остается одно — вручную вносить список параметров в p_paramvalue.
                        0
                        Мое мнение такое, что when others then… raise и rollback to savepoint в обработчике ошибок общего назначения — это как раз должно использоваться всегда, а вот отказ от них возможен в редких частных случаях. Такая практика поможет избежать множества сложновылавливаемых ошибок. Приведенные примеры показывают, как, создавая вроде бы полезную фичу для борьбы с ошибками, фактически добавлять новые ошибки, которые будут «стрелять» при обработке исключительных ситуаций. Как маскировать реальные причины проблемы.

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

                        Список параметров каждой процедуры есть в системных view. Можно написать функцию проверки наличия всех параметров в тексте пакета/процедуры и запускать эту функцию по триггеру в момент компиляции пакета/процедуры на тестовом или разработческом стенде.
                        Тогда программист не сможет забыть указать параметр.
                          0
                          Мне понятна ваша позиция, но я с ней не согласен от слова совсем.
                          Мое мнение такое, что when others then… raise и rollback to savepoint в обработчике ошибок общего назначения — это как раз должно использоваться всегда, а вот отказ от них возможен в редких частных случаях.

                          За свою практику работы с Oracle я наблюдаю диаметрально противоположную картину — во многих компаниях используется концепция: «если транзакция падает с ошибкой (типа «when others then»), то данную транзакцию завершают полным откатом изменений т.е. либо алгоритм отрабатывает без ошибок и сохраняем результат, либо завершаем алгоритм и не сохраняем вообще ничего».

                          Такая практика поможет избежать множества сложновылавливаемых ошибок.

                          Я бы сказал, что все наоборот. Такая практика только усложняет понимание корректности полученных данных. Поясню на примере, у вас есть функция которая рассчитывает процент по кредиту для клиента. Внутри этой функции вызовы множества процедур, которые рассчитывают различные атрибуты, параметры которые необходимы при расчете итогового процента по кредиту. Но вот в одной из процедур возникла неизвестная «when others then» ошибка и что делать тогда? В нашем случае мы уроним весь расчет и залогируем ошибку с параметрами запуска процедуры в которой произошла ошибка. После того, как исправят данную ошибку, то можно будет перезапустить расчет процента для указанного клиента. В вашем же случае, произойдет откат до сохраненной точки, а дальше что? Вы продолжите рассчитывать остальные атрибуты и на основании их посчитаете процент по кредиту? Т.е. у вас есть некий итог работы функции, но вот как понять что он корректный?

                          Это я все к тому, что способы написания кода бывают разные, у вас свои взгляды и нас свои. Вопросы rollback, raise и savepoint я в статьях не затрагиваю т.к. смысл статей не в этом. Еще раз раз повторюсь, я лишь показываю как можно отловить ошибку с её параметрами в момент её возникновения. Что вы (либо другой читатель) будете делать после возникновения ошибки это дело непосредственно ваше.

                          Список параметров каждой процедуры есть в системных view. Можно написать функцию проверки наличия всех параметров в тексте пакета/процедуры и запускать эту функцию по триггеру в момент компиляции пакета/процедуры на тестовом или разработческом стенде.

                          Если я так сделаю на каком-либо проекте, то меня будут вспоминать проклиная и ненавидя… На самом деле это не такая частая проблема, да иногда забывают и ничего не мешает добавить позже, все решается обычным кодревью.
                            0
                            «если транзакция падает с ошибкой (типа «when others then»), то данную транзакцию завершают полным откатом изменений т.е. либо алгоритм отрабатывает без ошибок и сохраняем результат, либо завершаем алгоритм и не сохраняем вообще ничего».

                            Так как раз ваш предлагаемый подход в КАЖДОЙ процедуре глушить все иск.ситуации и делать rollback противоречит этой здравой концепции.
                            Предположим у нас есть управляющая процедура Main (в ней одна транзакция, в конце процедуры commit), которая вызывает по очереди три процедуры, которые делают какие-то этапы общей транзакции, выполняют dml. Назовем их Step1, Step2, Step3. Если в Step2 произойдет exception, то произойдет откат dml, выполненных в Step1 и Step2. Далее, поскольку raise не сделан, управление перейдет в Step3, и затем общий commit в Main. В итоге мы получили в базе не «все или ничего», а только dml из Step3, что при разборе крайне загадочно и поставит разработчика в тупик.
                            Если же использовать rollback to savepoint Step2 и raise, управляющая процедура получит информацию, что Step2 не выполнен и сможет далее принять решение, можно ли переходить к Step3 или нужно аварийно завершиться без commit. Если в Main программист не предусмотрел блок обработки ошибок, то после exception в Step2 Main завершится аварийно без commit и мы получим «все или ничего».
                              0
                              Не надо судить о всей статье, о всей предложенной концепции, об архитектуре БД и архитекторе только по нескольким примерам данной статьи. Это только пример, делайте как хотите. Еще раз говорю, это примеры написанные «на коленке». То, как будет вести себя алгоритм до и после ошибки это уже отдельная история.
                                0
                                Я не сужу о всей статье, и подход в целом здравый. Я критикую конкретный код, потому что вся суть предложенной в статье концепции логирования всех исключительных ситуаций — это обработчик этих ситуаций в каждой процедуре и логирование в нем.
                                Куда логировать, в таблицы или во внешний инструмент, как потом мониторить, это уже детали. Суть в правиле — каждый exception должен логироваться в той процедуре, в которой он произошел. И типовой обработчик — это не просто пример, а важнейшая часть этой концепции, которую нужно один раз написать как шаблон, а потом постоянно вставлять в код всем разработчикам. Ошибки в этом шаблоне, раскопированные по всему коду, могут просто похоронить проект.
                                  0
                                  На самом деле шаблон такой процедуры есть (часть 2), но что-то мне подсказывает, что он вам не понравится.
                                  Также, прошу обратить внимание на выходные параметры процедуры-шаблона
                                  p_errcode out number
                                  p_errtext out varchar2
                                  обычно на них идет условие дальнейшего выполнения алгоритма, либо его завершения, но в примерах текущей статьи я их не использовал.
                                    0
                                    Если имеется ввиду код под «спойлером» «Исходный код демонстрационной процедуры, то там уже лучше, так как в блоке when others нет rollback.
                                    Но все равно недостаточно хорошо, потому что там не инициализируются p_errcode и p_errtext и при этом нет raise. Таким образом вызывающая процедура не знает о том, что в данной процедуре произошла непредвиденная ситуация. Что приведет к сложновылавливаемым ошибкам.
                                    Ну и в принципе под when others подпадает и ошибка отсутствия нужной партиции для вставки записи, и ora-600 и много других непредвиденных системных сбоев, которые должны, просто обязаны останавливать исполнение всей последовательности вызовов и „орать“ в мониторинг о срочном вмешательстве поддержки. Поэтому в when others должен быть raise.
                                      0
                                      Спасибо за комментарий! Добавил raise в исключение в блоке when others и присвоил значения исходящим параметрам
                                      p_errcode и p_errtext.
                              0
                              Если я так сделаю на каком-либо проекте, то меня будут вспоминать проклиная и ненавидя…
                              Почему? Например, компиляцию нельзя выполнить, если в коде есть синтаксическая ошибка. Это же не расстраивает программиста, а наоборот, помогает.
                              Если у вас на проекте имеются требования к оформлению кода, то автоматический контроль соблюдения этих требований упростит жизнь программистам. За это они спасибо скажут. Главное делать этот контроль оптимально, чтобы только реальные проблемы не допускал, а не вставлял палки в колеса.

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

                              Я часто делаю код ревью. При этом предлагаете пересчитывать на глаз количество параметров в заголовке процедуры и в ее хвосте, которые могут быть за сотни строк друг от друга? А параметров может быть штук 10. Или так ПО КАЖДОЙ процедуре? И на это будет тратить свое время самый ценный трудовой ресурс команды (обычно ревью делает тимлид)?
                                0
                                Почему? Например, компиляцию нельзя выполнить, если в коде есть синтаксическая ошибка. Это же не расстраивает программиста, а наоборот, помогает.

                                Мой опыт работы в команде разработчиков говорит мне только о том, что большинство разработчиков (к сожалению) только пишет код, не думая о том как с этим кодом будут работать другие в будущем. Заставлять людей делать что-то «принудительно» не работает, либо работает до тех пор пока есть кому требовать и контролировать выполнение требований. Да, наверное так можно сделать, но я такой метод использовать не стал бы.

                                Я часто делаю код ревью. При этом предлагаете пересчитывать на глаз количество параметров в заголовке процедуры и в ее хвосте, которые могут быть за сотни строк друг от друга? А параметров может быть штук 10. Или так ПО КАЖДОЙ процедуре?

                                Да, в каждой процедуре вручную вставлять параметр и это удобно делать когда у вас есть контроль версий (Tortoise, git и прочее). Со временем уже на автомате видишь новый параметр и ниже смотришь его в блоке исключений.
                                Если вы никогда не использовали данный метод это не значит, что он не работоспособный.
                        0

                        Мы используем Logger, который рекомендовал Том Кайт. Единственное что, мы секционировали таблицу по дням, и чистим с помощью дропа партиций, а не с помощью удаления строк. То есть мы сделали изменения относительно оригинала, но они минимальные.

                        Из плюсов:

                        • эта штука поддерживает переключение, например, с уровня info на уровень debug на лету;

                        • при использовании log_error в эксепшен-блоке, в лог автоматом вставится стек трейс, то есть нам достаточно только словами описать, что у нас не получилось сделать, а остальное за нас сделает логгер;

                        • если мы в начале процедуры положили входные параметры в специальную коллекцию, то в случае log_error мы их тоже увидим.

                        Но главный плюс, конечно, в том, что мы не стали тратить время на написание собственного логгера на pl/sql.

                          0
                          Спасибо, видел раньше похожую реализацию (может это и она была). Всегда можно взять готовую реализацию, а можно сделать свою (изобрести очередной велосипед, но зато свой). Мне кажется у такого логера есть плюсы помимо тех, что вы описали это простота. Но есть и минусы и главный минус как мне кажется это то, что такой лог быстро превращается в «славку».
                          Опять же, все зависит от команды и отношения к логированию. Просто в данной статье я показал один из способов логирования.
                          Также, наверное необходимо эту статью рассматривать совместно со второй (третьей и четвертой которые в разработке) частью.
                          0
                          «Все объекты базы данных (функции, процедуры) в обязательном порядке должны завершаться блоком обработки исключений с последующим логированием события».

                          А почему обязательно все? Почему не ловить exception только в процедурах верхнего уровня, которые вызываются пользовательской сессии или из джоба?
                          Ну или хотя бы только в процедурах, которые есть в заголовке пакета, и потенциально могут быть вызваны пользовательской сессии или из джоба?
                            0
                            Есть такая поговорка: «Если правило не обязательно к исполнению, то оно не будет работает!». Примерно также можно сказать и к логированию. Если вы логируете не все, а только «в нужных местах», «в важных процедурах» или «в процедурах верхнего уровня», то в момент возникновения очень и очень критичной ошибки вы можете обнаружить, что у вас есть логирование ошибок но нет информации о конкретной ошибке (которая вам так необходима) в таблице лога. И как правило после таких инцидентов весь функционал логирования умирает.
                            Моя позиция такая, что проще вставить кусок кода в блок exception в момент написания процедуры один раз и навсегда, нежели его не писать или рассуждать о том нужен он или нет. И суть данного метода логирования ошибок состоит в том, чтобы отловить максимально те параметры на которых возникла ошибка, чтобы в дальнейшем использовать их для воспроизведения ошибки на тестовом стенде и быстрого исправления. Но тут уже выбор каждой команды как им вести или вообще не вести логирование ошибок.

                          Only users with full accounts can post comments. Log in, please.