Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
7e9458e
feat: introduce alloy role to replace promtail in partition
ma-hartma May 4, 2026
7354a06
feat: use legacy_positions_file to avoid replaying syslog
ma-hartma May 5, 2026
17cb50e
feat: loki write endpoints allow remote_timeout
ma-hartma May 5, 2026
04731eb
docs(alloy): README with promtail migration guide
ma-hartma May 6, 2026
1ce97d1
chore: remove prometheus promtail targets
ma-hartma May 6, 2026
ad2eacc
chore: simplify naming
ma-hartma May 6, 2026
ef67f5b
fix: use correct alloy image from release
ma-hartma May 6, 2026
673f6be
feat: journal-file alloy snippet allows migrating from promtail posit…
ma-hartma May 7, 2026
f830d3d
fix: unify naming
ma-hartma May 7, 2026
8b3c47a
chore: unify label names
ma-hartma May 7, 2026
3759237
feat: label for journal level filtering
ma-hartma May 7, 2026
5155e05
docs(alloy): improve cursor/WAL part
ma-hartma May 12, 2026
f5758a9
docs(promtail): add important admonition for promtail deprecation
ma-hartma May 12, 2026
73f7b69
feat: add toggles and docs for different migration scenarios
ma-hartma May 12, 2026
1b1d28f
chore(metrics): bring back prometheus promtail job as it is optional
ma-hartma May 12, 2026
dccafd4
chore: provide promtail_migrate_stop and promtail_migrate_cleanup var…
ma-hartma May 13, 2026
d743431
chore: use recommended file_match block for syslog snippet
ma-hartma May 13, 2026
edd079e
docs: Ansible meta deprecation and README
ma-hartma May 19, 2026
5210a30
chore: disable alloy reporting
ma-hartma May 20, 2026
1d6a4d1
fix: deprecation warning
ma-hartma May 20, 2026
dd19f15
chore: partition defaults
ma-hartma May 28, 2026
cefbe81
fix: remove deprecation meta info
ma-hartma Jun 1, 2026
26bfc33
docs: credential rename
ma-hartma Jun 2, 2026
575a8f4
Merge branch 'master' into promtail-alloy-migration
ma-hartma Jun 2, 2026
a7a3853
chore: ansible lint
ma-hartma Jun 2, 2026
5ba4ded
docs: finalize documentation
ma-hartma Jun 3, 2026
1b66423
fix: remove unnecessary alloy restarts
ma-hartma Jun 3, 2026
e1c6cad
Generated-By: [Claude Sonnet 4.6] find and remove stale alloy config …
ma-hartma Jun 3, 2026
8631ca8
fix(alloy): depend on ansible-common
ma-hartma Jun 11, 2026
a8ca359
fix(alloy) remove redundant default filters
ma-hartma Jun 11, 2026
0b1746c
fix(alloy) use port exposal instead of host network
ma-hartma Jun 11, 2026
2165b21
fix(alloy) assert that snippets and raw are not used at the same time
ma-hartma Jun 11, 2026
d6bd0f6
fix(alloy): simplify snippet handling
ma-hartma Jun 11, 2026
db713a5
review(promtail): old variable still present
ma-hartma Jun 11, 2026
2128297
review(promtail): remove _enabled vars and move promtail cleanup to a…
ma-hartma Jun 11, 2026
f1ab72b
review(promtail): improve cleanup
ma-hartma Jun 12, 2026
876a2b6
chore: cleanup _enabled
ma-hartma Jun 12, 2026
4ffdf3b
fix: reload systemd location
ma-hartma Jun 15, 2026
8e40fc0
feat: allow custom snippets
ma-hartma Jun 15, 2026
c83903a
fix: lint errors
ma-hartma Jun 15, 2026
ad0565b
Merge branch 'master' into promtail-alloy-migration
ma-hartma Jun 15, 2026
48d152b
chore: add comment for each snippet
ma-hartma Jun 16, 2026
d8ecfb9
chore: assert that only supported snippets are used
ma-hartma Jun 16, 2026
e9b2a97
Merge branch 'master' into promtail-alloy-migration
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 @@ -98,6 +98,8 @@
capms_controller_name: "docker-images.metal-stack.kubernetes.cluster-api-provider-metal-stack-controller.name"
capms_controller_tag: "docker-images.metal-stack.kubernetes.cluster-api-provider-metal-stack-controller.tag"
# third party
alloy_image_tag: "docker-images.third-party.partition.alloy.tag"
alloy_image_name: "docker-images.third-party.partition.alloy.name"
event_exporter_name: "docker-images.third-party.control-plane.event-exporter.name"
event_exporter_tag: "docker-images.third-party.control-plane.event-exporter.tag"
nsq_image_tag: "docker-images.third-party.control-plane.nsq.tag"
Expand Down
2 changes: 1 addition & 1 deletion partition/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ You can look up all the default values [here](partition-defaults/main.yaml).
| [metal-bmc](roles/metal-bmc) | Deploys metal-bmc |
| [metal-core](roles/metal-core) | Deploys metal-core |
| [pixiecore](roles/pixiecore) | Deploys pixiecore |
| [promtail](roles/promtail) | Deploys promtail |
| [alloy](roles/alloy) | Deploys alloy |

