Skip to content
Go back

Grafana Alloy: Replacing the Agent After Deprecation

By SumGuy 11 min read
Grafana Alloy: Replacing the Agent After Deprecation

You Have 40 Boxes Still Running Grafana Agent. Now What?

November 2025. Grafana Labs officially sunset the Grafana Agent. Not “we’re slowing down development.” Not “please consider migrating eventually.” End. Of. Life. No more patches, no more security fixes, no support tickets. If you’re still running it, you’re on your own.

If your homelab looks anything like mine — a mix of Proxmox nodes, LXC containers, a few bare-metal machines, and some Raspberry Pis stuffed in a closet — you’ve got Grafana Agent scattered across everything like confetti. And now you need to migrate all of it to Grafana Alloy before the next CVE drops and you’re left holding a deprecated binary.

Here’s the thing though: Alloy isn’t a punishment. It’s actually a better tool. Once you stop mourning the Agent and start actually using Alloy, you’ll appreciate what they built here.


What Alloy Actually Is

Alloy is the direct successor to Grafana Agent’s Flow mode — the version that used a declarative, graph-based configuration language called River. If you were still using the older “static mode” Agent, congratulations, you’ve got the biggest migration ahead of you.

Grafana Agent Flow was itself positioned as the future of the Agent. Alloy is what happened when they decided to stop calling it a beta experiment and ship it as the canonical tool. It’s OTel-aligned, meaning it works natively with the OpenTelemetry ecosystem — not as a wrapper, but as a first-class participant. Alloy can receive OTLP signals, process them, forward to Grafana Cloud or your self-hosted stack, and do it all in a single binary.

The key facts:

It’s not a re-brand. The architecture is genuinely different from static-mode Agent, and the River language is actually good once you stop fighting it.


River: The Config Language You’ll Learn to Like

River is to Alloy what HCL is to Terraform — a purpose-built DSL that looks alarming for about 15 minutes and then starts making sense. If you’ve written Terraform, you’ll recognize the pattern immediately.

The three building blocks are components, exports, and imports.

Components are the workers. Each component has a type (prometheus.scrape, loki.write, otelcol.receiver.otlp) and a user-defined label that makes it unique. They receive arguments and optionally expose exports.

Exports are the outputs of a component. When you want one component’s output to feed another component’s input, you reference it by component_type.label.export_name. This is what makes the graph view on :12345 so useful — the data flow is explicit in the syntax.

Imports let you pull in reusable modules, either from local files or from a remote git repo. You can share common scraping configs across your fleet without copy-pasting.

Here’s the simplest possible Alloy config to ground the mental model:

prometheus.scrape "node" {
targets = [{"__address__" = "localhost:9100"}]
forward_to = [prometheus.remote_write.mimir.receiver]
}
prometheus.remote_write "mimir" {
endpoint {
url = "http://mimir:9009/api/v1/push"
}
}

Read it top to bottom: scrape node_exporter, forward the result to the mimir remote_write component. The forward_to argument takes a list of receivers — that’s how you fan out to multiple destinations.

No YAML indentation hell. No wondering if that key belongs to the parent block or the child. Just explicit, readable data flow.


A Real Config: Metrics, Logs, and Traces

This is the config I’d actually deploy on a typical homelab server. It covers the three pillars: metrics via Prometheus scraping to Mimir, logs via journald to Loki, and OTLP traces received over gRPC and forwarded to Tempo.

