[Из песочницы] Действительно простой логгер для C++
Однажды, как и автору похожего топика, захотелось приделать простенькое журналирование к многопоточной консольной утилите. Причём подумалось, что быстрее будет запилить свой велосипед на пару экранов кода, чем прикручивать уже изобретённый. Правда, потом эти 2 экрана немного расползлись, так как захотелось еще раскрасить вывод в зависимости от «уровня» записи и для win32-версии ещё и перекодировать текст в кодовую страницу консоли (в русской винде это CP866), чтобы русские буквы читались с экрана без перенаправления в файл.Что получилось:
для журналирования используются стандартные потоки std: clog и std: cerr; не требует изменения кода, достаточно прилинковать объектник; разные треды могут вести запись в журнал одновременно, без «сливания» записей. Недостатки: отсутствуют гибкость и настраиваемость; невозможно надёжное журналирование на этапе инициализации глобальных статических переменных (то есть до входа в main); недостаточная «платформонезависимость», поддержка многопоточности реализована с помощью библиотеки Boost.Thread. Что касается недостатков, то для их преодоления уже существует множество разнообразных библиотек журналирования: два варианта Boost.Log — один, второй, Panthios, log4cxx, glog, и т.д. По поводу второго пункта — речь идёт о неопредённом порядке инициализации статических переменных в разных единицах трансляции. Обычно это ограничение обходится синглтоном, однако в нашем случае просто не будем ничего писать в журнал из конструкторов статических переменных. Впрочем, даже если и писать, ничего страшного не произойдёт — просто будет задействован стандартный поток std: cerr.Теперь, вкратце о реализации.
Чтобы можно было заменить буфер, в который пишут потоки std: clog и std: cerr, наш логгер должен наследоваться от std: streambuf и как минимум переопределять виртуальные методы overflow и xsputn. Буферизация будет построчная, с опустошением буфера при записи в него символа конца строки '\n'. С помощью boost: thread_specific_ptr для каждого треда будет организован свой строковый буфер.
using boost: thread_specific_ptr;
class logger: public std: streambuf
{
public:
typedef char char_type;
typedef std: char_traits
struct line_buffer;
explicit logger (sys: raw_handle console, bool prepend_time = true); ~logger ();
protected: virtual int_type overflow (int_type c); virtual std: streamsize xsputn (const char_type* buf, std: streamsize size);
private: line_buffer* buffer (); void write_line (const char* data, size_t size); void write_line (const std: string& line) { write_line (line.data (), line.size ()); }
sys: raw_handle m_con;
thread_specific_ptr
class logger_init { public: logger_init (); ~logger_init ();
private: logger* m_clog; logger* m_cerr; std: streambuf* m_clog_native; std: streambuf* m_cerr_native; sys: file_handle m_file; } std_stream_logger;
logger_init: logger_init () : m_clog (0) , m_cerr (0) , m_clog_native (0) , m_cerr_native (0) { sys: raw_handle con = sys: io: err (); if (! sys: handle: valid (con) || ! sys: file_handle: valid (con)) return; m_clog = new logger (con); m_cerr = new logger (con); m_clog_native = std: clog.rdbuf (m_clog); m_cerr_native = std: cerr.rdbuf (m_cerr); }
logger_init::~logger_init () { if (m_clog) { std: cerr.rdbuf (m_cerr_native); std: clog.rdbuf (m_clog_native); delete m_cerr; delete m_clog; } } В пространстве имён sys определены обёртки над низкоуровневыми системными вызовами.«Магия» thread-local storage реализована в методе logger: buffer (), который возвращает указатель на буфер, локальный для каждого потока.
logger: line_buffer* logger:: buffer () { line_buffer* bufptr = m_buffer.get (); if (! bufptr) m_buffer.reset (bufptr = new line_buffer (this)); return bufptr; } Метод overflow добавляет символ в буфер и опустошает его, если был записан символ конца строки.
logger: int_type logger:: overflow (int_type c) { // требование стандарта — при попытке записи eof (), надо вернуть not_eof () if (traits_type: eq_int_type (c, traits_type: eof ())) return traits_type: not_eof ©;
char_type chr = traits_type: to_char_type ©; if (traits_type: eq (chr, '\n')) buffer ()→flush (); else buffer ()→append (&chr, 1); return ©; } Метод xsputn ищет в добавляемой последовательности символы конца строки и соответственно обновляет буфер.
std: streamsize logger:: xsputn (const char_type* buf, std: streamsize sz) { line_buffer* bufptr = buffer (); for (std: streamsize size = sz; size > 0;) { const char* nl = traits_type: find (buf, size, '\n'); if (! nl) { // символ конца строки не встретился — добавляем строку целиком bufptr→append (buf, size); break; } if (nl!= buf) { // добавляем в буфер часть строки до символа '\n' bufptr→append (buf, nl-buf); } bufptr→flush (); // опустошаем буфер ++nl; size -= nl — buf; buf = nl; } return sz; } Наконец, реализация строкового буфера.
struct logger: line_buffer { static const size_t s_limit = 1000; // максимальная длина строки
explicit line_buffer (logger* owner) : m_owner (owner) { }
void append (const char* buf, size_t size); void flush ();
private: void append_time (); void append_crlf ();
logger* m_owner; std: string m_text; };
void logger: line_buffer:: append (const char* buf, size_t size) { if (m_owner→m_prepend_time && m_text.empty ()) append_time (); while (size + m_text.size () > s_limit) { assert (m_text.size () < s_limit); size_t chunk = std::min (s_limit - m_text.size(), size); m_text.append (buf, chunk); flush(); size -= chunk; buf += chunk; if (size && m_owner->m_prepend_time) append_time (); } if (size) m_text.append (buf, size); }
void logger: line_buffer:: flush () { append_crlf (); m_owner→write_line (m_text); m_text.clear (); }
inline void logger: line_buffer:: append_crlf () { #ifdef _WIN32 m_text.append (»\r\n», 2); #else m_text.push_back ('\n'); #endif }
inline void logger:: write_line (const char* data, size_t size) { sys: write_file (m_con, data, size); } Перед каждой строкой добавляется время с миллисекундами и идентификатор треда.
#ifdef _WIN32
void logger: line_buffer:: append_time () { char cvtbuf[32]; SYSTEMTIME time; GetLocalTime (&time); int rc = _snprintf (cvtbuf, sizeof (cvtbuf),»%02d:%02d:%02d.%03d [%04lu] », time.wHour, time.wMinute, time.wSecond, time.wMilliseconds, GetCurrentThreadId ()); if (rc < 0 || rc > int (sizeof (cvtbuf))) rc = sizeof (cvtbuf); m_text.append (cvtbuf, rc); }
#else
void logger: line_buffer:: append_time () { char cvtbuf[32]; struct timeval sys_time; int rc = :: gettimeofday (&sys_time, NULL); if (rc!= -1) { struct tm time; localtime_r (&sys_time.tv_sec, &time); rc = :: snprintf (cvtbuf, sizeof (cvtbuf),»%02d:%02d:%02d.%03d [%08x] », time.tm_hour, time.tm_min, time.tm_sec, int (sys_time.tv_usec/1000), (unsigned) pthread_self ()); if (rc > int (sizeof (cvtbuf))) rc = sizeof (cvtbuf); } if (rc!= -1) m_text.append (cvtbuf, rc); }
#endif // _WIN32 Рабочая версия кода несколько сложнее, поскольку добавлена расцветка для консоли, перенаправление в файл при инициализации и перекодировка в кодовую страницу консоли для win32. Замечу ещё раз, что для низкоуровнего ввода/вывода используются «обертки» над системными вызовами, объявленные в пространстве имён sys. Их разбор уже выходит за рамки этой заметки, просто приведу ссылку для ознакомления.