Baeldung Pro – Ops – NPI EA (cat = Baeldung on Ops)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

1. Overview

Logging details of a playbook is very crucial in Ansible. Moreover, it can become even more important when a playbook runs across multiple servers.

Particularly, in Ansible, logging helps understand what’s happening, when it’s happening, and whether it was successful. Thus, by capturing a task output, we can trace changes.

In this tutorial, we’ll see how to log task output to a file in Ansible.

2. Requirements

Before we move on, we need to check for some prerequisites:

  • Ansible installed on the controller machine
  • SSH connectivity between the controller and remote clients
  • inventory file that lists the remote systems that the playbook targets

Additionally, to test the working of the examples below, we can set up a test environment.

With the above requirements in place, we can start logging task output to a file.

3. Using the log_path Configuration

Ansible uses the log_path parameter to save playbook events to a single file. This option can be enabled fairly easily. Also, it needs no code changes in existing playbooks.

Let’s see how to change ansible.cfg to use log_path.

3.1. Understanding ansible.cfg

The ansible.cfg file controls the runtime behavior of Ansible. In essence, it looks for the config files in a specific order:

  • ANSIBLE_CONFIG environment variable
  • ansible.cfg in the current directory
  • ~/.ansible.cfg
  • /etc/ansible/ansible.cfg

In the current case, we use the config file placed in the project directory.

3.2. Enabling Global log_path

Let’s specify the log file path in ansible.cfg so that all logs are saved to the designated location:

$ cat ansible.cfg
# ansible.cfg
[defaults]
log_path = ./logs/ansible-execution.log

Importantly, the logs directory should be present before we run the playbook. Of course, the file should have write permissions for saving the logs.

3.3. Running a Playbook

Let’s create a playbook to test the setup:

$ cat log_path.yml
---
- name: Test logging to file
  hosts: web
  gather_facts: false
  tasks:
    - name: Run a sample shell command
      shell: echo "This is a test run on {{ inventory_hostname }}"

The above playbook runs a simple shell command to print a message. Here, the inventory_hostname parameter is replaced by the name of each target host in the web group.

During the playbook execution, Ansible writes one line per event (task start, task result, play recap) to STDOUT:

$ ansible-playbook log_path.yml -i hosts.ini -v
Using /home/vagrant/ansible-projects/ansible.cfg as config file
PLAY [Test logging to file] ******************************************************************
TASK [Run a sample shell command] ************************************************************
changed: [web1] => {"ansible_facts"... "changed": true, "cmd": "echo \"This is a test run on web1\"", "delta": "0:00:00.002893", "end": "2025-07-01 05:00:59.621860", "msg": "", "rc": 0, "start": "2025-07-01 05:00:59.618967", "stderr": "", "stderr_lines": [], "stdout": "This is a test run on web1", "stdout_lines": ["This is a test run on web1"]}
PLAY RECAP ***********************************************************************************
web1                       : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Moreover, the same output gets saved to the log_path location, ./logs/ansible-execution.log:

