Rethinking Audit Logging in Rails

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 the use of 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 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?