Loki Logging With Grafana Alloy¶
What You'll Learn¶
- How log lines travel from a container or host file into Loki and onto a Grafana panel
- Why Grafana Alloy replaced Promtail, and how to convert an existing Promtail config
- How to choose Loki labels that keep queries fast instead of exploding stream counts
Promtail reached end of life on March 2, 2026
Promtail entered long-term support in February 2025 and is no longer maintained. Grafana Alloy is the supported collector for Loki, and the monitoring lab uses it. If you still run Promtail somewhere, follow the migration steps below.
Why This Matters¶
Metrics tell you that the error rate jumped; logs tell you which request failed and why. A logging pipeline that silently drops lines, or one whose labels make every query scan the whole store, fails exactly when you need it during an incident.
Mental Model¶
Loki stores streams. A stream is every log line that shares the exact same label set. The collector (Alloy) decides which files or containers to read and which labels to attach; Loki only indexes those labels, never the log text itself.
flowchart LR
C["Container stdout/stderr"] --> A["Grafana Alloy"]
H["Host files: /var/log/*.log"] --> A
A -->|"push: /loki/api/v1/push"| L["Loki"]
L --> G["Grafana: Explore and dashboards"]
How the Flow Works¶
- Alloy discovers log sources — Docker containers through the Docker socket, and host files by path glob.
- Alloy attaches labels (container, job, environment) and batches lines.
- Alloy pushes batches to Loki's push API.
- Loki stores the lines compressed, grouped by stream, and indexes only the labels.
- Grafana queries Loki with LogQL.
Important Files¶
| File | Purpose |
|---|---|
loki/loki-config.yml |
Loki storage, retention, and limits |
alloy/config.alloy |
Log discovery, labels, and where to push |
promtail/promtail-config.yml |
Legacy collector config, if you are migrating — convert it, then retire it |
A Working Alloy Config¶
// Discover running Docker containers
discovery.docker "containers" {
host = "unix:///var/run/docker.sock"
}
// Turn the "/orders-api" container name into a clean label
discovery.relabel "containers" {
targets = []
rule {
source_labels = ["__meta_docker_container_name"]
regex = "/(.*)"
target_label = "container"
}
}
loki.source.docker "containers" {
host = "unix:///var/run/docker.sock"
targets = discovery.docker.containers.targets
relabel_rules = discovery.relabel.containers.rules
forward_to = [loki.write.local.receiver]
}
// Host log files
local.file_match "system" {
path_targets = [{"__path__" = "/var/log/*.log", "job" = "varlogs"}]
}
loki.source.file "system" {
targets = local.file_match.system.targets
forward_to = [loki.write.local.receiver]
}
loki.write "local" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
}
Run it next to Loki in Compose:
services:
alloy:
image: grafana/alloy:latest
command:
- run
- --server.http.listen-addr=0.0.0.0:12345
- --storage.path=/var/lib/alloy/data
- /etc/alloy/config.alloy
ports:
- "12345:12345" # Alloy UI: component graph and health
volumes:
- ./alloy/config.alloy:/etc/alloy/config.alloy:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- /var/log:/var/log:ro
depends_on:
- loki
Pin grafana/alloy to a specific release tag once the lab works — latest is fine for a first run, not for anything you need to reproduce.
Migrating From Promtail¶
Alloy ships a converter, so you rarely rewrite a Promtail config by hand:
Then:
- Read the generated file — the converter reports anything it could not translate.
- Start Alloy alongside Promtail against a test Loki, and compare the streams in Grafana Explore.
- Stop Promtail and remove its service from Compose once the label sets match.
Choosing Labels¶
Keep high-cardinality values such as request IDs inside the log body or parsed fields, not as Loki stream labels. Every unique label combination creates a new stream; a request_id label turns one stream into millions.
| Good labels (low cardinality) | Keep in the log body |
|---|---|
service, container, environment, cluster, job |
request_id, trace_id, user_id, IP addresses, URLs |
Make application logs queryable by emitting stable fields rather than relying only on text. A useful event includes timestamp, level, service, environment, event name, request ID, and trace ID when tracing is enabled.
{"level":"ERROR","service":"orders-api","event":"payment_authorization_failed","request_id":"req-8f52","trace_id":"4bf92f..."}
Query the body fields at read time instead:
Quick Check¶
docker compose logs -f alloy
docker compose logs -f loki
curl http://localhost:3100/ready # Loki: "ready"
open http://localhost:12345 # Alloy UI: every component should be healthy
Common Mistakes¶
- Starting a new setup on Promtail because an older tutorial used it — it no longer receives fixes, including security fixes.
- Adding
request_id,user_id, or a full URL as a label, creating a stream explosion that makes Loki slow and expensive. - Forgetting to mount the Docker socket or
/var/logread-only into the Alloy container, so the pipeline starts "healthy" with nothing to read. - Writing application logs to a file inside the container instead of stdout/stderr, where the collector never sees them.
- Shipping secrets or personal data because nothing redacts them before
loki.write.
Interview Questions¶
- What is a Loki stream, and why does label cardinality matter so much more in Loki than in a full-text log store?
- Why did Grafana replace Promtail with Alloy, and how would you migrate a production Promtail deployment safely?
- A developer wants to add
request_idas a label to make searches faster. What do you tell them?
Next¶
Continue to Python Logging in Practice to emit the structured JSON logs this pipeline is built to query. For how the same pattern works inside a cluster, see Kubernetes Logging.