## Examples

Expand Down
197 changes: 197 additions & 0 deletions partition/roles/alloy/README.md

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions partition/roles/alloy/defaults/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
# Set to true to deploy and start the Alloy service.
# Set to false to skip service deployment — useful when alloy is being staged or promtail is still running.
alloy_enabled: false
Comment thread
ma-hartma marked this conversation as resolved.
Outdated

alloy_config_host_dir: "/etc/alloy"
alloy_docker_log_driver: json-file

# List of Loki push endpoints used by the base loki.write component.
# Each entry: {url: <push-url>, remote_timeout?: <duration>, basic_auth?: {username, password}}
# alloy_loki_write_endpoints:
# - url: http://loki.example.com:8080/loki/api/v1/push
# remote_timeout: 10s
# basic_auth:
# username: "username"
# password: "password"

# List of snippet names to enable. Each entry must correspond to
# templates/snippets/<name>.alloy.j2 in this role.
alloy_config_snippets: []

# Provide a fully custom Alloy River config as a string to bypass the
# snippet assembly system entirely. When set, alloy_loki_write_endpoints
# and alloy_config_snippets are ignored.
# alloy_config_raw: |

# Port to listen for metrics and HTTP API traffic on
alloy_port: 12345

# Enable migration mode: imports cursor state from the legacy promtail positions file
# on first start so Alloy resumes from where promtail left off. Set to true when
# migrating from promtail; leave false for fresh deployments.
alloy_migrate_from_promtail: false

# Path to the legacy promtail positions file used by the syslog snippet on first start
# after migration to continue tailing /var/log/syslog from where promtail left off.
# Only active when alloy_migrate_from_promtail is true.
alloy_syslog_legacy_positions_file: "/var/log/promtail-positions.yaml"

# Path to the persistent journal directory used by the journal-file snippet.
alloy_journal_path: "/var/log/journal"

# Path to the legacy promtail positions file used by the journal-file snippet on first
# start after migration to resume from where promtail left off.
# Only active when alloy_migrate_from_promtail is true.
alloy_journal_legacy_positions_file: "/var/log/promtail-positions.yaml"

# Job name used in the legacy promtail positions file for the journal scrape config.
# Must match the job_name of the journal scrape_config in your old promtail config.
# Required when alloy_migrate_from_promtail is true and the journal-file snippet is used.
# No default — must be set explicitly.
# alloy_journal_legacy_position_name:
7 changes: 7 additions & 0 deletions partition/roles/alloy/handlers/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
- name: restart alloy
service:
name: alloy
enabled: true
state: restarted
when: alloy_enabled | default(false)
17 changes: 17 additions & 0 deletions partition/roles/alloy/meta/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
galaxy_info:
role_name: alloy
author: metal-stack
description: Deploys alloy.
license: MIT
min_ansible_version: "2.10"
galaxy_tags: []

platforms:
- name: Debian
versions:
- all

dependencies:
- role: metal-roles/common/roles/defaults
Comment thread
ma-hartma marked this conversation as resolved.
- role: metal-roles/partition/roles/defaults
124 changes: 124 additions & 0 deletions partition/roles/alloy/tasks/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
---
- name: Gather release versions
setup_yaml:

- name: Check mandatory variables for this role are set
assert:
fail_msg: "not all mandatory variables given, check role documentation"
quiet: true
that:
- alloy_image_tag is defined
- alloy_image_name is defined
- alloy_config_raw is defined or (alloy_loki_write_endpoints is defined and (alloy_loki_write_endpoints | length > 0))
Comment thread
ma-hartma marked this conversation as resolved.

- name: Check alloy_journal_legacy_position_name is set when migrating journal-file from promtail
assert:
fail_msg: "alloy_journal_legacy_position_name must be set — it must match the job_name of the journal scrape_config in your old promtail config"
quiet: true
that:
- alloy_journal_legacy_position_name is defined
when:
- "'journal-file' in alloy_config_snippets"
- alloy_migrate_from_promtail | default(false)
Comment thread
ma-hartma marked this conversation as resolved.
Outdated

