Auditing Ansible at Scale with Structured Action Logging (JSONL)
Why stdout Is Not an Audit Trail
Ansible is excellent at executing tasks but weak at explaining afterwards what actually happened in a machine-readable way.
When managing hundreds of hosts across multiple environments, tracking what changed, when, and why becomes critical for:
- Security audits and compliance
- Change management and rollback planning
- Performance analysis and optimization
- Incident investigation
This article introduces rf_action_logger, an Ansible meta-logger that emits structured JSONL execution events for every action.
The Problem with Traditional Ansible Logging
Standard Ansible output is designed for human readability:
TASK [Ensure SSH config is hardened] **************
changed: [srv-wireguard-01]
ok: [srv-database-01]
failed: [srv-legacy-02] => {"msg": "Permission denied"}
This format is:
- Hard to parse programmatically
- Lacks structured metadata
- Difficult to correlate across runs
- Impossible to query efficiently
Execution as Data
rf_action_logger transforms every task execution into a self-contained JSON record:
{
"timestamp": "2026-01-18T04:12:31.492Z",
"task": "Ensure SSH config is hardened",
"action": "ansible.builtin.template",
"host": "srv-wireguard-01",
"changed": true,
"failed": false,
"duration_ms": 127,
"customer": "c_00000_acme",
"environment": "prod",
"playbook": "security-baseline.yml",
"run_id": "550e8400-e29b-41d4-a716-446655440000"
}
Why JSONL
JSON Lines format offers several advantages:
- Append-only: New events simply append to the file
- Stream-friendly: Process logs line-by-line without loading everything
- Tool compatibility: Works with jq, grep, and standard Unix tools
- SIEM-ready: Direct ingestion into Elasticsearch, Splunk, etc.
- Diffable: Compare runs to see exactly what changed
Real-World Use Cases
1. Compliance Reporting
# Find all changes in production this month
jq 'select(.environment == "prod" and .changed == true)' \
/var/log/ansible/2026-01-*.jsonl
2. Failure Analysis
# Track failure patterns across hosts
jq 'select(.failed == true) |
{task: .task, host: .host, error: .msg}' \
actions.jsonl | sort | uniq -c
3. Performance Monitoring
# Find slow tasks
jq 'select(.duration_ms > 5000) |
{task: .task, duration: .duration_ms}' \
actions.jsonl | sort -k2 -nr
4. Change Tracking
# Generate change report for specific host
jq --arg host "srv-database-01" \
'select(.host == $host and .changed == true)' \
actions.jsonl
Configuration
Enable logging with environment variables:
export RF_ACTION_LOGGER_ENABLED=true
export RF_ACTION_LOGGER_PATH=/var/log/ansible/actions.jsonl
export RF_ACTION_LOGGER_ROTATE=daily
export RF_ACTION_LOGGER_RETAIN_DAYS=90
No playbook changes required — the logger hooks into Ansible's callback system.
Advanced Features
Structured Context
Each log entry includes execution context:
{
"git_commit": "a3f4c2d",
"git_branch": "main",
"operator": "patrick@mpowr-it.com",
"ci_pipeline": "jenkins-123"
}
Sensitive Data Handling
# Mark sensitive results
- name: Generate API token
command: generate-token.sh
no_log: true # rf_action_logger respects this
Custom Fields
# Add metadata to specific tasks
- name: Database migration
command: migrate.sh
vars:
rf_action_logger_meta:
criticality: high
approval_ticket: "CHG-2026-0142"
Integration Examples
Elasticsearch Pipeline
# Stream to Elasticsearch
tail -f actions.jsonl | \
jq -c '. + {[@timestamp]: .timestamp}' | \
curl -X POST "elasticsearch:9200/_bulk" \
--data-binary @-
Prometheus Metrics
# Export metrics
for line in open('actions.jsonl'):
event = json.loads(line)
if event['failed']:
ansible_task_failures.labels(
task=event['task'],
host=event['host']
).inc()
Production Patterns
Log Rotation
/var/log/ansible/*.jsonl {
daily
rotate 90
compress
delaycompress
notifempty
create 0640 ansible ansible
}
Centralized Collection
# Ship logs to central store
- name: Upload execution logs
ansible.builtin.synchronize:
src: "{{ rf_action_logger_path }}"
dest: "rsync://logstore/ansible/{{ inventory_hostname }}/"
mode: push
Performance Considerations
rf_action_logger adds minimal overhead:
- ~1-2ms per task
- Async writes don't block execution
- Configurable buffering for high-frequency tasks
For a typical playbook with 100 tasks across 50 hosts, this adds less than 1 second total.
Security Benefits
- Forensic Analysis: Reconstruct exact execution flow after incidents
- Change Attribution: Link every change to operator and approval
- Anomaly Detection: Spot unusual patterns in automation behavior
- Compliance Evidence: Prove controls were applied consistently
Conclusion
rf_action_logger turns Ansible executions into auditable data, enabling traceability, compliance, and post-run analysis at scale.
For teams running Ansible in regulated environments or at scale, structured logging transforms operations from black-box automation to transparent, auditable infrastructure management.
The plugin is open source and has been proven in environments with strict audit requirements and thousands of managed hosts.
rf_action_logger has been deployed across numerous high-security environments, from financial services to critical infrastructure providers.