Grafana Alloy
What Alloy Is
Grafana Alloy is a distribution of the OpenTelemetry Collector. It embeds upstream Collector components and adds Grafana’s own, replaces the YAML pipeline config with a programmable configuration language, and includes the collection features that used to live in Promtail and the Grafana Agent.
It replaces all of them. Grafana Agent (both static and flow modes) and Promtail are deprecated in its favour, with Promtail’s support ending in 2026, which is why so many existing setups are mid-migration.
Read the Collector page first. Receivers, processors, exporters, the agent and gateway patterns, batching and backpressure are all the same here. This page is only the delta.
The Delta
| Upstream Collector | Alloy | |
|---|---|---|
| Config | YAML, pipelines as lists | Alloy syntax, a component graph with references |
| Composition | Fixed pipeline order | Any DAG, components wired by expression |
| Logic in config | None | Expressions, loops over discovery results, conditionals |
| Prometheus scraping | Via the prometheus receiver, a nested Prometheus config |
Native, first-class components |
| Service discovery | Inside the Prometheus receiver | Standalone discovery.* components usable by anything |
| Log file collection | filelog receiver |
Native, the Promtail lineage |
| Profiles | Not in core | pyroscope.* components, including eBPF |
| Clustering | External load balancer plus loadbalancing exporter |
Built-in gossip cluster with target sharding |
| UI | zpages |
A real web UI at :12345 showing the live graph |
The honest summary: if the workload is pure OTLP in, OTLP out, upstream is simpler and has no vendor lean. If the workload mixes Prometheus scraping, log tailing, profiling and OTLP, Alloy is materially better because all four are first-class rather than bolted into one receiver.
The Component Graph
Alloy configuration declares components and wires them by reference. There is no pipeline list; the graph is implied by which component’s output another component reads.
// Discovery produces a list of targets
discovery.docker "containers" {
host = "unix:///var/run/docker.sock"
}
// Relabelling is its own component, consuming that list
discovery.relabel "containers" {
targets = discovery.docker.containers.targets
rule {
source_labels = ["__meta_docker_container_name"]
regex = "/(.*)"
target_label = "container"
}
rule {
source_labels = ["__meta_docker_container_label_com_docker_compose_service"]
target_label = "service"
}
}
// Two unrelated components consume the SAME discovery output
prometheus.scrape "containers" {
targets = discovery.relabel.containers.output
forward_to = [prometheus.remote_write.default.receiver]
}
loki.source.docker "containers" {
host = "unix:///var/run/docker.sock"
targets = discovery.relabel.containers.output
forward_to = [loki.process.default.receiver]
}
That reuse is the actual advantage. In upstream YAML, the Prometheus receiver’s discovery and the filelog receiver’s file matching are separate configurations that have to be kept consistent by hand. Here one discovery.relabel block feeds metrics scraping and log collection, so labels cannot drift apart.
forward_to takes a list, so fanning out to two destinations is adding an element. Components with an input expose it as .receiver.
Alloy Syntax
It is HCL-like: blocks, attributes, and expressions. Not YAML, and not a general-purpose language.
// Read from the environment or a file
local.file "token" {
filename = "/etc/alloy/tempo_token"
is_secret = true
}
prometheus.remote_write "default" {
endpoint {
url = sys.env("PROM_URL")
basic_auth {
username = "alloy"
password = local.file.token.content
}
queue_config {
capacity = 10000
max_samples_per_send = 2000
}
}
external_labels = {
cluster = "knowledge-lab",
host = sys.env("HOSTNAME"),
}
}
// Declare a reusable subgraph
declare "scrape_node" {
argument "address" {}
argument "forward_to" {}
prometheus.scrape "node" {
targets = [{"__address__" = argument.address.value, "job" = "node"}]
forward_to = argument.forward_to.value
}
}
scrape_node "lab" {
address = "192.168.2.205:9100"
forward_to = [prometheus.remote_write.default.receiver]
}
declare is the module system, and it is what makes a large Alloy config maintainable rather than a wall of near-identical blocks. Configuration is also reloadable: SIGHUP or a POST to /-/reload applies changes without dropping state.
Component Families
| Prefix | Purpose |
|---|---|
discovery.* |
Produce target lists: kubernetes, docker, file, dns, ec2, relabel |
prometheus.* |
scrape, relabel, remote_write, receive_http, exporter.* |
loki.* |
source.file, source.docker, source.journal, process, write, relabel |
otelcol.* |
The embedded upstream components: receiver.*, processor.*, exporter.*, connector.* |
pyroscope.* |
scrape, ebpf, java, write |
local.*, remote.* |
Files, HTTP, Vault, and Kubernetes secrets as config inputs |
declare, import.* |
Modules and reuse |
prometheus.exporter.* is worth knowing about. Alloy embeds many common exporters, so prometheus.exporter.unix is node_exporter running inside Alloy with no separate container, and the same exists for postgres, redis, blackbox and others. One process instead of six.
prometheus.exporter.unix "host" {
set_collectors = ["cpu", "diskstats", "filesystem", "loadavg", "meminfo", "netdev"]
}
prometheus.scrape "host" {
targets = prometheus.exporter.unix.host.targets
forward_to = [prometheus.remote_write.default.receiver]
}
Mixing OTel And Grafana Components
otelcol.* components are the upstream ones, and they connect to the native ones through converters.
otelcol.receiver.otlp "default" {
grpc { endpoint = "0.0.0.0:4317" }
http { endpoint = "0.0.0.0:4318" }
output {
traces = [otelcol.processor.batch.default.input]
metrics = [otelcol.processor.batch.default.input]
logs = [otelcol.processor.batch.default.input]
}
}
otelcol.processor.batch "default" {
output {
traces = [otelcol.exporter.otlp.tempo.input]
metrics = [otelcol.exporter.prometheus.default.input] // converter
logs = [otelcol.exporter.loki.default.input] // converter
}
}
otelcol.exporter.otlp "tempo" {
client {
endpoint = "tempo:4317"
tls { insecure = true }
}
}
// Converters: OTLP metrics -> Prometheus remote write
otelcol.exporter.prometheus "default" {
forward_to = [prometheus.remote_write.default.receiver]
}
// Converters: OTLP logs -> Loki push
otelcol.exporter.loki "default" {
forward_to = [loki.write.default.receiver]
}
otelcol.exporter.prometheus and otelcol.exporter.loki are not exporters in the upstream sense. They are adapters from the OTel data model into Alloy’s native Prometheus and Loki paths, which is how one Alloy instance accepts OTLP from applications and Prometheus scrapes from exporters and sends both onward through the same remote write.
Clustering
Alloy instances gossip to form a cluster and shard scrape targets among themselves.
alloy run /etc/alloy/config.alloy \
--cluster.enabled=true \
--cluster.join-addresses=alloy-0:12345,alloy-1:12345prometheus.scrape "shared" {
targets = discovery.kubernetes.pods.targets
forward_to = [prometheus.remote_write.default.receiver]
clustering { enabled = true }
}
With clustering enabled on a component, each instance scrapes only the targets hashed to it, and the set rebalances when an instance joins or leaves. This solves horizontal scaling of scraping without the usual hashmod relabelling by hand, and without duplicate scrapes.
It does not solve the tail-sampling problem. Routing all spans of a trace to one instance still needs otelcol.exporter.loadbalancing with a trace ID routing key, exactly as upstream.
The UI
Alloy serves a web UI on :12345 showing the live component graph: every component, its health, its current config after expression evaluation, and the data flowing between them.
This is a genuine advantage over upstream’s zpages. A component that is unhealthy shows why, and the evaluated config view resolves the question of what a sys.env() or local.file reference actually became at runtime, which is otherwise guesswork.
Migrating
# Promtail, Grafana Agent, Prometheus and upstream Collector configs all convert
alloy convert --source-format=promtail --output=config.alloy promtail.yaml
alloy convert --source-format=prometheus --output=config.alloy prometheus.yml
alloy convert --source-format=otelcol --output=config.alloy otelcol.yaml
alloy convert --source-format=static --output=config.alloy agent.yaml
# Validate before restarting anything
alloy fmt config.alloy
alloy run --stability.level=generally-available config.alloyThe converter output is correct and verbose, producing a component per config block with no attempt to share discovery. Treat it as a starting point to refactor, since the reuse described above is the main reason to be on Alloy at all.
Choosing Between Them
Use upstream Collector when the pipeline is OTLP in and OTLP out, when vendor neutrality is a stated requirement, when the deployment is already standardised on it, or when the operator model in Kubernetes matters.
Use Alloy when the same agent must scrape Prometheus targets, tail log files, collect profiles and accept OTLP, when the Grafana stack is the destination, when target sharding across a fleet is needed without external coordination, or when the configuration is complex enough that expressions and modules beat repeated YAML.
Do not use Promtail for anything new. It is deprecated and feature-frozen. Existing deployments work; new ones should be Alloy.
Both speak OTLP, so this is a reversible decision, which is the entire point of having instrumented with OpenTelemetry in the first place.
Where Next
- The OTel Collector for the pipeline concepts underneath all of this.
- Fluent Bit and Vector for the non-Grafana alternatives.
- Loki and Pyroscope for two of the destinations.