Pull to refresh

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

Reading time 7 min
Views 21K

Система логирования — незаменимый инструмент для протоколирования работы приложений. Для тех, кто не хочет реализовывать его самостоятельно, на 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++.
Tags:
Hubs:
+23
Comments 22
Comments Comments 22

Articles