Pull to refresh

Comments 23

Определённо интересный проект для изучения и исследования некоторого множества тонкостей языка программирования C++. Это хороший опыт)

Однако у меня есть кое-какие рекомендации к форматированию самой статьи, т.к. её немного "сложно" читать (если так можно выразиться).

Поскольку авторы на Хабре часто описывают результаты своей практической или теоретической деятельности есть определённые правила по "красивому оформлению статьи". Очень рекомендую с ней ознакомится, т.к. они в общем и целом позволят улучшить форматирование Вашей статьи.

Написал я логгер на C++ для C++
https://github.com/Fallet666/logger

А еще важная информация, я ищу работу C++ разработчика, пожалуйста напишите мне https://t.me/born_in_void, если можете помочь, я студент в Москве)

Такое введение в статье выглядит немного странно. Всё таки Хабр не является ресурсом, прямой целью которого является помощь в трудоустройстве (хотя косвенно она присутствует) - для этого есть соседний сайт Хабр.Карьера, рекомендую заглянуть, может быть так Вы быстрее найдете работу. Вдобавок в конце статьи Ваши контакты уже видны (в т.ч. ссылка на Telegram), зачем повторяться?

В целом ссылки по типу "https://github.com/Fallet666/logger" или "https://t.me/born_in_void" лучше оформлять как пример_1 и пример_2. Т.е. текстом, а не ссылкой. Потому что интерес читателя перейти по ссылке будет больше, если для перехода по ней достаточно одного нажатия. Сейчас же ссылки нужно выводить с помощью мышки, чтоб по ней перейти.

Форматирование для кода на Хабре поддерживается и достаточно успешно. Код будет выглядеть красивее, если при вставки кода выбрать конкретный язык программирования (делается это сверху в выпадающем меню "Язык"). Достаточно сравнить:

До:

Logger::Logger log("FileLogger", std::ofstream("log.txt"));

После:

Logger::Logger log("FileLogger", std::ofstream("log.txt"));

По поводу заголовков:

Тут у заголовков "плывёт" размер. Казалось бы "Упрощенные функции для каждого уровня" стоит сделать больше, чем нумерованные наименования глобальных функций логгера, т.к. он описывает свой подраздел. Здесь же кажется, что как раз наименование подраздела это обычный выделенные текст, а сам подраздел - элемент нумерованного списка, который не должен быть вообще заголовком H3 (сейчас он такой). Для задания "жирного" оттенка тексту лучше использовать свойство для стилизации текста при его выделении, если это требуется. Но не стоит этим перенасыщать статью. Например, текст "Пример использования" можно вообще не выделять жирным, а методы в нумерованных списках выделять без выделения номера (но это уже мелочи).

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

Успехов в поиске работы и дальнейшем написании статей!

P.S. Сам я тоже студент и во время обучения меня нехило так "прокачали" преподаватели по оформлению своих работ (ГОСТы всякие). Они не просто так придуманы, с их помощью реально читать работы проще и легче. Курсовые, статьи в журналах, ВКР, диссертации - для всего этого есть свои ГОСТы, которые делают работу лучше. Статью, которую легко читать, чаще плюсуют ;)

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

исправил, как смог)

я бы использовал recursive_mutex и string_view

спасибо, поменяю, как будет время)

Годный велосипед для дипломной работы.

Я бы измерил производительность, есть несколько очевидных улучшений, вроде передачи в логгер std::string_view вместо string - совсем незачем лишний раз аллоцировать и копировать строку.

Из более сложных - разбор строки форматирования во время компиляции. Ну или хотя бы один раз при установке, а не на каждое сообщение.

о, спасибо) вообще я чисто от скуки написал, не для диплома, мне до него ещё 2-3 года)

Если от скуки, то можете ещё во время компиляции добавить непосредственно в строку форматирования имя файла и номер строки вместо %S и %#.

 Logger::Logger log("FileLogger", std::ofstream("log.txt"));

В этом примере логи будут записываться в файл log.txt.

В этом примере логи будут писаться вникуда, так как конструктор принимает второй аргумент по ссылке на базовый класс и сохраняет указатель на временный объект.

Действительно, проблема есть. Думаю, что автор имел ввиду что-то такое:

std::ofstream file("log.txt");
Logger::Logger log("FileLogger", file);

Сейчас конструктор определён так:

explicit Logger(std::string name, std::ostream &out = std::cout);

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

Думаю, чтобы не было проблем, имеет смысл переписать конструктор таким образом:

