AnsibleXLIII · Observability and Auditing of AutomationObservability and auditing
Changing what a run tells you
What you'll learn
- Distinguish stdout, aggregate and notification callbacks and their cardinality
- Set stdout_callback and callbacks_enabled from configuration rather than per invocation
- Produce readable YAML task results without the removed yaml callback
- Enable callbacks for ad-hoc ansible commands, which do not load them by default
- Assess what a callback change does to secret exposure and to reviewability
Prerequisites
Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11
Nothing about the output of an Ansible run is fixed. The green ok: lines,
the task banners padded with asterisks, the recap at the end — all of it is
one plugin’s opinion about an event stream, and it can be replaced without
touching a playbook.
That is more useful than it sounds. Once you know the output is a plugin decision, three operational problems become configuration rather than tooling:
- “We have no idea which task makes this run take forty minutes.”
- “CI stores 60,000 lines of green output per job and nobody reads it.”
- “We need a record of this run that a program can consume.”
All three are answered by choosing callbacks. This lesson is about what is actually available, which is narrower than most documentation suggests.
Three types, two cardinalities
Stdout callbacks — exactly one is active. This is the thing that formats
the run for a human terminal. Setting a second one does not stack them; the
last setting wins. default is the default.
Aggregate callbacks — as many as you like. These consume the same event stream and produce something alongside stdout: timing summaries, structured files, JUnit XML.
Notification callbacks — as many as you like. These send events outward: chat, email, a webhook, an external run recorder. Lesson 7 treats the operational cost of these separately, because sending run data to a third party is a decision with a security dimension.
The configuration is two settings, and both should live in ansible.cfg
under source control rather than in someone’s shell profile:
$ ansible-config list | grep -A 12 'DEFAULT_STDOUT_CALLBACK:'DEFAULT_STDOUT_CALLBACK:
default: default
description:
- Set the main callback used to display Ansible output. You can only have one at
a time.
- You can have many other callbacks, but just one can be in charge of stdout.
- See :ref:`callback_plugins` for a list of available options.
env:
- name: ANSIBLE_STDOUT_CALLBACK
ini:
- key: stdout_callback
section: defaults
name: Main display callback plugin; ansible.cfg
[defaults]
stdout_callback = default
callbacks_enabled = ansible.posix.profile_tasks, ansible.posix.timer
callback_result_format = yaml
callbacks_enabled defaults to an empty list. Its own description explains
why: “not all callbacks need enabling, but many of those shipped with
Ansible do as we don’t want them activated by default”.
What ansible-core actually ships
This is where the received wisdom is out of date, and the only reliable check is the controller in front of you:
$ ansible-doc -t callback -lansible.builtin.default default Ansible screen output
ansible.builtin.junit write playbook output to a JUnit file
ansible.builtin.minimal minimal Ansible screen output
ansible.builtin.oneline oneline Ansible screen output
ansible.builtin.tree Save host events to filesFive. Everything else — the timing callbacks, the JSON and JSONL artefact callbacks, every notification callback — lives in a collection you must install and pin.
And two of the five are on their way out. Verified by running them on 2.21.3:
$ ANSIBLE_STDOUT_CALLBACK=oneline ansible-playbook -i hosts.ini secret.yml[DEPRECATION WARNING]: oneline has been deprecated. Use another callback plugin,
or vendor and/or move the oneline callback to a collection. This feature will be
removed from ansible-core version 2.23.
Origin: <Config env: ANSIBLE_STDOUT_CALLBACK>
onelineansible.builtin.tree carries the identical notice. If either is wired into
your CI, that is a dated migration rather than an open-ended preference.
The difference is visible immediately. The same task, twice, verified on 2.21.3:
$ ansible-playbook -i hosts.ini secret.ymlTASK [Task without no_log] *****************************************************
ok: [web1] => {
"msg": "token is REPLACE_ME_TOKEN_VALUE"
}$ ANSIBLE_CALLBACK_RESULT_FORMAT=yaml ansible-playbook -i hosts.ini secret.ymlTASK [Task without no_log] *****************************************************
ok: [web1] =>
msg: token is REPLACE_ME_TOKEN_VALUEMarginal on one line; transformative on a task that returns a hundred lines of nested structure, which is where people actually read output.
Timing: the callbacks you will want and do not have
The three timing callbacks people reach for live in ansible.posix, and
they are not part of ansible-core:
| Callback | What it adds |
|---|---|
ansible.posix.profile_tasks | Per-task timing and a slowest-tasks summary |
ansible.posix.timer | Total playbook wall-clock time in the stats |
ansible.posix.profile_roles | Timing aggregated per role |
[defaults]
callbacks_enabled = ansible.posix.profile_tasks, ansible.posix.timer
Their existence and names were confirmed against the ansible.posix
collection documentation. Their output is not reproduced in this lesson,
because the validation controller for this course runs a bare ansible-core
with no collections installed, and this course does not print output it
did not produce. Install the collection and run them; the shape is a table
of the slowest tasks appended after the recap.
The reason to want them is Part XLIII lesson 6: run duration is one of the four metrics that predicts an automation failure, and per-task timing is what turns “the run got slower” into “this task got slower”.
Ad-hoc commands load no callbacks at all
A quiet asymmetry that costs people an afternoon:
$ ansible-config list | grep -A 10 'DEFAULT_LOAD_CALLBACK_PLUGINS:'DEFAULT_LOAD_CALLBACK_PLUGINS:
default: false
description:
- Controls whether callback plugins are loaded when running /usr/bin/ansible. This
may be used to log activity from the command line, send notifications, and so
on. Callback plugins are always loaded for ``ansible-playbook``.
env:
- name: ANSIBLE_LOAD_CALLBACK_PLUGINS
ini:
- key: bin_ansible_callbacks
section: defaultsansible-playbook always loads callbacks. The ansible ad-hoc command
loads none, unless you turn them on:
ANSIBLE_LOAD_CALLBACK_PLUGINS=1 ansible webservers -i inventory/ -m pingor permanently, bin_ansible_callbacks = True in [defaults].
This is the mechanism behind the auditability gap that Part V describes: ad-hoc commands are the least-recorded thing an operator does, and by default not even the notification and logging callbacks see them.
Knowledge check
Knowledge check · 4 questions
Q1. A repository sets stdout_callback = yaml to get readable task results on a current ansible-core. What happens and what should be used instead?
Q2. An operator runs an ad-hoc ansible command and is surprised that the notification callback configured in ansible.cfg did not fire. Why?
Q3. Which statements about callback plugins are accurate? Select all that apply.
Q4. Switching CI from the default stdout callback to minimal is a good way to reduce log volume without losing diagnostic value.
Passing score: 75%. Answers are checked in this browser.