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.
Last updated: July 15, 2025
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.
Before we move on, we need to check for some prerequisites:
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.
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.
The ansible.cfg file controls the runtime behavior of Ansible. In essence, it looks for the config files in a specific order:
In the current case, we use the config file placed in the project directory.
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.
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.
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:
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.
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.
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:
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.
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.
With Ansible, we can append the result of a specific task directly into a file on the remote host.
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:
Finally, we can access disk_usage.stdout to get the command’s output as a single string.
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.
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:
Let’s see how we can use callback plugins for logging work.
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.
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.
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.
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.
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.
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.
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.
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.
In this article, we saw different ways of logging the output from Ansible to a file:
Also, we used different playbook examples to understand each use case and its distinct purpose.