explicit Logger(const std::string& name, const std::ostream& out = std::cout);

Тогда вызов

Logger::Logger log("FileLogger", std::ofstream("log.txt"));

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

Вдобавок, было бы неплохо добавить ещё множество конструкторов по умолчанию. Например, конструктор копирования, без параметров, присваивания и т.п.. Перегрузка операторов ещё может быть полезным механизмом, чтобы, например, постоянно не вызывать методы логгера. Условно можно в файл записать лог и через такую конструкцию:

log += "Новая строка";
log += BuildLogMessage(date, type, "Ещё одна новая строка");

Можно несколько концептуальных мыслей? На основе нашего опыта.

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

Мы пришли к следующим требованиям:

  • Уровень логирования должен задаваться в конфигурации, а не на этапе сборки. Более того, если какой-то модуль предполагает длительную работу, то конфигурация желательно "теплая" - указывается "время жизни" через которое она перечитывается. Это позволяет "на ходу" включать или выключать нужный уровень логирования.
    Конфигурация, это опять же, таблица в БД.

  • У каждого уровня логирования есть свое время жизни сообщений в логе. Например, ошибки хранятся неделю (этого вполне достаточно для проведения расследований - мониторинг логов идет постоянно, статистика по логам рассылается ежедневно те, то отвечает за данный модуль). Бизнес сообщения могут хранится, скажем, 3-6 месяцев (если того пожелает бизнес). Трейсы хранятся 1-3 дня - это оперативная информация, больше не нужно.
    Соответственно, есть функция прочистки лога которая читает конфигурацию и удаляет из лога то, у чего закончился срок жизни. Иногда это не функция, а просто SQL скрипт, ежедневно запускаемый сопровождением в указанное время.

  • Лог может быть один на весь комплекс (много модулей). И тогда логирование для каждого модуля настраивается отдельно.

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

Фактически у нас все сводится к нескольким функциям:

  • LogMessage - вывод в лог сообщения в свободном формате

  • LogError - вывод в лог "структурированной ошибки" (это уже платформозависмое - тут используется такая концепция ошибки в виде структуры из кода ошибки + набора данных, есть специальные "message file" где на каждый код есть текстовка ошибки с указанием куда в текст подставляются данные).

  • AllowLog - "легкая" функция, которая возвращает разрешено ли (в конфигурации) логирование данного уровня сообщения для данной программы. Дело в том, что LogMessage (которая используется для бизнес-сообщений и трейсов) может быть достаточно "тяжелой":
    ECLLogText(MP_PGM_NAME: dsLogKey: 'T':
    'Закрыта существующая запись для совпадения с ' + dsELC01.ELCCUS + dsELC01.ELCCLC + ' ' +
    dsELC01.ELCPRIM +
    ' с EVT = ' + dsELC01.ELCEVT);
    на склейку строки уходит много времени и ресурсов, но если в настоящее время трейсы выключены, это просто пустая трата времени. Поэтому такие вещи оборачиваются в
    if ECLAllowLog(MP_PGM_NAME: 'T');
    ECLLogText(MP_PGM_NAME: dsLogKey: 'T':
    'Закрыта существующая запись для совпадения с ' +
    dsELC01.ELCCUS + dsELC01.ELCCLC + ' ' +
    dsELC01.ELCPRIM +
    ' с EVT = ' + dsELC01.ELCEVT);
    endif;

  • ClearLog - прочистка лога от старых сообщений

Вот как-то так... Такой вот опыт.

Хм, а не смотрели на какие-то промышленные логеры, типа log4j/logback (из мира Java).
Там достаточно хорошо проработаны подходы к организации логов, работа с пайплайном и так далее.
Например, время жизни логов - не часть логики логики логгера, а часть логики системы хранения логов.
Обычно выделяют логгер (часть системы, которая публикует логи), форматтер (преобразование логов к нужному формату), лог-коллектор, лог-транспорт, хранилище логов.

Хм, а не смотрели на какие-то промышленные логеры, типа log4j/logback (из мира Java).

Нет. Мы работаем с ядром АБС. Платформа IBM i, основной язык - RPG. Java не используется потому что и близко не дает ни той производительности, ни той эффективности по ресурсам.

Там достаточно хорошо проработаны подходы к организации логов, работа с пайплайном и так далее.

Логи хранятся в таблицах БД. Это удобно во всех отношениях. Намного удобнее текста.

