Simplify error and rescue output

Hi,

I have a task for configuring, testing and restarting apache. It works as expected, but the output is rather verbose. I’ve included sample output for a malformed configuration.

For the error I would like to do something like outputting only the stderr field.

The restore and delete backup tasks seem to output the whole task result. I would like to either output nothing at all, or perhaps only the file name or something, though I’m not sure exactly what the output is.

Using ansible core 2.19.4.

- name: Configure apache
  block:
    - name: Copy files
      ansible.builtin.copy:
        src: apache/{{ item }}
        dest: "/usr/local/etc/apache24/Includes/{{ item }}"
        backup: true
      with_items:
        - 'macro-redirect-https.conf'
      register: updated

    - name: "Test config"
      ansible.builtin.shell: 'apachectl -t'
      when: updated is changed
      notify:
        - Restart apache
  rescue:
    - name: Restore backup
      ansible.builtin.copy:
        remote_src: true
        src: "{{ item['backup_file'] }}"
        dest: "{{ item['dest'] }}"
      with_items: "{{ updated.results }}"
      when: updated is changed
  always:
    - name: Delete backup
      ansible.builtin.file:
        path: "{{ item['backup_file'] }}"
        state: absent
      with_items: "{{ updated.results }}"
      when: updated is changed

Problematic output:

TASK [freebsd : Test config] *************************************************************************************************************
[ERROR]: Task failed: Module failed: non-zero return code
Origin: /home/e/ansible_quickstart/roles/freebsd/tasks/apache.yaml:27:7

25       register: updated
26
27     - name: "Test config"
         ^ column 7

fatal: [web01]: FAILED! => {
    "changed": true,
    "cmd": "apachectl -t",
    "delta": "0:00:00.044928",
    "end": "2026-03-23 13:03:58.560124",
    "msg": "non-zero return code",
    "rc": 1,
    "start": "2026-03-23 13:03:58.515196",
    "stderr": "AH00526: Syntax error on line 2 of macro 'redirecthttps' (defined on line 1 of \"/usr/local/etc/apache24/Includes/macro-redirect-https.conf\") used on line 4 of \"/usr/local/etc/apache24/vhost.conf\":\nInvalid command 'RedirectMatch2', perhaps misspelled or defined by a module not included in the server configuration",
    "stderr_lines": [
        "AH00526: Syntax error on line 2 of macro 'redirecthttps' (defined on line 1 of \"/usr/local/etc/apache24/Includes/macro-redirect-https.conf\") used on line 4 of \"/usr/local/etc/apache24/vhost.conf\":",
        "Invalid command 'RedirectMatch2', perhaps misspelled or defined by a module not included in the server configuration"
    ],
    "stdout": "",
    "stdout_lines": []
}

TASK [freebsd : Restore backup] **********************************************************************************************************
changed: [web01] => (item={'diff': [{'before_header': '/usr/local/etc/apache24/Includes/macro-redirect-https.conf', 'before': '<Macro RedirectHTTPS $hostname>\n    RedirectMatch permanent ^/((?!\\.well-known/acme-challenge/).*)$ https://$hostname/\n</Macro>\n<Macro RedirectHTTPS2 $hostname>\n    RedirectMatch2 permanent ^/((?!\\.well-known/acme-challenge/).*)$ https://$hostname/\n</Macro>\n', 'after_header': '/home/e/ansible_quickstart/roles/freebsd/files/apache/macro-redirect-https.conf', 'after': '<Macro RedirectHTTPS $hostname>\n    RedirectMatch permanent ^/((?!\\.well-known/acme-challenge/).*)$ https://$hostname/\n    RedirectMatch2 permanent ^/((?!\\.well-known/acme-challenge/).*)$ https://$hostname/\n</Macro>\n'}], 'dest': '/usr/local/etc/apache24/Includes/macro-redirect-https.conf', 'src': '/home/e/.ansible/tmp/ansible-tmp-1774267431.6175117-1991-16980776241965/.source.conf', 'md5sum': '1e390764aa09d46e448c23ef01634fed', 'checksum': 'ea2418ad9d069e6fccea1e6e67e12ded165b94eb', 'changed': True, 'backup_file': '/usr/local/etc/apache24/Includes/macro-redirect-https.conf.81850.2026-03-23@13:03:58~', 'uid': 0, 'gid': 0, 'owner': 'root', 'group': 'wheel', 'mode': '0644', 'state': 'file', 'size': 216, 'invocation': {'module_args': {'backup': True, 'dest': '/usr/local/etc/apache24/Includes/macro-redirect-https.conf', 'src': '/home/e/.ansible/tmp/ansible-tmp-1774267431.6175117-1991-16980776241965/.source.conf', '_original_basename': 'macro-redirect-https.conf', 'follow': False, 'checksum': 'ea2418ad9d069e6fccea1e6e67e12ded165b94eb', 'force': True, 'remote_src': False, 'unsafe_writes': False, 'content': None, 'validate': None, 'directory_mode': None, 'local_follow': None, 'mode': None, 'owner': None, 'group': None, 'seuser': None, 'serole': None, 'selevel': None, 'setype': None, 'attributes': None}}, 'failed': False, 'item': 'macro-redirect-https.conf', 'ansible_loop_var': 'item'})

TASK [freebsd : Delete backup] ***********************************************************************************************************
--- before
+++ after
@@ -1,4 +1,4 @@
 {
     "path": "/usr/local/etc/apache24/Includes/macro-redirect-https.conf.81850.2026-03-23@13:03:58~",
-    "state": "file"
+    "state": "absent"
 }

changed: [web01] => (item={'diff': [{'before_header': '/usr/local/etc/apache24/Includes/macro-redirect-https.conf', 'before': '<Macro RedirectHTTPS $hostname>\n    RedirectMatch permanent ^/((?!\\.well-known/acme-challenge/).*)$ https://$hostname/\n</Macro>\n<Macro RedirectHTTPS2 $hostname>\n    RedirectMatch2 permanent ^/((?!\\.well-known/acme-challenge/).*)$ https://$hostname/\n</Macro>\n', 'after_header': '/home/e/ansible_quickstart/roles/freebsd/files/apache/macro-redirect-https.conf', 'after': '<Macro RedirectHTTPS $hostname>\n    RedirectMatch permanent ^/((?!\\.well-known/acme-challenge/).*)$ https://$hostname/\n    RedirectMatch2 permanent ^/((?!\\.well-known/acme-challenge/).*)$ https://$hostname/\n</Macro>\n'}], 'dest': '/usr/local/etc/apache24/Includes/macro-redirect-https.conf', 'src': '/home/e/.ansible/tmp/ansible-tmp-1774267431.6175117-1991-16980776241965/.source.conf', 'md5sum': '1e390764aa09d46e448c23ef01634fed', 'checksum': 'ea2418ad9d069e6fccea1e6e67e12ded165b94eb', 'changed': True, 'backup_file': '/usr/local/etc/apache24/Includes/macro-redirect-https.conf.81850.2026-03-23@13:03:58~', 'uid': 0, 'gid': 0, 'owner': 'root', 'group': 'wheel', 'mode': '0644', 'state': 'file', 'size': 216, 'invocation': {'module_args': {'backup': True, 'dest': '/usr/local/etc/apache24/Includes/macro-redirect-https.conf', 'src': '/home/e/.ansible/tmp/ansible-tmp-1774267431.6175117-1991-16980776241965/.source.conf', '_original_basename': 'macro-redirect-https.conf', 'follow': False, 'checksum': 'ea2418ad9d069e6fccea1e6e67e12ded165b94eb', 'force': True, 'remote_src': False, 'unsafe_writes': False, 'content': None, 'validate': None, 'directory_mode': None, 'local_follow': None, 'mode': None, 'owner': None, 'group': None, 'seuser': None, 'serole': None, 'selevel': None, 'setype': None, 'attributes': None}}, 'failed': False, 'item': 'macro-redirect-https.conf', 'ansible_loop_var': 'item'})

You can reduce the block to a single copy task by using the validate option.

