logme channels are more than named loggers

At first, logme channels can look like ordinary logger names. You may have channels such as HTTP, DB, TLS, or UI. At first glance, each log call seems to add nothing more than the corresponding label.

A channel is a full runtime object. It decides whether a record is accepted, how it is formatted. Where channel is routed, and which backends eventually receive it.

For that reason, logme channels offer more than a readable message prefix. They form a logging graph that you can manage at runtime. No logging calls need to change.

A channel is a routing and acceptance point

Every channel has its own state. It can be enabled or disabled, use a separate minimum level, define its own output flags, own a set of backends, and optionally link to another channel.

For example, an HTTP channel can be created separately:

LOGME_CHANNEL(HTTP_CH, "http");

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

Creating the channel does not make messages visible by itself. A user-defined channel does not output anything until it has a backend or a link to another channel that already has one.

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

httpChannel->AddBackend(file);

After that, this call:

LogmeI(HTTP_CH, "request started");

does not merely append http to the text. It sends the record to a specific channel object, which applies its own policy and forwards the record to its FileBackend.

That is the essential difference between a channel and a conventional logger name.

A name is only an identifier. A logme channel is an active point for filtering, formatting, and delivery.

One channel can write to several destinations

A channel can be connected to multiple backends at the same time. For example, the same HTTP records can be written to a file and temporarily kept in a ring buffer for diagnostics.

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);

In this configuration, each accepted record is delivered to both backends. The channel policy remains shared: one enabled state, one level filter, and one set of output flags.

This is the right approach when the same message should be delivered to several destinations in the same way.

For example, if HTTP records need the same format in both a log file and an in-memory buffer, bind both backends to the same channel.

Links are not a second backend

A channel link is not just another output target.

When one channel is linked to another, the record is not passed as already formatted text. The linked channel receives the same logical logging context and applies its own rules again: enabled state, level filter, output flags, display filters, links, and backend bindings.

A link is therefore a routing chain, not an inheritance hierarchy.

For example, an HTTP channel can keep a detailed local file while forwarding selected records to the general 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);

Now an HTTP record may be written to the local http.log file and then pass through the default ::CH channel, which may already be connected to console output.

This becomes especially useful when local and global delivery must use different policies. The HTTP channel may write detailed records with source location and subsystem information, while the default channel may show only warnings and errors in a compact console format.

Linked channels do not inherit configuration

Linked channels do not share one configuration.

If HTTP_CH links to ::CH, it does not inherit the default channel’s level, output flags, or backends. Changing the default channel does not automatically change the HTTP channel either.

Each channel remains independent.

For example:

HTTP channel
  DEBUG level
  detailed file output
  -> default channel

Default channel
  INFO level
  compact console output

With this arrangement, an HTTP debug message can be stored in a detailed http.log file without appearing in the console, because the default channel filters out DEBUG.

An HTTP warning or error, however, can pass through both channels: it remains in the detailed HTTP file and also appears in the general console log.

This is why links are intended for routing, not for copying configuration.

Backend binding and channel routing solve different problems

The distinction matters when designing a logging architecture.

Use multiple backends on the same channel when one channel policy should write to several destinations.

Use a channel link when a record must pass through another policy layer with a different format, level filter, routing rule, or delivery configuration.

In practical terms:

Several backends = one channel policy, several destinations
Channel link     = another routing and filtering stage

Trying to replace channel links with backend bindings quickly makes it difficult to separate local diagnostics from the general application log.

On the other hand, turning every backend into a separate linked channel can make the logging graph unnecessarily complex.

A good design is usually simple: create channels where separate policies are genuinely needed, and let backends focus on delivery.

A channel does not need to exist for every module

Not every source file, class, or namespace needs its own channel.

Channels are useful when destinations, retention rules, levels, output formats, runtime behavior, or routing differ. For example, requests, security, audit, performance, and image can be separate logme channels when they write to different files or follow different lifecycle rules.

But when all records still go to the same file and only differ by functional origin, separate channels are often unnecessary.

That is what subsystems are for.

A subsystem answers “where from,” not “where to”

A subsystem is a compact functional tag attached to a record. It has no backends, cannot be linked, and does not exist as an independent output policy.

It simply identifies the part of the application that produced the message.

LOGME_SUBSYSTEM(SUBSID, "http");

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

Here, the channel controls the route: where the record goes and which backends can receive it.

The http subsystem controls the origin: which logical part of the application created it.

This makes it possible to keep one routing topology while temporarily enabling or disabling selected functional areas.

For example, an application may use one main channel that writes to app.log, while subsystems such as http, tls, db, and cache make it possible to isolate a specific area during an investigation.

Channel   = output route
Subsystem = functional tag

Subsystem filters are applied before regular channel delivery. If a subsystem is blocked, its record does not reach a file backend, console backend, buffer, or linked channel.

This is valuable when one noisy subsystem must be temporarily disabled without rebuilding the entire logging graph.

Allowed and blocked subsystem filters support different diagnostic modes

Subsystem filtering supports two lists: blocked and allowed.

A blocked subsystem never logs. When the allowed list is empty, all other subsystem messages can pass. When the allowed list contains entries, only explicitly allowed subsystems remain visible.

For example, during an investigation, you can keep only HTTP and TLS records:

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

Or temporarily suppress a noisy cache subsystem:

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

This is simpler than creating channels only to silence one functional area.

Records without a subsystem are not affected by subsystem filtering. That matters for general application messages that do not need an additional functional tag.

A trace point is neither a channel nor a subsystem

Trace points solve a third problem.

A normal logging statement is either enabled or filtered. A trace point remains in production code as a dormant diagnostic location. While disabled, it does not create records, but it still counts hits. You can see whether that code location was reached and how often.

LogmeTPt(HTTP_CH, "request entered handler");

When a trace point is enabled through runtime control, it starts producing regular log records. From there, the record follows the normal path: subsystem filtering, channel rules, links, and backends.

A trace point does not create a new output path. It enables or disables a specific diagnostic call site.

This makes trace points useful for rare branches, retries, and state changes. They also help when you need both frequency data and detailed context.

For example, you can see that HandleRequest was reached thousands of times even while its trace point was disabled. You can then enable trace points around that function. This collects details without restarting the application:

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

After the investigation, disable the trace point again and reset its counter if needed.

Channels, subsystems, and trace points work together

These mechanisms do not compete with each other.

A channel defines routing and delivery. A subsystem marks the functional area. Meanwhile, a trace point controls detailed logging at one code location.

Consider an HTTP request path:

Trace point: request parser entered
Subsystem: http
Channel: requests
Backends: requests.log + ring buffer
Link: default channel for warnings and errors

This design lets you control different concerns independently.

You can disable the entire requests channel when those destinations are not needed. You can also block the http subsystem when it becomes too noisy. Alternatively, enable a single trace point in the parser. Detailed logging remains off for the rest of the request path.

You can also temporarily add a backend to the requests channel through runtime control without changing source code.

That is what turns logme channels into a logging graph rather than a collection of names.

Where channels save work on the hot path

Channels matter not only architecturally, but also for performance.

When a logging macro receives an explicit ChannelPtr, logme can check the channel’s active state and level before later arguments are evaluated. This is particularly important for expensive diagnostics.

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

This example is still not ideal because BuildRequestDump() appears in the arguments and may be evaluated before the macro can reject the message, depending on the form of the call.

For genuinely expensive work, use the _Do form:

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

The explicit channel still gives logme an early opportunity to determine that the channel is disabled, inactive, or filters out DEBUG. In that case, the record never enters the regular routing and backend path.

A practical model for logme channels

A useful starting model is straightforward:

  • Use channels for separate output policies.
  • Use subsystems for functional areas.
  • Use trace points for specific dormant diagnostic locations.

For example, an application may have main, requests, security, and audit channels. Inside the requests channel, it can use http, tls, proxy, and cache subsystems. Complex paths can use trace points for parsers, retries, and timeouts.

This avoids the false choice between putting everything into one app.log file and creating a separate logger for every class.

The logging graph gets exactly the level of detail that is useful in production.

Conclusion

logme channels are not just named loggers.

A channel is a runtime object. It accepts records, applies its own level and enabled policy, and uses its output flags. Then it sends records to backends or forwards them through links.

Links provide routing, not inheritance. Backend bindings provide several destinations under one policy. Subsystems filter functional areas without changing the routing topology. Trace points activate specific diagnostic call sites and then use the same channel flow.

Once these roles are separated, logging becomes manageable.

As a result, you can change destinations, verbosity, scope, and diagnostic points independently. Logging calls stay unchanged, and production logs avoid unnecessary noise.

Leave a Reply