Кроме того, мы можем выводить логи с очередь сообщений (message queue, *MSGQ). Это отдельный тип объекта на нашей платформе, туда можно выводить, например, трейсы. В этом случае при работающей программе весь трейсинг можно видеть в реальном времени просто подключившись в другом задании к очереди сообщений.

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

Логгер - это несколько функций (фактически, 3-4). Под разные задачи могут быть разные логгеры т.к. специфика выводимой информации может быть разной. Бывает необходимость вообще в лог выводить полный образ записи в БД на которой произошла ошибка.

Более того, нам не нужны единые логи для всего. У нас порядка 200 "программных комплексов", около 30 000 "программных объектов". И каждая команда сама занимается логированием "своих" комплексов - чужие им не интересны. И выводит туда ту информацию, которая нужна им для расследования возможных инцидентов. И/или ту, что хочет видеть бизнес.

Форматтер вообще не нужен. Логи хранятся в БД и работать с ними можно обычным SQL запросом.

Вот была задачка где идет поиск неких соответствий между 96 000 000 строк с одной стороны и 8 000 строк с другой. И если включить трейсинг, то объем информации в логе просто астрономический. Но. При соответсвующей организации лога обычным SQL запросом очень легко отфильтровывается именно тот участок трейснга, который интересен. И никакой форматтер тут просто не нужен.

Или, как пример, прочистка лога

with 
  LOGSET as (
    select A.EC0VAL PGM,
           coalesce(C.EC0VAL, 'D002') DEPT,
           coalesce(D.EC0VAL, 'M001') DEPB,
           coalesce(E.EC0VAL, 'M001') DEPE
      from EC0PF A
 left join EC0PF C on (C.EC0GRP, C.EC0PRM, C.EC0ACT) = (A.EC0GRP, 'LOGDEEPT', 'Y')
 left join EC0PF D on (D.EC0GRP, D.EC0PRM, D.EC0ACT) = (A.EC0GRP, 'LOGDEEPB', 'Y')
 left join EC0PF E on (E.EC0GRP, E.EC0PRM, E.EC0ACT) = (A.EC0GRP, 'LOGDEEPE', 'Y')
     where A.EC0PRM = 'PGMNAME'
       and A.EC0ACT = 'Y'
  ),

  DEPTH as (
    select PGM,
           substr(DEPT, 1, 1) UNTT, 
           dec(substr(DEPT, 2, 3), 3) CNTT,
           substr(DEPB, 1, 1) UNTB,
           dec(substr(DEPB, 2, 3), 3) CNTB,
           substr(DEPE, 1, 1) UNTE,
           dec(substr(DEPE, 2, 3), 3) CNTE
      from LOGSET
  ),

  LMTDTES as (
    select PGM,
           case UNTT
             when 'D' then dec(varchar_format(CURRENT_DATE - CNTT days,   'YYYYMMDD'), 8) - 19000000 
             when 'M' then dec(varchar_format(CURRENT_DATE - CNTT months, 'YYYYMMDD'), 8) - 19000000
             when 'Y' then dec(varchar_format(CURRENT_DATE - CNTT years,  'YYYYMMDD'), 8) - 19000000
           end LMTDTET,
           case UNTB
             when 'D' then dec(varchar_format(CURRENT_DATE - CNTB days,   'YYYYMMDD'), 8) - 19000000 
             when 'M' then dec(varchar_format(CURRENT_DATE - CNTB months, 'YYYYMMDD'), 8) - 19000000
             when 'Y' then dec(varchar_format(CURRENT_DATE - CNTB years,  'YYYYMMDD'), 8) - 19000000
           end LMTDTEB,
           case UNTE
             when 'D' then dec(varchar_format(CURRENT_DATE - CNTE days,   'YYYYMMDD'), 8) - 19000000 
             when 'M' then dec(varchar_format(CURRENT_DATE - CNTE months, 'YYYYMMDD'), 8) - 19000000
             when 'Y' then dec(varchar_format(CURRENT_DATE - CNTE years,  'YYYYMMDD'), 8) - 19000000
           end LMTDTEE
      from DEPTH
  )

delete 
  from ECLLOGPF
  join LMTDTES
    on PGM = ECLLOGPGMN
 where (ECLLOGTYPE = 'T' and ECLLOGDT <= LMTDTET) or
       (ECLLOGTYPE = 'B' and ECLLOGDT <= LMTDTEB) or
       (ECLLOGTYPE = 'E' and ECLLOGDT <= LMTDTEE)
;  

