Loki
storing and querying logs
Loki centralizes logs so they can be queried without connecting to every machine. It does not create events or draw panels: it receives the lines sent by an agent, stores them and answers LogQL queries from tools such as Grafana.
From code to panel
Suppose the worker cannot generate a report because its template is missing. The application records the event using key-value pairs:
log.atError()
.addKeyValue("report_id", reportId)
.addKeyValue("reason", "template_not_found")
.log("Report generation failed");
With a compatible JSON encoder, stdout receives a single line like this one
—formatted here for readability—:
{
"timestamp": "2026-08-29T10:15:42Z",
"level": "ERROR",
"message": "Report generation failed",
"report_id": "report-482",
"reason": "template_not_found",
"correlation_id": "corr-913",
"trace_id": "7ac..."
}
From there, the log follows this path:
- Alloy collects the line. It reads new logs from
stdoutand adds stable information such as the service and environment. - Loki groups the logs. The
service_nameandenvironmentlabels determine which group each line belongs to. Loki calls that group a stream. - Loki compresses and stores the lines. It only indexes the labels and time range, not every word in the message.
- Grafana queries Loki. It first narrows the search by service and time
range. LogQL then parses the JSON and searches the matching lines for
reason="template_not_found":
{service_name="report-worker", environment="production"}
| json
| reason="template_not_found"
Loki returns the lines that match the filter and Grafana displays them in
Explore or a panel. Because the line includes trace_id, it can also link to the
corresponding trace in Tempo.
log.atError() -> encoder -> stdout -> runtime -> Alloy -> Loki -> Grafana
What determines the cost
Labels create streams, so they should have few possible values. service_name,
environment and region are usually good candidates. report_id,
request_id and trace_id should remain in the log or as structured metadata:
using them as labels would create thousands of streams and degrade ingestion
and queries.
Volume, retention and the queried time range also matter. Searching one service for an hour requires reading far fewer chunks than searching every service over thirty days.