-
Notifications
You must be signed in to change notification settings - Fork 1
Structured Output
LunarLog's JSON and XML formatters produce structured log entries with the raw template, a hash for grouping, and typed properties.
minta::LunarLog logger(minta::LogLevel::INFO, false);
logger.addSink<minta::FileSink, minta::JsonFormatter>("app.json");
logger.info("User {username} logged in from {ip}", "username", "Alice", "ip", "192.168.1.1");Output:
{
"level": "INFO",
"timestamp": "2026-02-17T12:00:00.000Z",
"messageTemplate": "User {username} logged in from {ip}",
"templateHash": "a1b2c3d4",
"message": "User Alice logged in from 192.168.1.1",
"properties": {
"username": "Alice",
"ip": "192.168.1.1"
}
}| Field | Always Present | Description |
|---|---|---|
level |
Yes | Log level string |
timestamp |
Yes | ISO 8601 with milliseconds |
messageTemplate |
Yes | Raw template with placeholders |
templateHash |
Yes | FNV-1a hash (8-char hex) |
message |
Yes | Rendered message |
properties |
When placeholders exist | Name-value map |
file |
When source location captured | Source file path |
line |
When source location captured | Line number |
function |
When source location captured | Function name |
context |
When context is set | Context key-value map |
The @ operator causes type detection in JSON:
logger.info("Count: {@n}, Active: {@flag}", "n", 42, "flag", true);{
"properties": {
"n": 42,
"flag": true
}
}Without @, all values are strings:
{
"properties": {
"n": "42",
"flag": "true"
}
}The $ operator forces string output:
logger.info("ID: {$id}", "id", 42);
// "properties": {"id": "42"}logger.addSink<minta::FileSink, minta::XmlFormatter>("app.xml");
logger.info("User {username} logged in", "username", "Alice");Output:
<log level="INFO" timestamp="2026-02-17T12:00:00.000Z">
<messageTemplate>User {username} logged in</messageTemplate>
<templateHash>a1b2c3d4</templateHash>
<message>User Alice logged in</message>
<properties>
<property name="username">Alice</property>
</properties>
</log>logger.info("{@count} items, ID: {$id}", "count", 42, "id", 100);<properties>
<property name="count" destructure="true">42</property>
<property name="id" stringify="true">100</property>
</properties>Every log entry gets a templateHash — an FNV-1a hash of the message template string, rendered as 8-character hex.
This enables log aggregation tools (Seq, Elasticsearch, Splunk) to group entries by template without expensive string comparison:
logger.info("User {name} logged in", "name", "Alice");
logger.info("User {name} logged in", "name", "Bob");
// Both have the same templateHash — same template, different values- Group related logs in dashboards
- Count occurrences of specific log patterns
- Detect new log patterns (unknown hash = new code path)
FNV-1a with 32 bits can produce collisions. The hash is for grouping efficiency, not cryptographic uniqueness. Rare collisions in log aggregation are tolerable.
Parsed templates are cached to avoid re-parsing on repeated log calls:
// Default: 128 entries
logger.setTemplateCacheSize(256); // increase for apps with many templates
logger.setTemplateCacheSize(0); // disable caching entirely- First time a template is seen → parse and cache
- Subsequent calls with the same template → use cached parse result
- When cache is full → existing entries stay cached, new templates are parsed every time (cap-without-eviction)
- High-cardinality templates (many unique messages): Increase cache size or disable
- Few recurring templates (typical apps): Default 128 is fine
- Memory-constrained environments: Reduce or disable
Template parsing involves string scanning and placeholder extraction. For hot-path logging with the same template, caching eliminates this cost entirely. The cache uses std::unordered_map keyed on the template string.
Enable source location capture for debugging:
logger.setCaptureSourceLocation(true);
logger.info("Something happened");JSON output:
{
"level": "INFO",
"message": "Something happened",
"file": "/path/to/main.cpp",
"line": 42,
"function": "main"
}Human-readable output:
[2026-02-17 12:00:00.000] [INFO] [main.cpp:42] Something happened
logger.setContext("service", "auth");
logger.setContext("version", "1.2.3");
logger.info("Request received");JSON output:
{
"message": "Request received",
"context": {
"service": "auth",
"version": "1.2.3"
}
}minta::LunarLog logger(minta::LogLevel::INFO, false);
logger.addSink<minta::FileSink, minta::JsonFormatter>("structured.json");
logger.setCaptureSourceLocation(true);
logger.setContext("env", "production");
logger.setLocale("en_US");
logger.info("Order {@orderId} total: {amount:n}", "orderId", 12345, "amount", 9999.99);{
"level": "INFO",
"timestamp": "2026-02-17T12:00:00.000Z",
"messageTemplate": "Order {@orderId} total: {amount:n}",
"templateHash": "f3a1b2c4",
"message": "Order 12345 total: 9,999.99",
"file": "main.cpp",
"line": 10,
"function": "main",
"properties": {
"orderId": 12345,
"amount": "9999.99"
},
"context": {
"env": "production"
}
}Note: @orderId → native number in JSON. amount without @ → string.
For production pipelines where payload size matters, use CompactJsonFormatter instead. It produces single-line JSONL with short keys and flattened properties.
See Compact JSON Formatter for the full guide.