Переход с glog на logme: LOG, CHECK, PLOG и VLOG в новой модели

Переход с glog на logme не обязан начинаться с полной переписи всех мест логирования. В больших C++ проектах это почти всегда плохая идея. Логи разбросаны по коду, рядом с ними часто есть проверки, условия, диагностические сообщения, CHECK, PLOG, VLOG и debug-only вызовы. Если пытаться заменить все сразу, легко получить огромный diff и потерять уверенность в поведении программы.

Поэтому в библиотеке логирования logme есть два уровня перехода.

Первый уровень — compatibility macros. Они позволяют перенести привычные конструкции glog: LOG(INFO), LOG_IF, CHECK, PLOG, PCHECK, DLOG, VLOG, LOG_FIRST_N, LOG_EVERY_N. Такой слой нужен, чтобы проект мог переехать без немедленной переделки всей логики.

Второй уровень — нативная модель logme. После первичного перехода код можно постепенно переводить на channels, backend-ы, runtime-control, _Do, _Once, _Every, _Collapse, trace points и output formats. Именно там появляется основная польза logme: логирование становится не просто выводом строк, а управляемой диагностической подсистемой.

Зачем переходить с glog на logme

glog хорошо известен и удобен для классического application logging. Его макросы простые и привычные: LOG(INFO) пишет информационное сообщение, CHECK проверяет условие, PLOG добавляет системную ошибку, VLOG управляет подробностью вывода.

Но со временем в больших проектах появляются задачи, которые выходят за рамки обычного logging API. Нужно включать подробную диагностику без перезапуска. Порой нужно писать разные запросы в разные файлы. Нужно управлять объемом логов, rotation и retention. Иногда нужно временно включать отдельные trace points. Нужно не вычислять дорогую диагностику, если сообщение все равно не будет записано.

Именно здесь logme дает другую модель. Он сохраняет привычные сценарии через compatibility macros, но не ограничивается ими. Вместо глобального потока сообщений появляется структура из channels и backend-ов. А вместо постоянного debug-шума — runtime-control. Вместо ручного ограничения повторов — _Once, _Every, _Collapse. И, что не менее важно, все это работает поверх очень быстрого logging path. По текущим измерениям logbench logme заметно быстрее glog в ключевых сценариях, включая файловую запись.

Производительность тоже важна

Переход на logme имеет смысл не только из-за runtime-control и более развитой модели логирования. Важная практическая причина — производительность.

glog удобен, но это не самая легкая библиотека для сценариев с большим объемом логирования. В проектах, где через логгер проходят миллионы сообщений или гигабайты данных, разница становится заметной. Особенно это важно для записи в файл, потому что файловый лог — основной рабочий режим большинства production-сервисов.

По текущим измерениям logbench logme значительно быстрее glog в ключевых сценариях. Это относится не только к синтетическому “null output”, но и к реальной файловой записи. Для больших C++ систем это принципиально: логгер не должен становиться главным потребителем CPU и не должен мешать приложению под нагрузкой.

При этом скорость в logme не достигается отказом от функциональности. Наоборот, более высокая производительность сочетается с channels, backend-ами, runtime-control, rotation, retention, output formats и макросами для управления потоком сообщений. Поэтому переход с glog на logme — это не выбор между скоростью и возможностями. Это возможность получить и то, и другое.

LOG(INFO) и уровни сообщений

Самый простой случай — обычный LOG.

В glog код часто выглядит так:

LOG(INFO) << "request started: " << request_id;
LOG(WARNING) << "slow request: " << elapsed_ms;
LOG(ERROR) << "failed to open file: " << path;

При использовании compatibility header такой код можно оставить почти без изменений:

#include <Logme/GlogCompat.h>

LOG(INFO) << "request started: " << request_id;
LOG(WARNING) << "slow request: " << elapsed_ms;
LOG(ERROR) << "failed to open file: " << path;

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

Нативный logme-код обычно лучше писать явно:

LogmeI("request started: %s", requestId.c_str());
LogmeW("slow request: %u", elapsedMs);
LogmeE("failed to open file: %s", path.c_str());

или через format-style API:

fLogmeI("request started: {}", requestId);
fLogmeW("slow request: {}", elapsedMs);
fLogmeE("failed to open file: {}", path);

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

LOG(FATAL), CHECK и fatal handler

В glog LOG(FATAL) и failed CHECK завершают процесс. Это важная семантика, и ее нельзя заменять обычным critical log-ом без объяснения.

В logme critical logging отделен от политики завершения процесса. LogmeC пишет critical record, но по умолчанию не вызывает abort() и не завершает программу. Это сделано намеренно. В существующих проектах и тестах critical-сообщения могут использоваться как обычный уровень логирования, и библиотека не должна внезапно менять поведение.

