Логирование сообщений с Easylogging++


    Система логирования — незаменимый инструмент для протоколирования работы приложений. Для тех, кто не хочет реализовывать его самостоятельно, на C++ уже существует бессчётное количество готовых библиотек (Log4cplus, Apache log4cxx, Boost.Log и тд.), однако Easylogging++ отличается простотой использования и компактностью, не требует сторонних библиотек или инсталляции. Весь её код содержится в одном единственном заголовочном файле, который просто необходимо включить в код приложения.

    Данная статья предлагает краткий обзор функционала Easylogging++ и примеры использования этой библиотеки.

    Сразу начнём с короткого примера, который должен продемонстрировать, насколько просто можно начать пользоваться Easylogging++:

    #include "easylogging++.h"
    
    _INITIALIZE_EASYLOGGINGPP
    
    int main(int argv, char* argc[]) 
    {
       LOG(INFO) << "Привет Habrahabr";
       return 0;
    }
    

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

    Справка:


    • Сайт библиотеки: easylogging.org
    • Лицензия: MIT
    • Язык: C++ 11
    • Зависит от: --
    • Платформы: Windows, Mac OSX, Debian (Ubuntu, Mint), Fedora
    • Компиляторы: GCC 4.7+, Visual C++ 11.0+, Intel C++ 13.0+, MinGW, Cygwin

    Зачем:


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

    • Кроссплатформенность
    • Многопоточность
    • Настройка формата выдачи
    • Выдача custom типов данных (есть готовая реализации для STL контейнеров)
    • Переключение файлов журнала (rolling)
    • Условная и периодическая выдача
    • Проверки и обработка сбоев программы
    • Расчет времени исполнения методов

    Настройка формата


    Несмотря на то, что по нашему мнению главным преимуществом библиотеки является её автономность, компактность и простота, иногда всё же возникает потребность изменить что-то в её поведении. Например, по умолчанию формат выдачи выглядит так:
    11/06/2014 11:23:29,218 INFO  [default] Simple info message
    11/06/2014 11:23:29,218 ERROR [default] Simple error message
    11/06/2014 11:23:29,218 WARN  [default] This is just a warning
    

    Для начала Easylogging предоставляет разработчику возможность настроить набор выводимых полей, а также их порядок.
    Пример ниже демонстрирует, как можно поставить тип сообщения в начало, затем дату в измененном формате, далее сигнатуру метода, из которого был сделан вызов и само сообщение:
    Настройка полей
    el::Loggers::reconfigureAllLoggers(el::ConfigurationType::Format, "%level %datetime{%H:%m:%s} (%func): %msg");
    LOG(INFO) << "После изменения формата";
    

    Результат на экране будет выглядеть так:
    INFO  11:33:58 (int main(int, char**)): После изменения формата
    


    Кроме стандартных вещей в описании формата, таких как тип сообщения, дата или имя файла (cpp), можно использовать и собственные макросы для выдачи в журнал дополнительной информации. Например, так:
    Свои поля
    const char* getMode(void) 
    {
    	switch(mode)
    	{
    		case 1: 
    			return "Service"; 
    		default: 
    			return "App"; 
    	}
    	return ""; // just in case
    }
    int main(int argv, char* argc[]) 
    {
    	el::Helpers::installCustomFormatSpecifier(el::CustomFormatSpecifier("%mode", getMode));
    	el::Loggers::reconfigureAllLoggers(el::ConfigurationType::Format, "%level %mode: %msg");
    	LOG(INFO) << "После изменения формата";
    	return 0;
    }
    


    Еще есть целый ряд флагов меняющих различные аспекты поведения Easylogging. По умолчанию, например, вся выдача дублируется в stdout. Кроме того, что это легко совсем отключить, можем, напротив, сделать всё более наглядным, установив флаг для активации цветной выдачи:
    Установка флагов
    int main(int argv, char* argc[]) 
    {
    	/* Включаем выдачу на экран (она и так включена) */
    	el::Loggers::reconfigureAllLoggers(el::ConfigurationType::ToStandardOutput, "true");
    	/* Включаем флаг цветной печати */
    	el::Loggers::addFlag(el::LoggingFlag::ColoredTerminalOutput);
    	LOG(INFO) << "Обычное сообщение";
    	LOG(ERROR) << "Ошибка должна стать цветной";
    	LOG(WARNING) << "А предупреждение желтым";
    	return 0;
    }
    

    Результат на экране будет выглядеть так:


    Любую настройку можно изменить не только из кода: дополнительно библиотека поддерживает файлы конфигурации в собственном формате (обычный конф с табами), которые можно загружать во время запуска или работы приложений. Также библиотека умеет читать параметры из командной строки: для этого в main нужно поместить соответствующий вызов:
    Чтение командной строки
    int main(int argv, char* argc[]) 
    {
       _START_EASYLOGGINGPP(argc, argv);
    }
    


    Пользовательские типы


    Библиотека позволяет выводить в журнал содержимое объектов произвольного типа. Сделать это можно двумя способами:
    • Наследовать свой объект от некоего el::Loggable и реализовать соответствующий виртуальный метод
    • Реализовать статический метод (оператор) для отправки своего объекта в журнал

    Второй вариант нам представляется более удобным, потому что не надо «пачкать» свои классы непонятными предками и, кроме того, если вы решите убрать логирование, не придется чистить все классы. Покажем, как будет выглядеть реализация вторым способом:
    Пользовательские типы
    /* Просто такой класс */
    class Dummy
    {
    public:
        Dummy(int i, std::string s) : m_Int(i), m_String(s){}
        int getInt() const{return m_Int;}    
    	std::string const& getString() const {return m_String;}
    private:
        int m_Int;
        std::string m_String;
    };
    
    /* Используем макрос из библиотеки, который объявит за нас operator<< */
    inline MAKE_LOGGABLE(Dummy, obj, os) 
    {
    	/* Делаем выдачу нужных атрибутов в поток журнала */
        os << "{i:"<<obj.getInt()<<", s:"<<obj.getString()<<"}";
        return os;
    }
    
    int main(void) 
    {
        Dummy object(1, "Habrahabr");
        LOG(INFO) << "Dummy: " << object;
        return 0;
    }
    /*
    Результат:
    	11:03:27 INFO  Dummy: {i:1, s:Habrahabr}
    */
    


    Файлы журнала


    По умолчанию файл журнала будет расположен в рабочей директории приложения и будет иметь путь ../logs/myeasylog.log.
    Изменить имя файла журнала или его расположение можно в любой момент. Это может быть полезно, если требуется, например, переключаться на новый файл каждые сутки. Приведем пример ручного переключения файла журнала:
    Ручное переключение
    int main(void)
    {
        LOG(INFO) << "Попадет в файл журнала по умолчанию";
        el::Loggers::reconfigureAllLoggers(el::ConfigurationType::Filename, "logs/20140205.log");
        LOG(INFO) << "Попадет в другой файл журнала";
        return 0;
    }
    


    После выполнения этого в папке logs окажется два файла: myeasylog.log и 20140205.log. Каждое сообщение окажется в своем файле. Нетрудно видеть, что аналогично можно в «ручном» режиме выполнять переключение журналов по заданному алгоритму (например сделать суточные журналы или выделять определенное число записей на каждый файл)

    Easylogging++ умеет переключать журналы автоматически, но исключительно на основании размера их файла. Настройки позволяют задать пороговое значение на размер файла, после которого текущий журнал будет обнулен. Перед наступлением этого события вам будет предоставлена возможность скопировать предыдущий файл журнала. Ниже приводится пример, где мы устанавливаем 1Кб лимит для размера файлов и перед переключением на новый журнал сохраняем резервную копию старого:
    Скрытый текст
    /* Счетчик номеров журнала. Если журналы заполняются плавно 
     * то можно брать timestamp.*/
    int log_sequence = 1;
    
    /* Callback будет вызван когда старый файл журнала fname заполнен */
    void LogsRollout(const char* fname, size_t fsize)
    {	
    	/* Допишем к имени файла текущий номер последовательности (или время) */
    	string fileName = fname;
    	size_t position = fileName.find(".");
    	string extractName = (string::npos == position)? fileName : fileName.substr(0, position);
    	extractName = extractName + to_string(log_sequence++) + ".log";
    	
    	/* Старый фал журнала уже закрыт, переименуем его */
    	int status = rename(fname, extractName.c_str());	
    	if(status)
    	{
    		/* Не смогли переименовать */
    	}
    }
    
    int main(void) 
    {
    	/* Ставим флаг для переключения журналов по размеру файла */
    	el::Loggers::addFlag(el::LoggingFlag::StrictLogFileSizeCheck);
    	/* Ставим проговое значение на размер файла в 1Кб */
    	el::Loggers::reconfigureAllLoggers(el::ConfigurationType::MaxLogFileSize, "1024");
    	/* Передаем callback на переключение файлов */
    	el::Helpers::installPreRollOutCallback(LogsRollout);
    	/* Добавляем много сообщений */
    	for(int i=0; i<1024; i++)
    	{
    		LOG(INFO) << "Message #"<< i+1;	
    	}
        return 0;
    }
    


    Условная и периодическая выдача


    В библиотеке объявлен макрос LOG_IF для выдачи сообщения в журнал только в случае выполнения указанного условия:
    Скрытый текст
    LOG_IF(true, INFO) << "Всегда выводится";
    LOG_IF(false, WARNING) << "Никогда не выводится";
    


    Периодическая выдача также может быть полезна в ряде случаев. Такой способ означает, что в журнал будет попадать не каждое сообщение, а, например, каждое 10е или 100е. При этом реализовано две возможности:
    • LOG_EVERY_N(n, LEVEL) — выводит каждое n-ое сообщение
    • LOG_AFTER_N(n, LEVEL) — выводит сообщение только после n срабатываний

    Проверки и обработка сбоев программы


    В библиотеке объявлен целый ряд макрос для выдачи сообщения в журнал и аварийного завершения только в случае не выполнения некоторого критического условия: CHECK, CHECK_EQ, CHECK_NE, CHECK_NOTNULL и т.д. Эти макросы очень напоминают assert, только с выдачей в журнал.
    Пользоваться ими достаточно удобно:
    Скрытый текст
    CHECK(true) << "Истина в вине";
    CHECK_LE(2, 1) << "2 < 1 ???";
    CHECK_STREQ(argv[3], "1") << "Передан аргумент отличный от 1";
    

    Вот так падает при невыполнении условия:
    07:12:51,375 FATAL Check failed: [2 <= 1] 2 < 1 ???
    07:12:51,375 WARN Aborting application. Reason: Fatal log at [/home/borg/Temp/App/main.cpp:34]
    Aborted
    


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

    Дополнительно, библиотека по умолчанию обрабатывает сбои приложения. Это поведение, конечно, можно отключить, но иногда может быть удобно получать логи на ряд сигналов, например: SIGFPE, SIGILL, SIGSEGV и т.д. Для gcc можно также выдавать в журнал call stack.
    Скрытый текст
    #include "easylogging++.h"
    _INITIALIZE_EASYLOGGINGPP
    
    void CustomCrashHandler(int signal) 
    {
        LOG(ERROR) << "Всё сломалось!!"; 
    	/* Вызываем на помошь библиотеку */    
        el::Helpers::crashAbort(signal);
    }
    int main(int argv, char* argc[]) 
    {
    	/* Установим свой обработчик */
        el::Helpers::setCrashHandler(CustomCrashHandler);
    	/* Деление на 0 */
        int a = 0;
    	a = 1/a; 
    
        return 0;
    }
    

    Вот так падает:
    17:46:53,074 ERROR Всё сломалось!!
    Aborted
    


    Выводы


    Библиотека Easylogging++ представляет более менее полный набор функционала, который можно было бы ожидать увидеть в таком продукте.
    К несомненным плюсам следует отнести: её компактность, простоту работы и кроссплатформенность.
    К недостаткам мы бы отнесли лишь требование к использованию C++ 11, хотя, вероятно, авторы могли бы обойтись и «обычным» C++.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 22

      +3
      По поводу недостатка — для нас очень большое ограничение, у нас обязательная поддержка старого GCC 3.4 в проекте, поэтому пока останемся на своем детском трехколесном велосипедике.

      В syslog умеет писать?
        0
        syslog поддерживает там, где есть syslog.h
        В плане кода с вашей стороны почти никакой разницы: что типа макросов SYSLOG(LEVEL) вместо LOG(LEVEL)
          0
          Насчет C++11, ругается из-за хедера type_traits, который сразу проверяет поддержку нового стандарта
            0
            Да я не то чтобы бросал рваться переписывать стабильный код который в продакшене больше года крутится) Так просто смотрю альтернативы…
            Меня сейчас больше всего MUSL приглянулся, но увы, он не подерживает пока mmuless девайсы. Так что пока (может к счастью) все довольно просто и примитивно)
          +6
          Если честно, не очень порадовал сам формат библиотеки. Один заголовочный файл на 6000 строк с макросами и шаблонами… и его я должен заинклюдить в каждом своем translation unit…

          И еще я пока не понял по API, можно ли как-то динамически изменить уровень вывода в лог?

          Допустим. У меня есть отладочный вывод на устройстве. Которое… маленькое. Файлы не попишешь. Я могу подключиться к устройству по телнет, и отдать команду изменить уровень дебажного вывода в одном из контекстов. Можно ли с этой либой подобного достичь?
          Не подумайте что я как-то ругаю или подбное, простое любопытство насчет возможностей.
            0
            6000 строк с макросами и шаблонами… и его я должен заинклюдить в каждом своем translation unit…

            Вероятно, многие из хедеров которые вы используете даже больше, а, кроме того, 6000 строк будет только один раз при первом вхождении, разве нет?
            И еще я пока не понял по API, можно ли как-то динамически изменить уровень вывода в лог?

            Да, можно.
            Во-первых в командной строке (но это без динамики, только при старте), ну и во-вторых на лету тоже:
            Скрытый текст
            int main(int argv, char* argc[]) 
            {
                /* Копия конфигурации по умолчанию */
                el::Configurations defaultConf;
                /* Изменим в ней флаг для вывода инфо сообщений */
                defaultConf.set(el::Level::Info, 
                        el::ConfigurationType::Enabled, "false");
                /* Применим к логгеру по умолчанию */
                el::Loggers::reconfigureLogger("default", defaultConf);
                LOG(INFO)<<"Не будет выведено";
                LOG(WARNING)<<"Будет выведено";
            
                return 0;
            }
            

              0
              Про изменение логирования — уже неплохо. А про «первое» вхождение Вы как-то либо неправы, либо не поняли меня =)
              Впрочем я полагаю, эту проблему как раз легко решит precompiled header.
                +1
                только один раз при первом вхождении

                Он говорил про разные translation unit, а не про множественные включения в один.
              0
              _INITIALIZE_EASYLOGGINGPP. Он требуется для инициализации некоторых статических переменных и установки обработчика для крэшей.

              Вот это смущает, почему нельзя всё это инициализировать просто при первом обращении к библиотеке?
                0
                Вероятно, на тот случай, если у вас будет крэш до того, как Вы что-то выведете в журнал (т.е. до первого обращения к библиотеке).
                  –1
                  Потому что это — не инициализация, а определения переменных (обратите внимание, эта строчка должна находиться за пределами main). В идеальном мире вся эта радость должна была быть в отдельном файле, easylogging++.cpp, — но так как библиотека, по-видимому, заточена под программы класса все-в-одном-файле — то соответствующий код перенесли из отдельного модуля в макрос.
                    0
                    А что такого нужно объявлять в cpp-файле, чего нельзя было бы объявить в заголовочном?
                      0
                      Глобальные переменные же.
                        +2
                        struct Global
                        {
                          static int& variable()
                          {
                            static int v;
                            return v;
                          }
                        };
                        
                        

                        Пример объявления не константной глобальной переменной, доступной через вызов функции, который широко используется при создании синглтонов.
                          –1
                          Может, автор библиотеки не знал.
                  +4
                  Каждый программист должен написать плеер, аську, обертку над браузерным движком и конечно же библиотеку логгирования.
                    +1
                    Да, а потом, как раз, можно посмотреть по сторонам и оценить, как это реализовали другие )
                      0
                      Забыли про текстовый редактор :)
                      +1
                      Поиграться можно, а использовать — рано. Всё-таки syslog (если есть) более удобен: все стандартно. Или же log4cplus: можно и динамически, и статически прилинковать, если надо монолитный бинарник. + формат настроек как в log4j позволяет не париться с настройками логирования(опять же все стандартно)
                        +1
                        Я использую этот логгер в одном своем проекте. Не нравится ровно две вещи: во-первых, нельзя без смены макросов логировать в syslog/stderr (типа, выбора бэкэнда логирования), во-вторых, при парсинге argc/argv он не выкидывает свои опции из списка аргументов, поэтому следующий за ним getopt не очень удобно применять (например, указывая на неправильный формат аргументов). Впрочем, первое ограничение является самым неудобным, а кроме того, достаточно сложным в фиксе.
                          +1
                          потокобезопасна? В частности во момент ротации логов.
                            0
                            Да. По умолчанию потокобезопасность отключена. Для включения нужно задефайнить "_ELPP_THREAD_SAFE".

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