Site icon Заметки разработчика

Каналы logme — это не просто именованные логгеры

Каналы logme: routing, links и backend binding

Каналы logme часто сначала воспринимают как обычные имена логгеров: есть HTTP, DB, TLS, UI, и каждый вызов просто попадает в лог с соответствующей меткой.

Но каналы logme устроены иначе. Канал — это полноценный runtime-объект, который определяет, будет ли сообщение принято, как оно будет оформлено, куда оно пойдет дальше и какие backend-ы получат его в итоге.

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

Канал — это точка принятия и маршрутизации

У каждого канала есть собственное состояние. Он может быть включен или выключен, иметь свой минимальный уровень, собственные output flags, набор backend-ов и, при необходимости, link на другой канал.

Например, канал для HTTP-трафика можно создать отдельно:

LOGME_CHANNEL(HTTP_CH, "http");

auto httpChannel = Logme::Instance->CreateChannel(HTTP_CH);

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

auto file = std::make_shared<Logme::FileBackend>(httpChannel);
file->CreateLog("http.log");

httpChannel->AddBackend(file);

После этого вызов:

LogmeI(HTTP_CH, "request started");

не просто добавляет http к строке. Он направляет запись в конкретный объект канала, который применяет собственные правила и передает ее своему FileBackend.

Именно в этом разница между channel и обычным logger name. Имя — только идентификатор. Канал — это действующая точка фильтрации, форматирования и доставки.

Один канал может писать в несколько мест

Канал может быть связан сразу с несколькими backend-ами. Например, один и тот же HTTP-log может одновременно записываться в файл и временно сохраняться в ring buffer для диагностики.

auto httpChannel = Logme::Instance->CreateChannel(HTTP_CH);

auto file = std::make_shared<Logme::FileBackend>(httpChannel);
file->CreateLog("http.log");

auto ring = std::make_shared<Logme::RingBufferBackend>(httpChannel);

httpChannel->AddBackend(file);
httpChannel->AddBackend(ring);

В таком случае один accepted record доставляется в оба backend-а. При этом правила канала остаются общими: один level filter, один enabled state, один набор output flags.

Это правильный вариант, когда сообщение нужно вывести в несколько destinations одинаковым образом.

Например, если HTTP-сообщение должно иметь одинаковый формат в файле и в memory buffer, достаточно привязать оба backend-а к одному каналу.

Links нужны не для второго backend-а

Link — это не просто еще один output target.

Когда канал связан с другим каналом, запись передается не в уже готовом текстовом виде. Связанный канал получает тот же логический context и снова применяет уже свои правила: enabled state, filter level, output flags, display filter и backend bindings.

Поэтому link — это routing chain, а не inheritance hierarchy.

Например, можно сделать отдельный HTTP-channel с подробным файловым логом и одновременно направлять часть сообщений в общий application channel:

auto httpChannel = Logme::Instance->CreateChannel(HTTP_CH);

auto file = std::make_shared<Logme::FileBackend>(httpChannel);
file->CreateLog("http.log");

httpChannel->AddBackend(file);
httpChannel->AddLink(::CH);

Теперь HTTP-сообщение может попасть в локальный http.log, а затем пройти через default channel ::CH, который по умолчанию уже связан с console output.

Это особенно полезно, когда локальная и общая доставка должны отличаться. Например, HTTP-channel может писать подробные сообщения в файл с полным location и subsystem information, а default channel может выводить в консоль только warnings и errors в коротком формате.

Links не создают наследование

У linked channels нет общей конфигурации.

Если HTTP_CH связан с ::CH, это не означает, что HTTP-channel наследует уровень, flags или backend-и default channel. И наоборот: изменение default channel не делает HTTP-channel автоматически другим.

Каждый канал остается независимым.

Можно сделать такую схему:

HTTP channel
  DEBUG level
  detailed file output
  -> default channel

default channel
  INFO level
  compact console output

В этой конфигурации HTTP debug-message может быть записан в подробный http.log, но не попасть в console output, потому что default channel сам отфильтрует DEBUG.

А HTTP warning или error может пройти через оба канала: остаться в подробном HTTP-файле и одновременно появиться в общем console log.

Именно поэтому links полезны для routing, а не для копирования настроек.

Backend binding и routing решают разные задачи

Это различие важно сохранить в архитектуре.

Если один и тот же channel policy должен писать в несколько destinations, используются несколько backend-ов на одном канале.

Если запись должна пройти через другой уровень, получить другой формат, другой level filter или отдельную policy, используется link на другой канал.

Условно:

several backends = same channel policy, several destinations
channel link     = another routing and filtering stage

Если пытаться заменить links только набором backend-ов, быстро становится трудно разделять локальную диагностику и общий application log. Если же каждый backend превращать в отдельный channel link, logging graph становится избыточной.

Хорошая схема обычно довольно проста: channels выделяются там, где действительно нужны отдельные policies, а backend-ы отвечают только за доставку.

Канал не обязан соответствовать каждому модулю

Не каждый исходный файл, класс или namespace должен иметь собственный channel.

Channels полезны, когда отличаются destinations, retention, levels, output formats, runtime behavior или routing. Например, requests, security, audit, performance и image могут быть отдельными channels, если они пишутся в разные файлы или имеют разные lifecycle rules.

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

Subsystem отвечает не на вопрос «куда», а на вопрос «откуда»

Subsystem — это компактный functional tag сообщения. Он не имеет backend-ов, не может быть linked и не существует как отдельная output policy.

Он просто говорит, к какой области приложения относится запись.

LOGME_SUBSYSTEM(SUBSID, "http");

void HandleRequest()
{
  LogmeI(HTTP_CH, "request accepted");
}

