Nested Diagnostic Context, log4cpp и Boost asio

В данной статье я хочу показать, как можно использовать NDC в асинхронных операциях на примере log4cpp и boost.asio

Nested Diagnostic Context (NDC) — контекст, который добавляется в лог. Этот контекст может использоваться для дальнейшей фильтрации лог файла. Особенно это полезно, если производится несколько операций, и эти операции связаны между собой, например: выборка данных из БД, обработка, упаковка в сообщение, передача сообщения по сети клиенту и т.д… Если таких операций много и происходят они параллельно (или асинхронно), то по логу иногда бывает тяжело восстановить последовательность операций. Для этого и используется NDC: вначале мы создаем уникальный(псевдо) идентификатор, и затем помечаем каждую операцию логгирования в нашей цепочке этим идентификатором.

В теории все хорошо: генерируем уникальный ID и передаем его в логгер, однако на практике возникает несколько проблем:

  • Реализация NDC в библиотеке log4cpp основана на механизме Thread Local Storage (Thread Specific Ptr), таким образом NDC хранится только для одного потока. Соответственно встает вопрос передачи NDC между потоками
  • Из первого пункта также вытекает следующая проблема: асинхронные операции, например в boost::asio::io_service. Так как asio позволяет в одном (или нескольких) потоках выполнять множество асинхронных операций, то из-за особенностей log4cpp мы не сможем увидеть в логе правильный NDC. Нужен специальный механизм, который обеспечит корректность NDC в асинхронных операциях asio

Подготовка к старту

  • CMake-2.8 в качестве системы сборки
  • boost-1.49 из репозитория debian wheezy
  • log4cpp — библиотека логгирования
  • gcc-4.7.1 с включенной поддержкой C++11 (использовались Variadic templates)
  • Исходный код находится на GitHub в поддиректории asio_log4cpp
Краткое описание примера

В примере реализован клиент и сервер, которые обмениваются сообщениями

Принцип работы сервера:

  • Сервер ожидает подключения с любого адреса на порту 12345
  • после подключения он генерирует произвольное число в диапазоне от 0 до 1000 (для генерации используется библиотека boost.random, в качестве генератора — Вихрь Мерсенна)
  • Отправляет сгенерированное число клиенту
  • Запускает асинхронный таймер на указанное число миллисекунд
  • После того как таймер отработал, запускает асинхронное чтение, получает сообщение от клиента и завершает сессию

Аналогичным образом работает клиент:

  • Запускает 100 асинхронных коннектов на адрес 127.0.0.1:12345
  • после подключения он вычитывает от сервера сообщение, в котором содержится таймаут
  • Запускает асинхронный таймер на указанное число миллисекунд
  • После того как таймер отработал, запускает асинхронную запись, послыает серверу прощальное сообщение.

В данном примере я специально сделал произвольный таймаут, чтобы показать как обрабатываются NDC в асинхронных операциях.

Также в примере реализован примитивный механизм управления управления соединениями (для корректной остановки, а также для корректной генерации NDC). В принципе можно подробно рассказать о самой реализации клиента и сервера, но это выходит за рамки этой статьи.

Запускаем клиента и сервера без NDC

Если мы запустим примеры с параметром simple, то мы увидим на экране строки примерно следующего содержания (для клиента):

17 INFO connection status: system:0, message: Success  17 INFO starting asynchronous reading...  ... 33 INFO answer from server readed: [552]  33 INFO starting asynchronous timeout 00:00:00.552000 ...  ... 141 INFO timer status: system:0, message: Success  141 INFO starting asynchronous write...  141 INFO write status: system:0, message: Success, bytes written: 8

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

Детали реализации

Сначала включаем поддержку NDC в log4cpp. Для этого используем класс PatternLayout, с помощью метода setConversionPattern мы формируем видимую нам информацию. За вывод NDC отвечает параметр %x (смотри consts::ndcLayoutPattern в файле common.h)

Вариант #1

На ум приходит следующая идея: так как asio принимает любой CompletionHandler (который удовлетворяет сигнатуре), то мы можем сделать обертку над нашими CompletionHandler примерно следующего вида:

struct NdcHolder : private boost::noncopyable { 	NdcHolder(const std::string & ndc) 	{ 		log4cpp::NDC::push(ndc); 	}  	~NdcHolder() 	{ 		log4cpp::NDC::pop(); 	}  };  //...  template <typename Oldhandler> void newHandler(OldHandler func, const std::string & ndc) { NdcHolder ndcHolder(ndc); func(); } 

Естественно, мы должны завернуть func и ndc в еще один Handler, например, с помощью boost::bind, или своей функции

С помощью данного кода мы устанавливаем NDC перед выполнением нашего Handler и убираем его после выполнения.

Мы обязаны сбросить NDC после завершения работы нашего Handler, иначе мы можем увидеть в логах сообщения с неверным NDC

Вариант #2

Более каноничный вариант, который предлагается библиотекой asio — использование механизма Handler Invocation
Данный механизм позволяет написать свою функцию для выполнения (invoke) Handler.

В принципе, для наших нужд оба варианта примерно одинаковы, но вариант с handler invocation более гибкий, позволяет реализовать другие возможности, например, отложенное исполнение, исполнение по приоритету

По умолчанию данная функция определена в boost/asio/handler_invoke_hook.hpp следующим образом:

template <typename Function> inline void asio_handler_invoke(Function function, ...) {   function(); }

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

Используется она в boost/asio/detail/handler_invoke_helpers.hpp примерно следующим образом (выкинул макросы):

template <typename Function, typename Context> inline void invoke(Function& function, Context& context) {   using boost::asio::asio_handler_invoke;   asio_handler_invoke(function, boost::addressof(context)); }

Для того чтобы вызывалась нужная нам функция, необходимо чтобы она была выше приоритетом при выборе, и могла принимать два параметра, первый параметр — сам Handler, и второй параметр — некий контекст, в котором будет исполняться наш Handler. В нашем случае function и context совпадают (грубо говоря, asio берет Handler и использует его и как функцию, и как контекст). Таким образом, наша задача сводится к тому, чтобы написать специального вида CompletionHandler и функцию asio_handler_invoke

Начнем с реализации Handler:

template <typename HandlerT> class NdcDecorator { public: 	explicit NdcDecorator(HandlerT handler, const std::string & ndc) 	: mHandler(handler), 	  mNdc(ndc) 	{} 	void operator()() 	{ 		mHandler(); 	} 	template <typename Arg1> 	void operator() (Arg1 arg1) 	{ 		mHandler(arg1); 	} 	template <typename Arg1, typename Arg2> 	void operator() (Arg1 arg1, Arg2 arg2) 	{ 		mHandler(arg1, arg2); 	} 	const std::string & ndc() const 	{ 		return mNdc; 	} private: 	HandlerT mHandler; 	std::string mNdc; };

Asio требует, чтобы наш Handler удовлетворял требованиям, поэтому в нем присутствует реализация операторов ().

Данный шаблон по сути является обычной оберткой над ранее сконструированным Handler, единственное отличие — это то, что он дополнительно хранит NDC, который мы должны будем выставить перед выполнением и снять после.

Для того чтобы использовать данный Handler, нам необходимо определить функцию asio_handler_invoke с такой сигнатурой:

template <typename FunctionT, typename HandlerT> void asio_handler_invoke(FunctionT func, NdcDecorator<HandlerT> * ndcHandler) { 	NdcHolder ndcHolder(ndcHandler->ndc()); 	func(); }

Как мы видим, реализация тривиальна: мы устанавливаем NDC и вызываем наш Handler, все просто.

Теперь нам необходимо заставить asio вызывать именно нашу функцию, потому что обычный Handler по-прежнему будет вызываться с помощью старой функции asio_handler_invoke.
Рассмотрим пример запуска асинхронного чтения:

mSocket->async_read_some(to_asio_buffer(mInputMsg), 	 boost::bind(&server_connection::onRead, 		shared_from(this), 		placeholders::error, 		placeholders::bytes_transferred));

Здесь мы создаем Handler с помощью boost::bind. Для того чтобы задать наш контекст — обернем созданный Handler в наш NdcDecorator с помощью функции decorate:

mSocket->async_read_some(to_asio_buffer(mInputMsg), 	decorate( 		boost::bind(&server_connection::onRead, 			shared_from(this), 			placeholders::error, 			placeholders::bytes_transferred)));

Как мы видим, изменение несущественное — а именно вызов еще одной функции, но эта функция и создает нужный нам контекст и ставит NDC:

template <typename HandlerT> NdcDecorator<HandlerT> decorate(HandlerT handler, const std::string & ndc = log4cpp::NDC::get()) { 	return NdcDecorator<HandlerT>(handler, ndc); }

Реализация получается тоже достаточно тривиальна: второй параметр задан значением по умолчанию — равным текущему значению NDC. Текущее значение NDC может быть задано ранее (в нашем примере это происходит сразу после создания соединения — при запуске первой асинхронной операции).

Запускаем клиента и сервера с NDC

Запускаем теперь с параметром ndc (сервер):

9179 INFO starting asynchronous write... <SID=1> 9179 INFO starting asynchronous accept...  9179 INFO write status: system:0, message: Success, bytes written: 4 <SID=1> 9179 INFO starting asynchronous timeout 00:00:00.765000 ... <SID=1> ... 9595 INFO read status: system:0, message: Success <SID=3> 9595 INFO answer from client readed: [GOODBYE] <SID=3> 9595 INFO timer status: system:0, message: Success <SID=65> 9595 INFO starting asynchronous read... <SID=65> 9598 INFO read status: system:0, message: Success <SID=12> 9598 INFO answer from client readed: [GOODBYE] <SID=12>

В логе сервера мы видим, что все операции действительно перемешались, но теперь нам это видно. Более того, мы можем использовать, например, grep для того чтобы отфильтровать нужные нам операции:

cat ndc_server.log | grep "<SID=50>" > 50.log

9199 INFO starting asynchronous write... <SID=50> 9199 INFO write status: system:0, message: Success, bytes written: 4 <SID=50> 9199 INFO starting asynchronous timeout 00:00:00.879000 ... <SID=50> 10079 INFO timer status: system:0, message: Success <SID=50> 10079 INFO starting asynchronous read... <SID=50> 10100 INFO read status: system:0, message: Success <SID=50> 10100 INFO answer from client readed: [GOODBYE] <SID=50>

Список использованной литературы:

  • Asio samples — посчитал нужным добавить, так как именно эта статья натолкнула меня на решение этой задачи

ссылка на оригинал статьи http://habrahabr.ru/post/157353/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *