Skip to main content
RunBook Academy

AnsibleXLIII · Observability and Auditing of AutomationObservability and auditing

Changing what a run tells you

Intermediate⏱ ~25 minansible-coreansible-docansible-config

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

Not yet marked complete on this device.

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:

Read-only / Safethe two settings that matter, read from the controller
$ 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:

Read-only / Safeevery callback available on a bare controller
$ ansible-doc -t callback -l
ansible.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 files

Five. 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:

Read-only / Safetwo of the five are deprecated
$ 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>

oneline

ansible.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:

Read-only / Safethe default JSON result format
$ ansible-playbook -i hosts.ini secret.yml
TASK [Task without no_log] *****************************************************
ok: [web1] => {
  "msg": "token is REPLACE_ME_TOKEN_VALUE"
}
Read-only / Safethe same run with result_format set to yaml
$ ANSIBLE_CALLBACK_RESULT_FORMAT=yaml ansible-playbook -i hosts.ini secret.yml
TASK [Task without no_log] *****************************************************
ok: [web1] =>
  msg: token is REPLACE_ME_TOKEN_VALUE

Marginal 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:

CallbackWhat it adds
ansible.posix.profile_tasksPer-task timing and a slowest-tasks summary
ansible.posix.timerTotal playbook wall-clock time in the stats
ansible.posix.profile_rolesTiming 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:

Read-only / Safethe setting that explains why ad-hoc output is different
$ 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: defaults

ansible-playbook always loads callbacks. The ansible ad-hoc command loads none, unless you turn them on:

Read-only / Safecallbacks for an ad-hoc command
ANSIBLE_LOAD_CALLBACK_PLUGINS=1 ansible webservers -i inventory/ -m ping

or 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

  1. 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?

  2. Q2. An operator runs an ad-hoc ansible command and is surprised that the notification callback configured in ansible.cfg did not fire. Why?

  3. Q3. Which statements about callback plugins are accurate? Select all that apply.

  4. 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.