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"
Enter fullscreen mode Exit fullscreen mode
Generate the migration:
bin/rails generate rails_audit_log:install
bin/rails db:migrate
Enter fullscreen mode Exit fullscreen mode
Then make a model auditable:
class Article < ApplicationRecord
include RailsAuditLog::Auditable
end
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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"
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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)
Enter fullscreen mode Exit fullscreen mode
Or inspect the previous state associated with a particular entry:
entry.reify
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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?
답글 남기기