Как сделать логи читаемыми: практические приёмы на примере logme

Основное назначение библиотек логирования — вовсе не максимальная производительность и даже не удобство интерфейса. Их ключевая задача — помогать создавать читаемые логи, с которыми можно эффективно разбираться при возникновении проблем в работе программы.

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

Что же делает логи читаемыми? Сама библиотека — лишь инструмент. Каким бы мощным он ни был, решающую роль играет разработчик: как он конфигурирует логирование и где именно добавляет сообщения. Тем не менее, именно возможности библиотеки определяют, насколько удобно и качественно это можно сделать.

В этой статье разберём несколько практических аспектов управления логами на примере библиотеки logme.


Распределение информации по отдельным логам

Первое, о чём стоит сказать — это каналы (в некоторых библиотеках аналогом являются инстансы логгера).

Разделение информации по разным логам в зависимости от задач — одно из самых простых и одновременно самых эффективных улучшений читаемости.

Например, если у вас есть прокси-сервер, обрабатывающий сотни или тысячи запросов, разумно вести отдельный лог для каждого запроса. При этом ошибки или агрегированная информация могут отправляться в центральный лог.

Такая организация даёт сразу несколько преимуществ:

  • проще анализировать конкретный запрос;
  • меньше “шума” в каждом отдельном логе;
  • быстрее локализуются проблемы.

Отсюда очевидна ценность возможности создавать множество каналов.


Встраивание логов из сторонних библиотек

Рассмотрим ситуацию: есть код обработки TLS-соединения, и внутри него используется сторонняя библиотека, которая тоже пишет в лог.

Пока мы внутри методов своего класса — всё просто: мы явно указываем канал. Но как быть с внешними функциями?

Передавать канал во все вызовы — неудобно и перегружает код.

Решение — установка канала на уровне потока через LogmeThreadChannel(id). Тогда все функции, выполняемые в этом потоке и не указывающие канал явно, будут писать в нужное место.

void LibraryCall()
{
  LogmeI("message 1");
}

void MyObject::Method()
{
  LogmeThreadChannel(PCH);
  LogmeI(PCH, "message 1");

  LibraryCall();
}

Оба сообщения попадут в канал PCH, даже если библиотека ничего о нём не знает.


Использование встроенного дампа данных

Часто приходится работать с бинарными данными — например, при разработке прокси или сетевых сервисов. Возможность увидеть содержимое пакета в логах бывает крайне полезной.

Вместо написания собственного кода можно использовать встроенный хелпер:

void HandlePacket(const void* data, size_t size) 
{ 
  auto dump = Logme::DumpBuffer(data, size, 0); 
  fLogmeI(MyChannel, "packet:\n{}", dump); 
}

Пример вывода:

2026-04-23 18:33:34:539   [:1234] HandlePacket(): packet
 16 03 01 07 16 01 00 07 12 03 03 d3 d1 e3 f4 5b | ...............[
 c0 a7 1c e9 b9 7a 42 41 93 54 29 a0 3b 8f 98 9b | .....zBA.T).;...
 0e 33 3c b4 c4 06 e1 54 f0 d8 bc 20 e4 ee 60 d5 | .3<....T... ..`.
 95 0f 4a 24 c2 e2 1e d0 78 9f ac ed 93 c9 19 ca | ..J$....x.......
 16 5b c4 e7 99 ff 4a e0 eb 6e c0 2f 00 20 aa aa | .[....J..n./. ..

Чтобы избежать этого, используются макросы с суффиксом _Do:void HandlePacket(const void* data, size_t size)

{
  fLogmeI_Do(
    MyChannel
    , auto dump = Logme::DumpBuffer(data, size, 0)
    , "packet:\n{}"
    , dump
  );
}

 В этом случае код генерации выполнится только если лог действительно нужен. Более подробно о Do макро можно прочитать в статье «Логирование тормозит систему даже когда ничего не пишет«


Именование потоков

При многопоточности идентификатор потока крайне важен. Но числовые ID плохо читаются и мало что говорят о роли потока. В logme можно задавать имена потоков:

LogmeThreadName("ClientIO");

Теперь вместо цифрового идентификатора потока такого как [:1234] мы увидим: [:ClientIO]. А при наличии бита OutputFlags::ThreadTransition в логе будет также история присваивания потоку имен:

2026-04-23 18:33:34:635   [:1D3C] [h2]::H2()
...
2026-04-23 18:33:34:635   [:1D3C -> StartIO] [h2]::StartIO(): Starting I/O
2026-04-23 18:33:34:635   [:StartIO] [proto]::RunServerIO(): Starting server I/O
2026-04-23 18:33:34:635   [:StartIO -> ClientIO] [h2]::DoIO(): >> IO(cli)
...
2026-04-23 18:35:56:151   [:ClientIO -> StartIO] .
2026-04-23 18:35:56:151   [:StartIO -> 1D3C] .

По приведенному отрывкам из лога видно, что в контексте потока 1D3C создался объект H2. Когда у него был вызван метод StartIO, он вызвал LogmeThreadName("StartIO"). После выполнения кода инициализации, этот поток далее используется для I/O с клиентом и было вызвано LogmeThreadName("ClientIO"). Когда произошел выход из функций, был выполнен возврат к предыдущим именам потока. Таким образом можно проследить всю цепочку и узнать какой численный идентификатор у потока, при этом всегда можно по логу увидеть роль потока.


Трассировка функций

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

int MyProc(int i, const char* name)
{
  LogmePV(ARGS(i, name));
  return i + 1;
}


>> MyProc(): 123, abc
<< MyProc(): [2ms]

Укорачивание имён методов

Флаги форматирования часто позволяют добавлять в лог имя функции, из которой пришло сообщение. Это удобно — можно сразу понять источник, не прибегая к поиску по коду.

Однако полные имена функций не всегда лаконичны. Если в логе десятки или сотни сообщений из одного пространства имён, они начинают засорять вывод и усложнять чтение.

Например, пусть у нас есть код:

namespace Connection
{
  namespace Protocol
  {
    namespace H2
    {
       void Func()
       {
          LogmeI(PCH, "blablabla");
       }
    }
  }
}

По умолчанию в логе мы увидим:

2026-04-23 18:33:34:539 [:1234] Connection::Protocol::H2::Func(): blablabla

Такая запись информативна, но избыточна — значительная часть строки повторяется и не несёт новой информации.

В logme можно задать правила сокращения имён:

static const Logme::ShortenerPair shorteners[] =
{
  {"Connection::Protocol::H2", "[h2]"},
  {nullptr, nullptr}
};

PCH->SetShortenerPair(shorteners);

 После этого лог станет компактнее и заметно проще для восприятия:

2026-04-23 18:33:34:539 [:1234] [h2]::Func(): blablabla
В результате уменьшается визуальный шум, а полезная информация остаётся на виду — именно это и нужно от хорошего лога.

Выводы

Читаемые логи — это не побочный эффект, а результат осознанного проектирования. Библиотека лишь предоставляет инструменты, но именно разработчик определяет, насколько эффективно они будут использованы.

На практике наибольший вклад в читаемость дают:

  • разделение логов по каналам;
  • прозрачная интеграция стороннего кода;
  • удобное представление данных (например, дампы);
  • понятные имена потоков;
  • автоматическая трассировка;
  • сокращение лишнего текста.

Если всё это использовать в комплексе, лог превращается из “потока текста” в полноценный инструмент диагностики. А это уже напрямую влияет на скорость разработки и стабильность системы.

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