// ── Metrics ──────────────────────────────────────────────────────────────
prometheus.exporter.unix "node" {}
prometheus.scrape "node" {
targets = prometheus.exporter.unix.node.targets
forward_to = [prometheus.remote_write.mimir.receiver]
scrape_interval = "60s"
}
prometheus.remote_write "mimir" {
endpoint {
url = "http://mimir.internal:9009/api/v1/push"
basic_auth {
username = env("MIMIR_USER")
password = env("MIMIR_PASS")
}
}
external_labels = {
cluster = "homelab",
node = env("HOSTNAME"),
}
}
// ── Logs ─────────────────────────────────────────────────────────────────
loki.relabel "journal" {
forward_to = []
rule {
source_labels = ["__journal__systemd_unit"]
target_label = "unit"
}
rule {
source_labels = ["__journal__hostname"]
target_label = "host"
}
}
loki.source.journal "system" {
forward_to = [loki.write.default.receiver]
relabel_rules = loki.relabel.journal.rules
max_age = "12h"
labels = {job = "systemd-journal"}
}
loki.write "default" {
endpoint {
url = "http://loki.internal:3100/loki/api/v1/push"
}
}
// ── Traces ───────────────────────────────────────────────────────────────
otelcol.receiver.otlp "grpc" {
grpc {
endpoint = "0.0.0.0:4317"
}
output {
traces = [otelcol.exporter.otlphttp.tempo.input]
}
}
otelcol.exporter.otlphttp "tempo" {
client {
endpoint = "http://tempo.internal:4318"
}
}

A few things worth calling out:

The prometheus.exporter.unix component is Alloy’s built-in node_exporter. You don’t need a separate node_exporter binary running on every host — Alloy ships it embedded. One less thing to maintain.

The env() function pulls environment variables at runtime. Use this for secrets instead of hardcoding credentials. Pair it with a systemd EnvironmentFile= directive pointing at /etc/alloy/secrets.env and you’re done.

The OTLP receiver listens on :4317 for gRPC. If your apps are sending HTTP instead, add an http {} block in the otelcol.receiver.otlp component — same syntax, different protocol.


Migrating From Static Mode: The alloy convert Command

If you’ve been running Grafana Agent in static mode (the YAML-based one), Alloy ships a migration helper:

Terminal window
alloy convert --source-format=static --output=alloy.alloy static-agent.yaml

Honest review: it gets you 70% of the way there. For straightforward configs — a couple of Prometheus scrape jobs and a remote_write — it produces valid River that you can actually run. For anything involving integrations, custom relabeling chains, or the older metrics subsystem, it produces something that at least compiles but might not behave identically.

Always diff what it generates against what you expect. Common failure modes:

For Flow-mode Agent configs, the conversion is more reliable since the underlying model is the same. The syntax changed slightly between Agent Flow’s version of River and Alloy’s version, but alloy convert --source-format=flow handles most of it.


The UI on :12345 Is Genuinely Good

Alloy exposes a web UI at http://localhost:12345. I know, another port to remember. It’s worth it.

The graph view shows every component in your config as a node, with edges representing data flow. When something’s broken — a remote_write endpoint is timing out, a scrape target is failing — the affected component lights up and the UI shows you exactly where the pipeline is stuck. No more grepping through logs to figure out why your metrics disappeared at 3 AM.

The component inspector lets you click on any component and see its current arguments, exports, and health status in real time. You can see what targets prometheus.scrape is currently polling and what the last scrape result was.

If you’re deploying Alloy on a server without a browser, the same data is available as JSON at http://localhost:12345/api/v0/web/components. Script it, alert on it, put it in your monitoring (meta, I know).

To make the UI accessible remotely without punching a hole in your firewall:

# /etc/alloy/config.alloy or via --server.http.listen-addr flag
Terminal window
alloy run /etc/alloy/config.alloy \
--server.http.listen-addr=0.0.0.0:12345 \
--stability.level=generally-available

Put it behind Caddy with auth if you’re exposing it at all. The UI has no built-in authentication.


Pyroscope: The Integration You Didn’t Know You Wanted

Alloy has native integration with Grafana Pyroscope, the continuous profiling backend. If you’re self-hosting Pyroscope (and if you’re not, you should be — it’s surprisingly lightweight), you can pull profiles from your Go and Python services without modifying their code.

pyroscope.scrape "go_services" {
targets = [
{"__address__" = "myapp:6060", "service_name" = "myapp"},
]
forward_to = [pyroscope.write.local.receiver]
profiling_config {
profile.process_cpu {
enabled = true
}
profile.memory {
enabled = true
}
}
}
pyroscope.write "local" {
endpoint {
url = "http://pyroscope.internal:4040"
}
}