Thanks, but that is not sufficient in this instance, as I’m only providing partial configuration, which won’t validate without first having loaded the rest of Apache’s configuration.

I think the problem is you want to use the job log as a report. You could include a task which creates a report with just the data you want (say, using a template). But the job log is just a job log.

I don’t think you’re going to get a satisfactory answer. Sorry.

I’m not sure what job logs and reports refer to, exactly. There are callback plugins that control output in various ways, so I thought it might have been possible to accomplish something like this.

Nevertheless, I have made some progress. I realised that the rescue and delete tasks, of course, output the result of the original copy task. I was able to achieve perfect results by doing this:

  rescue:
    - name: Restore backup
      ansible.builtin.copy:
        remote_src: true
        src: "{{ item.backup_file }}"
        dest: "{{ item.dest }}"
      with_items: "{{ updated.results }}"
+     loop_control:
+       label: "{{ item.item }}"
      when: updated is changed

I was able to simplify error output a little bit by doing this:

    - block:
        - name: "Test config"
          ansible.builtin.shell: 'apachectl -t'
          register: config_test
          failed_when: False

        - ansible.builtin.assert:
            that: config_test.rc == 0
            fail_msg: "{{ config_test.stderr }}"
          no_log: true
          notify:
            - Restart apache
      when: updated is changed

But it’s not a significant improvement compared to using YAML output.

[ERROR]: Task failed: Action failed: AH00526: Syntax error on line 2 of macro 'redirecthttps' (defined on line 1 of "/usr/local/etc/apache24/Includes/macro-redirect-https.conf") used on line 4 of "/usr/local/etc/apache24/vhost.conf":
Invalid command 'RedirectMatch2', perhaps misspelled or defined by a module not included in the server configuration
Origin: /home/e/ansible_quickstart/roles/freebsd/tasks/apache.yaml:35:11

33           #   - Restart apache
34
35         - name: Print the gateway for each host when defined
             ^ column 11

fatal: [web01]: FAILED! =>
    censored: 'the output has been hidden due to the fact that ''no_log: true'' was specified
        for this result'
    changed: false
1 Like

Sorry; I guess I was in “read-my-mind” mode when I wrote that.

The job log is what’s printed to the screen or log file as Ansible runs. A report is text you generate, typically from templates, separate from the job log, and that may contain selected registered data, introspective data about the job itself, or other data gathered from the systems you are managing.

There’s a popular anti-pattern where people try to turn the job log into a report with varying degrees of success and frustration. The fact that you can embed mini-reports in the job log through the ansible.builtin.debug module (which is indispensable and I’m not arguing against that at all) blurs the distinction between job logs and reports. That’s perhaps where people new to Ansible get the idea to continue trying to coerce the log into a report beyond a practical point.

There are lots of “knobs” that control behavior of callbacks to change what ends up in a job log beyond -v and --diff. As you found out, loop_control.label does pretty much what you were looking for, and I had forgotten about it completely when I replied earlier.

Glad you found a solution you can live with, and for the reminder about loop_control.label.

2 Likes

I see. As a hint, take a look at jtyr.config_encoder_filters.

3 Likes

For showing only stderr on failure, the cleanest approach is to register the result and then use a debug task in the rescue block with just the field you want:

- block:
    - name: Test Apache config
      command: apachectl configtest
      register: configtest_result
      changed_when: false
  rescue:
    - name: Show config error
      debug:
        msg: "{{ configtest_result.stderr }}"

    - name: Restore backup
      copy:
        src: /etc/httpd/conf/httpd.conf.bak
        dest: /etc/httpd/conf/httpd.conf
        remote_src: yes

For the restore and backup tasks that show too much output, add no_log: true if you want them completely silent, or use the YAML callback plugin (ANSIBLE_STDOUT_CALLBACK=yaml) which formats output more readably than the default JSON dump.

If you want to suppress the full traceback from the failed task itself (the big red block), set ANSIBLE_DISPLAY_FAILED_STDERR=yes in your ansible.cfg. That shows only stderr from failed commands rather than the entire result dictionary.

1 Like