-
Notifications
You must be signed in to change notification settings - Fork 40
Structured metadata
Structured metadata lets you attach key/value pairs to an individual log line without turning them into stream labels. It is the third place a value can live in a Loki push - alongside labels and the log body - and for high-cardinality data it is usually the right one.
| Stream labels | Structured metadata | Log body (JSON) | |
|---|---|---|---|
| Sink option |
labels, propertiesAsLabels
|
propertiesAsStructuredMetadata, traceIdMode/spanIdMode
|
the formatter (everything else) |
| Indexed? | Yes - defines the stream | No | No |
| Cardinality cost? | Yes - each distinct value is a new stream | None | None |
| How you query it | label selector {app="web_app"}
|
label filter, no parser stage | needs a json parser stage |
| Best for | low-cardinality, stable (app, env, level) | high-cardinality you still filter on (request / user / trace IDs) | the full structured payload |
The key insight: structured metadata is filterable like a label but free of a label's cardinality cost. It is not indexed, so attaching a unique RequestId to every line does not create a stream per request - the trap described in Mapping properties to labels.
propertiesAsStructuredMetadata is the high-cardinality sibling of propertiesAsLabels: name the properties you want attached per line.
.WriteTo.GrafanaLoki(
"http://localhost:3100",
labels: [new LokiLabel { Key = "app", Value = "web_app" }],
propertiesAsStructuredMetadata: ["RequestId", "UserId"]){
"Name": "GrafanaLoki",
"Args": {
"uri": "http://localhost:3100",
"labels": [ { "key": "app", "value": "web_app" } ],
"propertiesAsStructuredMetadata": [ "RequestId", "UserId" ]
}
}A promoted property is also kept in the log body (the same rule propertiesAsLabels follows), so nothing is lost. Property names follow the same key sanitisation as labels - a key starting with a digit, such as 0, becomes param0 - and values are rendered as strings.
Routing TraceId / SpanId to structured metadata is the recommended pattern: they are high-cardinality and you almost always want to filter by them. Set traceIdMode / spanIdMode to StructuredMetadata:
.WriteTo.GrafanaLoki(
"http://localhost:3100",
traceIdMode: LokiFieldDestination.StructuredMetadata,
spanIdMode: LokiFieldDestination.StructuredMetadata)The full three-way choice (None, Body, StructuredMetadata) is covered on Trace and span enrichment.
Structured metadata is the optional third element of each entry in a stream's values array (an entry is normally [ timestamp, line ]):
{
"streams": [
{
"stream": { "app": "web_app", "level": "info" },
"values": [
[ "1700000000000000000", "{\"Message\":\"...\"}", { "RequestId": "abc-123", "TraceId": "0af7651916cd43dd8448eb211c80319c" } ]
]
}
]
}When nothing is configured (the default), the third element is omitted entirely and the payload is byte-for-byte the classic [ timestamp, line ] shape.
Structured metadata is filterable directly, with no parser stage:
{app="web_app"} | RequestId="abc-123"
{app="web_app"} | TraceId="0af7651916cd43dd8448eb211c80319c"
Compare a plain body field, which needs | json first (see Using LokiJsonTextFormatter and Grafana Loki json parser):
{app="web_app"} | json | RequestId="abc-123"
In Grafana Explore, structured metadata appears as fields on each log line, and trace IDs can be wired to a tracing backend such as Tempo using derived fields.
Structured metadata is a relatively new Loki capability, so the receiver has to support it:
- Loki 3.0+ has it enabled by default.
-
Loki 2.9 supports it only with
allow_structured_metadata: trueand a TSDB v13 schema inschema_config. - Older Loki versions do not support it at all.
Loki also enforces per-line limits (configurable via limits_config): by default roughly 64 KB of metadata and 128 entries per line.
If the receiver does not accept structured metadata, it rejects the whole push with HTTP 400 (for example
structured metadata is disabled). The sink reports the failure throughSerilog.SelfLog, and the batch is retried or dropped per yourretryTimeLimit. These options are opt-in and off by default precisely so the sink stays compatible with any Loki version out of the box - turn them on only once you know your Loki accepts them.
- Mapping properties to labels - the indexed sibling, and when a label is the wrong choice
-
Trace and span enrichment - the full
traceIdMode/spanIdModestory - Application settings - every option as JSON