// Alloy — Docker container logs + journald → Loki. River syntax. // Per-LXC config; only the `host:` label changes via alloy_host_label var. // ============================================================ // Docker — discover + scrape container logs // ============================================================ discovery.docker "containers" { host = "unix:///var/run/docker.sock" refresh_interval = "15s" } 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_log_stream"] target_label = "stream" } rule { source_labels = ["__meta_docker_container_label_com_docker_compose_service"] target_label = "service" } rule { source_labels = ["__meta_docker_container_label_com_docker_compose_project"] target_label = "compose_project" } rule { target_label = "job" replacement = "docker" } rule { target_label = "host" replacement = "{{ alloy_host_label }}" } } loki.source.docker "containers" { host = "unix:///var/run/docker.sock" targets = discovery.relabel.containers.output forward_to = [loki.process.docker.receiver] } loki.process "docker" { // Replay protection on first boot — Loki rejects > 7d. stage.drop { older_than = "24h" drop_counter_reason = "older_than_24h" } // pulse-agent broken-pipe noise on hosts running pulse-agent --enable-host. stage.drop { expression = ".*broken pipe.*" drop_counter_reason = "broken_pipe_noise" } forward_to = [loki.write.default.receiver] } // ============================================================ // Journald — LXC systemd units // ============================================================ loki.source.journal "host" { path = "/var/log/journal" max_age = "1m" forward_to = [loki.process.journal.receiver] relabel_rules = loki.relabel.journal.rules labels = { job = "journald", host = "{{ alloy_host_label }}", } } loki.relabel "journal" { forward_to = [] rule { source_labels = ["__journal__systemd_unit"] target_label = "unit" } rule { source_labels = ["__journal__hostname"] target_label = "instance" } rule { source_labels = ["__journal_priority_keyword"] target_label = "severity" } } loki.process "journal" { forward_to = [loki.write.default.receiver] } // ============================================================ // Loki — push // ============================================================ loki.write "default" { endpoint { url = "{{ alloy_loki_url }}" } } // ============================================================ // Prometheus — embedded node_exporter (replaces standalone) // ============================================================ prometheus.exporter.unix "node" { // Container view of host filesystems — bind-mounted in compose. rootfs_path = "/host/rootfs" procfs_path = "/host/proc" sysfs_path = "/host/sys" // Default collectors match upstream node_exporter set. } prometheus.scrape "node" { targets = prometheus.exporter.unix.node.targets forward_to = [prometheus.relabel.node.receiver] scrape_interval = "15s" job_name = "{{ alloy_prom_job | default('node_lxc') }}" } // Rewrite the instance + job labels to match the historical // node_exporter scrape, so existing dashboards continue to slice by // the same instance string. Alloy's exporter component injects // job="integrations/unix" on its targets — we have to overwrite it // here; `job_name` in prometheus.scrape only names the scrape pool, // it doesn't relabel the metrics. prometheus.relabel "node" { forward_to = [prometheus.remote_write.observe.receiver] rule { target_label = "instance" replacement = "{{ alloy_prom_instance | default(ansible_default_ipv4.address + ':9100') }}" } rule { target_label = "job" replacement = "{{ alloy_prom_job | default('node_lxc') }}" } // Match the static labels Prom previously injected at scrape time // (see homelab-ansible-vm-observability/roles/configs/files/prometheus.yml). rule { target_label = "group" replacement = "{{ alloy_prom_group | default('lxc') }}" } rule { target_label = "hostname" replacement = "{{ alloy_prom_hostname | default(alloy_host_label) }}" } } prometheus.remote_write "observe" { endpoint { url = "{{ alloy_prom_remote_write | default('http://observe.lan.balders.ca:9090/api/v1/write') }}" } }