Rethinking Audit Logging in Rails: Building a Modern Alternative to PaperTrail
Audit trails are one of those features most applications eventually need.
Whether it's compliance, debugging production issues, understanding who changed a record, or reconstructing a historical state, having a reliable change history becomes incredibly valuable.
For years, PaperTrail has been the default solution in the Rails ecosystem. It's mature, battle-tested, and widely adopted.
But after using PaperTrail across multiple projects, I found myself wanting something simpler, more modern, and easier to reason about.
That led me to build rails_audit_log.
The Problem with Traditional Audit Logging
Audit logging sounds straightforward:
Record who changed something, when they changed it, and what changed.
In practice, things get complicated.
- Applications eventually need:
- Actor tracking
- Historical reconstruction
- Metadata
- Bulk imports
- Multi-tenancy
- Data retention policies
- Encryption
- Async writes
- Dashboarding
- API access
Over time, I found myself writing additional code around PaperTrail to support these concerns.
I also wanted:
- JSON instead of YAML serialization.
- Better storage efficiency.
- Faster writes.
- Easier querying.
- A cleaner API.
- A migration path from existing PaperTrail applications.
Introducing rails_audit_log
rails_audit_log records create, update, and destroy events as structured JSON entries.
The goal wasn't to replace every feature of PaperTrail.
The goal was to provide a modern, Rails-native approach with sensible defaults and a simpler mental model.
Features include:
- Structured JSON storage
- Automatic actor tracking
- Time-travel reconstruction
- Batch writes
- Async writes
- Retention policies
- Encryption support
- Multi-tenant support
- Event streaming
- Built-in web dashboard
- Testing helpers
- Migration tools for PaperTrail users
Installation
Getting started is intentionally simple.
Add the gem:
gem "rails_audit_log"
Generate the migration:
bin/rails generate rails_audit_log:install
bin/rails db:migrate
Then make a model auditable:
class Article < ApplicationRecord
include RailsAuditLog::Auditable
end
That's it.
Every create, update, and destroy is automatically recorded.┄
Tracking Who Made Changes
Most audit systems are only useful if they answer:
Who changed this?
Adding actor tracking requires a single declaration:
class ApplicationController < ActionController::Base
include RailsAuditLog::Controller
audit_log_actor { current_user }
end
Every audit entry automatically captures:
- Actor type
- Actor ID
- Display name snapshot
This ensures audit records remain meaningful even if the original user record is deleted.
A Built-In Dashboard
One feature I always wished audit libraries provided out of the box was a way to browse changes.
Instead of requiring a custom admin interface, rails_audit_log it ships with a mountable dashboard:
mount RailsAuditLog::Engine, at: "/audit"
Visiting /audit provides a searchable history of all changes without any additional setup.
Bulk Operations Without N+1 Inserts
Large imports expose an inefficiency common to audit systems.
If 50 records are created, traditional approaches often perform 50 additional insert statements.
batch_audit buffers entries and writes them in a single operation:
RailsAuditLog.batch_audit do
records.each do |attrs|
Post.create!(attrs)
end
end
This dramatically reduces database overhead during imports and batch jobs.
Historical Reconstruction
Audit logs become much more powerful when they allow you to answer questions like:
What did this record look like last week?
Reconstructing the state is straightforward:
snapshot = RailsAuditLog.version_at(article, 1.week.ago)
Or inspect the previous state associated with a particular entry:
entry.reify
This makes debugging and forensic analysis much easier.
Storage Efficiency
One of the biggest differences between rails_audit_log and PaperTrail is that rails_audit_log uses JSON instead of YAML.
Benchmarking showed:
- Approximately 60% smaller entries.
- Faster writes.
- Simpler querying.
- Less storage overhead.
As audit tables grow into millions of rows, those savings become significant.
Performance
Benchmark comparisons against PaperTrail showed encouraging results.
Create throughput
rails_audit_log was approximately 18% faster.
Update throughput
Around 32% faster than PaperTrail.
Query performance
Fetching the latest 25 entries was roughly 23% faster.
Batch inserts
Using batch_audit, throughput doubled compared to PaperTrail.
These gains come largely from:
- JSON serialization
- Fewer metadata lookups
- Bulk inserts
- Avoiding YAML overhead
Multi-Tenancy and Retention
Applications often need more than an infinite append-only history.
rails_audit_log includes:
- Per-record version limits
- Time-based retention policies
- Scheduled pruning
- Multi-tenant isolation
allowing audit history to remain useful without growing indefinitely.
Encryption Support
Some audit records contain sensitive information.
For applications using Rails 7.1+, audit data can be encrypted with ActiveRecord Encryption:
class Payment < ApplicationRecord
include RailsAuditLog::Auditable
audit_log encrypt: true
end
Decryption is transparent, allowing existing APIs to continue working normally.
Event Streaming
Another feature I wanted was the ability to treat audit logs as events.
Every entry can be streamed to external systems through adapters.
This makes it possible to publish audit events to:
- ActiveSupport::Notifications
- ActiveJob
- Kafka
- SQS
- Custom transports
without changing the application code.
Migrating from PaperTrail
One of the design goals was to make migration easy.
rails_audit_log includes:
- A migration generator.
- Conversion from YAML to JSON.
- Compatibility helpers.
- Familiar APIs.
Applications can move gradually instead of rewriting everything at once.
Why I Built It
PaperTrail remains an excellent library and has served the Rails community well for years.
But I wanted something that embraced modern Rails conventions:
- JSON-first storage.
- Simpler APIs.
- Better performance.
- Built-in tooling.
- Easier extensibility.
- Lower storage overhead.
rails_audit_log is the result.
It aims to make audit logging feel like a natural part of a Rails application instead of another subsystem developers have to build around.
And hopefully, it makes answering one very important question easier:
What changed, who changed it, and when?
Top comments (0)