$ cat logs/ansible-execution.log 
2025-07-01 05:00:57,130 p=2055 u=vagrant n=ansible | Using /home/vagrant/ansible-projects/ansible.cfg as config file
2025-07-01 05:00:57,559 p=2055 u=vagrant n=ansible | PLAY [Test logging to file] *******
2025-07-01 05:00:57,567 p=2055 u=vagrant n=ansible | TASK [Run a sample shell **********
2025-07-01 05:00:59,656 p=2055 u=vagrant n=ansible | 
2025-07-01 05:00:59,656 p=2055 u=vagrant n=ansible | changed: [web1] => {"ansible_facts":.... "changed": true, "cmd": "echo \"This is a test run on web1\"", "delta": "0:00:00.002893", "end": "2025-07-01 05:00:59.621860", "msg": "", "rc": 0, "start": "2025-07-01 05:00:59.618967", "stderr": "", "stderr_lines": [], "stdout": "This is a test run on web1", "stdout_lines": ["This is a test run on web1"]}
2025-07-01 05:00:59,657 p=2055 u=vagrant n=ansible | PLAY RECAP ***********************************************************************************
2025-07-01 05:00:59,657 p=2055 u=vagrant n=ansible | ...

In the above output, every record contains a timestamp, host, task arguments, and status.

4. Using Command Line Redirection

Shell redirection can quickly save Ansible playbook output to a file. In this case, the whole output gets written to a log file. Unlike the above method, no output trace appears on STDOUT.

Let’s reuse the above log_path.yml playbook.

In this case, we run the playbook with the verbose output option -v. Moreover, we capture STDOUT and STDERR:

$ ansible-playbook log_path.yml -vvv -i hosts.ini -v 1>logs/ansible.out 2>logs/ansible.err

As a result, no output appears on STDOUT. The output goes to two files:

  • ansible.out
  • ansible.err

So, let’s check the result in both:

$ cat ansible.out
Using /home/vagrant/ansible-projects/ansible.cfg as config file
PLAY [Test logging to file] ****************************************************
TASK [Run a sample shell command] **********************************************
changed: [web1] => {"ansible_facts": ..."changed": true, "cmd": "echo \"This is a test run on web1\"", "delta": "0:00:00.002329", "end": "2025-07-01 05:25:03.761030", "msg": "", "rc": 0, "start": "2025-07-01 05:25:03.758701", "stderr": "", "stderr_lines": [], "stdout": "This is a test run on web1", "stdout_lines": ["This is a test run on web1"]}
PLAY RECAP *********************************************************************
web1                       : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Similarly, we can check the error file:

$ cat ansible.err
[WARNING]: Platform linux on host web1 is using ...
...
...

Thus, redirection can be very convenient as an alternative for logging output.

5. Using the copy Module With the content Parameter

Ansible’s copy module copies a file or a directory from a local host to a remote one. Moreover, with the content parameter, copy can create dynamic log files within playbooks.

Let’s use Ansible’s copy module to write a log file on the remote host.

5.1. Single Task Example

Let’s see an example, where we first save the output of the date command and then write the changes to the remote file:

$ cat copy_module.yml
---
- name: Log message using copy module
  hosts: web
  gather_facts: false
  become: yes
  tasks:
    - name: Prepare log content
      shell: date
      register: datetime
    - name: Write to remote log file
      copy:
        content: |
          Log Entry:
          Host: {{ inventory_hostname }}
          Date: {{ datetime.stdout }}
          Status: Task completed successfully.
        dest: /tmp/ansible_custom_log.txt
        owner: root
        group: root
        mode: '0644'

Several operations run in this playbook:

  • the shell task runs the date command on the remote host and stores the output in the datetime variable
  • the copy task writes log content to the remote directory
  • the content module structures the format for the log file
  • proper file permissions are set to the remote log file

Let’s run the above playbook against the hosts.ini file:

$ ansible-playbook copy_module.yml -v -i hosts.ini
Using /home/vagrant/ansible-projects/ansible.cfg as config file
PLAY [Log message using copy module] *******************************************
TASK [Prepare log content] *****************************************************
...
TASK [Write to remote log file] ************************************************
changed: [web1] => {"changed": true, "checksum": "...."dest": ...
PLAY RECAP *********************************************************************
web1                       : ok=2    changed=2    unreachable=0    failed=0...

At this point, we can check the log file on the remote machine:

$ cat /tmp/ansible_custom_log.txt 
Log Entry:
Host: web1
Date: Tue Jul  1 09:46:13 AM UTC 2025
Status: Task completed successfully.

As a result, we only get relevant information in the log file.

5.2. Full Playbook Example

Let’s use a Jinja2 template file for the log content. This way, we increase maintainability and promote the separation of logic.

To begin with, let’s create a file named log_template.j2 in the Ansible project:

$ cat log_template.j2
Log Entry:
Host: {{ inventory_hostname }}
Date: {{ datetime.stdout }}
Status: Task completed successfully.

Then, we update the previous playbook, copy_module.yml:

$ cat copy_module.yml
---
- name: Log message using copy module
  hosts: web
  gather_facts: false
  become: yes
  tasks:
    - name: Get current date and time
      shell: date
      register: datetime
    - name: Write log entry using template
      template:
        src: log_template.j2
        dest: /tmp/ansible_custom_log.txt
        owner: root
        group: root
        mode: '0644'

Again, let’s run the playbook for the host group web:

$ ansible-playbook copy_module.yml -v -i hosts.ini
PLAY [Log message using copy module] *******************************************
TASK [Get current date and time] ***********************************************
...

At this point, we can check the /tmp/ansible_custom_log.txt file on the remote host:

$ cat /tmp/ansible_custom_log.txt
Log Entry:
Host: web1
Date: Wed Jul  2 06:26:22 AM UTC 2025
Status: Task completed successfully.

As a result, we can see the logs on the remote host. Since the playbook creates a unique file every time, the logs never collide.

6. Using lineinfile for Appending

With Ansible, we can append the result of a specific task directly into a file on the remote host.

6.1. Example Playbook

Let’s write a playbook that stores its output in a log file under /var/log/custom_tasks.log:

$ cat lineinfile.yml
- name: Run shell command and log output using lineinfile
  hosts: web
  become: yes
  tasks:
    - name: Get disk usage
      shell: df -h /
      register: disk_usage
    - name: Log output to file
      lineinfile:
        path: /var/log/custom_tasks.log
        line: "Disk Usage: {{ disk_usage.stdout }}"
        create: yes
        insertafter: EOF

In the above script, the shell module runs the df -h command to check the root partition usage. Then, it registers the command result in a variable.

The lineinfile module has several attributes:

  • path: target file to write the log into
  • line: combines the time and command output into a single string
  • create: ensures the file is created if it doesn’t exist
  • insertafter: EOF: appends the line at the end of the file

Finally, we can access disk_usage.stdout to get the command’s output as a single string.

6.2. Running the Playbook

Let’s run the above playbook:

$ ansible-playbook lineinfile.yml 
PLAY [Run shell command and log output using lineinfile] ***********************
TASK [Gathering Facts] *********
...

As a result, the output of the df -h / command is sent to the remote log file:

$ cat /var/log/custom_tasks.log 
Disk Usage: Filesystem                         Size  Used Avail Use% Mounted on
/dev/mapper/ubuntu--vg-ubuntu--lv   31G  4.0G   25G  14% /

The lineinfile module works particularly well for single-line operations. However, it can create problems while working with a block of lines. In this case, we can use the blockinfile module.

7. Using Callback Plugins

Let’s move on to the callback plugin approach to logging. In this case, Ansible provides real-time updates and summaries of the actions or events as they unfold.

Generally, callback plugins can do several tasks:

  • print a custom message on the host screen
  • send a notification to other applications
  • log the details of the entire operation to a file

Let’s see how we can use callback plugins for logging work.

7.1. Enabling the Default log_plays Plugin

The log_plays is a part of the community.general collection. Moreover, we can install it using ansible-galaxy command:

$ ansible-galaxy collection install community.general

Let’s verify the installation of the log_plays plugin:

$ ansible-doc -t callback community.general.log_plays
> CALLBACK community.general.log_plays (/home/vagrant/.ansible/collections/ansi>
  This callback writes playbook output to a file per host in the
  `/var/log/ansible/hosts' directory.
...

Consequently, the above output shows the plugin is already present.

Moreover, we can check the callback plugin list to ensure log_plays is present on the system:

$ tree ~/.ansible/collections/ansible_collections/community/general/plugins/callback/
/home/vagrant/.ansible/collections/ansible_collections/community/general/plugins/callback/
├── cgroup_memory_recap.py
├── context_demo.py
├── counter_enabled.py
├── default_without_diff.py
├── dense.py
├── diy.py
├── elastic.py
├── jabber.py
├── loganalytics.py
├── logdna.py
├── logentries.py
├── log_plays.py
...

Notably, log_plays writes the output of a playbook run to a file /var/log/ansible/hosts.

7.2. Setting Up ansible.cfg

To use the log_plays plugin, we enable (whitelist) it in ansible.cfg:

$ cat ansible.cfg
[defaults]
inventory = hosts.ini
callback_whitelist = community.general.log_plays
stdout_callback = default # or 'debug' or 'json', etc.

Notably, the JSON callback provides a structured output.

7.3. Setting Up the Log Location

Further, we make sure the hosts directory for saving the logs exists and is writable:

$ sudo mkdir -p /var/log/ansible/hosts
$ sudo chown $(whoami) /var/log/ansible/hosts

Particularly, we receive the entries on a per-host basis.

7.4. Sample Playbook

So, let’s write a simple playbook for the above callback setup:

$ cat test_play.yml 
---
- name: Test Playbook for log_plays callback plugin
  hosts: web
  gather_facts: no
  tasks:
    - name: Create a file to test logging
      ansible.builtin.file:
        path: "/tmp/ansible_test_file"
        state: touch

Consequently, this playbook simply creates a file on the remote host.

7.5. Running the Playbook

The log_plays plugin uses the ANSIBLE_LOG_PLAYS_DIR to know where to write logs.

So, let’s set that variable at runtime:

$ ANSIBLE_LOG_PLAYS_DIR=/var/log/ansible/hosts ansible-playbook -i hosts.ini test_play.yml
{
"custom_stats": {},
"global_custom_stats": {},
"plays": [
{
"play": {
"duration": {
"end": "2025-07-06T11:09:46.444416Z",
"start": "2025-07-06T11:09:45.301734Z"
... 

If the playbook runs correctly, we should see a log file for the target host:

$ ls /var/log/ansible/hosts/ -ll
total 4
-rw-rw-r-- 1 vagrant vagrant 2810 Jul  6 10:28 web1

As a result, a log file, web1, appears on the controller host.

8. Custom Logging With debug and tee

Sometimes Ansible’s built-in logging can be too verbose. For lightweight, task-specific logging, a combination of the debug module, the shell or command module, and the tee command usually works well.

8.1. Using the debug Module as a Logger

The debug module prints any variable to standard output (STDOUT).

Let’s write an example that targets the web group:

$ cat debug_tee.yml
---
- name: Get CPU Information from web servers
  hosts: web
  gather_facts: false
  tasks:
    # This task runs the 'cat' command on the remote host
    - name: Read CPU info from /proc/cpuinfo
      command: cat /proc/cpuinfo
      register: cpu_info_output  # The result is saved here
    # This task prints the 'stdout' part of the saved result
    - name: Display CPU info
      debug:
        msg: "{{ cpu_info_output.stdout }}"

In this playbook, we read the CPU info from the /proc/cpuinfo file. The first task saves the result in a variable. In the same way, the second task prints the result to STDOUT.

8.2. Running the Playbook

Let’s run the above playbook and save the result to a log file, report.log:

$ ansible-playbook debug_tee.yml | sudo tee /var/log/ansible/report.log
PLAY [Get CPU Information from web servers] ************************************
TASK [Read CPU info from /proc/cpuinfo] ****************************************
[WARNING]: Platform linux on host web1 is using the discovered Python
...

Importantly, the sudo command runs the tee command with elevated privileges.

Let’s see the content of the report.log file:

$ cat /var/log/ansible/report.log
< PLAY [Get CPU Information from web servers]... TASK [Read CPU info from /proc/cpuinfo] **************************************** changed: [web1] TASK [Display CPU info] ******************************************************** ok: [web1] => {
...
}
PLAY RECAP *********************************************************************
web1 : ok=2 changed=1 unreachable=0 failed=0
...

Clearly, the logs also redirect to the report.log file.

9. Conclusion

In this article, we saw different ways of logging the output from Ansible to a file:

  • global configuration
  • ad-hoc shell redirection
  • module-driven file manipulation
  • callback plugins

Also, we used different playbook examples to understand each use case and its distinct purpose.