This scrapes pprof endpoints on your Go services and pushes continuous profiles to Pyroscope. Combined with Tempo traces and Loki logs, you now have the full Grafana Labs LGTM stack (Loki, Grafana, Tempo, Mimir) plus profiling — all fed by one Alloy binary per host.


Performance: What You’re Signing Up For

Memory footprint: similar to Grafana Agent in comparable configurations. In practice, on a node doing standard monitoring (node metrics, system logs, forwarding traces), Alloy sits at 50–80 MB RSS. Not nothing, but not a concern on anything with more than 512 MB RAM.

CPU is negligible under normal load. The profile spikes if you’re doing heavy regex relabeling or processing high-cardinality metrics, but that’s a config problem, not an Alloy problem.

What Alloy is genuinely more flexible on is the pipeline topology. In static-mode Agent, you had fixed subsystems (metrics, logs, traces) with a fairly rigid flow. In Alloy, you can do things that were previously impossible or required running multiple agents: fan-in from multiple receivers into a single exporter, apply shared relabeling across different input types, conditionally route data based on metric labels.

For a homelab, you’ll never push Alloy to its limits. For a production-ish setup where you’re consolidating observability agents across multiple teams’ services, the flexibility matters.


When You’d Pick Raw OTel Collector Instead

Alloy is the right choice if you’re already in the Grafana ecosystem — Mimir, Loki, Tempo, Grafana Cloud. It speaks that language natively and the integrations are tight.

But if you need to be vendor-neutral, the OpenTelemetry Collector is still the cleaner choice. Reasons you’d reach for it instead:

Alloy can actually wrap OTel Collector components — many of the otelcol.* components in Alloy are direct ports. But if you’re going native OTel anyway, you might as well just run the Collector.

The short version: homelab on Grafana stack? Alloy. Enterprise multi-vendor? OTel Collector.


Deploying It: Systemd Edition

Install Alloy from Grafana’s APT/YUM repo (it’s separate from the old Agent repo):

Terminal window
# Add the Grafana APT repo if you haven't already
wget -q -O - https://apt.grafana.com/gpg.key | gpg --dearmor | \
sudo tee /etc/apt/keyrings/grafana.gpg > /dev/null
echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] \
https://apt.grafana.com stable main" | \
sudo tee /etc/apt/sources.list.d/grafana.list
sudo apt-get update && sudo apt-get install -y alloy

The package drops a systemd unit at /lib/systemd/system/alloy.service. The config file goes in /etc/alloy/config.alloy by default. For secrets:

/etc/alloy/secrets.env
MIMIR_USER=myuser
MIMIR_PASS=supersecret
HOSTNAME=my-node-01
# Add to the [Service] section in a systemd override
# sudo systemctl edit alloy
[Service]
EnvironmentFile=/etc/alloy/secrets.env

Then:

Terminal window
sudo systemctl daemon-reload
sudo systemctl enable --now alloy
sudo systemctl status alloy

Check the UI at http://your-host:12345 and verify the component graph looks right. All green? You’re done.


The Bottom Line

Grafana Agent is gone. It’s not coming back. The migration tax is real — if you’ve got 40 boxes on static-mode Agent, you’re spending a weekend on this whether you like it or not. The alloy convert command takes the edge off, but you still need to eyeball every config it produces.

The upside is that what you’re migrating to is actually better. The River config language is more readable than Agent’s YAML once you stop comparing it to YAML and start comparing it to other pipeline configs. The built-in node_exporter removes a dependency. The UI graph view will save you debugging hours. And when your app team eventually asks you to collect traces, you’re already set up for it — no new agent, no new port, just a few more lines in the same config file.

The architecture matches how telemetry pipelines actually work: sources, transforms, sinks, wired together explicitly. Once that clicks, you’ll wonder why you were ever maintaining separate configs for metrics and logs.

Your 2 AM self will appreciate the graph view. Trust me on that one.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
RustDesk vs MeshCentral: Self-Hosted Remote Desktop
Next Post
Nerdctl vs Docker CLI

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts