Skip to content

Ansible

Every playbook run becomes a run event, including the ones that failed, from one line in ansible.cfg. No playbook changes, no roles to import.

Two ways in. The callback plugin runs on the control node and reports each playbook execution as it finishes. The AWX webhook reports from AWX or AAP without touching the control node at all. Most teams want the callback; pick the webhook when the control node is somebody else's to configure.

The callback plugin

ansible-galaxy collection install lumatrack.ledger

The plugin uses the Python standard library only, so nothing installs into your control node's interpreter.

# ansible.cfg
[defaults]
callbacks_enabled = lumatrack.ledger.lumatrack

[callback_lumatrack]
automation_map = site.yml=os-patching,backup.yml=nightly-backup
export LUMATRACK_KEY=lmt_your_ingest_key

The key is the only required setting. Runs go to https://lumatrack.io unless you point them elsewhere, which is only needed on a self-hosted or custom domain:

export LUMATRACK_URL=https://ledger.your-company.com

Create the key under Settings, API keys. An ingest-scope key is enough: it records runs and cannot read your ledger back, which is what you want on a machine that runs jobs.

Run any playbook and the last line reads:

LumaTrack: recorded success run for 'os-patching'

Settings

Each one reads from ansible.cfg or the environment.

Option Environment variable Notes
url LUMATRACK_URL Defaults to https://lumatrack.io. Set it only for a self-hosted or custom domain, no trailing path
key LUMATRACK_KEY Ingest scope is enough
automation LUMATRACK_AUTOMATION Forces every playbook onto one slug
automation_map LUMATRACK_AUTOMATION_MAP playbook=slug, comma separated
timeout LUMATRACK_TIMEOUT Seconds to wait, default 10

An environment variable overrides the ansible.cfg value for the same option. That is the usual explanation for a mapping in the file that appears to be ignored.

Writing the mapping

One line, while it fits:

[callback_lumatrack]
automation_map = site.yml=os-patching,backup.yml=nightly-backup

Past a handful of playbooks, wrap it. ansible.cfg is parsed by configparser, which joins indented continuation lines into one value, and commas still separate the entries, so every line except the last ends with one:

[callback_lumatrack]
automation_map =
    site.yml=os-patching,
    backup.yml=nightly-backup,
    compliance/cis-hardening.yml=cis-hardening,
    network/config-backup.yml=network-config-backup

Both forms produce the same mapping. Anything without an = is skipped instead of raising, so a stray trailing comma is harmless and a typo costs one mapping rather than the run. LUMATRACK_AUTOMATION_MAP has no continuation syntax and has to be a single line, which is a reason to keep long maps in the file and only the key in the environment.

Which automation a playbook reports against

automation wins if it is set. Otherwise automation_map is matched against the playbook's full path, then its file name, then its stem. Anything still unmatched falls back to the slugified stem, so Patch Windows.yml reports against patch-windows.

Write the mapping out for anything you care about. The fallback exists so the first run works with no configuration, and it means renaming a playbook file quietly starts reporting against a different automation.

What lands in the ledger

Field Where it comes from
status failure if any task failed or any host was unreachable
units Hosts processed
duration_seconds Wall clock for the whole playbook
external_id The AWX/AAP job id when present, so retries dedupe
failure_reason The first task that failed, or the unreachable host
metadata Playbook name and the ok/changed/failed/unreachable counts

An unreachable host counts as a failure. The host was never contacted, so the work did not happen, and a ledger that books it as a save is telling you something false. Tasks marked ignore_errors: true are not counted, because the play author already decided they are not failures.

units being the host count means a sweep over 240 machines can be valued differently from one over 3. Value those automations per unit so the difference shows up.

Roles, and what one run event covers

Roles are covered. A role only runs inside a playbook, and the callback reports the playbook, so everything the role did is inside that run. A failure names the role and the task together, as task failed: nginx_patch : Restart nginx.

The unit is one playbook execution, not one role. A site.yml that runs five roles records a single run event against site.yml's slug; the roles do not appear separately. Where a role is really its own automation with its own baseline, give it its own playbook and map that playbook to its own slug.

Ad-hoc ansible commands record nothing by default, because the ad-hoc CLI does not load notification callbacks unless bin_ansible_callbacks = True is set under [defaults]. Leaving it off is usually right: a one-off ansible fleet -m ping is not an automation run with a manual baseline behind it. If you do enable it, set LUMATRACK_AUTOMATION for those commands, since Ansible calls the synthetic playbook __adhoc_playbook__ and the fallback slug becomes adhoc-playbook.

Reporting never breaks a play

A LumaTrack that is down, a bad key, a timeout, or an unrecognised slug all produce a warning and let the play finish with its real exit status. A patching run should not fail because a telemetry endpoint was slow.

Watch for the warning in job output though. A silently wrong slug means runs that never land, and an empty automation is easy to miss on a report.

AWX and Ansible Automation Platform

Set the environment variables on the job template, or build a custom credential type that injects them. The job id becomes external_id on its own, so a retried job records once instead of twice.

If the control node is not yours to change, AWX's webhook notification template posts job results without any collection installed. Point it at your ingest endpoint and attach it to job templates for both success and failure events.

Response codes

Status Meaning What to do
201 Recorded. Nothing.
200 Replay: this external_id was already recorded, so nothing double-counted. Nothing.
202 Recorded but held, because the plan's run cap is reached. Held runs land when capacity frees or the plan is raised.
400 The payload was rejected, often an executed_at in a closed month. Fix the mapping.
404 No automation matches that slug. Create the automation, or correct automation_map.
429 Rate limited. Back off and retry after Retry-After.

From run events to a value figure

  1. Enable the callback and let a few days of runs land.
  2. Enter each automation's manual baseline: the tech minutes one manual run takes, at the role's loaded rate. This is your assumption and every report labels it as one.
  3. Open the ledger. Each figure drills down to the runs and the arithmetic behind it.

Historical runs from an existing job log belong on the backfill endpoint rather than replayed through the callback.

Field reference: Recording runs. Source and issues: LumaTrack/ansible-collection-lumatrack-ledger. The collection lives in its own repository, so its releases and issues are separate from the product's.