Однажды, как и автору похожего топика, захотелось приделать простенькое журналирование к многопоточной консольной утилите. Причём подумалось, что быстрее будет запилить свой велосипед на пару экранов кода, чем прикручивать уже изобретённый. Правда, потом эти 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<char_type> traits_type;
typedef traits_type::int_type int_type;
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<line_buffer>
m_buffer;
bool m_console_output;
bool m_prepend_time;
};
Чтобы заменить буфер у стандартных потоков, объявим статический инициализатор:
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 (c);
char_type chr = traits_type::to_char_type (c);
if (traits_type::eq (chr, 'n'))
buffer()->flush();
else
buffer()->append (&chr, 1);
return (c);
}
Метод 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 ("rn", 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. Их разбор уже выходит за рамки этой заметки, просто приведу ссылку для ознакомления.
Автор: poddav