Здесь channel отвечает за route: куда попадет запись и какие backend-ы ее увидят. Subsystem http отвечает за происхождение: какая логическая часть программы ее создала.

Это позволяет держать одну routing topology, но временно включать или выключать отдельные области приложения.

Например, application может использовать один main channel, который пишет в app.log, а subsystems http, tls, db и cache помогают отфильтровать нужную область во время расследования.

channel   = output route
subsystem = functional tag

Subsystem filters применяются раньше обычной channel delivery. Если subsystem заблокирован, сообщение не попадет ни в file backend, ни в console, ни в buffer, ни в linked channel.

Это особенно полезно, когда один noisy subsystem нужно временно выключить, не перестраивая всю logging graph.

Allowed и blocked filters дают разные режимы диагностики

Subsystem filtering поддерживает два списка: blocked и allowed.

Blocked subsystem никогда не логируется. Если allowed list пуст, проходят все остальные subsystem messages. Если allowed list заполнен, остаются только явно разрешенные subsystems.

Например, во время расследования можно оставить только HTTP и TLS:

{
  "subsystems": {
    "allowed": ["http", "tls"]
  }
}

Или временно убрать шумный subsystem:

{
  "subsystems": {
    "blocked": ["cache"]
  }
}

Это намного удобнее, чем создавать отдельные channels только ради возможности отключить несколько строк из одного функционального участка.

При этом сообщения без subsystem не попадают под subsystem filtering. Это важно для общих application messages, которым не нужна дополнительная функциональная метка.

Trace point — это не channel и не subsystem

Trace points решают третью задачу.

Обычный log statement либо включен, либо отфильтрован. Trace point остается в production code как dormant diagnostic point. Пока он выключен, он не пишет сообщение, но считает hits: можно увидеть, достигалось ли это место вообще и как часто.

LogmeTPt(HTTP_CH, "request entered handler");

Когда trace point включается через runtime control, он начинает создавать обычные log records. После этого запись проходит через привычный flow: subsystem filtering, channel rules, links и backend-и.

То есть trace point не создает новый путь вывода. Он включает или выключает конкретный diagnostic call site.

Это делает trace points особенно удобными для редких веток, retry loops, state-machine transitions и мест, где важны both frequency and details.

Например, можно увидеть, что HandleRequest был вызван тысячи раз, даже пока trace point выключен. Затем включить только trace points вокруг этой функции и собрать подробности без перезапуска:

logmectl -p 7791 trace stat '*HandleRequest*'
logmectl -p 7791 trace enable '*HandleRequest*'

После диагностики trace point можно выключить, а counter при необходимости сбросить.

Каналы, subsystems и trace points работают вместе

Эти механизмы не конкурируют друг с другом.

Channel определяет routing и delivery policy. Subsystem маркирует функциональную область. Trace point определяет, нужно ли в данный момент создавать подробную запись из конкретного места кода.

Представим HTTP request path:

trace point: request parser entered
subsystem: http
channel: requests
backends: requests.log + ring buffer
link: default channel for warnings and errors

В таком дизайне можно независимо управлять разными вопросами.

Можно выключить весь requests channel, если запись в эти destinations сейчас не нужна. А можно заблокировать subsystem http, если именно этот тип сообщений стал слишком шумным. Можно включить один trace point внутри parser-а, не включая подробные логи для всего request path. Кроме этого, можно временно добавить backend к requests channel через runtime control, не меняя source code.

Именно это превращает channels logme в logging graph, а не в коллекцию имен.

Где начинается экономия на hot path

Channels важны не только архитектурно, но и для производительности.

Если logging macro получает явный ChannelPtr, logme может проверить его active state и level before later arguments are evaluated. Это особенно важно для дорогой диагностики.

LogmeD(httpChannel,
  "request body: %s",
  BuildRequestDump(request).c_str());

Такой пример все равно плох, потому что BuildRequestDump() находится в аргументах и может быть вычислен до вызова макроса в зависимости от формы вызова. Для действительно дорогой работы нужно использовать _Do:

LogmeD_Do(httpChannel,
  std::string dump = BuildRequestDump(request),
  "request body: %s",
  dump.c_str());

Но сам explicit channel дает logme возможность рано понять, что channel disabled, inactive или отфильтровывает DEBUG. Это позволяет не заходить в обычный routing and backend path, когда сообщение заведомо не будет записано.

Практическая модель

Хорошая исходная модель обычно выглядит так: channels выделяются по разным output policies, subsystems — по функциональным областям, trace points — по конкретным dormant diagnostic locations.

Например, можно иметь main, requests, security и audit channels. Внутри requests использовать subsystems http, tls, proxy и cache. А в сложных местах добавить trace points для parser-а, retry loop-а и обработки timeout.

Тогда не нужно выбирать между “все в одном app.log” и “отдельный logger для каждого класса”. Logging graph получает ровно ту детализацию, которая действительно нужна в эксплуатации.

Итог

Каналы logme — это не просто имена логгеров.

Channel — это runtime object, который принимает запись, применяет level и enabled policy, использует свои output flags, доставляет запись в backend-и и при необходимости передает ее дальше через link.

Links дают routing, но не inheritance. Backend bindings дают несколько destinations в рамках одной policy. Subsystems позволяют фильтровать функциональные области без изменения routing topology. Trace points включают конкретные diagnostic call sites и затем используют тот же channel flow.

Когда эти роли разделены, logging становится управляемым. Можно менять destinations, уровень детализации, функциональный scope и отдельные diagnostic points независимо друг от друга — без переписывания вызовов логирования и без превращения production logs в бесконечный поток шума.

Exit mobile version