Ссылка на GitHub проект

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

Logger Library

Этот проект представляет собой простую и гибкую библиотеку для логирования на C++. Библиотека поддерживает разные уровни логирования, форматирование сообщений и возможность записи логов в разные потоки.

Основные возможности

  • Поддержка различных уровней логирования: DEBUG, INFO, WARN, ERROR.

  • Настраиваемое форматирование логов.

  • Возможность логирования в несколько потоков с защитой от гонок данных.

  • Логирование в разные выходные потоки (std::cout, файлы и т.д.).

  • Возможность использования глобального логгера.

Установка

  1. Склонируйте репозиторий:

    git clone <https://github.com/Fallet666/logger.git>
  2. Перейдите в директорию проекта и соберите его с использованием CMake:

    cd logger
    mkdir build
    cd build
    cmake ..
    make

Основные концепции

  • Logger: основной класс для логирования. Создавайте экземпляр этого класса для логирования в определенный поток.

  • Глобальный логгер: Singleton-экземпляр Logger, который можно использовать по умолчанию во всем проекте.

Функции логирования

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

  • DEBUG — Для отладочной информации.

  • INFO — Для общих информационных сообщений.

  • WARN — Для предупреждений.

  • ERROR — Для ошибок, требующих внимания.

Базовые функции

LOG_MESSAGE(Logger& logger, LogLevel level, const std::string& message)

Записывает сообщение с заданным уровнем. Эта функция является универсальной и может использоваться для логирования сообщений любого уровня.

Пример использования:

Logger::Logger log("Logger");
LOG_MESSAGE(log, Logger::DEBUG, "I'm MESSAGE");

Упрощенные функции для каждого уровня

Эти функции предназначены для упрощения вызова логирования на определённом уровне:

  1. logDebug(Logger& logger, const std::string& message)

Логирует сообщение с уровнем DEBUG.

Пример использования:

logDebug(log, "This is a debug message");
  1. logInfo(Logger& logger, const std::string& message)

Логирует сообщение с уровнем INFO.

Пример использования:

logInfo(log, "This is an info message");
  1. logWarn(Logger& logger, const std::string& message)

Логирует сообщение с уровнем WARN.

Пример использования:

logWarn(log, "This is a warning message");
  1. logError(Logger& logger, const std::string& message)

Логирует сообщение с уровнем ERROR.

Пример использования:

logError(log, "This is an error message");

Глобальные функции логирования

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

Аналогично базовым функциям существуют следующие глобальные функции логирования:

  • logMessageGlobal(LogLevel level, const std::string& message)

  • logDebugGlobal(const std::string& message)

  • logInfoGlobal(const std::string& message)

  • logWarnGlobal(const std::string& message)

  • logErrorGlobal(const std::string& message)

Уровень логирования

Можно установить уровень логирования через Cmake, тогда будут отображаться логи не ниже заданного уровня. Например, если вы установите уровень Warn, то будут отображаться логи уровня Warn и Error.

Пример использования:

set(DEFAULT_LOG_LEVEL DEBUG)
add_compile_definitions(GLOBAL_LOG_LEVEL=${DEFAULT_LOG_LEVEL})

Примечание

Каждая из функций логирования автоматически добавляет информацию о файле и строке, откуда был вызван лог, используя макросы FILE и LINE. Это позволяет легче отслеживать источник логов в коде.

Форматирование логов

Форматирование по умолчанию:

Строка форматирования: "%L: %T [%N]: %M\n"

Пример вывода:

DEBUG: 12:34:56 [MyLogger]: This is a debug message

Расширенное форматирование

Логи можно кастомизировать с помощью строки форматирования. По умолчанию строка форматирования может содержать следующие компоненты:

  • %L — Уровень логирования (DEBUG, INFO, WARN, ERROR).

  • %T — Время записи лога в формате HH:MM:SS.

  • %N — Имя логгера.

  • %M — Сообщение лога.

  • %t — Идентификатор потока, который записал лог.

  • %S — Имя файла, из которого был вызван лог.

  • %# — Номер строки в файле, откуда был вызван лог.

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

Строка форматирования задается следующим образом:

Logger::Logger log("custom formating");
log.setFormatString("%L: %T [%N]: %M (%S: %#)\n");

Использование цветов в логах

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

