-
Notifications
You must be signed in to change notification settings - Fork 1
Scoped Context
RAII scoped context that automatically injects key-value pairs into log entries for the lifetime of the scope, then cleanly removes them on destruction.
minta::LunarLog logger(minta::LogLevel::INFO);
{
auto scope = logger.scope({{"requestId", "req-001"}, {"userId", "u-42"}});
logger.info("Processing request");
// Output: Processing request [requestId=req-001, userId=u-42]
}
// scope destroyed → context removed
logger.info("No context here");
// Output: No context hereScopes can be nested. Inner scopes add to the context; their pairs are removed when the inner scope exits:
{
auto outer = logger.scope({{"txn", "tx-001"}});
logger.info("Start");
// context: {txn=tx-001}
{
auto inner = logger.scope({{"step", "validate"}});
logger.info("Validating");
// context: {txn=tx-001, step=validate}
}
// "step" removed
logger.info("Continue");
// context: {txn=tx-001}
}When the same key appears in multiple scopes, inner scopes shadow outer scopes:
{
auto outer = logger.scope({{"env", "outer"}});
logger.info("Outer");
// context: {env=outer}
{
auto inner = logger.scope({{"env", "inner"}});
logger.info("Inner");
// context: {env=inner} ← inner shadows outer
}
logger.info("Restored");
// context: {env=outer} ← original restored
}Within the same frame, if add() is called with a duplicate key, the last value wins.
Use add() to append key-value pairs to an active scope. Returns *this for chaining:
auto scope = logger.scope({{"requestId", "req-001"}});
scope.add("userId", "u-42").add("role", "admin");
logger.info("Request");
// context: {requestId=req-001, userId=u-42, role=admin}Calling add() on a moved-from scope is a safe no-op.
LogScope is move-only (non-copyable). This enables returning scopes from functions:
minta::LogScope createRequestScope(minta::LunarLog& logger, const std::string& reqId) {
return logger.scope({{"requestId", reqId}});
}
void handleRequest(minta::LunarLog& logger) {
auto scope = createRequestScope(logger, "req-001");
logger.info("Handling");
// scope active until end of function
}Move assignment cleans up any existing scope before taking ownership:
auto scope = logger.scope({{"a", "1"}});
scope = logger.scope({{"b", "2"}}); // "a" scope destroyed, "b" scope activeImportant: Scoped context is thread-wide, not per-logger instance.
All LunarLog instances on the same thread share the same scope stack. If you use multiple loggers on one thread, scoped context appears in log entries from all of them:
minta::LunarLog loggerA(minta::LogLevel::INFO, false);
minta::LunarLog loggerB(minta::LogLevel::INFO, false);
{
auto scope = loggerA.scope({{"shared", "yes"}});
loggerA.info("From A"); // context: {shared=yes}
loggerB.info("From B"); // context: {shared=yes} ← also sees it
}This is by design — most applications use a single logger, and thread-wide scoping avoids the complexity of passing logger pointers into scope objects.
Scoped context merges with global context (setContext). If both have the same key, scoped context wins:
logger.setContext("env", "production");
{
auto scope = logger.scope({{"env", "staging"}});
logger.info("Override");
// context: {env=staging} ← scope shadows global
}
logger.info("Original");
// context: {env=production} ← global restoredLogScope is exception-safe. If an exception is thrown within a scope, the destructor still runs and the scope is properly cleaned up:
try {
auto scope = logger.scope({{"txn", "tx-001"}});
logger.info("Start");
throw std::runtime_error("fail");
} catch (...) {
// scope destroyed, context removed
}
logger.info("No leaks");
// context: (empty)Scoped context appears in all output formats:
[2026-02-17 12:00:00.000] [INFO] Processing [requestId=req-001, userId=u-42]
{
"level": "INFO",
"message": "Processing",
"context": {
"requestId": "req-001",
"userId": "u-42"
}
}<log level="INFO" timestamp="...">
<message>Processing</message>
<context>
<entry key="requestId">req-001</entry>
<entry key="userId">u-42</entry>
</context>
</log>LunarLog has two RAII context helpers:
| Feature | LogScope |
ContextScope |
|---|---|---|
| Created via | logger.scope({...}) |
ContextScope(logger, key, val) |
| Multiple keys | Yes (initializer list + add()) |
One key per scope |
| Mechanism | Thread-local scope stack |
setContext() / clearContext()
|
| Scope | Thread-wide | Per-logger instance |
| Move semantics | Move-only | Non-movable |
| Use case | Request tracing, transactional context | Simple per-logger key scoping |
Prefer LogScope for new code — it's more flexible and avoids the key-collision risk that ContextScope has when multiple scopes use the same logger key.
void handleHttp(minta::LunarLog& logger, const Request& req) {
auto scope = logger.scope({
{"requestId", req.id},
{"method", req.method},
{"path", req.path}
});
logger.info("Request received");
auto result = process(req);
logger.info("Response: {status}", "status", result.status);
}
// All context removed automaticallyvoid processOrder(minta::LunarLog& logger, const Order& order) {
auto orderScope = logger.scope({{"orderId", order.id}});
for (const auto& item : order.items) {
auto itemScope = logger.scope({{"itemId", item.id}});
logger.info("Processing {name}", "name", item.name);
// context: {orderId=..., itemId=...}
}
// itemScope removed each iteration
logger.info("Order complete");
// context: {orderId=...}
}| Method | Description |
|---|---|
logger.scope({{"k","v"}, ...}) |
Create RAII scope with initial key-value pairs |
scope.add(key, value) |
Append a pair to the active scope (chainable) |
| Move constructor | Transfer ownership, source becomes inactive |
| Move assignment | Clean up existing scope, take ownership |
| Destructor | Remove scope's frame from the thread-local stack |
See also: API Reference — Context, Context Capture, Enrichers
Precedence: When enrichers and scoped context set the same key, scoped context wins: enricher <
setContext< scoped context. See Enrichers — Precedence Rules.