Project

provenance

0.0
The project is in a healthy, maintained state
Provenance records user actions and ActiveRecord model changes in Rails applications. It groups changes per request and transaction, sanitizes sensitive data, tracks bulk operations and has_and_belongs_to_many changes, and ships structured audit events to any sink through configurable hooks.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

 Project Readme

๐Ÿ“œ Provenance

A drop-in audit trail for Rails โ€” every user action and model change, captured.

CI Gem Version License: MIT Ruby


Provenance watches your Rails app and records who did what, to which record, when โ€” then ships a structured event anywhere you want: a log pipeline, a data warehouse, an external SIEM, or just Rails.logger. You wire it in once; it stays out of your controllers and models.

# An audit event Provenance produces for a successful request
{
  "event_type": "create_users",
  "status": 201,
  "username": "admin@example.com",
  "remote_ip": "203.0.113.1",
  "message": {
    "count": 1,
    "changes": [
      { "model": "User", "model_id": 42, "action": "create",
        "changes": { "attributes": { "email": "user@example.com", "password": "[FILTERED]" } } }
    ]
  },
  "source": "myapp_production"
}

โœจ Features

  • ๐Ÿงพ Model change tracking โ€” create / update / destroy captured straight from ActiveRecord callbacks.
  • ๐ŸŒ Controller auditing โ€” one around_action records every change made during a request and emits a single event.
  • ๐Ÿงฌ Transaction-aware โ€” changes are grouped per transaction, flushed only after every transaction commits, and discarded on rollback.
  • ๐Ÿ’ฅ Error reporting โ€” emit a dedicated audit event for failed requests with audit_error.
  • ๐Ÿ—‚๏ธ Bulk operations โ€” opt-in tracking for update_all / delete_all, which normally bypass callbacks.
  • ๐Ÿ”— has_and_belongs_to_many โ€” join-table writes are tracked through SQL notifications.
  • ๐Ÿ›ก๏ธ Sensitive-data filtering โ€” recursive [FILTERED] redaction with global and per-model attribute lists.
  • ๐Ÿ”Œ Pluggable providers & hooks โ€” decide how to resolve the actor and where events are delivered.
  • ๐Ÿชถ Fail-safe โ€” auditing never breaks the underlying request or operation.

๐Ÿ“ฆ Installation

Add it to your Gemfile:

gem "provenance"

Then install:

bundle install

๐Ÿš€ Quick start

1. Configure the initializer

# config/initializers/provenance.rb
require "provenance"

Provenance.configure do |config|
  config.source_name = "myapp_#{Rails.env}"
  config.sensitive_attributes = %w[password password_confirmation token secret_key api_key]

  # Auditing is disabled in the test environment by default. Override if needed:
  # config.enabled = true
end

# How to resolve the actor and request metadata (each receives the controller):
Provenance.setup_username_provider(->(controller) { controller.current_user&.email })
Provenance.setup_roles_provider(->(controller) { controller.current_user&.roles || [] })
Provenance.setup_remote_ip_provider(->(controller) { controller.request.remote_ip })
Provenance.setup_origin_ip_provider(->(controller) { ENV["SERVER_IP"] || "127.0.0.1" })
Provenance.setup_session_id_provider(->(controller) { controller.request.headers["Authorization"]&.split(" ")&.last })

# Where audit events go (you can register more than one hook):
Provenance.config.add_audit_hook do |audit_data|
  Rails.logger.info("AUDIT: #{audit_data.to_json}")
end

2. Mix the concerns in

# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  include Provenance::Auditable       # records changes per request
  include Provenance::ErrorReporting  # adds audit_error(error, status)
end
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  include Provenance::Trackable
end

That's it. Every write that happens inside a request now produces an audit event.

๐Ÿงญ How it works

Request โ”€โ–ถ Auditable (around_action)
              โ”‚  opens a Journal for this request
              โ–ผ
        Trackable callbacks  โ”€โ”€โ–ถ  Journal  โ—€โ”€โ”€  BulkOperations / HABTM SQL
        (create/update/destroy)   (grouped by transaction)
              โ”‚
              โ–ผ
        all transactions committed?
              โ”‚ yes                       โ”‚ rollback
              โ–ผ                           โ–ผ
        audit hooks receive          changes discarded
        one assembled event

The Journal lives in fiber-local storage for the duration of the request, so concurrent requests never see each other's changes.

๐Ÿ’ฅ Error logging

Call audit_error from your rescue handlers to record failures:

def render_errors(errors, status: :unprocessable_entity)
  audit_error(errors, status)
  render json: { errors: Array(errors) }, status: status
end

rescue_from ActiveRecord::RecordNotFound do
  audit_error("Not found", :not_found)
  head :not_found
end

Symbolic statuses (:not_found, :unauthorized, :forbidden, :unprocessable_entity, :conflict) are mapped to their numeric codes automatically; anything else defaults to 500.

๐Ÿ›ก๏ธ Sensitive data filtering

Filtered values are replaced with [FILTERED] โ€” in model attributes, request params, and nested hashes/arrays alike.

# Global (applies everywhere)
Provenance.configure do |config|
  config.sensitive_attributes = %w[password token api_key]
end

# Per-model (takes priority over the global list)
class Payment < ApplicationRecord
  sensitive_attributes :card_number, :cvv, :token
end
# in                                  # out
{ user: {                            { user: {
    email: "user@example.com",           email: "user@example.com",
    password: "secret",                  password: "[FILTERED]",
    profile: { api_key: "abc123" }       profile: { api_key: "[FILTERED]" }
} }                                  } }

๐Ÿ—‚๏ธ Bulk operations

update_all and delete_all skip ActiveRecord callbacks, so they are tracked separately and must be enabled explicitly:

Provenance.configure do |config|
  config.track_bulk_operations = true   # default: false
  config.bulk_operations_max_ids = 1000 # cap ids per record; over the cap sets truncated: true
end

The affected ids are collected with a pluck before the statement runs, so factor in one extra query on large result sets. Operations outside an HTTP request (migrations, rake tasks, background jobs) are not tracked. A bulk change looks like:

{
  model: "Comment",
  model_ids: ["101", "102"],
  action: "bulk_update",      # or "bulk_delete"
  count: 2,
  changes: { status: "deleted", deleted_at: "2026-06-05T12:00:00Z" }
}

๐Ÿ”— has_and_belongs_to_many

Join-table inserts and deletes never trigger model callbacks, so Provenance observes them through sql.active_record notifications and folds them into the owner's change as an *_ids update. No extra setup is required beyond including Provenance::Trackable in the participating models.

Note: the SQL reconstruction for HABTM is tuned for PostgreSQL bind placeholders ($1, $2). Insert tracking is portable; delete tracking depends on that placeholder style.

โš™๏ธ Fine-tuning

Skip auditing per action

class UsersController < ApplicationController
  skip_audit_logging :index, :show       # no event at all
  skip_model_change_tracking :index      # event, but without model diffs
end

Custom event types

class SessionsController < ApplicationController
  custom_audit_event_type :create, "user_login"
  custom_audit_event_type :destroy, "user_logout"
end

Automatic event-type generation

When you don't override it, Provenance derives the event type from the controller and action:

Action Event type Example (UsersController)
index read_{controller} read_users
show show_{singular} show_user
create create_{controller} create_users
update update_{singular} update_user
destroy destroy_{singular} destroy_user
custom action {action}_{controller} archive_users

Namespaced controllers are flattened with _, e.g. Admin::UsersController#index โ†’ read_admin_users.

Delivery hooks

Provenance.config.add_audit_hook { |event| ExternalAuditService.deliver(event) }
Provenance.config.add_audit_hook { |event| Rails.logger.info("AUDIT: #{event.to_json}") }
Provenance.config.clear_audit_hooks  # remove all hooks

๐Ÿ”ง Configuration reference

Option Default Description
source_name "app_#{Rails.env}" Identifies the emitting application in every event.
sensitive_attributes [] Global attribute names to redact.
enabled !Rails.env.test? Master switch for the whole pipeline.
track_bulk_operations false Track update_all / delete_all.
bulk_operations_max_ids 1000 Max ids recorded per bulk change.
audit_hooks [] Delivery callbacks (use add_audit_hook).

Providers: username, roles, remote_ip, origin_ip, session_id โ€” each set via Provenance.setup_<name>_provider(callable).

๐Ÿ“ Event structure

Successful request

{
  "timestamp": "2026-06-05T12:00:00.000Z",
  "event_type": "create_users",
  "status": 201,
  "message": {
    "count": 1,
    "changes": [
      {
        "model": "User",
        "model_id": 123,
        "action": "create",
        "changes": { "attributes": { "email": "user@example.com", "name": "John Doe" } },
        "timestamp": "2026-06-05T12:00:00.000Z"
      }
    ],
    "params": { "user": { "email": "user@example.com" } }
  },
  "username": "admin@example.com",
  "remote_ip": "203.0.113.1",
  "origin_ip": "192.168.1.100",
  "session_id": "token123",
  "roles": ["admin"],
  "request_id": "req-123",
  "source": "myapp_production"
}

Failed request (via audit_error)

{
  "timestamp": "2026-06-05T12:00:00.000Z",
  "event_type": "create_users",
  "status": "422",
  "message": {
    "error_type": "ActiveRecord::RecordInvalid",
    "error_message": "Validation failed: Email has already been taken",
    "params": { "user": { "email": "invalid" } }
  },
  "username": "admin@example.com",
  "source": "myapp_production"
}

๐Ÿงช Development

bundle install
bundle exec rspec     # run the test suite
bundle exec rubocop   # lint

๐Ÿค Contributing

Bug reports and pull requests are welcome โ€” see CONTRIBUTING.md.

๐Ÿ“„ License

Released under the MIT License.