Этот скрипт читает настройки глубин хранения разных типов сообщений для всех логируемых программ (которые хранятся в виде M001 - 1 месяц или D003 - 3 дня и т.п.) и удаляет из лога все, для чего истекло время хранения. Скрипт запускается сопровождением автоматически раз в сутки.

Мы еще в 18-м году прорабатывали "единый сервис логирования", но в итоге пришли к тому, что он будет очень громоздкий и неудобный. Проще оказалось разработать набор базовых требований, а дальше на их основе уже каждая команда делает то, что им удобно. Больше скажу - под разные программные комплексы форматы логов могут быть разными. У нас есть ситуации, когда можно включить трейсинг не просто для отдельной программы, но для конкретного пользователя (профайла из под которого программа запущена) и даже трейсить не всю программу, а отдельные функции (точки логирования). И все это задается в настройках, без пересборки. Т.е. если что-то идет не так, можно просто попросить сопровождение "установить такие-то значения для таких-то полей в такой-то таблице, а потом сделать выгрузку таблицы лога с прома".

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

Эффективное логирование - это не просто "тогда-то и там-то что-то пошло не так", а информация о том, что пошло не так и что было до и что было после. На каких данных? В каком месте?

Как пример - в рассылке приходит выборка из лога по одной задаче. Там куча ошибок "2020" (структурированная ошибка с кодом KSM2020)

KSM2020 Someone else has just added this record. Your update has not been made

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

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

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

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

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

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

Т.е. подключаешься по UDP к модулю логирования и в реальном времени видишь все, что пишется там в лог. С возможность. менять уровень логирования на ходу.

Там просто ситуация была - объекты по всему городу (и в соседних городах), работали в режиме 24/7 (останавливать процесс нельзя) и если возникали какие-то непонятки, то можно было быстро подключиться и посмотреть что там на объекте реально происходит в реальном времени.

Ситуация, конечно, специфическая, но бывают и такие случаи.

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

Ещё я так понял у тебя логгер поддерживает только строки, что не всегда удобно, если например нужно для отладки отправить числа какие-нибудь с сообщением что конкретно происходит. Это конечно можно решить с помощью конкатенации строк и чисел, но всё равно не совсем удобно.

Я например делал свой логгер, пару месяцев назад (ещё не доделал) вот пример моей реализации:

Скрытый текст
#include <iostream>

#include <condition_variable>
#include <unordered_map>
#include <fstream>
#include <sstream>
#include <iomanip>
#include <thread>
#include <atomic>
#include <mutex>
#include <queue>
using namespace std;

class Logger {
private:
    enum Level_log : uint8_t {
        info,
        debug,
        warning,
        error
    };
    
private:
    template<uint8_t>
    struct Level {};
    
public:
    using Info = Level<Level_log::info>;
    using Debug = Level<Level_log::debug>;
    using Warning = Level<Level_log::warning>;
    using Error = Level<Level_log::error>;
    
public:
    class Message {
    private:
        uint8_t level_log;
        ostringstream message;
        
    public:
        template<uint8_t lvl>
        Message(const Level<lvl>& id)
            : level_log(lvl) {
        }
        
        Message(Message&&) = default;
        
        ~Message() {
           if (message.tellp() != 0) {
               Logger::get_instance().add_to_queue(level_log, move(message.str()));
            }
        }
        
    public:
        template<class Text>
        Message& operator<<(const Text& text) {
            message << text;
            return *this;
        }

        Message& operator<<(ostream& (*manip)(ostream&)) {
            message << manip;
            return *this;
        }
    };
    
private:
    queue<pair<uint8_t, string>> queue_messages;
    unordered_map<uint8_t, ofstream> files;
    atomic<bool> is_work;
    thread printer;
    mutex mtx;
    condition_variable status_queue;
    
private:
    Logger()
        : is_work(true)
        , printer(&Logger::print, this) {
        auto open_file = [this](uint8_t key, string_view name) {
            auto* file = &files[key];
            
            file->open(name.data());
            
            if (file->is_open() == false) {
                files.erase(key);
            }
        };
        
        open_file(Level_log::info, "info.log");
        open_file(Level_log::debug, "debug.log");
        open_file(Level_log::warning, "warning.log");
        open_file(Level_log::error, "error.log");
    }
    
    Logger(Logger&&) = delete;
    Logger(const Logger&) = delete;
    Logger& operator=(Logger&&) = delete;
    Logger& operator=(const Logger&) = delete;
    
