The project is in a healthy, maintained state
A lightweight audit trail library for tracking changes to hashes and objects with actor, action, diff, and timestamp support.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

philiprehberger-audit_trail

Tests Gem Version Last updated

Generic audit trail for tracking changes with who, what, and when

Requirements

  • Ruby >= 3.1

Installation

Add to your Gemfile:

gem "philiprehberger-audit_trail"

Or install directly:

gem install philiprehberger-audit_trail

Usage

require "philiprehberger/audit_trail"

tracker = Philiprehberger::AuditTrail::Tracker.new

# Record an event directly
tracker.record(
  entity_id: "user:1",
  entity_type: "User",
  action: :create,
  changes: { name: { from: nil, to: "Alice" } },
  actor: "admin"
)

Automatic Diff and Record

before = { name: "Alice", email: "alice@example.com" }
after  = { name: "Alice", email: "alice@new.com", role: "admin" }

tracker.record_change(
  entity_id: "user:1",
  entity_type: "User",
  before: before,
  after: after,
  actor: "system"
)
# Automatically computes: { email: { from: "alice@example.com", to: "alice@new.com" },
#                           role:  { from: nil, to: "admin" } }

Querying History

# All events for an entity
tracker.history(entity_id: "user:1")

# Filter by entity type
tracker.history(entity_id: "user:1", entity_type: "User")

# All events
tracker.events

Query Builder

# Filter by actor
tracker.query(actor: "admin")

# Filter by action
tracker.query(action: :update)

# Filter by entity_id
tracker.query(entity_id: "user:1")

# Filter by time range
tracker.query(after: Time.now - 86_400, before: Time.now)

# Combine multiple filters
tracker.query(actor: "admin", action: :update, after: Time.now - 7 * 86_400)

Batch Recording

tracker.record_batch([
  { entity_id: "1", entity_type: "User", action: :create, actor: "admin" },
  { entity_id: "2", entity_type: "Post", action: :update, actor: "editor" },
  { entity_id: "3", entity_type: "User", action: :delete, actor: "admin" }
])

Retention Policy

# Remove events older than 90 days
tracker.prune(before: Time.now - 90 * 86_400)

Export

# Export as JSON
json_output = tracker.export(:json)
# => '[{"entity_id":"1","entity_type":"User","action":"create",...}]'

# Export as CSV
csv_output = tracker.export(:csv)
# => "entity_id,entity_type,action,actor,timestamp\n1,User,create,admin,..."

Summary

# Count events grouped by actor
tracker.summary(group_by: :actor)
# => { "admin" => 5, "editor" => 3 }

# Count events grouped by action
tracker.summary(group_by: :action)
# => { create: 4, update: 3, delete: 1 }

# Count events grouped by entity_id
tracker.summary(group_by: :entity_id)
# => { "1" => 3, "2" => 2 }

Hash Diffing

diff = tracker.diff(
  { name: "Alice", age: 30 },
  { name: "Bob", age: 30 }
)
# => { name: { from: "Alice", to: "Bob" } }

Pluggable Storage

# Default: in-memory store
tracker = Philiprehberger::AuditTrail::Tracker.new

# Custom store (must respond to push, push_all, select, reject!, all, clear!, size)
tracker = Philiprehberger::AuditTrail::Tracker.new(store: MyCustomStore.new)

API

Method / Class Description
Tracker.new(store:) Create a tracker with pluggable storage (default: MemoryStore)
Tracker#record(entity_id:, entity_type:, action:, ...) Record an audit event
Tracker#record_change(entity_id:, entity_type:, before:, after:, ...) Diff and record an update event
Tracker#record_batch(entries) Record multiple events in one call
Tracker#diff(before, after) Compute field-level diff between two hashes
Tracker#history(entity_id:, entity_type:) Query events by entity
Tracker#query(actor:, action:, entity_id:, after:, before:) Filter events by multiple criteria
Tracker#prune(before:) Delete events older than the specified time
Tracker#export(format) Export events as :json or :csv
Tracker#summary(group_by:) Aggregate counts by :actor, :action, or :entity_id
Tracker#events Return all stored events
Tracker#clear! Remove all events
Event.new(entity_id:, entity_type:, action:, ...) Create an audit event
Event#to_h Hash representation of the event
Differ.call(before:, after:) Compute diff between two hashes
MemoryStore.new Thread-safe in-memory event store
MemoryStore#push(event) Append an event
MemoryStore#push_all(events) Append multiple events
MemoryStore#select(&block) Filter events
MemoryStore#reject!(&block) Remove matching events
MemoryStore#all Return all events
MemoryStore#clear! Remove all events
MemoryStore#size Count of events

Development

bundle install
bundle exec rspec
bundle exec rubocop

Support

If you find this project useful:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT