Monitoring intermediate ⏱ 45 minutes

Grafana Alloy: The New Unified Collector (Promtail/Agent Migration)

Migrate from Promtail and Grafana Agent to Alloy. Unified telemetry collection for 2025 and beyond.

By homelabbing.io • 2025-12-16
#grafana#alloy#promtail#opentelemetry#migration

Introduction

The observability landscape is evolving rapidly, and Grafana Labs has made a significant strategic shift. In 2024, they announced that Promtail (their dedicated log shipper) is entering maintenance mode, with all future development focused on Grafana Alloy—a unified, vendor-neutral observability collector built on OpenTelemetry standards.

If you’re running Promtail or Grafana Agent today, this guide will help you understand why migration matters, how Alloy differs, and provide a comprehensive deployment strategy for homelabs.

What you’ll learn:

  • Why Grafana deprecated Promtail in favor of Alloy
  • Understanding Alloy’s architecture and OpenTelemetry integration
  • Deploying Alloy with Docker for logs, metrics, and traces
  • Migrating from Promtail and Grafana Agent configurations
  • Advanced configurations for Docker, Kubernetes, and system logs
  • Troubleshooting common issues and performance optimization

Prerequisites:

  • Docker and Docker Compose installed
  • Existing Loki or Grafana stack (or willingness to deploy)
  • Basic understanding of logging and metrics collection
  • 512MB+ RAM available for Alloy collector

Why Grafana Alloy? The Promtail Sunset Explained

The Problem with Multiple Collectors

Historically, Grafana’s observability ecosystem required multiple agents:

  • Promtail: Log collection for Loki
  • Grafana Agent: Metrics, logs, and traces (static mode)
  • Grafana Agent Flow: Dynamic configuration with pipelines
  • OpenTelemetry Collector: Industry-standard telemetry collection

This fragmentation created confusion, duplicate efforts, and configuration complexity.

Enter Grafana Alloy: One Collector to Rule Them All

Grafana Alloy consolidates all collection capabilities into a single binary:

FeaturePromtailGrafana AgentAlloy
Log CollectionYesYesYes
Metrics CollectionNoYesYes
Trace CollectionNoYesYes
OpenTelemetry NativeNoPartialYes
Dynamic ConfigurationNoFlow mode onlyYes
Configuration LanguageYAMLYAML + RiverRiver
Active DevelopmentMaintenanceMaintenanceActive
Future SupportLimitedLimitedFull

Key differentiator: Alloy is built on OpenTelemetry standards, making it vendor-neutral and future-proof.

When Should You Migrate?

Migrate immediately if:

  • You need OpenTelemetry support for modern application instrumentation
  • You want unified configuration for logs, metrics, and traces
  • You’re starting a new observability deployment

Migrate within 6-12 months if:

  • You’re running Promtail in production (maintenance mode = security patches only)
  • You rely on Grafana Agent static mode (deprecated)
  • You want to simplify your agent footprint

You can wait if:

  • You’re running Grafana Agent Flow (Alloy is rebranded Flow with enhancements)
  • Your Promtail setup works and you don’t need new features

Understanding Alloy Architecture

Core Components

Alloy uses a component-based pipeline architecture:

Sources        Processors        Exporters
────────       ────────────      ─────────
Files     →    Filters      →    Loki
Docker    →    Transforms   →    Prometheus
Journald  →    Enrichment   →    Tempo
OTel      →    Sampling     →    OTLP endpoints

Configuration flow example:

1. local.file_match discovers log files
2. loki.source.file tails discovered files
3. loki.process transforms log lines
4. loki.write sends logs to Loki endpoint

River Configuration Language

Alloy uses River, a declarative configuration language designed for observability pipelines.

River syntax basics:

// Component syntax: type "label" { config }
loki.source.file "local_logs" {
  targets = [
    {
      __path__ = "/var/log/*.log",
      job = "system",
    },
  ]
  forward_to = [loki.write.default.receiver]
}

loki.write "default" {
  endpoint {
    url = "http://loki:3100/loki/api/v1/push"
  }
}

Key differences from YAML:

  • Block-based syntax (similar to HCL/Terraform)
  • First-class support for expressions and functions
  • Built-in validation and type checking
  • Hot-reloading without restarts

Part 1: Basic Deployment with Docker

Quick Start: Alloy + Loki Stack

We’ll deploy a complete logging stack with Alloy and Loki.

Create project directory:

mkdir -p ~/alloy-stack
cd ~/alloy-stack

Create docker-compose.yml:

version: '3.8'

services:
  # Grafana Loki - Log aggregation database
  loki:
    image: grafana/loki:latest
    container_name: loki
    ports:
      - "3100:3100"
    command: -config.file=/etc/loki/local-config.yaml
    volumes:
      - loki-data:/loki
    networks:
      - observability
    restart: unless-stopped

  # Grafana Alloy - Unified observability collector
  alloy:
    image: grafana/alloy:latest
    container_name: alloy
    ports:
      - "12345:12345"  # UI and API
      - "4317:4317"    # OTLP gRPC
      - "4318:4318"    # OTLP HTTP
    volumes:
      - ./alloy-config.river:/etc/alloy/config.river
      - /var/log:/var/log:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
    command:
      - run
      - /etc/alloy/config.river
      - --server.http.listen-addr=0.0.0.0:12345
      - --storage.path=/var/lib/alloy/data
    depends_on:
      - loki
    networks:
      - observability
    restart: unless-stopped
    environment:
      - HOSTNAME=alloy-collector

  # Grafana - Visualization and dashboards
  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
      - GF_USERS_ALLOW_SIGN_UP=false
    volumes:
      - grafana-data:/var/lib/grafana
      - ./grafana-datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml
    depends_on:
      - loki
    networks:
      - observability
    restart: unless-stopped

volumes:
  loki-data:
  grafana-data:

networks:
  observability:
    driver: bridge

Create alloy-config.river:

// Discover all Docker containers
discovery.docker "containers" {
  host = "unix:///var/run/docker.sock"
}

// Tail Docker container logs
loki.source.docker "containers" {
  host       = "unix:///var/run/docker.sock"
  targets    = discovery.docker.containers.targets
  forward_to = [loki.process.add_labels.receiver]
}

// Process and enrich logs
loki.process "add_labels" {
  forward_to = [loki.write.loki_endpoint.receiver]

  // Add hostname label
  stage.static_labels {
    values = {
      hostname = env("HOSTNAME"),
    }
  }

  // Extract log level from common formats
  stage.regex {
    expression = "(?i)level=(?P<level>\w+)"
  }

  stage.labels {
    values = {
      level = "",
    }
  }
}

// Send logs to Loki
loki.write "loki_endpoint" {
  endpoint {
    url = "http://loki:3100/loki/api/v1/push"

    // Optional: Add authentication
    // basic_auth {
    //   username = "loki"
    //   password = "secret"
    // }
  }
}

// System logs from journald (Linux only)
loki.source.journal "system" {
  forward_to    = [loki.write.loki_endpoint.receiver]
  relabel_rules = loki.relabel.journal.rules
  labels        = {
    job = "systemd-journal",
  }
}

loki.relabel "journal" {
  forward_to = []

  rule {
    source_labels = ["__journal__systemd_unit"]
    target_label  = "unit"
  }

  rule {
    source_labels = ["__journal__hostname"]
    target_label  = "hostname"
  }
}

Create grafana-datasources.yml:

apiVersion: 1

datasources:
  - name: Loki
    type: loki
    access: proxy
    url: http://loki:3100
    isDefault: true
    jsonData:
      maxLines: 1000

Start the stack:

docker compose up -d

Verify deployment:

# Check all containers are running
docker compose ps

# View Alloy logs
docker logs alloy

# Check Alloy UI
curl http://localhost:12345/

Access services:

  • Grafana: http://localhost:3000 (admin/admin)
  • Loki: http://localhost:3100/ready
  • Alloy UI: http://localhost:12345

Part 2: Migrating from Promtail

Configuration Comparison

Promtail YAML configuration:

server:
  http_listen_port: 9080

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: system
    static_configs:
      - targets:
          - localhost
        labels:
          job: varlogs
          __path__: /var/log/*.log
    pipeline_stages:
      - regex:
          expression: '^(?P<timestamp>S+) (?P<level>S+) (?P<message>.*)$'
      - labels:
          level:

Equivalent Alloy River configuration:

// File discovery with glob patterns
local.file_match "system_logs" {
  path_targets = [{
    __path__ = "/var/log/*.log",
    job      = "varlogs",
  }]
}

// Tail discovered files
loki.source.file "system" {
  targets    = local.file_match.system_logs.targets
  forward_to = [loki.process.parse_logs.receiver]
}

// Pipeline processing
loki.process "parse_logs" {
  forward_to = [loki.write.loki.receiver]

  // Extract fields with regex
  stage.regex {
    expression = "^(?P<timestamp>\S+) (?P<level>\S+) (?P<message>.*)$"
  }

  // Promote level to label
  stage.labels {
    values = {
      level = "",
    }
  }
}

// Write to Loki
loki.write "loki" {
  endpoint {
    url = "http://loki:3100/loki/api/v1/push"
  }
}

Automated Migration Tool

Grafana provides a migration tool for converting Promtail configs:

# Install Alloy
docker pull grafana/alloy:latest

# Convert Promtail config
docker run --rm -v $(pwd):/configs grafana/alloy:latest   convert --source-format=promtail   --output=/configs/alloy-config.river   /configs/promtail-config.yml

# Review generated config
cat alloy-config.river

Migration checklist:

  • Convert configuration using migration tool
  • Test Alloy in parallel with Promtail (different labels)
  • Verify logs appear in Loki with new labels
  • Validate log volumes match (check Loki metrics)
  • Monitor Alloy resource usage
  • Decommission Promtail after 7-day parallel run

Part 3: Advanced Configurations

Docker Container Logs with Custom Parsing

Collect logs from specific containers with advanced parsing:

// Discover only specific containers
discovery.docker "app_containers" {
  host = "unix:///var/run/docker.sock"

  filter {
    name   = "label"
    values = ["logging=alloy"]
  }
}

// Collect Docker logs
loki.source.docker "apps" {
  host       = "unix:///var/run/docker.sock"
  targets    = discovery.docker.app_containers.targets
  forward_to = [loki.process.docker_logs.receiver]
}

// Advanced log processing pipeline
loki.process "docker_logs" {
  forward_to = [loki.write.loki.receiver]

  // Drop noisy health check logs
  stage.match {
    selector = "{container_name="nginx"}"

    stage.drop {
      expression = ".*GET /health.*"
    }
  }

  // Parse JSON logs
  stage.match {
    selector = "{container_name="api"}"

    stage.json {
      expressions = {
        level   = "level",
        message = "msg",
        trace_id = "trace_id",
      }
    }

    stage.labels {
      values = {
        level = "",
      }
    }
  }

  // Extract and redact sensitive data
  stage.replace {
    expression = "password=\S+"
    replace    = "password=REDACTED"
  }

  // Add consistent timestamp
  stage.timestamp {
    source = "timestamp"
    format = "RFC3339"
  }
}

Label containers for collection:

# docker-compose.yml
services:
  my-app:
    image: my-app:latest
    labels:
      - "logging=alloy"

File-Based Log Collection with Multiline Support

Handle multiline logs (stack traces, JSON formatting):

local.file_match "app_logs" {
  path_targets = [{
    __path__ = "/var/log/app/*.log",
    app      = "backend",
  }]
}

loki.source.file "applications" {
  targets    = local.file_match.app_logs.targets
  forward_to = [loki.process.multiline.receiver]
}

loki.process "multiline" {
  forward_to = [loki.write.loki.receiver]

  // Combine Java stack traces into single log entry
  stage.multiline {
    firstline     = "^\d{4}-\d{2}-\d{2}"  // Lines starting with date
    max_wait_time = "3s"
  }

  // Parse structured logs
  stage.regex {
    expression = "^(?P<timestamp>\S+\s+\S+)\s+\[(?P<thread>\S+)\]\s+(?P<level>\w+)\s+(?P<logger>\S+)\s+-\s+(?P<message>[\s\S]+)"
  }

  // Promote important fields to labels
  stage.labels {
    values = {
      level  = "",
      thread = "",
    }
  }

  // Parse timestamp
  stage.timestamp {
    source = "timestamp"
    format = "2006-01-02 15:04:05.000"
  }
}

OpenTelemetry Integration

Receive logs via OpenTelemetry protocol:

// OTLP receiver for logs
otelcol.receiver.otlp "default" {
  grpc {
    endpoint = "0.0.0.0:4317"
  }

  http {
    endpoint = "0.0.0.0:4318"
  }

  output {
    logs    = [otelcol.exporter.loki.default.input]
    metrics = [otelcol.exporter.prometheus.default.input]
    traces  = [otelcol.exporter.otlp.tempo.input]
  }
}

// Export OTLP logs to Loki
otelcol.exporter.loki "default" {
  forward_to = [loki.write.loki.receiver]
}

// Export metrics to Prometheus
otelcol.exporter.prometheus "default" {
  forward_to = [prometheus.remote_write.mimir.receiver]
}

// Export traces to Tempo
otelcol.exporter.otlp "tempo" {
  client {
    endpoint = "tempo:4317"

    tls {
      insecure = true
    }
  }
}

// Prometheus remote write
prometheus.remote_write "mimir" {
  endpoint {
    url = "http://mimir:9009/api/v1/push"
  }
}

// Loki write endpoint
loki.write "loki" {
  endpoint {
    url = "http://loki:3100/loki/api/v1/push"
  }
}

Application instrumentation example (Python):

from opentelemetry import trace, metrics
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

# Configure OTLP exporter
trace.set_tracer_provider(TracerProvider())
otlp_exporter = OTLPSpanExporter(endpoint="http://alloy:4317", insecure=True)
trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(otlp_exporter))

tracer = trace.get_tracer(__name__)

# Use in application
with tracer.start_as_current_span("process_request"):
    # Application logic
    print("Processing request with automatic trace correlation")

Kubernetes DaemonSet Deployment

Deploy Alloy on every Kubernetes node for cluster-wide log collection:

apiVersion: v1
kind: ConfigMap
metadata:
  name: alloy-config
  namespace: monitoring
data:
  config.river: |
    // Discover all pods
    discovery.kubernetes "pods" {
      role = "pod"
    }

    // Relabel pod metadata
    discovery.relabel "pods" {
      targets = discovery.kubernetes.pods.targets

      rule {
        source_labels = ["__meta_kubernetes_namespace"]
        target_label  = "namespace"
      }

      rule {
        source_labels = ["__meta_kubernetes_pod_name"]
        target_label  = "pod"
      }

      rule {
        source_labels = ["__meta_kubernetes_pod_container_name"]
        target_label  = "container"
      }
    }

    // Tail pod logs
    loki.source.kubernetes "pods" {
      targets    = discovery.relabel.pods.output
      forward_to = [loki.write.loki.receiver]
    }

    loki.write "loki" {
      endpoint {
        url = "http://loki.monitoring.svc:3100/loki/api/v1/push"
      }
    }
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: alloy
  namespace: monitoring
spec:
  selector:
    matchLabels:
      name: alloy
  template:
    metadata:
      labels:
        name: alloy
    spec:
      serviceAccountName: alloy
      containers:
      - name: alloy
        image: grafana/alloy:latest
        args:
          - run
          - /etc/alloy/config.river
          - --server.http.listen-addr=0.0.0.0:12345
          - --storage.path=/var/lib/alloy/data
        ports:
          - containerPort: 12345
            name: http
        volumeMounts:
          - name: config
            mountPath: /etc/alloy
          - name: varlog
            mountPath: /var/log
            readOnly: true
          - name: varlibdockercontainers
            mountPath: /var/lib/docker/containers
            readOnly: true
        resources:
          requests:
            memory: "256Mi"
            cpu: "200m"
          limits:
            memory: "512Mi"
            cpu: "500m"
      volumes:
        - name: config
          configMap:
            name: alloy-config
        - name: varlog
          hostPath:
            path: /var/log
        - name: varlibdockercontainers
          hostPath:
            path: /var/lib/docker/containers
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: alloy
  namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: alloy
rules:
  - apiGroups: [""]
    resources:
      - nodes
      - nodes/proxy
      - services
      - endpoints
      - pods
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: alloy
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: alloy
subjects:
  - kind: ServiceAccount
    name: alloy
    namespace: monitoring

Deploy to Kubernetes:

kubectl apply -f alloy-daemonset.yaml
kubectl -n monitoring get pods -l name=alloy
kubectl -n monitoring logs -l name=alloy -f

Part 4: Metrics and Traces (Beyond Logs)

Prometheus Metrics Collection

Alloy can replace Prometheus scraping:

// Scrape Prometheus metrics from discovered targets
prometheus.scrape "services" {
  targets = [
    {"__address__" = "localhost:9090", "job" = "prometheus"},
    {"__address__" = "localhost:3000", "job" = "grafana"},
    {"__address__" = "cadvisor:8080", "job" = "cadvisor"},
  ]
  forward_to = [prometheus.remote_write.mimir.receiver]
}

// Scrape Kubernetes service monitors
prometheus.operator.servicemonitors "k8s" {
  forward_to = [prometheus.remote_write.mimir.receiver]
}

// Remote write to Prometheus/Mimir
prometheus.remote_write "mimir" {
  endpoint {
    url = "http://mimir:9009/api/v1/push"

    queue_config {
      capacity             = 10000
      max_shards           = 10
      max_samples_per_send = 2000
    }
  }
}

Distributed Tracing with Tempo

Collect and forward traces:

// Receive traces via OTLP
otelcol.receiver.otlp "traces" {
  grpc {
    endpoint = "0.0.0.0:4317"
  }

  output {
    traces = [otelcol.processor.batch.traces.input]
  }
}

// Batch processing for efficiency
otelcol.processor.batch "traces" {
  output {
    traces = [otelcol.exporter.otlp.tempo.input]
  }
}

// Send to Tempo
otelcol.exporter.otlp "tempo" {
  client {
    endpoint = "tempo:4317"
    tls {
      insecure = true
    }
  }
}

Full LGTM Stack Configuration

Logs, Grafana, Tempo, Mimir in one Alloy config:

/********************************************
 * LOGS - Loki
 ********************************************/
loki.source.docker "containers" {
  host       = "unix:///var/run/docker.sock"
  targets    = discovery.docker.all.targets
  forward_to = [loki.write.loki.receiver]
}

loki.write "loki" {
  endpoint {
    url = "http://loki:3100/loki/api/v1/push"
  }
}

/********************************************
 * METRICS - Prometheus/Mimir
 ********************************************/
prometheus.scrape "default" {
  targets = [
    {"__address__" = "localhost:9090", "job" = "prometheus"},
  ]
  forward_to = [prometheus.remote_write.mimir.receiver]
}

prometheus.remote_write "mimir" {
  endpoint {
    url = "http://mimir:9009/api/v1/push"
  }
}

/********************************************
 * TRACES - Tempo
 ********************************************/
otelcol.receiver.otlp "default" {
  grpc {
    endpoint = "0.0.0.0:4317"
  }

  output {
    traces = [otelcol.exporter.otlp.tempo.input]
  }
}

otelcol.exporter.otlp "tempo" {
  client {
    endpoint = "tempo:4317"
    tls {
      insecure = true
    }
  }
}

/********************************************
 * DISCOVERY
 ********************************************/
discovery.docker "all" {
  host = "unix:///var/run/docker.sock"
}

Part 5: Troubleshooting and Optimization

Common Issues and Solutions

Issue 1: Logs not appearing in Loki

Diagnosis:

# Check Alloy is running
docker logs alloy

# Verify Loki endpoint is reachable from Alloy
docker exec alloy wget -O- http://loki:3100/ready

# Check Alloy UI for component status
curl http://localhost:12345/graph

Solutions:

  • Verify loki.write URL is correct
  • Check Docker network connectivity
  • Review Alloy logs for authentication errors
  • Ensure Loki is accepting pushes (check Loki logs)

Issue 2: High memory usage

Symptoms: Alloy container using 1GB+ RAM

Solutions:

// Add memory limits to pipeline
loki.source.file "logs" {
  targets    = local.file_match.all.targets
  forward_to = [loki.write.loki.receiver]

  // Limit in-memory buffer
  sync_period = "10s"
}

// Batch writes for efficiency
loki.write "loki" {
  endpoint {
    url = "http://loki:3100/loki/api/v1/push"

    // Reduce batch size
    batch_size  = 512  // Default: 1048576
    batch_wait  = "1s"
  }
}

Issue 3: Configuration syntax errors

Diagnosis:

# Validate configuration
docker run --rm -v $(pwd):/configs grafana/alloy:latest   fmt /configs/alloy-config.river

# Check for errors
docker logs alloy 2>&1 | grep -i error

Solutions:

  • Use alloy fmt to auto-format config
  • Check component names are unique
  • Verify forward_to references exist
  • Use Alloy UI to visualize component graph

Performance Optimization

Reduce CPU usage:

// Increase batch intervals
loki.write "loki" {
  endpoint {
    url = "http://loki:3100/loki/api/v1/push"

    batch_wait = "5s"  // Wait longer before sending
  }
}

// Sample high-volume logs
loki.source.file "noisy_app" {
  targets    = local.file_match.app.targets
  forward_to = [loki.process.sample.receiver]
}

loki.process "sample" {
  forward_to = [loki.write.loki.receiver]

  // Keep only 10% of logs
  stage.sampling {
    rate = 0.1
  }
}

Reduce network traffic:

loki.write "loki" {
  endpoint {
    url = "http://loki:3100/loki/api/v1/push"

    // Enable compression
    compression = "snappy"
  }
}

Monitoring Alloy Itself

Alloy exposes metrics about its own performance:

// Scrape Alloy's own metrics
prometheus.scrape "alloy" {
  targets = [
    {"__address__" = "localhost:12345", "job" = "alloy"},
  ]
  forward_to = [prometheus.remote_write.mimir.receiver]
}

Key metrics to monitor:

  • alloy_component_evaluations_total - Component execution count
  • alloy_loki_write_sent_bytes_total - Data sent to Loki
  • alloy_loki_write_dropped_entries_total - Lost log entries (should be 0)
  • go_memstats_alloc_bytes - Memory usage

Create Grafana dashboard:

  1. Import dashboard ID: 19419 (Official Alloy Monitoring)
  2. Select Prometheus data source
  3. View component health and performance metrics

Part 6: Migration Strategies

Phased Migration Approach

Phase 1: Parallel Deployment (Week 1-2)

// Run Alloy alongside Promtail with different labels
loki.source.docker "alloy_test" {
  host       = "unix:///var/run/docker.sock"
  targets    = discovery.docker.all.targets
  forward_to = [loki.process.add_source.receiver]
}

loki.process "add_source" {
  forward_to = [loki.write.loki.receiver]

  stage.static_labels {
    values = {
      collector = "alloy",  // Distinguish from Promtail
    }
  }
}

Validation queries in Grafana:

// Compare log volumes
sum by (collector) (count_over_time({job="containers"}[1h]))

// Verify same containers are logged
count by (container_name, collector) (count_over_time({job="containers"}[1h]))

Phase 2: Gradual Cutover (Week 3)

  • Increase Alloy confidence: Monitor for 7 days
  • Validate log volumes match within 5%
  • Check for any missing labels or parsing issues

Phase 3: Decommission Promtail (Week 4)

# Stop Promtail
docker stop promtail

# Monitor for 48 hours
# If no issues, remove completely
docker rm promtail

Rollback Plan

If migration fails:

# Quick rollback to Promtail
docker start promtail

# Or redeploy from backup
docker compose -f promtail-compose.yml up -d

Keep Promtail config backed up:

cp promtail-config.yml promtail-config.yml.backup
tar czf promtail-backup-$(date +%F).tar.gz promtail-config.yml docker-compose.yml

Conclusion

Grafana Alloy represents the future of observability collection in the Grafana ecosystem. While the transition from Promtail requires initial effort, Alloy’s unified approach, OpenTelemetry compatibility, and active development make it a strategic investment.

Key takeaways:

  • Alloy consolidates logs, metrics, and traces into one collector
  • River configuration language is more powerful than YAML
  • OpenTelemetry support future-proofs your observability stack
  • Migration tools simplify the transition from Promtail
  • Phased migration reduces risk and validates correctness

Your migration timeline:

This week:

  1. Deploy Alloy in parallel with existing Promtail
  2. Convert Promtail config using migration tool
  3. Validate logs appear in Loki

This month: 4. Test advanced features (Docker discovery, multiline, OTLP) 5. Monitor Alloy performance and resource usage 6. Prepare rollback plan

This quarter: 7. Migrate all log collection to Alloy 8. Add metrics and traces collection 9. Decommission Promtail 10. Optimize configuration for your workload

Resources:

The observability landscape is evolving toward OpenTelemetry as the universal standard. By adopting Alloy now, you’re positioning your homelab infrastructure for long-term success with vendor-neutral, standards-based telemetry collection.

Happy collecting!


Guide Version: 1.0 Last Updated: December 2024 Alloy Version: 1.0+ Difficulty Level: Intermediate Estimated Time: 45-90 minutes (deployment + migration)