    ~Logger() {
        terminate();
    }
    
public:
    static Logger& get_instance() {
        static Logger instance;
        return instance;
    }

    static void terminate() {
        Logger& logger = get_instance();
        
        logger.is_work = false;
        logger.status_queue.notify_one();
        
        if (logger.printer.joinable()) {
            logger.printer.join();
        }
        
        for (auto& it : logger.files) {
            it.second.close();
        }
    }

public:
    template<class Text>
    Message operator<<(const Text& text) {
        return move(Message(Info()) << text);
    }

    Message operator<<(ostream& (*manip)(ostream&)) {
        return move(Message(Info()) << manip);
    }
    
    template<uint8_t lvl>
    Message operator<<(const Level<lvl>& id) {
        return Message(id);
    }

private:
    void print() {
        auto print_in_file = [this](uint8_t key, const string& message) {
            auto it = files.find(key);
            
            if (it != files.end()) {
                it->second << message;
            }
        };
        
        while (is_work == true || queue_messages.empty() == false) {
            unique_lock<mutex> lock(mtx);
            status_queue.wait(lock, [this] { return !queue_messages.empty() || !is_work; });
            
            while (!queue_messages.empty()) {
                auto [key, message] = move(queue_messages.front());
                queue_messages.pop();
                
                lock.unlock();
                
                cout << message;
                
                switch (key) {
                case Level_log::debug:
                case Level_log::warning:
                case Level_log::error:
                    print_in_file(key, message);
                    
                default:
                    print_in_file(Level_log::info, message);
                }
                
                lock.lock();
            }
        }
    }
    
    void add_to_queue(uint8_t level_log, string&& message) {
        lock_guard<std::mutex> lock(mtx);
        queue_messages.push({level_log, move(message)});
        status_queue.notify_one();
    }
};

Logger& logg = Logger::get_instance();

string time_now() {
    ostringstream oss;
    
    auto now = chrono::system_clock::now();
    
    time_t time = chrono::system_clock::to_time_t(now);
    tm* ltm = localtime(&time);
    oss << "[" << setfill('0') << setw(2) << ltm->tm_hour << ":" << setfill('0') << setw(2) << ltm->tm_min << ":" << setfill('0') << setw(2) << ltm->tm_sec << "]";

    return oss.str();
}

#define INFO Logger::Info()

#define DEBUG Logger::Debug() << time_now() << '[' << __FILE__ << "][" << __FUNCTION__ << "] "

#define WARNING Logger::Warning() << '[' << __FILE__ << "][" << __FUNCTION__ << "] "

#define ERROR Logger::Error() << time_now() << '[' << __FILE__ << "][" << __FUNCTION__ << "][line send message: " << __LINE__ << "] "

void read_file(string_view name) {
    ifstream file(name.data());
    
    if (file.is_open() == false) { return; }
    
    cout << endl << endl;
    
    for (size_t i = 0; i < 40; ++i) {
        cout << '-';
    }
    
    cout << endl << "this is text from file -> " << name << endl << endl;
    string text;
    while (file.eof() == false) {
        getline(file, text);
        cout << text << endl;
    }
    
    file.close();
}

int main() {
    logg << "Starting application" << endl;
    logg << INFO << "This is an info message" << endl;
    logg << DEBUG << "Debugging information" << endl;
    logg << WARNING << "This is a warning" << endl;
    logg << ERROR << "An error has occurred" << endl;
    
    Logger::terminate();
    
    read_file("info.log");
    read_file("debug.log");
    read_file("warning.log");
    read_file("error.log");
    
    return 0;
}

В моем случае я пытался сохранить привычный синтаксис std::cout. Да я знаю, что использовать define DEBUG не лучшая идея, но это просто временная реализация для тестов. пока не совсем придумал, как лучше сделать.

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

Приветствую. В целом норм.

Замечания:

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

  • форматирование у вас идет в том же месте, где и логирование, тоже соотв-но тормозить это будет осн работу

Вот мой пример логера, где логирование и запись в файл развязаны мду собой. Пример только для демонстрации, использовать в проде призываю только распр решения (log4, glog..).

Я бы в такой ситуации попробовал бы логгер запустить отдельным процессом с открытием локального именованного UDP Unix-сокета (ну или что там больше по душе - пайпы те же...). А все остальные поток или процессы просто закидывали бы в это сокет данные для логирования.

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

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

Интересно, как вы себе представляете быструю передачу структурированных данных в сокет для логирования без сериализации? А если она есть, то она же и при логировании происходит.

