Pipeline Model
- The OTel Collector model
- The Alloy component model
- Data types do not mix
- Processor order
- Enrichment traps
- Related lessons
Both tools run the same upstream code for OTLP processing. They differ in how you describe a pipeline: a YAML list of named pipelines, or a graph of components that reference each other.
The OTel Collector model
| Building block | Role | Examples |
|---|---|---|
| Receiver | Gets data in (push or pull) | otlp, prometheus, filelog, k8s_events |
| Processor | Changes data in flight | memory_limiter, batch, filter, transform, k8sattributes, tail_sampling |
| Exporter | Sends data out | otlp, otlphttp, prometheusremotewrite, loadbalancing |
| Connector | Exporter of one pipeline and receiver of another | spanmetrics, routing, count |
| Extension | Not in the data path | health_check, pprof, file_storage, bearertokenauth |
Wiring lives in service.pipelines. A component defined but not listed there does nothing — silently.
extensions:
health_check: {}
file_storage: { directory: /var/lib/otelcol/queue }
service:
extensions: [health_check, file_storage] # defined above AND enabled here
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch] # order = execution order
exporters: [otlp/tempo]
- Extensions are listed twice — once defined, once enabled in
service.extensions. Defining without enabling is not an error. - A pipeline is per signal. One
otlpreceiver can feed atraces, ametricsand alogspipeline. - Processors in one pipeline run in list order. The same processor name in two pipelines is two independent instances.
The Alloy component model
Every block is a component: namespace.kind "label" { arguments }. It has arguments (inputs you set) and exports (values others read). A pipeline is nothing more than references between exports and arguments.
otelcol.receiver.otlp "default" {
grpc { endpoint = "0.0.0.0:4317" }
output {
traces = [otelcol.processor.batch.default.input] // reference: namespace.kind.label.export
}
}
otelcol.processor.batch "default" {
output { traces = [otelcol.exporter.otlp.tempo.input] }
}
otelcol.exporter.otlp "tempo" {
client { endpoint = "tempo-distributor.monitoring.svc.cluster.local:4317" }
}
| Property | Consequence |
|---|---|
| Declarative graph | Block order in the file does not matter; references define the flow |
| Reactive | When an export changes (a file read by local.file, a discovered target list), only dependents re-evaluate — no restart |
| Fan-out by listing | traces = [a.input, b.input] sends a copy to both |
| Reference checked at load | A typo in a reference stops the process at startup (see First pipeline exercise) |
| One file or a directory | A directory is merged into one namespace — labels must be unique across files |
The name “River” is retired; the language is the Alloy configuration syntax. Grafana Agent reached end of life on 2025-11-01.
Data types do not mix
Alloy carries the Prometheus, Loki and Pyroscope native pipelines alongside the OTel one. They use different types and different wiring:
| Family | Type passed | Wiring |
|---|---|---|
prometheus.* |
MetricsReceiver |
forward_to = [prometheus.remote_write.x.receiver] |
loki.* |
LogsReceiver |
forward_to = [loki.write.x.receiver] |
pyroscope.* |
ProfilesReceiver |
forward_to = [pyroscope.write.x.receiver] |
otelcol.* |
otelcol.Consumer |
output { traces/metrics/logs = [x.input] } |
discovery.* |
Targets |
targets = discovery.kubernetes.pods.targets |
Converters are the only bridge between families. Despite the names, they send nothing over the network:
| Component | Direction |
|---|---|
otelcol.exporter.prometheus |
OTLP metrics → Prometheus pipeline |
otelcol.exporter.loki |
OTLP logs → Loki pipeline |
otelcol.receiver.prometheus |
Prometheus pipeline → OTLP |
otelcol.receiver.loki |
Loki pipeline → OTLP |
Our gateway does exactly this for OTel Demo metrics: otelcol.receiver.otlp → otelcol.processor.batch "metrics" → otelcol.exporter.prometheus → prometheus.remote_write.
Two relabel components look alike and work at different stages:
| Component | Works on | Sees __meta_* |
|---|---|---|
discovery.relabel |
Targets, before the scrape — who and how to scrape | Yes |
prometheus.relabel |
Samples, after the scrape — what to keep | No; sees __name__ |
Processor order
Order changes both correctness and cost.
| Position | Processor | Why here |
|---|---|---|
| 1 | memory_limiter |
Must refuse data before anything allocates for it. Anywhere else it drops data that was already processed |
| 2 | filter / sampling |
Drop early; every later step processes less |
| 3 | k8sattributes, resource, transform |
Enrich what survived |
| last | batch |
Batches the final shape; filter after batch produces uneven, smaller batches |
Neither of our Alloy releases runs a memory_limiter today (lesson 06 covers what that costs).
Enrichment traps
-
otelcol.exporter.lokidoes not turn attributes into labels on its own. It reads the hint attributesloki.resource.labelsandloki.attribute.labels. Without them, logs arrive in Loki with no useful labels. Our gateway sets them explicitly:otelcol.processor.attributes "otel_logs" { action { key = "loki.resource.labels" action = "insert" value = "service.name, service.namespace, host.name" } // ... }Loki 3.x also has a native OTLP endpoint (
/otlp), which maps a fixed set of resource attributes to labels and the rest to structured metadata. It removes the need for hints. k8sattributesassociates by connection IP by default. On a gateway fed by another collector, the connection comes from the collector pod, so every record gets the collector’s namespace. Enrich where the source is visible (the node agent), or associate by an attribute the agent already set (k8s.pod.uid).- File-tailed logs have no pod IP. Associate by
k8s.pod.uidtaken from the file path. - A default routing branch must check for the missing attribute. A
transformthat sets a valuewhere attributes["x"] == nilis a default; without the condition it overwrites.
💪Exercise💪 (self-guided): First pipeline — build OTLP → debug → Prometheus in your own Alloy release, then break a reference and watch it refuse to start.