- name: Check journal and journal-file are not both enabled
assert:
fail_msg: "journal and journal-file snippets cannot both be enabled — this will produce duplicate log entries in Loki"
quiet: true
that:
- not ('journal' in alloy_config_snippets and 'journal-file' in alloy_config_snippets)

- name: Create alloy config directory
file:
path: "{{ alloy_config_host_dir }}"
state: directory
owner: "nobody"
group: "nogroup"
mode: "0755"

- name: Create alloy config snippets directory
file:
path: "{{ alloy_config_host_dir }}/conf.d"
state: directory
owner: "nobody"
group: "nogroup"
mode: "0755"

- name: Template alloy base configuration
template:
src: config.alloy.j2
dest: "{{ alloy_config_host_dir }}/conf.d/00-base.alloy"
when: alloy_config_raw is not defined

- name: Find existing alloy config snippet files
find:
paths: "{{ alloy_config_host_dir }}/conf.d"
patterns: "50-*.alloy"
register: existing_snippets
when: alloy_config_raw is not defined

- name: Remove stale alloy config snippets
file:
path: "{{ item.path }}"
state: absent
loop: "{{ existing_snippets.files | default([]) }}"
when:
- alloy_config_raw is not defined
- item.path | basename | regex_replace('^50-(.+)\.alloy$', '\1') not in alloy_config_snippets

- name: Template alloy config snippets
template:
src: "snippets/{{ item }}.alloy.j2"
dest: "{{ alloy_config_host_dir }}/conf.d/50-{{ item }}.alloy"
loop: "{{ alloy_config_snippets }}"
when: alloy_config_raw is not defined

- name: Assemble alloy config
Comment thread
ma-hartma marked this conversation as resolved.
Outdated
assemble:
Comment thread
ma-hartma marked this conversation as resolved.
Outdated
src: "{{ alloy_config_host_dir }}/conf.d"
dest: "{{ alloy_config_host_dir }}/config.alloy"
delimiter: "\n"
notify: restart alloy
when: alloy_config_raw is not defined

- name: Write raw alloy config
copy:
content: "{{ alloy_config_raw }}"
dest: "{{ alloy_config_host_dir }}/config.alloy"
notify: restart alloy
when: alloy_config_raw is defined

- name: Create alloy storage directory
file:
path: /var/lib/alloy
state: directory
owner: "nobody"
group: "nogroup"
mode: "0755"
when: alloy_enabled | default(false)

- name: Deploy alloy service
include_role:
name: ansible-common/roles/systemd-docker-service
vars:
systemd_service_name: alloy
systemd_docker_image_name: "{{ alloy_image_name }}"
systemd_docker_image_tag: "{{ alloy_image_tag }}"
systemd_service_after: docker.service
systemd_service_requires: docker.service
systemd_docker_network: host
Comment thread
ma-hartma marked this conversation as resolved.
Outdated
systemd_docker_volumes:
- "{{ alloy_config_host_dir }}:/etc/alloy:ro"
- "/var/lib/alloy:/var/lib/alloy"
- "/var/log:/var/log"
- "/run/log/journal:/run/log/journal:ro"
- "/etc/machine-id:/etc/machine-id:ro"
- "/var/run/docker.sock:/var/run/docker.sock"
systemd_docker_command:
- run
- --server.http.listen-addr=0.0.0.0:{{ alloy_port }}
- --storage.path=/var/lib/alloy
- --disable-reporting
- /etc/alloy/config.alloy
systemd_docker_log_driver: "{{ alloy_docker_log_driver }}"
when: alloy_enabled | default(false)
30 changes: 30 additions & 0 deletions partition/roles/alloy/templates/config.alloy.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Log level and format; write_to forwards Alloy's own logs to Loki when alloy-meta is enabled
logging {
level = "info"
format = "logfmt"
{% if "alloy-meta" in alloy_config_snippets %}
write_to = [loki.relabel.alloy_self.receiver]
{% endif %}
}

