Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
cb7615b
feat: introduce alloy role to replace promtail in control-plane
ma-hartma May 4, 2026
19537b4
feat: configure control-plane alloy similar to promtail
ma-hartma May 5, 2026
d9a422b
docs: improve migration recommendations
ma-hartma May 8, 2026
a4f3ea7
fix: add app label
ma-hartma May 8, 2026
17344c4
fix: use correct defaults for prometheus write endpoints
ma-hartma May 11, 2026
1d12419
fix: avoid default cluster=loki label
ma-hartma May 11, 2026
683380a
feat: support instance and component labels
ma-hartma May 11, 2026
f6659b6
feat: conditional or parallel deployment of alloy or promtail with guide
ma-hartma May 11, 2026
b81023a
feat: provide tasks to cleanup promtail
ma-hartma May 19, 2026
d55af1a
chore: add flag for event exporter deployment
ma-hartma May 20, 2026
40d4a2d
chore: backwards compatibility
ma-hartma May 20, 2026
39d0f4b
fix: cutover docs
ma-hartma May 20, 2026
6b3fe91
docs: credential rename
ma-hartma Jun 2, 2026
634811d
chore: ansible lint
ma-hartma Jun 2, 2026
be794f1
fix: remove loki and prometheus write endpoint defaults
ma-hartma Jun 3, 2026
f4b4a80
feat: flag for alloy helm chart service monitors
ma-hartma Jun 8, 2026
992295d
chore: undo gardener-logging changes
ma-hartma Jun 8, 2026
cbbc26d
docs: finalize
ma-hartma Jun 10, 2026
0486a8f
docs: mention service monitor dependency
ma-hartma Jun 10, 2026
29c7530
fix: add metrics relabelings for service monitors
ma-hartma Jun 10, 2026
5000fd9
fix: empty defaults
ma-hartma Jun 10, 2026
e4b14b5
fix: alloy values service monitor nesting
ma-hartma Jun 10, 2026
fde8a2d
review(alloy): deploy alloy and remove promtail and event-exporter pe…
ma-hartma Jun 12, 2026
198a489
review(alloy): remove unneeded port var
ma-hartma Jun 12, 2026
1122109
review(alloy): remove unneeded port var completely
ma-hartma Jun 12, 2026
99ac123
review(alloy): file based pod logs instead of API calls
ma-hartma Jun 12, 2026
4903960
chore: move shared components to logging-common
ma-hartma Jun 15, 2026
fff0405
docs: defaults
ma-hartma Jun 15, 2026
0b95153
chore: rename alloy job monitoring/event-exporter to events
ma-hartma Jun 15, 2026
a9c6e3b
Merge branch 'master' into promtail-alloy-migration-control-plane
ma-hartma Jun 15, 2026
10e2b73
feat: add alerts for alloy (#623)
ma-hartma Jun 15, 2026
c75e9aa
fix: pod discovery filters
ma-hartma Jun 16, 2026
00dd443
chore: ansible role deps and setup_yaml
ma-hartma Jun 16, 2026
78a0d56
chore: rename logging_ to logging_common_ for logging-common vars
ma-hartma Jun 16, 2026
8569a06
Merge branch 'master' into promtail-alloy-migration-control-plane
Gerrit91 Jun 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions common/roles/defaults/defaults/main.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
metal_registry_auth_enabled: false

Check failure on line 2 in common/roles/defaults/defaults/main.yaml

View workflow job for this annotation

GitHub Actions / ansible-lint

var-naming[no-role-prefix]

Variables names from within roles should use defaults_ as a prefix. (vars: metal_registry_auth_enabled)
metal_registry_auth_user: ""

Check failure on line 3 in common/roles/defaults/defaults/main.yaml

View workflow job for this annotation

GitHub Actions / ansible-lint

var-naming[no-role-prefix]

Variables names from within roles should use defaults_ as a prefix. (vars: metal_registry_auth_user)
metal_registry_auth_password: ""

Check failure on line 4 in common/roles/defaults/defaults/main.yaml

View workflow job for this annotation

GitHub Actions / ansible-lint

var-naming[no-role-prefix]

Variables names from within roles should use defaults_ as a prefix. (vars: metal_registry_auth_password)
metal_registry_url: "https://index.docker.io/v1/"

Check failure on line 5 in common/roles/defaults/defaults/main.yaml

View workflow job for this annotation

GitHub Actions / ansible-lint

var-naming[no-role-prefix]

Variables names from within roles should use defaults_ as a prefix. (vars: metal_registry_url)

metal_stack_release:

Check failure on line 7 in common/roles/defaults/defaults/main.yaml

View workflow job for this annotation

GitHub Actions / ansible-lint

var-naming[no-role-prefix]

Variables names from within roles should use defaults_ as a prefix. (vars: metal_stack_release)
mapping:
# binaries
metal_hammer_image_tag: "binaries.metal-stack.metal-hammer.version"
Expand Down Expand Up @@ -165,6 +165,8 @@
metal_helm_chart_tag: "helm-charts.metal-stack.metal-control-plane.tag"
logging_chart_version: "helm-charts.logging.loki.version"
logging_chart_repo: "helm-charts.logging.loki.repository"
logging_alloy_chart_version: "helm-charts.logging.alloy.version"
logging_alloy_chart_repo: "helm-charts.logging.alloy.repository"
logging_promtail_chart_version: "helm-charts.logging.promtail.version"
logging_promtail_chart_repo: "helm-charts.logging.promtail.repository"
gardener_logging_promtail_chart_version: "helm-charts.logging.promtail.version"
Expand Down
118 changes: 118 additions & 0 deletions control-plane/roles/logging-common/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# logging-common

Provides the shared Alloy DaemonSet deployment used by the [logging](../logging/) and [gardener-logging](../gardener-logging/) roles. Not intended for standalone use — it is included as a dependency by those roles.

## What Alloy does

Alloy runs as a Kubernetes DaemonSet. It:

- **Collects pod logs** from the node filesystem (`/var/log/pods`, `loki.source.file`). Pod discovery is limited to the local node via a `spec.nodeName` field selector — each DaemonSet pod only collects logs for pods scheduled on its own node. The node name is read from `K8S_NODE_NAME`, which the Alloy Helm chart injects automatically via the Kubernetes downward API.
- **Collects Kubernetes events** cluster-wide via `loki.source.kubernetes_events`. Alloy's built-in peer clustering elects a single leader — only that pod watches the events API and ships events to Loki. Without clustering every pod would independently watch the API and produce N duplicate copies in Loki.
- **Forwards everything to Loki** via `logging_common_alloy_loki_write_endpoints`.

Alloy's positions file (tracking the read offset for each log file) is persisted via a `hostPath` volume at `/var/lib/alloy/data`, so already-shipped lines are not re-read after a pod restart. The directory is created automatically on first run (`DirectoryOrCreate`).

## Variables

You can look up all the default values of this role [here](defaults/main.yaml).

| Name | Mandatory | Default | Description |
| ------------------------------------------------------ | --------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| logging_common_alloy_namespace | | `monitoring` | Target namespace |
| logging_common_alloy_chart_version | yes | | Helm chart version for alloy (release vector) |
| logging_common_alloy_chart_repo | yes | | Repository for alloy (release vector) |
| logging_common_alloy_cluster_label | yes | | Value for the `cluster=` label set on all log and metric streams |
| logging_common_alloy_loki_write_endpoints | yes\* | `[]` | List of Loki push endpoints. Required unless `logging_common_alloy_config_raw` is set. Each entry: `{url, remote_timeout?: duration, basic_auth?: {username, password}}` |
| logging_common_alloy_service_monitor_enabled | | `false` | Enable a Prometheus ServiceMonitor for Alloy self-metrics. **_(logging role only)_** — not supported in seed clusters. Requires the [monitoring role](../monitoring/) to have been deployed first. |
| logging_common_alloy_prometheus_write_endpoints | | `[]` | Prometheus remote_write endpoints for Alloy self-metrics. When empty, self-metrics are disabled. Each entry: `{url, remote_timeout?: duration, basic_auth?: {username, password}}` |
| logging_common_alloy_prometheus_wal_truncate_frequency | | `2h` | How often the WAL is compacted |
| logging_common_alloy_prometheus_wal_max_keepalive_time | | `8h` | Maximum time undelivered samples are kept in the WAL before being dropped |
| logging_common_alloy_config_raw | | | Full Alloy River config override. When set, bypasses all structured vars and the template. |
| logging_common_alloy_kubeconfig | | | Kubeconfig dict for the target cluster. Omit for the local cluster; required for remote clusters (e.g. shooted seeds). |

## Labels

### Pod logs (`loki.source.file`)

| Label | Source |
| ----------- | --------------------------------------------------------------------------------------------------------------- |
| `cluster` | `logging_common_alloy_cluster_label` (relabel rule in `discovery.relabel`) |
| `namespace` | `__meta_kubernetes_namespace` |
| `pod` | `__meta_kubernetes_pod_name` |
| `container` | `__meta_kubernetes_pod_container_name` |
| `pod_uid` | `__meta_kubernetes_pod_uid` |
| `node_name` | `__meta_kubernetes_pod_node_name` |
| `app` | `app.kubernetes.io/name` pod label, falling back to `app` label, controller name (hash stripped), then pod name |
| `instance` | `app.kubernetes.io/instance` pod label, falling back to `instance` label (empty if neither is set) |
| `component` | `app.kubernetes.io/component` pod label, falling back to `component` label (empty if neither is set) |
| `job` | `namespace/app` (using the computed `app` value above) |

### Kubernetes events (`loki.source.kubernetes_events`)

| Label | Value |
| ----------- | --------------------------------------------------------------------- |
| `cluster` | `logging_common_alloy_cluster_label` (relabel rule in `loki.relabel`) |
| `job` | `events` |
| `namespace` | Namespace of the event |

Alloy watches events in all namespaces, which requires cluster-scope RBAC. The Alloy Helm chart includes the required `events` rule in its default `rbac.rules`, so no additional configuration is needed.

`loki.source.kubernetes_events` uses Alloy's built-in clustering to elect a single leader across all DaemonSet pods — only that leader actively watches the events API and ships events to Loki. The other pods stand by and take over if the leader is restarted or evicted. Without this, every DaemonSet pod would independently watch the same events API and produce N duplicate copies in Loki (one per node). Clustering is enabled in both the River config (`clustering { enabled = true }` on the events source) and the Helm values (`alloy.clustering.enabled: true`).

## Meta-monitoring

Self-metrics are **disabled by default**. Choose one option when needed:

**Option A — Pull (ServiceMonitor).** Set `logging_alloy_service_monitor_enabled: true` (logging role) or `gardener_logging_alloy_service_monitor_enabled: true` (gardener-logging role). **Available in the logging role only** — Gardener seed Prometheus instances use annotation-based discovery restricted to extension namespaces and do not reach the `monitoring` namespace where Alloy runs.

> **Requires the monitoring role to have been deployed first.** The `ServiceMonitor` CRD is installed by kube-prometheus-stack. If it does not exist yet, Ansible fails with "no kind ServiceMonitor is registered".

**Option B — Push (remote_write).** Set `logging_alloy_prometheus_write_endpoints` (logging role) or `gardener_logging_alloy_prometheus_write_endpoints` (gardener-logging role). Works in both roles.

```yaml
# logging role (same for gardener_logging_alloy_prometheus_write_endpoints in gardener-logging role)
logging_alloy_prometheus_write_endpoints:
- url: "https://{{ monitoring_thanos_receive_ingress_dns }}/api/v1/receive"
remote_timeout: 60s
basic_auth:
username: "{{ monitoring_thanos_receive_ingress_basic_auth_user }}"
password: "{{ monitoring_thanos_receive_ingress_basic_auth_password }}"
```

See the [monitoring role migration guide](../monitoring/README.md#thanos-receive-ingress-credentials) for credential setup.
Do not set both options simultaneously — for the logging role, `logging_alloy_service_monitor_enabled` and `logging_alloy_prometheus_write_endpoints` are mutually exclusive; for the gardener-logging role, `gardener_logging_alloy_prometheus_write_endpoints` is the only option.

### Logs

Alloy runs as a DaemonSet, so its own pod logs are captured by `loki.source.file` automatically.

## Migration from Promtail

Alloy is deployed by default by both the logging and gardener-logging roles. Existing Promtail installations are automatically removed when the roles run — no manual cleanup steps are required.

> **Promtail is deprecated.** Setting `*_promtail_enabled: true` emits a deprecation warning on every run. Promtail support will be removed in a future release.

Alloy's label derivation is identical to Promtail's, so dashboards, alerts, and LogQL queries continue to work without changes. What has changed compared to Promtail:

- **Kubernetes events are now built-in.** Alloy collects events natively via `loki.source.kubernetes_events`, labelled `job="events"`. _(logging role)_ The separate event-exporter Deployment is no longer needed — `event_exporter_enabled` defaults to `false` in the monitoring role and any existing resources are **removed automatically** when the monitoring role runs.
> **Breaking change:** The event `job` label was renamed from `monitoring/event-exporter` to `events`. Update any existing LogQL queries, dashboard filters, and alert rules that reference `{job="monitoring/event-exporter"}` to `{job="events"}`.
- **Metric collection is now explicit.** Alloy self-metrics are disabled by default. Configure push via `logging_alloy_prometheus_write_endpoints` (logging role) or `gardener_logging_alloy_prometheus_write_endpoints` (gardener-logging role), or _(logging role only)_ pull via `logging_alloy_service_monitor_enabled`. See [Meta-monitoring](#meta-monitoring).
- **Metric WAL is new.** Alloy buffers undelivered self-metrics on disk (default: 8h). Promtail had no equivalent.

| Scenario | `*_alloy_enabled` | `*_promtail_enabled` | Notes |
| -------------------------- | ----------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| **Default** | `true` | `false` | Alloy is deployed; any existing Promtail release is removed automatically. |
| **Parallel run** | `true` | `true` | Both collectors ship logs. Loki receives duplicate entries during this window. Deprecated — emits a warning on every run. |
| **Promtail only** (legacy) | `false` | `true` | Deprecated — emits a warning on every run. |

**To migrate an existing Promtail installation:**

From the current release, Alloy is the default. Re-running the role deploys Alloy and removes Promtail automatically. _(logging role: also re-run the [monitoring](../monitoring/) role to remove the event-exporter.)_

1. If you are pushing Alloy self-metrics to Thanos Receive, migrate the credentials first — see the [monitoring role migration guide](../monitoring/README.md#thanos-receive-ingress-credentials).
2. Ensure `*_alloy_loki_write_endpoints` is set in your inventory and you have decided on a push or pull approach for self-metrics (see [Meta-monitoring](#meta-monitoring)).
3. Re-run the role _(and the [monitoring](../monitoring/) role if using the logging role)_. Alloy is deployed, the Promtail Helm release is removed, and _(logging role)_ the event-exporter resources (Deployment, ServiceAccount, ConfigMap, ClusterRole, ClusterRoleBinding) are removed automatically.
4. Verify Alloy is working: logs and Kubernetes events arrive in Loki and existing dashboards, alerts, and LogQL queries return results as expected.
5. **Optional:** Rotate the external Loki ingress credentials. The `loki-basic-auth` Kubernetes Secret is fully managed by Helm and holds a single entry derived from `logging_ingress_loki_basic_auth_user` and `logging_ingress_loki_basic_auth_password`. The default username remains `promtail` for backward compatibility — there is no need to change it. If you do want to rename the user (e.g. to `alloy`), re-running the role with updated variables replaces the secret automatically. If those credentials are also used by partition Promtail or Alloy to authenticate the `remote_write` to Loki, update both in the same deployment to avoid auth failures.

For a controlled parallel window before cutting over, set `*_promtail_enabled: true` _(and `event_exporter_enabled: true` for the logging role)_. Both collectors will ship logs — Loki receives duplicate entries during this window. Once satisfied, remove the overrides from your inventory and re-run to have Promtail and the event-exporter removed automatically.
41 changes: 41 additions & 0 deletions control-plane/roles/logging-common/defaults/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
# Alloy target namespace.
logging_common_alloy_namespace: monitoring

# Whether to create a ServiceMonitor for Alloy self-metrics scraping.
# Only applicable in environments where Prometheus can reach the monitoring namespace.
# Never enable in Gardener seed clusters.
logging_common_alloy_service_monitor_enabled: false

# Loki push endpoints.
logging_common_alloy_loki_write_endpoints: []
# - url: "https://loki.example.com/loki/api/v1/push"
# remote_timeout: 60s
# basic_auth:
# username: alloy
# password: secret

# Value for the cluster= label attached to all log and metric streams via relabel rules.
# Override per consuming role or pass via include_role vars.
logging_common_alloy_cluster_label: ""

# Prometheus remote_write endpoints for Alloy self-metrics (prometheus.exporter.self).
# Alloy scrapes its own /metrics and pushes them via remote_write.
# Each entry: {url, remote_timeout?: duration, basic_auth?: {username, password}}
logging_common_alloy_prometheus_write_endpoints: []
# - url: "https://thanos.example.com/api/v1/receive"
# remote_timeout: 60s
# basic_auth:
# username: alloy
# password: secret

# WAL retention for prometheus.remote_write. Controls how long undelivered samples
# are buffered on disk before being dropped during a remote endpoint outage.
# truncate_frequency: how often the WAL is compacted (default: Alloy built-in 2h)
# max_keepalive_time: samples older than this are dropped even if not yet delivered (default: Alloy built-in 8h)
logging_common_alloy_prometheus_wal_truncate_frequency: "2h"
logging_common_alloy_prometheus_wal_max_keepalive_time: "8h"

# Full Alloy River config override. When set, bypasses all structured vars above
# and the alloy-config.alloy.j2 template entirely.
# logging_common_alloy_config_raw: |
17 changes: 17 additions & 0 deletions control-plane/roles/logging-common/meta/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
galaxy_info:
role_name: logging-common
author: metal-stack
description: Deploys logging components common to all environments
license: MIT
min_ansible_version: "2.10"
galaxy_tags: []
platforms:
- name: GenericLinux
versions:
- all

dependencies:
- role: metal-roles/common/roles/defaults
- role: metal-roles/control-plane/roles/defaults
- role: ansible-common
28 changes: 28 additions & 0 deletions control-plane/roles/logging-common/tasks/deploy-alloy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
- name: Gather release versions
setup_yaml:

- name: Check mandatory variables for logging-common are set
Comment thread
Gerrit91 marked this conversation as resolved.
assert:
fail_msg: "not all mandatory variables given, check role documentation"
quiet: true
that:
- logging_common_alloy_chart_version is defined
- logging_common_alloy_chart_repo is defined
- logging_common_alloy_loki_write_endpoints | length > 0 or logging_common_alloy_config_raw | default('') | length > 0
- logging_common_alloy_cluster_label | length > 0

- name: Build Alloy config
set_fact:
logging_common_alloy_config: "{{ lookup('template', role_path + '/templates/alloy-config.alloy.j2') if (logging_common_alloy_config_raw | default('') | length == 0) else logging_common_alloy_config_raw }}"

- name: Deploy Alloy
kubernetes.core.helm:
name: alloy
chart_repo_url: "{{ logging_common_alloy_chart_repo }}"
chart_version: "{{ logging_common_alloy_chart_version }}"
chart_ref: alloy
namespace: "{{ logging_common_alloy_namespace }}"
values: "{{ lookup('template', role_path + '/templates/alloy-values.yaml') | from_yaml }}"
kubeconfig: "{{ logging_common_alloy_kubeconfig | default(omit) }}"
create_namespace: true
Loading
Loading