LogQL Best Practices :: Kloudfuse Docs

LogQL Best Practices

Table of Contents

The fastest LogQL queries on Kloudfuse touch the least data. Kloudfuse gives every log line two layers of structure before the raw text — indexed stream labels and ingest-time facets — and most string searches can be replaced by a filter on one of them. This page shows how to use each layer, in the order a query should apply them.

Know the three layers of a log line

Layer Examples Where to filter Cost
Indexed stream labels source, level, kube_namespace, kube_service, host, cluster_name, pod_name, availability_zone, project Stream selector: {source="nginx", level="error"} Cheapest — resolved against the label index before any data is read
Ingest-time facets Fields Kloudfuse extracted when the log arrived: statusCode, endpoint, duration, logger, and any other key your pipeline parses Label filter, no parser needed: ` statusCode="200"`
Raw log body The original message text Line filters: ` = "text",

Structure every query in that order: select streams by label, narrow by facet, and only then — if anything is still ambiguous — search the remaining text.

Start with the most selective stream selector

Every label matcher in the selector cuts the data the rest of the query reads. Name the source, and add every other indexed label you know:

{source="grafana", level="error"}

Two habits keep selectors fast:

Avoid match-everything selectors such as {source=~".+"} — they turn the whole time range into the query’s working set.

Filter on facets, not on strings

Kloudfuse extracts facets from structured log content at ingest and stores them alongside the line. In LogQL they behave like labels that are already extracted — filter on them directly with a label filter expression:

{source="grafana"} | statusCode="200"

No json or logfmt stage is required, and no log body is scanned. Numeric, duration, and bytes comparisons work the same way:

{source="grafana"} | duration > 100ms

Compare this with the string-search version of the same intent, {source="grafana"} |= "statusCode=200", which scans every line, breaks if the field order or spacing changes, and also matches lines that merely mention that text.

One placement rule to remember: facets work in label filters, not in the stream selector. {source="grafana", statusCode="200"} matches nothing, because the selector only consults indexed stream labels. Write the facet condition after the opening selector instead:

{source="grafana"} | statusCode="200" | endpoint="queryData"

Reach for a parser only when the facet does not exist

Parsers (json, logfmt, pattern, regexp) re-extract fields from the raw text on every line, on every query. That work is wasted when the field is already a facet. Facets feed metric queries directly — grouping and unwrapping included:

sum by (statusCode) (count_over_time({source="grafana"} | statusCode != "" [5m]))
avg_over_time({source="grafana"} | unwrap duration(duration) [5m]) by (endpoint)

Use a parser when the field genuinely is not extracted at ingest — a one-off pattern inside the message body, or a format your pipeline does not parse:

{source="nginx"} | regexp "(?P<method>[A-Z]+) (?P<path>[^ ]+)" | method="POST"

Make string search the last filter, not the first

When you do need to search text, position and choice of operator matter:

{source="payment", level="error"} | endpoint="checkout" |= "OutOfMemoryError"

Everything before |= in that query resolves from indexes and facet columns; the scan runs only over checkout-endpoint error lines.

Keep metric queries pushdown-friendly

Kloudfuse pushes metric computation down into the storage layer when the query shape allows it, which is dramatically faster than streaming raw lines back to the query engine. Two habits preserve that:

sum by (level) (count_over_time({source="grafana"}[5m]))

Measured impact

The following measurements compare each practice against its anti-pattern on a production-scale Kloudfuse cluster (busiest measured source: ~2.4 million lines/hour). Times are the server-reported query execution time from the Loki API’s query statistics; log queries use a 10-minute range with a limit of 10, metric queries evaluate a [5m] window once.

Intent Best practice Anti-pattern Exec time Speedup
Find error logs {source="grafana", level="error"} `{source="grafana"} = "level=error"` 9 ms vs 159 ms
Filter on a field `{source="grafana"} statusCode="200"` `{source="grafana"} = "duration="
Aggregate by level sum by (level) (…) sum without (host) (…) 12 ms vs 621 ms ~50×
Count with a clean pipeline count_over_time({…}[5m]) `count_over_time({...} line_format "..." [5m])` 28 ms vs 105 ms

Two notes on reading these numbers:

Bound the scope and the time range

Query cost scales with the data selected, and the time range is a multiplier on everything above. While iterating on a query, work against a short window — the last 5 or 15 minutes — and widen it only when the query is final. For dashboards, size the range window […​] to the panel’s step rather than defaulting to large windows: rate({…}[1m]) at a 1-minute step reads each line once.