Если проекту нужна glog-like fatal semantics, она включается явно через fatal handler:

Logme::Instance->SetFatalHandler([]()
{
  std::abort();
});

Можно использовать и другую политику:

Logme::Instance->SetFatalHandler([]()
{
  std::terminate();
});

Перед вызовом fatal handler logme форсированно выполняет FlushAll(). Это важно: critical-сообщение и предыдущие записи должны попасть в файлы до завершения процесса.

Такой подход дает больше контроля, чем жестко зашитый abort. Compatibility macros CHECK и LOG(FATAL) могут вести себя как в glog, если fatal handler установлен. Если handler не установлен, они запишут critical-сообщение, но процесс не будет завершен.

Пример:

CHECK(ptr != nullptr) << "ptr is null";
CHECK_EQ(state, READY) << "unexpected state";
LOG(FATAL) << "unrecoverable error";

В logme это работает через critical path. А приложение само выбирает, что означает critical failure: только запись, abort, terminate или собственная аварийная процедура.

CHECK_EQ, CHECK_NOTNULL и одиночное вычисление аргументов

У glog есть удобное семейство проверок: CHECK_EQ, CHECK_NE, CHECK_LT, CHECK_LE, CHECK_GT, CHECK_GE, CHECK_NOTNULL. При переходе важно сохранить две вещи.

Во-первых, выражения должны вычисляться один раз. Это критично для кода с функциями, счетчиками или объектами, у которых есть побочные эффекты.

Во-вторых, успешная проверка не должна вычислять текст после <<. Если проверка прошла, диагностическое сообщение не нужно.

Compatibility layer logme закрывает эти сценарии:

CHECK_EQ(left, right) << "values are different";
CHECK_NE(handle, INVALID_HANDLE_VALUE) << "bad handle";
CHECK_NOTNULL(connection)->Send(data);

Для нативного logme-кода можно использовать native check macros:

LogmeCheckEq(left, right) << "values are different";
LogmeCheckNotNull(connection)->Send(data);

При failed check запись идет как critical message. Дальнейшее завершение процесса зависит от установленного fatal handler.

PLOG и PCHECK

PLOG в glog удобен тем, что автоматически добавляет системную ошибку. Это особенно часто встречается рядом с файловыми операциями, socket API и системными вызовами.

Типичный glog-код:

PLOG(ERROR) << "open failed";
PCHECK(fd >= 0) << "open failed";

В logme compatibility layer такие макросы тоже есть:

PLOG(ERROR) << "open failed";
PCHECK(fd >= 0) << "open failed";

Важная деталь: системная ошибка должна быть захвачена сразу. На POSIX это errno, на Windows — GetLastError(). Если ошибка будет читаться позже, ее может перетереть промежуточный код. Поэтому PLOG/PCHECK должны захватывать error value в самом начале обработки макроса.

В нативном стиле можно писать через P-варианты logme:

LogmeE_P("open failed");
LogmePCheck(fd >= 0) << "open failed";

Такой код явно показывает, что сообщение связано с последней системной ошибкой.

DLOG и DCHECK

В glog DLOG и DCHECK предназначены для debug-only логирования. В release build такие вызовы обычно исчезают и не должны вычислять аргументы.

Это не то же самое, что обычный debug level. Debug level может быть нужен и в release, если приложение поддерживает runtime-диагностику. Например, production-сервис может временно включить подробный debug для одного channel-а. Поэтому LogmeD и DLOG — разные идеи.

Compatibility layer сохраняет привычную glog-семантику:

DLOG(INFO) << "debug-only message";
DLOG_IF(WARNING, suspicious) << "debug-only warning";
DCHECK(ptr != nullptr) << "ptr is null";

Для нового logme-кода стоит выбирать осознанно. Если сообщение нужно только в debug build, можно использовать debug-only compatibility macros. Если сообщение должно быть доступно в production при включенной диагностике, лучше использовать обычные logme debug-level механизмы и управлять ими через channels, levels и runtime-control.

VLOG: compatibility, а не лучшая модель для нового кода

VLOG(n) в glog — это numeric verbosity. Например, VLOG(1) может означать базовые подробности, VLOG(3) — более шумную диагностику. Обычно это управляется общим verbosity level и module-level настройками.

Для миграции это удобно, поэтому в logme compatibility layer есть VLOG, VLOG_IF и VLOG_IS_ON:

VLOG(1) << "request details";
VLOG(3) << "headers: " << headers;
if (VLOG_IS_ON(2))
{
  BuildExpensiveDiagnostics();
}

