How to Properly Format Ansible Playbook Task Output (stdout/stderr) for Readable Console Display


2 views

When running ansible-playbook, you might notice task outputs appearing as hard-to-read JSON blobs instead of the clean, line-by-line format you'd expect from standard streams. This happens because Ansible by default wraps command output in its result dictionary structure.

# Example of problematic output
TASK [Run database migration] **************************************************
ok: [webserver1]
{
  "changed": true,
  "cmd": "python manage.py migrate",
  "delta": "0:00:02.345678",
  "stdout": "Operations to perform:\n  Apply all migrations: admin, auth, contenttypes, sessions\nApplying contenttypes.0001_initial... OK",
  "stderr": "Warning: Debug mode is on\n",
  "stderr_lines": ["Warning: Debug mode is on"],
  "stdout_lines": [
    "Operations to perform:",
    "  Apply all migrations: admin, auth, contenttypes, sessions",
    "Applying contenttypes.0001_initial... OK"
  ]
}

Here are several approaches to improve output readability:

1. Using stdout_callback

Edit your ansible.cfg:

[defaults]
stdout_callback = debug
# or for more compact output:
# stdout_callback = yaml

2. Per-task Debugging with register

For specific tasks where you need clean output:

- name: Run database migrations
  command: python manage.py migrate
  register: migrate_output
  changed_when: false

- name: Display clean migration output
  debug:
    msg: "{{ migrate_output.stdout_lines }}"

3. Using the 'debug' Module for Critical Output

- name: Get system information
  shell: uname -a
  register: system_info

- debug:
    var: system_info.stdout

For production environments, consider these configurations:

[defaults]
# For better error visibility
display_failed_stderr = true

# For multi-line output handling
display_skipped_hosts = false
display_ok_hosts = false

Here's how to handle output in a Django deployment playbook:

- name: Collect static files
  command: python manage.py collectstatic --noinput
  register: collectstatic_out
  changed_when: "'0 static files copied' not in collectstatic_out.stdout"

- name: Show collectstatic output
  debug:
    msg: "{{ collectstatic_out.stdout_lines | join('\n') }}"

To properly format error streams:

- name: Run critical process
  command: /opt/app/start.sh
  register: app_start
  ignore_errors: yes

- name: Show application errors
  debug:
    msg: "{{ app_start.stderr_lines | default([]) | join('\n') }}"
  when: app_start.failed

When running ansible-playbook foo.yaml, you've likely encountered output like this:

TASK [Django: Create superuser] *********************
fatal: [lorem]: FAILED! => {"changed": false, "cmd": "python3 -m django createsuperuser\n  --noinput\n  --username \"admin\"\n  --email \"admin@example.com\"", "msg": "\n:stderr: CommandError: You must use --full_name with --noinput.\n", ...}

This unreadable JSON blob occurs because Ansible by default wraps command output in its result object structure.

Add these configurations to your ansible.cfg:

[defaults]
stdout_callback = yaml
bin_ansible_callbacks = True
display_failed_stderr = True

Or set these environment variables:

export ANSIBLE_STDOUT_CALLBACK=yaml
export ANSIBLE_DISPLAY_FAILED_STDERR=True

For specific tasks, use these YAML attributes:

- name: Run script with clean output
  command: /path/to/script.sh
  register: script_output
  changed_when: false
  no_log: false
  
- name: Display clean output
  debug:
    msg: "{{ script_output.stdout_lines }}"

Install and configure the debug callback plugin:

[defaults]
stdout_callback = debug

Or create a custom callback plugin by placing this in callback_plugins/clean_output.py:

from ansible.plugins.callback import CallbackBase

class CallbackModule(CallbackBase):
    CALLBACK_VERSION = 2.0
    CALLBACK_NAME = 'clean_output'
    
    def v2_runner_on_failed(self, result, **kwargs):
        self._display.display("FAILED: " + result._result.get('msg', ''))
        
    def v2_runner_on_ok(self, result):
        if 'stdout' in result._result:
            self._display.display(result._result['stdout'])

Here's how to properly handle Django management commands:

- name: Run Django migrate
  command: "python manage.py migrate"
  register: migrate_out
  changed_when: "'No migrations to apply' not in migrate_out.stdout"
  
- name: Show migration output
  debug:
    var: migrate_out.stdout_lines
    verbosity: 1

This produces clean, line-by-line output while still capturing all execution details.