Действительно простой логгер для C++

в 14:53, , рубрики: 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<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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js