Skip to content

Scoped Context

LunarECL edited this page Feb 18, 2026 · 2 revisions

Scoped Context (LogScope)

RAII scoped context that automatically injects key-value pairs into log entries for the lifetime of the scope, then cleanly removes them on destruction.

Basic Usage

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 here

Nested Scopes

Scopes 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}
}

Duplicate Key Shadowing

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.

Adding Pairs After Construction

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.

Move Semantics

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 active

Thread-Wide Design

Important: 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.

Interaction with Global Context

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 restored

Exception Safety

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

Structured Output

Scoped context appears in all output formats:

Human-Readable

[2026-02-17 12:00:00.000] [INFO] Processing [requestId=req-001, userId=u-42]

JSON

{
  "level": "INFO",
  "message": "Processing",
  "context": {
    "requestId": "req-001",
    "userId": "u-42"
  }
}

XML

<log level="INFO" timestamp="...">
  <message>Processing</message>
  <context>
    <entry key="requestId">req-001</entry>
    <entry key="userId">u-42</entry>
  </context>
</log>

LogScope vs ContextScope

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.

Common Patterns

Request Tracing

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 automatically

Nested Transaction Logging

void 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=...}
}

API Reference

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.

Clone this wiki locally