Key Takeaway
Grafana Alloy is Grafana's OpenTelemetry Collector distribution and the only supported successor: Grafana Agent reached end-of-life on 1 November 2025 and Promtail on 2 March 2026. Migrate with the built-in alloy convert command, then rewrite pipelines as connected components using the Alloy configuration syntax.
I run a small fleet of services on a self-hosted VPS, and for years the logging story was Promtail tailing Docker files and shipping to Loki. That story is over. Grafana consolidated Promtail, Grafana Agent Static, and Grafana Agent Flow into a single collector called Alloy, and then set end-of-life dates on everything it replaced. If you are still on Promtail or Agent, you are running unsupported software on borrowed time.
The good news: Alloy is not a rewrite from scratch. It is a genuine OpenTelemetry Collector distribution that is fully OTLP-compatible, so the same binary now handles logs, metrics, traces, and profiles through one config. This post is the migration I actually did — the tooling, the components, and where the automated converter leaves you to finish the work by hand.
Why Alloy, and why now
The deadlines are the forcing function. Grafana Agent (Static, Flow, and the Operator) hit end-of-life on 1 November 2025, meaning no more security patches or bug fixes. Promtail entered long-term support in February 2025 and reached end-of-life on 2 March 2026. All new feature development happens in Alloy only. Beyond the deadlines, the real win is consolidation: instead of Promtail for logs plus a separate Collector for OTLP traces, one Alloy process runs the whole telemetry pipeline with a single config language.
Do not treat this as optional maintenance. Once a component is end-of-life, a single upstream CVE in a Go dependency has no patch coming. Running Promtail after March 2026 is a security decision, not a convenience one.
The config syntax: components, not YAML blocks
Promtail was declarative YAML. Alloy uses a configuration syntax that started life under the name River — an HCL-like language built around components. A component is a named block that does one job: read files, relabel, batch, write to a backend. Each component exposes arguments you set and exported values other components consume. You wire them into a pipeline with the forward_to argument, which points one component's output at the next component's receiver. That reference graph is the pipeline; there is no separate pipelines section like the upstream Collector YAML has.
// alloy: a minimal Loki logging pipeline
// discover local files, process, then write to Loki
local.file_match "applogs" {
path_targets = [{"__path__" = "/var/log/app/*.log", "job" = "app"}]
}
loki.source.file "applogs" {
targets = local.file_match.applogs.targets
forward_to = [loki.process.parse.receiver]
}
loki.process "parse" {
stage.json {
expressions = { level = "level", ts = "timestamp" }
}
stage.labels {
values = { level = "" }
}
forward_to = [loki.write.default.receiver]
}
loki.write "default" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
}Step 1: let alloy convert do the mechanical work
Do not hand-translate your existing config. Alloy ships a convert subcommand that reads your old Promtail, Agent Static, or Agent Flow file and emits equivalent Alloy syntax. For Promtail it targets the v2.8.x schema. The converter is best-effort: it prints diagnostics for anything it cannot map cleanly, and you resolve those manually. I always convert into a file first so I can read the diff before running anything.
# Promtail -> Alloy (write to a file, review the diagnostics)
alloy convert \
--source-format=promtail \
--output=/etc/alloy/config.alloy \
/etc/promtail/config.yaml
# Grafana Agent Flow -> Alloy
alloy convert --source-format=flow \
--output=/etc/alloy/config.alloy /etc/agent/config.river
# OpenTelemetry Collector -> Alloy
alloy convert --source-format=otelcol \
--output=/etc/alloy/config.alloy /etc/otelcol/config.yaml
# validate before restarting the service
alloy fmt /etc/alloy/config.alloyStep 2: understand the OpenTelemetry Collector side
This is where Alloy earns its description. Native Prometheus and Loki components (the loki.* and prometheus.* families) coexist with the full OTLP component set under the otelcol.* namespace. If you receive OTLP from an instrumented NestJS service, otelcol.receiver.otlp takes it, otelcol.processor.batch groups it, and otelcol.exporter.otlphttp ships it onward. Bridging is first-class: otelcol.exporter.loki converts OTLP logs into Loki entries, and otelcol.receiver.loki goes the other direction, so you can mix a legacy Loki push path with a modern OTLP path in one file.
// alloy: receive OTLP over gRPC, batch, export to an OTLP/HTTP backend
otelcol.receiver.otlp "default" {
grpc { endpoint = "0.0.0.0:4317" }
http { endpoint = "0.0.0.0:4318" }
output {
metrics = [otelcol.processor.batch.default.input]
logs = [otelcol.processor.batch.default.input]
traces = [otelcol.processor.batch.default.input]
}
}
otelcol.processor.batch "default" {
output {
metrics = [otelcol.exporter.otlphttp.backend.input]
logs = [otelcol.exporter.otlphttp.backend.input]
traces = [otelcol.exporter.otlphttp.backend.input]
}
}
otelcol.exporter.otlphttp "backend" {
client { endpoint = "https://otlp.example.com" }
}Component name mapping
Most of the migration is learning which Alloy component replaces each old concept. This is the cheat sheet I kept open the whole time.
| Old concept | Alloy component | Role |
|---|---|---|
| Promtail scrape_configs (file) | loki.source.file | Tail log files by path |
| Promtail pipeline_stages | loki.process | Parse, relabel, extract fields |
| Promtail clients | loki.write | Push entries to Loki |
| Collector otlp receiver | otelcol.receiver.otlp | Ingest OTLP over gRPC or HTTP |
| Collector batch processor | otelcol.processor.batch | Group telemetry before export |
| Agent metrics scrape | prometheus.scrape | Pull Prometheus metrics |
Where the converter left me hand-work
- Docker service discovery. Promtail's docker_sd relabel rules converted, but I re-checked every __meta_docker_* label because dropped or renamed labels silently break Loki queries downstream.
- Secrets. Inline tokens became plain strings. I moved them to environment variables read via the sys.env helper so nothing sensitive sits in the committed config.
- Multiline stages. Complex regex multiline joins needed manual verification against real log samples before I trusted them.
- Live debugging. Alloy's built-in UI on port 12345 shows each component's health and the live data flowing between them — it replaced most of my guesswork.
Run the old and new collectors side by side into the same Loki, tagged with different job labels, for a few days. Compare line counts before you decommission Promtail. Converters get labels subtly wrong, and log gaps are invisible until you need the logs.
Running it: Docker Compose
On my VPS everything is Docker Compose, so Alloy is just another service. Mount the config, expose the debug UI locally, and point it at the same log volumes Promtail used. The run flag matters: --stability.level for non-stable components, and the config path last.
# docker-compose.yml (excerpt)
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:
- "127.0.0.1:12345:12345" # debug UI, localhost only
volumes:
- ./config.alloy:/etc/alloy/config.alloy:ro
- /var/log:/var/log:ro
- alloy-data:/var/lib/alloy/data
volumes:
alloy-data: {}That is the whole migration in shape: convert mechanically, learn the component graph, verify labels against real traffic, then cut over. The payoff is one agent instead of three, a single config language for logs and traces and metrics, and software that will still receive patches next quarter.

