BlogDetection

Log aggregation with Loki and structured logging

Ship JSON logs, label them well, and query with LogQL — centralized logs without a heavyweight indexing bill.

Apr 22, 2025·4 min readIntermediate·By the SecOpsLog team · command-tested

Grafana Loki indexes metadata — labels — not full log line text. You choose which dimensions to filter on (job, namespace, level, app) and ship JSON payloads as log content. Queries use LogQL, a Prometheus-inspired language for log streams. The result is centralized logs at a fraction of Elasticsearch indexing cost, provided you resist the urge to label every field (cardinality kills Loki like it kills Prometheus).

Single-binary Grafana stack (Loki + Mimir + Tempo) shares object storage backends — design labels once for metrics and logs so dashboards join without relabel hacks.

This note configures Promtail (or Grafana Alloy) to scrape structured app logs, sets label cardinality rules, and writes LogQL for incident response. Pair with Prometheus alerts on symptom metrics and dive deeper in Detection engineering.

Loki log pipeline

Low-cardinality labels on streams; high-cardinality data inside JSON line. Parse in query with json | line_format.

1App logs JSONstdout one object per line2Promtail scrapeconfigstatic + kubernetes labels3Label setjob, env, app only4Push to Lokitenant + auth if multi5LogQL in GrafanaExplore + dashboards6Metric queriesrate(count_over_time(...))7Alert on logpatternLoki ruler or Grafana

Structured logging from applications

Log one JSON object per line to stdout. Include level, msg, trace_id, and business fields inside the JSON — not as Loki labels. Parsing at query time with | json keeps label cardinality flat while still letting you filter | level="error".

OpenTelemetry trace ids in logs let you jump from a metric spike to the exact trace and span in Tempo or Jaeger when the stack is wired. Standardize field names across services (service.name, level) so platform dashboards work without per-team LogQL forks.

app logger output
{"time":"2026-07-24T09:15:00Z","level":"error","msg":"payment failed",
"service":"checkout","trace_id":"abc123","user_id":"u-9981","err":"timeout"}

Promtail scrape with restrained labels

Promtail attaches labels when shipping to Loki. Keep pipeline_stages for parsing if needed, but promote only stable keys to labels. Never label user_id, trace_id, or request ids — they explode stream count.

Retention in Loki is cheap relative to full-text indexing but not free — set retention_period and compactor rules per tenant. Hot queries hit last 24–72 hours; ship older chunks to object storage if compliance requires longer hold. Ruler alerts on log patterns (|~ "failed login") complement metric alerts for security use cases.

promtail-config.yml
scrape_configs:
- job_name: kubernetes-pods
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_namespace]
target_label: namespace
- source_labels: [__meta_kubernetes_pod_label_app]
target_label: app
pipeline_stages:
- json:
expressions:
level: level
msg: msg
- labels:
level:
LogQL — query and metricizelive
{namespace="prod", app="checkout"} | json | level="error"
Stream of payment failed lines with trace_id inside JSON
sum by (app) (rate({namespace="prod"} | json | level="error" [5m]))
Error rate metric derived from logs
Use in Grafana alongside Prometheus SLO panels
High-cardinality labels will hurt Loki and your budget
Each unique label combination is a stream. Labeling `pod` is OK in Kubernetes — it is bounded. Labeling `request_id` is not. If queries slow down or ingesters OOM, audit labels first before scaling hardware.
Loki vs full-text search (ELK)
Loki fits when
Label-filter then grep JSON
Metrics + logs in Grafana
Cost-sensitive log volume
Kubernetes label model already
ELK/OpenSearch fits when
Heavy ad-hoc full-text search
Complex text analytics
Existing ES skill + tooling
Long indexed retention on all fields

Where this goes next

Correlate Loki traces with Prometheus metrics using trace_id in logs and exemplars on histograms. Ship host logs from journald pipelines into the same Loki tenant. Build composite alerts in Detection engineering — metric SLO burn plus log error spike in one Grafana panel.

Dashboard variables should drive label selectors (namespace=$ns) — never hardcode prod streams in shared panels. Test LogQL in Explore with line limit before building alert rules; expensive queries at ingest scale hurt queriers.

Go deeper in a courseDetection engineeringLoki, Prometheus, Alertmanager, and incident-ready observability.View course

Related posts