Skip to content

Instantly share code, notes, and snippets.

@Envek
Created August 6, 2025 03:58
Show Gist options
  • Save Envek/c82dac248f97338a4c4c9e28529c94af to your computer and use it in GitHub Desktop.
Save Envek/c82dac248f97338a4c4c9e28529c94af to your computer and use it in GitHub Desktop.
Audit log for ActiveAdmin. Logs all non-read actions made via ActiveAdmin UI, works on controller level, tracks resource changes.

Audit log for ActiveAdmin. Logs all non-read actions made via ActiveAdmin UI, works on controller level, tracks resource changes.

Installation:

  1. Create database table and model to store audit log entries
  2. Create controller concern module AdminAuditable to hook into ActiveAdmin controllers
  3. Prepend it to all controllers on a resource declaration in ActiveAdmin initializer
  4. Create an admin panel page that will display audit log entries
  5. Enjoy!
# config/initializers/active_admin.rb
ActiveSupport.on_load(:active_admin_controller) do
ActiveSupport::Notifications.subscribe ActiveAdmin::Resource::RegisterEvent do |event|
event.payload[:active_admin_resource].controller.prepend AdminAuditable
end
end
# app/admin/admin_audit_log.rb
ActiveAdmin.register Admin::AuditLog, as: "Admin Audit Log" do
menu label: "Admin Audit Log", parent: :menu_settings
actions :index, :show
remove_filter :resource_changes, :params, :ip_address, :user_agent, :session_id, :controller, :action, :username
config.sort_order = "created_at_desc"
index title: "Audit Log", download_links: [:csv], pagination_total: false do
column "Time", sortable: :created_at do |log_entry|
link_to(log_entry.created_at.to_fs(:rfc822), admin_admin_audit_log_path(log_entry))
end
column :user do |log_entry|
next "#{log_entry.username} (deleted)" if log_entry.user.nil?
link_to(log_entry.username, log_entry.user)
end
column :action, sortable: false do |log_entry|
"#{log_entry.controller} # #{log_entry.action}"
end
column :resource do |log_entry|
next unless log_entry.resource_type && log_entry.resource_id
resource_name = log_entry.resource_type.underscore.humanize
next "#{resource_name} (deleted)" unless log_entry.resource
link_to("#{resource_name} #{log_entry.resource.to_param}", [:admin, log_entry.resource])
end
end
show title: ->(log_entry) { "Event ##{log_entry.id}" } do
attributes_table do
row :resource_type do |log_entry|
log_entry.resource_type&.underscore&.humanize
end
row :resource_id do |log_entry|
next unless log_entry.resource
link_to(log_entry.resource_id, [:admin, log_entry.resource])
end
row :action do |log_entry|
"#{log_entry.controller} # #{log_entry.action}"
end
row :user do |log_entry|
next "#{log_entry.username} (deleted)" if log_entry.user.nil?
link_to(log_entry.username, log_entry.user)
end
row :resource_changes do |log_entry|
table_for log_entry.resource_changes.to_a, style: "overflow-x:scroll;" do
column :attribute do |attribute, _changes|
attribute
end
column :before do |_attribute, changes|
changes[0].to_json
end
column :after do |_attribute, changes|
changes[1].to_json
end
end
end
row :params do |log_entry|
log_entry.params.to_json
end
row :ip_address do |log_entry|
log_entry.ip_address
end
row :user_agent do |log_entry|
log_entry.user_agent
end
row :session_id do |log_entry|
span log_entry.session_id
if log_entry.session_id != session.id && Session.exists?(session_id: log_entry.session_id)
small \
link_to "Revoke",
revoke_session_admin_user_path(log_entry.user, session_id: log_entry.session_id),
method: :delete,
data: {confirm: "Are you sure you want to revoke this session?"}
end
end
end
end
end
# app/controllers/concerns/admin_auditable.rb
module AdminAuditable
extend ActiveSupport::Concern
prepended do
before_action :initialize_audit_log, except: audit_ignored_actions
after_action :persist_audit_log, except: audit_ignored_actions
end
class_methods do
def audit_ignored_actions
return super if superclass.respond_to?(__method__)
%i[index new show edit]
end
def audit_ignored_changes
ignores = superclass.respond_to?(__method__) ? superclass.audit_ignored_changes : []
ignores + %w[updated_at]
end
end
protected
# As many custom actions don't use activeadmin's `update_resource` to change the resource
# we can't use activeadmin's callbacks and need to hook into the resource finding process to track changes
def find_resource
super.tap { |resource| audit_resource(resource) }
end
def build_resource
super.tap { |resource| audit_resource(resource) }
end
def audit_resource(resource)
audit_log = initialize_audit_log
audit_log.resource = resource
activeadmin_ctrl = self
resource.singleton_class.after_commit do
changes = destroyed? ? attributes.map { |k, v| [k, [v, nil]] }.to_h : previous_changes
audit_log.resource_changes = changes.except(*activeadmin_ctrl.class.audit_ignored_changes)
end
end
IGNORED_PARAMS = %w[controller action format id _method authenticity_token]
def initialize_audit_log
@audit_log ||= Admin::AuditLog.new(
user: current_user,
username: current_user.username,
controller: controller_path,
action: action_name,
params: request.filtered_parameters.except(*IGNORED_PARAMS),
ip_address: request.remote_ip,
user_agent: request.user_agent,
session_id: request.session.id,
)
end
def persist_audit_log
@audit_log&.save!
end
end
# app/models/admin/audit_log.rb
module Admin
class AuditLog < ApplicationRecord
self.table_name = "admin_audit_logs"
belongs_to :user
belongs_to :resource, polymorphic: true, optional: true
end
end
# db/migrate/20250806000000_create_admin_audit_log.rb
class CreateAdminAuditLog < ActiveRecord::Migration[8.0]
def change
create_table :admin_audit_logs, id: :uuid do |t|
t.references :user, foreign_key: {on_delete: :nullify, on_update: :cascade}, index: false
t.string :username, null: false
t.references :resource, polymorphic: true, index: false
t.string :controller, null: false
t.string :action, null: false
t.jsonb :params, null: false, default: {}
t.jsonb :resource_changes, null: false, default: {}
t.inet :ip_address, null: false
t.string :user_agent, null: false
t.string :session_id, null: false
t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
end
add_index :admin_audit_logs, [:user_id, :created_at], order: {created_at: :desc}
add_index :admin_audit_logs, [:resource_type, :resource_id, :created_at], order: {created_at: :desc}
add_index :admin_audit_logs, [:created_at], order: {created_at: :desc}
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment