-
Notifications
You must be signed in to change notification settings - Fork 1
Fluent Builder
Declarative, self-documenting logger configuration. LunarLog::configure() returns a LoggerConfiguration builder — chain method calls to set levels, add sinks, register enrichers and filters, then call build() to get a ready-to-use LunarLog instance.
The imperative API configures a logger through sequential method calls on an already-constructed LunarLog object:
minta::LunarLog logger(minta::LogLevel::DEBUG, false);
logger.addSink<minta::ConsoleSink>(minta::named("console"));
logger.addSink<minta::FileSink, minta::JsonFormatter>(minta::named("json-out"), "app.json.log");
logger.sink("json-out").level(minta::LogLevel::INFO);
logger.enrich(minta::Enrichers::threadId());
logger.addFilterRule("not message contains 'heartbeat'");The fluent builder expresses the same configuration as a single declaration:
auto logger = minta::LunarLog::configure()
.minLevel(minta::LogLevel::DEBUG)
.writeTo<minta::ConsoleSink>("console")
.writeTo<minta::FileSink, minta::JsonFormatter>("json-out",
[](minta::SinkProxy& s) { s.level(minta::LogLevel::INFO); },
"app.json.log")
.enrich(minta::Enrichers::threadId())
.filterRule("not message contains 'heartbeat'")
.build();Both produce identical loggers. The builder is an alternative style — the imperative API is unchanged and fully supported.
Why use the builder?
- Self-documenting — the entire logger configuration is visible in one place
- Declarative — describes what the logger should be, not how to build it step by step
-
Safe —
build()enforces single-use semantics and validates the configuration -
Inline sink configuration —
writeTowith a lambda configures the sink in the same expression where it is created
#include "lunar_log.hpp"
int main() {
auto logger = minta::LunarLog::configure()
.minLevel(minta::LogLevel::INFO)
.writeTo<minta::ConsoleSink>()
.writeTo<minta::FileSink>("app.log")
.build();
logger.info("Server started on port {port}", "port", 8080);
return 0;
}Returns a new LoggerConfiguration builder with default settings:
| Setting | Default |
|---|---|
| Minimum level | INFO |
| Source location capture | false |
| Rate limit | 1000 messages / 1000 ms |
| Template cache size | 128 |
| Locale |
"" (system default "C") |
| Enrichers | none |
| Filters | none |
| Sinks | none |
auto config = minta::LunarLog::configure();
// ... chain configuration ...
auto logger = config.build();Or more commonly, chain everything:
auto logger = minta::LunarLog::configure()
.minLevel(minta::LogLevel::DEBUG)
.writeTo<minta::ConsoleSink>()
.build();All methods return LoggerConfiguration& for chaining. The builder is move-only (non-copyable).
Sets the minimum log level. Messages below this level are dropped before reaching any sink.
.minLevel(minta::LogLevel::DEBUG)Maps to LunarLog::setMinLevel() in the imperative API.
Enables or disables capture of file, line, and function for each log entry.
.captureSourceLocation(true)Maps to LunarLog::setCaptureSourceLocation() in the imperative API.
Sets the rate limit. Messages exceeding the limit within the window are silently dropped.
.rateLimit(5000, std::chrono::milliseconds(1000)) // 5000 msgs/secDefault: 1000 messages per 1000 ms.
Sets the maximum number of parsed templates to cache. Set to 0 to disable caching.
.templateCacheSize(256)Default: 128.
Sets a shared observable log level. The logger reads from this LevelSwitch on every log call. Changing the switch from any thread takes effect immediately.
auto levelSwitch = std::make_shared<minta::LevelSwitch>(minta::LogLevel::INFO);
auto logger = minta::LunarLog::configure()
.minLevel(levelSwitch)
.writeTo<minta::ConsoleSink>()
.build();
// Later, from any thread:
levelSwitch->set(minta::LogLevel::DEBUG); // takes effect immediatelyMultiple loggers can share a single LevelSwitch. See Dynamic Configuration for details.
Watches a JSON configuration file for runtime changes. The file is polled at the given interval (checking mtime). On change, the config is parsed and applied atomically:
-
"minLevel": updates the global min level (orLevelSwitchif one is attached) -
"sinks": updates per-sink levels by name -
"filters": replaces global filter rules via COW
If the file is malformed, current settings are kept and a warning is logged.
auto logger = minta::LunarLog::configure()
.minLevel(minta::LogLevel::INFO)
.writeTo<minta::ConsoleSink>("console")
.watchConfig("logging.json", std::chrono::seconds(5))
.build();See Dynamic Configuration for the JSON file format and details.
Sets the global locale for culture-specific formatting.
.locale("de_DE")Maps to LunarLog::setLocale() in the imperative API.
Registers an enricher function that runs on every log entry. Call multiple times to register multiple enrichers — they run in registration order.
.enrich(minta::Enrichers::threadId())
.enrich(minta::Enrichers::processId())
.enrich(minta::Enrichers::machineName())
.enrich(minta::Enrichers::property("service", "auth-api"))
.enrich([](minta::LogEntry& entry) {
entry.customContext["correlationId"] = generateId();
})See Enrichers for the full guide on built-in and custom enrichers.
Adds compact filter rules using grep-inspired shorthand. Space-separated tokens are AND-combined.
.filter("WARN+ !~heartbeat ctx:env=prod")See Compact Filter for syntax.
Adds a DSL filter rule. Call multiple times — rules are AND-combined.
.filterRule("level >= WARN")
.filterRule("not message contains 'heartbeat'")See Filtering for the full DSL syntax.
writeTo adds a sink to the builder. There are six overloads covering all combinations of naming, formatting, and inline configuration.
Auto-named "sink_0", "sink_1", etc.
// Default formatter
.writeTo<minta::ConsoleSink>()
// With custom formatter
.writeTo<minta::FileSink, minta::JsonFormatter>("app.json.log")
// RollingFileSink with policy
.writeTo<minta::RollingFileSink>(
minta::RollingPolicy::daily("logs/app.log").maxFiles(30))Signature:
template<typename SinkType, typename... Args>
LoggerConfiguration& writeTo(Args&&... args);
template<typename SinkType, typename FormatterType, typename... Args>
LoggerConfiguration& writeTo(Args&&... args);Args are forwarded to the SinkType constructor.
Give the sink a human-readable name for later reference.
// Default formatter
.writeTo<minta::ConsoleSink>("console")
// With custom formatter
.writeTo<minta::FileSink, minta::JsonFormatter>("json-out", "app.json.log")Signature:
template<typename SinkType, typename... Args>
LoggerConfiguration& writeTo(const std::string& name, Args&&... args);
template<typename SinkType, typename FormatterType, typename... Args>
LoggerConfiguration& writeTo(const std::string& name, Args&&... args);The first argument is the sink name; remaining args are forwarded to the constructor.
SFINAE note: The named overload is disabled when
SinkTypeis constructible from(const std::string&, Args...)— this prevents the sink name from being silently consumed as a constructor argument. In practice, this meanswriteTo<FileSink>("my-sink", "file.log")works correctly:"my-sink"is the name,"file.log"goes to theFileSinkconstructor.
Configure the sink inline using a SinkProxy& callback. This is the most powerful overload — it lets you set level, filters, locale, output template, and tag routing in the same expression where the sink is created.
.writeTo<minta::FileSink>("errors",
[](minta::SinkProxy& s) {
s.level(minta::LogLevel::ERROR)
.outputTemplate("[{timestamp:HH:mm:ss}] [{level:u3}] {message}{exception}")
.only("critical");
}, "errors.log")
// With custom formatter
.writeTo<minta::FileSink, minta::CompactJsonFormatter>("pipeline",
[](minta::SinkProxy& s) {
s.level(minta::LogLevel::INFO)
.filter("!~health !~ping");
}, "logs/app.jsonl")Signature:
template<typename SinkType, typename ConfigFn, typename... CtorArgs>
LoggerConfiguration& writeTo(const std::string& name, ConfigFn&& configure, CtorArgs&&... args);
template<typename SinkType, typename FormatterType, typename ConfigFn, typename... CtorArgs>
LoggerConfiguration& writeTo(const std::string& name, ConfigFn&& configure, CtorArgs&&... args);The lambda receives a SinkProxy& — see the SinkProxy reference below for all available methods.
When using writeTo with a lambda, the callback receives a SinkProxy& for configuring the sink before build() is called. All methods return SinkProxy& for chaining.
| Method | Description |
|---|---|
level(LogLevel lvl) |
Set the sink's minimum log level |
filterRule(const std::string& dsl) |
Add a DSL filter rule |
filter(const std::string& compactExpr) |
Add compact filter rules (shorthand syntax) |
filter(FilterPredicate pred) |
Set a predicate filter |
locale(const std::string& loc) |
Set a per-sink locale |
formatter(std::unique_ptr<IFormatter> f) |
Replace the formatter |
outputTemplate(const std::string& templateStr) |
Set custom output format (HumanReadableFormatter only) |
only(const std::string& tag) |
Add an only-tag for tag routing |
except(const std::string& tag) |
Add an except-tag for tag routing |
clearFilter() |
Remove the predicate filter |
clearFilterRules() |
Remove all DSL filter rules |
clearFilters() |
Remove predicate + DSL rules (not tag filters) |
clearTagFilters() |
Remove all only/except tag filters |
.writeTo<minta::FileSink, minta::JsonFormatter>("audit",
[](minta::SinkProxy& s) {
s.level(minta::LogLevel::INFO)
.only("audit")
.only("security")
.filterRule("not message contains 'internal'")
.locale("en_US");
}, "audit.json.log")This is the same SinkProxy class used by logger.sink("name") in the imperative API. See API Reference — SinkProxy for full details.
subLogger adds a nested pipeline with its own independent filters, enrichers, and sinks. The sub-pipeline receives all log entries that pass the parent's global filter, then applies its own independent filters before dispatching to its own sinks. Sub-logger enrichers operate on a cloned copy of the entry and do not affect the parent.
.subLogger("errors", [](minta::SubLoggerConfiguration& sub) {
sub.filter("ERROR+")
.enrich(minta::Enrichers::property("pipeline", "error-alerts"))
.writeTo<minta::FileSink>("error-log", "errors.log");
}).subLogger([](minta::SubLoggerConfiguration& sub) {
sub.minLevel(minta::LogLevel::WARN)
.writeTo<minta::FileSink>("warnings.log");
})Signature:
template<typename ConfigFn>
LoggerConfiguration& subLogger(const std::string& name, ConfigFn&& configure);
template<typename ConfigFn>
LoggerConfiguration& subLogger(ConfigFn&& configure);The lambda receives a SubLoggerConfiguration& with the following methods:
| Method | Description |
|---|---|
enrich(EnricherFn fn) |
Add an enricher to the sub-pipeline (independent from parent) |
filter(const std::string& compact) |
Add compact filter rules (evaluated inside write()) |
filterRule(const std::string& dsl) |
Add a DSL filter rule |
filter(FilterPredicate pred) |
Set a predicate filter |
minLevel(LogLevel level) |
Set the sub-pipeline's ISink-level gate (checked before write()) |
writeTo<SinkType>(...) |
Add a sink to the sub-pipeline (named and unnamed variants) |
Filter layering: minLevel() is checked by the parent's dispatch loop via passesFilter() before write() is called. filter() / filterRule() / filter(predicate) are evaluated inside write() after the ISink gate has passed. Both layers must pass for an entry to reach the sub-sinks.
See Sub-Logger for the full guide.
Constructs the configured LunarLog instance and returns it by move.
auto logger = minta::LunarLog::configure()
.minLevel(minta::LogLevel::DEBUG)
.writeTo<minta::ConsoleSink>()
.build();Behavior:
- Creates a bare
LunarLog(no default console sink) - Applies all global settings (level, locale, rate limit, cache size, source location)
- Attaches
LevelSwitchif provided viaminLevel(shared_ptr<LevelSwitch>) - Registers all enrichers (in order)
- Adds all compact filter expressions
- Adds all DSL filter rules
- Adds all sinks (named and unnamed), including
SubLoggerSinkentries fromsubLogger() - Stores
watchConfigparameters (watcher starts lazily on first log call) - Returns the configured logger ready for use
- Returns the logger by move
Throws: std::logic_error if build() is called more than once on the same LoggerConfiguration.
auto config = minta::LunarLog::configure()
.writeTo<minta::ConsoleSink>();
auto logger = config.build(); // OK
auto second = config.build(); // throws std::logic_errorNo sinks warning: If no writeTo() calls were made, build() prints a warning to stderr and returns a "silent logger" that discards all messages:
[LunarLog] Warning: build() called with no sinks — logger will silently discard all messages.
Both approaches produce identical loggers. Choose the style that fits your codebase.
- Application entry point — configure the logger once at startup in a single expression
- Configuration-from-code — the entire logger setup is visible and self-documenting
-
Preventing post-init mutation — after
build(), theLoggerConfigurationis consumed
- Dynamic configuration — add sinks or change settings based on runtime conditions (CLI flags, config files)
- Gradual setup — configure the logger across multiple initialization phases
- Runtime reconfiguration — change levels, filters, or context while the logger is running
Builder:
auto logger = minta::LunarLog::configure()
.minLevel(minta::LogLevel::INFO)
.captureSourceLocation(true)
.enrich(minta::Enrichers::threadId())
.enrich(minta::Enrichers::property("service", "payment-api"))
.writeTo<minta::ConsoleSink>("console")
.writeTo<minta::FileSink, minta::JsonFormatter>("json-out",
[](minta::SinkProxy& s) {
s.level(minta::LogLevel::WARN)
.locale("de_DE");
}, "app.json.log")
.filterRule("not message contains 'heartbeat'")
.build();Imperative:
minta::LunarLog logger(minta::LogLevel::INFO, false);
logger.setCaptureSourceLocation(true);
logger.enrich(minta::Enrichers::threadId());
logger.enrich(minta::Enrichers::property("service", "payment-api"));
logger.addSink<minta::ConsoleSink>(minta::named("console"));
logger.addSink<minta::FileSink, minta::JsonFormatter>(minta::named("json-out"), "app.json.log");
logger.sink("json-out").level(minta::LogLevel::WARN).locale("de_DE");
logger.addFilterRule("not message contains 'heartbeat'");Both are equivalent. The builder groups everything into one expression; the imperative API allows interleaving configuration with logic.
Register enrichers in the builder chain. They run in the order they are added, and follow the same precedence rules as the imperative API: enricher < setContext < scoped context.
auto logger = minta::LunarLog::configure()
.captureSourceLocation(true)
.enrich(minta::Enrichers::threadId())
.enrich(minta::Enrichers::processId())
.enrich(minta::Enrichers::machineName())
.enrich(minta::Enrichers::environment())
.enrich(minta::Enrichers::property("service", "order-api"))
.enrich(minta::Enrichers::property("version", "2.1.0"))
.enrich(minta::Enrichers::fromEnv("AWS_REGION", "region"))
.enrich(minta::Enrichers::caller())
.writeTo<minta::ConsoleSink>()
.writeTo<minta::FileSink, minta::CompactJsonFormatter>("pipeline", "logs/app.jsonl")
.build();See Enrichers for built-in enrichers, custom enrichers, and precedence rules.
Exception attachment works identically on loggers created by the builder. All exception overloads (log.error(ex, ...), log.error(ex)) are available:
auto logger = minta::LunarLog::configure()
.writeTo<minta::ConsoleSink>("console")
.writeTo<minta::FileSink, minta::JsonFormatter>("json-out",
[](minta::SinkProxy& s) {
s.outputTemplate("[{timestamp:HH:mm:ss}] [{level:u3}] {message}\n{exception}");
}, "events.json.log")
.build();
try {
riskyOperation();
} catch (const std::exception& ex) {
logger.error(ex, "Operation failed for user {name}", "john");
}See Exception Attachment for nested exception unwinding, formatter output, and the {exception} output template token.
auto logger = minta::LunarLog::configure()
.writeTo<minta::ConsoleSink>()
.build();auto logger = minta::LunarLog::configure()
.minLevel(minta::LogLevel::DEBUG)
.captureSourceLocation(true)
.enrich(minta::Enrichers::threadId())
.enrich(minta::Enrichers::processId())
.enrich(minta::Enrichers::property("service", "auth-api"))
.writeTo<minta::ConsoleSink>("console")
.writeTo<minta::FileSink>("app-log", "app.log")
.writeTo<minta::FileSink, minta::JsonFormatter>("json-out", "app.json.log")
.build();auto logger = minta::LunarLog::configure()
.minLevel(minta::LogLevel::TRACE)
.writeTo<minta::ConsoleSink>("console",
[](minta::SinkProxy& s) {
s.level(minta::LogLevel::INFO)
.outputTemplate("[{timestamp:HH:mm:ss}] [{level:u3}] {message}");
})
.writeTo<minta::FileSink>("errors",
[](minta::SinkProxy& s) {
s.level(minta::LogLevel::ERROR)
.only("critical");
}, "errors.log")
.writeTo<minta::FileSink, minta::CompactJsonFormatter>("pipeline",
[](minta::SinkProxy& s) {
s.filter("!~health !~ping")
.locale("en_US");
}, "logs/app.jsonl")
.build();auto logger = minta::LunarLog::configure()
.minLevel(minta::LogLevel::INFO)
.writeTo<minta::ConsoleSink>()
.writeTo<minta::RollingFileSink>(
minta::RollingPolicy::daily("logs/app.log")
.maxSize(50 * 1024 * 1024)
.maxTotalSize(500 * 1024 * 1024))
.build();auto logger = minta::LunarLog::configure()
.minLevel(minta::LogLevel::DEBUG)
.writeTo<minta::ConsoleSink>("console")
.subLogger("errors", [](minta::SubLoggerConfiguration& sub) {
sub.filter("ERROR+")
.enrich(minta::Enrichers::property("pipeline", "error-alerts"))
.writeTo<minta::FileSink>("error-log", "errors.log");
})
.subLogger("audit", [](minta::SubLoggerConfiguration& sub) {
sub.filter("INFO+")
.writeTo<minta::FileSink, minta::JsonFormatter>("audit-log", "audit.json");
})
.build();auto levelSwitch = std::make_shared<minta::LevelSwitch>(minta::LogLevel::INFO);
auto logger = minta::LunarLog::configure()
.minLevel(levelSwitch)
.writeTo<minta::ConsoleSink>("console")
.writeTo<minta::FileSink, minta::JsonFormatter>("json-out", "app.jsonl")
.watchConfig("logging.json", std::chrono::seconds(5))
.build();
// Programmatic change (any thread):
levelSwitch->set(minta::LogLevel::DEBUG);
// Or edit logging.json — changes are picked up within 5 secondsA realistic production configuration combining multiple sinks, enrichers, filters, and per-sink customization:
auto logger = minta::LunarLog::configure()
.minLevel(minta::LogLevel::DEBUG)
.captureSourceLocation(true)
.templateCacheSize(512)
.locale("en_US")
// Enrichers — service identity and infrastructure metadata
.enrich(minta::Enrichers::threadId())
.enrich(minta::Enrichers::processId())
.enrich(minta::Enrichers::machineName())
.enrich(minta::Enrichers::environment())
.enrich(minta::Enrichers::property("service", "payment-api"))
.enrich(minta::Enrichers::property("version", "3.2.0"))
.enrich(minta::Enrichers::fromEnv("AWS_REGION", "region"))
// Global filters — suppress noise
.filter("!~heartbeat !~health")
// Console: human-readable, INFO+ for development
.writeTo<minta::ConsoleSink>("console",
[](minta::SinkProxy& s) {
s.level(minta::LogLevel::INFO)
.outputTemplate("[{timestamp:HH:mm:ss}] [{level:u3}] {message}");
})
// Compact JSON: pipeline output for ELK/Datadog/Loki
.writeTo<minta::FileSink, minta::CompactJsonFormatter>("pipeline",
[](minta::SinkProxy& s) {
s.level(minta::LogLevel::INFO);
}, "logs/service.jsonl")
// Full JSON: audit trail, restricted to audit-tagged messages
.writeTo<minta::FileSink, minta::JsonFormatter>("audit",
[](minta::SinkProxy& s) {
s.only("audit")
.only("security");
}, "logs/audit.json")
// Rolling file: debug-level diagnostics with rotation
.writeTo<minta::RollingFileSink>(
minta::RollingPolicy::daily("logs/debug.log")
.maxSize(50 * 1024 * 1024)
.maxFiles(7))
.build();See also: API Reference — LoggerConfiguration, Enrichers, Exception Attachment, Cookbook