Skip to content

Structured metadata

Mykhailo Shevchuk edited this page Jun 6, 2026 · 2 revisions

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.

The three places a value can live

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.

Properties as structured metadata

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.

Trace and span IDs

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.

What goes on the wire

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.

Querying it

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.

Loki requirements

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: true and a TSDB v13 schema in schema_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 through Serilog.SelfLog, and the batch is retried or dropped per your retryTimeLimit. 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.

See also

Clone this wiki locally