Но для нового кода numeric verbosity — не лучший способ выражать диагностику. Число 3 не объясняет, что именно включается. Через несколько лет трудно понять, чем VLOG(2) отличается от VLOG(4).

В logme лучше использовать named diagnostics: channels, subsystems, trace points и _Do. Они дают более понятную модель. Например, вместо “verbosity level 3” можно включить trace point для HTTP headers или subsystem для image decoding. Это лучше читается в коде и лучше управляется в production.

Поэтому VLOG стоит рассматривать как переходный слой. Он помогает мигрировать старый код, но в новом logme-коде лучше постепенно переходить на именованные диагностические точки.

LOG_FIRST_N и LOG_EVERY_N

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

Compatibility layer logme поддерживает привычные конструкции:

LOG_FIRST_N(WARNING, 5) << "first warnings only";
LOG_EVERY_N(INFO, 100) << "periodic status";

В native logme есть похожие механизмы для управления потоком сообщений. _Once полезен, когда событие нужно записать только один раз. _Every подходит для time-based ограничения. _Collapse и _CollapseIgnore помогают сворачивать повторяющиеся серии сообщений и сохранять читаемость лога.

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

RAW_LOG не эмулируется обычным логгером

У glog есть RAW_LOG для низкоуровневых ситуаций, где нельзя использовать обычный logging path. Это отдельная область: минимальная зависимость от runtime, отсутствие обычных блокировок и allocation, аварийные сценарии.

Logme compatibility layer не должен притворяться, что обычный логгер является полным аналогом RAW_LOG. Если logme проходит через channels, backend-ы, formatting и обычные механизмы доставки, это уже не raw logging в смысле glog.

Поэтому RAW_LOG лучше не переносить механически. Если проект активно использует его в allocator-е, signal handler-е или низкоуровневой synchronization logic, такие места нужно рассматривать отдельно. Обычная замена на LOG(ERROR) или LogmeE может быть неправильной.

Краткая таблица соответствий

glog logme
LOG(INFO) LOG(INFO) через GlogCompat.h или LogmeI
LOG(WARNING) LogmeW
LOG(ERROR) LogmeE
LOG(FATAL) LogmeC + optional fatal handler
LOG_IF compatibility LOG_IF или native conditional macros
CHECK CHECK через compatibility или LogmeCheck
CHECK_EQ и family compatibility macros или LogmeCheckEq/Ne/Lt/Le/Gt/Ge
CHECK_NOTNULL compatibility macro или LogmeCheckNotNull
PLOG compatibility PLOG или Logme*_P
PCHECK compatibility PCHECK или LogmePCheck
DLOG / DCHECK debug-only compatibility macros
VLOG compatibility layer; лучше постепенно заменить на trace points
LOG_FIRST_N compatibility macro или native FirstN
LOG_EVERY_N compatibility macro или native EveryN
RAW_LOG не эмулируется обычным logging path

Практическая стратегия перехода

Начинать лучше с compatibility header. Это позволяет быстро собрать проект на logme без полной переписи всех мест логирования.

После этого стоит пройти по коду и разделить вызовы на группы. Простые LOG(INFO) можно оставить или постепенно заменить на LogmeI/fLogmeI. CHECK и PLOG можно оставить через compatibility layer, если их поведение понятно и покрыто тестами. VLOG лучше со временем заменить на channels, subsystems и trace points.

Отдельного внимания заслуживают hot path и дорогая диагностика. Если сообщение требует построения большой строки, обхода структуры или сбора дополнительных данных, такой код лучше переносить в _Do. Тогда подготовка данных будет выполняться только тогда, когда соответствующее логирование действительно активно.

Также стоит заменить шумные сообщения на _Once, _Every, LOG_FIRST_N, LOG_EVERY_N или _Collapse. Это часто дает больше пользы, чем простая замена одного logging API на другой. Лог становится не только быстрее, но и полезнее.

Итог

Переход с glog на logme можно делать постепенно.

На первом этапе compatibility macros позволяют сохранить привычный стиль: LOG, CHECK, PLOG, DLOG, VLOG. Это снижает риск миграции и уменьшает размер начального diff.

На следующем этапе проект может использовать сильные стороны logme: channels, backend-ы, runtime-control, быстрый file output, output formats, rotation, retention, _Do, _Once, _Every, _Collapse и trace points.

Главная цель перехода не в том, чтобы заменить одно имя макроса на другое. Цель в том, чтобы получить более быструю и более управляемую модель логирования. glog хорошо решает задачу классического application logging. logme идет дальше: он быстрее в важных сценариях, лучше подходит для больших объемов файловых логов и помогает управлять логами как частью живой production-системы.

Возможно вас также может заинтересовать статья о переходе на logme с spdlog

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