DEV Community

Cover image for Rethinking Audit Logging in Rails
Chuck
Chuck

Posted on

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"
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?

Top comments (0)