Цветовая схема

  • DEBUG — Белый (\033[37m).

  • INFO — Зеленый (\033[32m).

  • WARN — Желтый (\033[33m).

  • ERROR — Красный (\033[31m).

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

Отключение цвета

Цвета автоматически отключаются, если вы используете другой поток вывода (например, запись логов в файл).

Логирование в многопоточных приложениях

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

Синхронизация потоков

Для обеспечения потокобезопасности в классе Logger используется мьютекс (std::mutex). Каждый раз, когда вызывается функция логирования или происходит изменение полей логгера, поток блокируется до завершения операции. Это предотвращает одновременный доступ к общим ресурсам (например, к выходному потоку или настройкам логгера), что гарантирует корректность и целостность данных в логах.

Пример многопоточного логирования:

Logger::Logger log("MultiThreadLogger");

std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
    threads.emplace_back([&log, i] {
        logInfo(log, "Message from thread " + std::to_string(i));
    });
}

for (auto &t : threads) {
    t.join();
}

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

Особенности многопоточного логирования

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

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

  • Вывод в разные потоки: Логгер позволяет использовать различные выходные потоки для разных экземпляров, что может быть полезно для разделения логов по категориям или источникам.

Логирование в разные выходные потоки

Проект предоставляет гибкую возможность настройки выходного потока для логгера. Это означает, что вы можете направлять логи в любой поток, поддерживающий интерфейс std::ostream, будь то стандартный вывод (std::cout), файлы, строковые потоки (std::ostringstream) или даже пользовательские потоки.

Установка выходного потока

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

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

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

Смена выходного потока

Вы также можете изменить выходной поток логгера в процессе работы программы с помощью метода setOutStream:

Logger::Logger log("Logger");
log.setOutStream(std::cerr);

Пример использования различных потоков вывода:

std::ofstream file("output.log");
std::ostringstream oss;

Logger::Logger logToFile("FileLogger", file);
Logger::Logger logToStringStream("StringStreamLogger", oss);
Logger::Logger logToConsole("ConsoleLogger");

logInfo(logToFile, "This log goes to a file");
logWarn(logToStringStream, "This log goes to a string stream");
logError(logToConsole, "This log goes to the console");

file.close();

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

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


  1. dan_sw
    25.08.2024 15:07
    +5

    Определённо интересный проект для изучения и исследования некоторого множества тонкостей языка программирования 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. Сам я тоже студент и во время обучения меня нехило так "прокачали" преподаватели по оформлению своих работ (ГОСТы всякие). Они не просто так придуманы, с их помощью реально читать работы проще и легче. Курсовые, статьи в журналах, ВКР, диссертации - для всего этого есть свои ГОСТы, которые делают работу лучше. Статью, которую легко читать, чаще плюсуют ;)


    1. Fallet Автор
      25.08.2024 15:07

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


      1. Fallet Автор
        25.08.2024 15:07
        +1

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


  1. AndreyAf
    25.08.2024 15:07

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


    1. Fallet Автор
      25.08.2024 15:07

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


  1. mentin
    25.08.2024 15:07
    +1

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

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

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


    1. Fallet Автор
      25.08.2024 15:07

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


      1. hiewpoint
        25.08.2024 15:07

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


  1. Deosis
    25.08.2024 15:07

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

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

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


    1. dan_sw
      25.08.2024 15:07

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

      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, "Ещё одна новая строка");


  1. SpiderEkb
    25.08.2024 15:07
    +2

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

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

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

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

    • У каждого уровня логирования есть свое время жизни сообщений в логе. Например, ошибки хранятся неделю (этого вполне достаточно для проведения расследований - мониторинг логов идет постоянно, статистика по логам рассылается ежедневно те, то отвечает за данный модуль). Бизнес сообщения могут хранится, скажем, 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 - прочистка лога от старых сообщений

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


    1. dph
      25.08.2024 15:07

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


      1. SpiderEkb
        25.08.2024 15:07

        Хм, а не смотрели на какие-то промышленные логеры, типа 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

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

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

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

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

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


  1. SpiderEkb
    25.08.2024 15:07

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

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

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

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


  1. crea7or
    25.08.2024 15:07

    Чем лучше spdlog'a?


  1. Acaunt
    25.08.2024 15:07

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

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

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

    Скрытый текст
    #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 не лучшая идея, но это просто временная реализация для тестов. пока не совсем придумал, как лучше сделать.


  1. Tyiler
    25.08.2024 15:07

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

    Замечания:

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

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

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


    1. SpiderEkb
      25.08.2024 15:07

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

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

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


      1. hiewpoint
        25.08.2024 15:07

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


  1. devoln
    25.08.2024 15:07

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

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

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