// Loki write endpoint shared by all snippets; external_labels are appended to every log entry
loki.write "default" {
external_labels = {
host = "{{ inventory_hostname }}",
partition = "{{ metal_partition_id }}",
}
{% for endpoint in alloy_loki_write_endpoints %}
endpoint {
url = "{{ endpoint.url }}"
{%if endpoint.remote_timeout is defined %}
remote_timeout = "{{ endpoint.remote_timeout }}"
{% endif %}
{% if endpoint.basic_auth is defined %}
basic_auth {
username = "{{ endpoint.basic_auth.username }}"
password = "{{ endpoint.basic_auth.password }}"
}
{% endif %}
}
{% endfor %}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Meta-monitoring: forward Alloy's own logs to Loki
loki.relabel "alloy_self" {
forward_to = [loki.write.default.receiver]

rule {
target_label = "job"
replacement = "alloy"
}
}
27 changes: 27 additions & 0 deletions partition/roles/alloy/templates/snippets/docker.alloy.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
discovery.docker "docker" {
host = "unix:///var/run/docker.sock"
refresh_interval = "5s"
}

discovery.relabel "docker" {
targets = []

rule {
source_labels = ["__meta_docker_container_name"]
regex = "/(.*)"
target_label = "container"
}

rule {
target_label = "job"
replacement = "docker"
}
}

loki.source.docker "docker" {
host = "unix:///var/run/docker.sock"
targets = discovery.docker.docker.targets
forward_to = [loki.write.default.receiver]
relabel_rules = discovery.relabel.docker.rules
refresh_interval = "5s"
}
32 changes: 32 additions & 0 deletions partition/roles/alloy/templates/snippets/journal-file.alloy.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
discovery.relabel "journal" {
targets = []

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

rule {
source_labels = ["__journal_priority_keyword"]
target_label = "level"
}
}

loki.source.journal "journal" {
path = "{{ alloy_journal_path }}"
relabel_rules = discovery.relabel.journal.rules
forward_to = [loki.write.default.receiver]
labels = {
job = "systemd-journal",
}
{% if alloy_migrate_from_promtail | default(false) %}
// migration only: resumes reading from where promtail left off on first start.
// ignored if the file does not exist on the host.
// once alloy has written its own positions file, this can be removed.
// https://grafana.com/docs/alloy/latest/reference/components/loki/loki.source.journal/#legacy_position
legacy_position {
Comment thread
simcod marked this conversation as resolved.
file = "{{ alloy_journal_legacy_positions_file }}"
name = "{{ alloy_journal_legacy_position_name }}"
}
{% endif %}
}
21 changes: 21 additions & 0 deletions partition/roles/alloy/templates/snippets/journal.alloy.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
discovery.relabel "journal" {
targets = []

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

rule {
source_labels = ["__journal_priority_keyword"]
target_label = "level"
}
}

loki.source.journal "journal" {
relabel_rules = discovery.relabel.journal.rules
forward_to = [loki.write.default.receiver]
labels = {
job = "systemd-journal",
}
}
18 changes: 18 additions & 0 deletions partition/roles/alloy/templates/snippets/syslog.alloy.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
loki.source.file "syslog" {
targets = [{
__path__ = "/var/log/syslog",
job = "syslog",
}]
forward_to = [loki.write.default.receiver]

file_match {
enabled = true
}
{% if alloy_migrate_from_promtail | default(false) %}
// migration only: resumes tailing from where promtail left off on first start.
// ignored if the file does not exist on the host.
// once alloy has written its own positions file, this can be removed.
// https://grafana.com/docs/alloy/latest/reference/components/loki/loki.source.file/#arguments
legacy_positions_file = "{{ alloy_syslog_legacy_positions_file }}"
{% endif %}
}
2 changes: 1 addition & 1 deletion partition/roles/monitoring/prometheus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ you define them adequately as well.
| prometheus_frr_exporter_targets | | FRR exporter targets to scrape from |
| prometheus_metal_core_targets | | metal-core targets to scrape from |
| prometheus_node_exporter_targets | | Node exporter targets to scrape from |
| prometheus_promtail_targets | | Promtail targets to scrape from |
| prometheus_alloy_targets | | Alloy targets to scrape from (default port `12345`) |
Comment thread
ma-hartma marked this conversation as resolved.
| prometheus_ping_targets | | Ping targets to scrape from |
| prometheus_sonic_exporter_targets | | Sonic exporter targets to scrape from |
| prometheus_blackbox_exporter_targets | | Blackbox exporter targets to scrape from |
Expand Down
6 changes: 6 additions & 0 deletions partition/roles/monitoring/prometheus/defaults/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@ prometheus_alertmanager_target:
prometheus_alertmanager_basic_auth_username: alertmanager
prometheus_alertmanager_basic_auth_password:
prometheus_remote_write: []
# - url: "https://monitoring.{{ metal_control_plane_ingress_dns }}/api/v1/receive"
# basic_auth:
# username: thanos-receive
# password: "{{ basic_auth_pass }}"

prometheus_frr_exporter_targets: []
prometheus_metal_core_targets: []
prometheus_node_exporter_targets: []
prometheus_promtail_targets: []
prometheus_alloy_targets: []
Comment thread
ma-hartma marked this conversation as resolved.
prometheus_ping_targets: []
prometheus_ipmi_exporter_targets: []
prometheus_sonic_exporter_targets: []
Expand Down
Loading
Loading