Тут думать надо как это реализовать. Но передать в сокет можно что угодно. Любой байтовый массив. Это бинарный транспорт.

Я бы пошел по пути структурированной ошибки. Т.е. код ошибки + набор данных. А текстовка, тип данных и куда их подставлять - это уже на стороне логгера - пусть он с этим разбирается.

Мы работаем со структурированными ошибками. Правда, ограничились маскимум тремя текстовыми полями по 10 символов каждое (хотя система позволяет до 50-ти байт передавать в поле "данные".

 messageID = "ECL0035"        // message ID. Set it if not match with OBJECTNAME
 messageText = "Нарушена последовательность загрузки списков &1: &2 загружен после &3" // message
 messageSeverity = "20"       // message severity

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

ECL0035 - код ошибки

&1, &2, &3 - куда будут данные подставляться

Вот так оно выглядит в *MSGF

                          Показать описания сообщений                           
                                                            Система:   ALFALAB1 
 Файл сообщений:   KSMMSGF        Библиотека:   KLIBVPN                         
                                                                                
 Поместить на  . . . . . .             ИД сообщения                             
                                                                                
 Введите опции, нажмите Enter.                                                  
   5=Показать сведения   6=Печать                                               
                                                                                
 Опц  ИД сообщения  Серьезность  Текст сообщения                                
        ECL0035          20      ECL0035 Нарушена последовательность загрузки с 
        ECL0036          20      ECL0036 Противоречие в наборе параметров: субъ 
        ECL0037          20      ECL0037 Обнаружены коды маркировки: &1&2&3     
        ECL0038          20      ECL0038 Некорректное значение "&1" параметра & 
        ECL0039          20      ECL0039 Не переданы параметры для поиска совпа 
        ECL0040          20      ECL0040 Не найдена запись по активному совпаде 
        ECL0041          20      ECL0041 Субект &1&2 не активен                 
        ECL0042          20      ECL0042 Не найдено ни одного активного имени/н 
        ECL0043          20      ECL0043 Не найдена запись для субъекта &1&2 в  
        ECL0101          10      ECL0101 Вы уверены, что хотите удалить запись 
                                                                        Еще...  
 F3=Выход   F5=Обновить   F12=Отмена                                            

Структура имеет следующий вид:

dcl-ds dsError qualified;
  @ERM   char(7);
  @PM1   char(10);
  @PM2   char(10);
  @PM3   char(10);
  @PMALL char(30)  samepos(@PM1);
end-ds;

Заполнение

 dsError.@ERM = 'ECL0035';
 dsError.@PM1 = kLST;
 dsError.@PM2 = %char(z7LDT);
 dsError.@PM3 = %char(dsGZLastHDRecord.GZLDT);

Дальше она уже отдается куда надо...

А там есть API которое по коду и данным вернет полный текст ошибки с подставленными данными. Например

str = %msg(dsError.@erm: 'KSMMSGF': dsError.@pmall);

KSMMSGF - имя message файла где искать описание ошибки. НА выходе получим

"Нарушена последовательность загрузки списков <значение kLST>: <значение %char(z7LDT)> загружен после <значение %char(dsGZLastHDRecord.GZLDT)>"

Это сокращенная форма. В системе есть сокращенная и развернутая формы, возвращаемые системными API

 typedef struct Qus_EC
    {
       int  Bytes_Provided;
       int  Bytes_Available;
       char Exception_Id[7];
       char Reserved;
     /*char Exception_Data[];*/           /* Varying length        */
    } Qus_EC_t;

 typedef struct Qus_ERRC0200
    {
       int Key;
       int Bytes_Provided;
       int Bytes_Available;
       char Exception_Id[7];
       char Reserved;
       int CCSID;
       int Offset_Exc_Data;
       int Length_Exc_Data;
     /*char *Reserved2;*/
     /*char Exception_Data[];*/           /* Varying Length    @B1A*/
    } Qus_ERRC0200_t;

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

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

Можно обойтись без макросов, если задействовать std::source_location. Например, мой самодельный логгер в моём проекте вызывается так:

Log().Error("строка форматирования {}", 42);

Выводит имя файла, номер строки и имя функции, время и отформатированный текст - всё красным цветом.

Я все понимаю, но... fmt::format / std::format, speedlogs, чем не нравятся? Проектов на первых фичах до фига, немного макросов и это самые быстрые логи на счх

Sign up to leave a comment.

Articles