class CreateAuditLogs < ActiveRecord::Migration[6.1]
def change
create_table :audit_logs do |t|
t.references :user, null: true, foreign_key: true
t.string :action, null: false
t.string :resource_type
t.bigint :resource_id
t.jsonb :metadata, default: {}
t.jsonb :changes, default: {}
t.inet :ip_address
t.string :user_agent
t.timestamp :created_at, null: false
end
add_index :audit_logs, [:resource_type, :resource_id]
add_index :audit_logs, :action
add_index :audit_logs, :created_at
add_index :audit_logs, :metadata, using: :gin
end
end
class AuditLog < ApplicationRecord
belongs_to :user, optional: true
validates :action, presence: true
def self.log(action:, user: nil, resource: nil, changes: {}, metadata: {}, request: nil)
create!(
user: user,
action: action,
resource_type: resource&.class&.name,
resource_id: resource&.id,
changes: changes,
metadata: metadata,
ip_address: request&.remote_ip,
user_agent: request&.user_agent
)
end
end
class UpdateUserRoleService
def initialize(user:, new_role:, performed_by:, request:)
@user = user
@new_role = new_role
@performed_by = performed_by
@request = request
end
def call
old_role = @user.role
@user.update!(role: @new_role)
AuditLog.log(
action: 'user.role_changed',
user: @performed_by,
resource: @user,
changes: { role: [old_role, @new_role] },
metadata: { reason: 'admin_action' },
request: @request
)
Result.success(user: @user)
rescue StandardError => e
Result.failure(error: e.message)
end
end
Audit logs provide accountability and forensic capabilities for sensitive operations like permission changes, data deletion, or financial transactions. I store audit events in a dedicated table with who performed the action, what changed, when it occurred, and the request context (IP, user agent). For data changes, I use paper_trail gem to track all versions of critical models automatically. Each audit entry includes a JSON payload with before/after states so I can reconstruct history or implement undo functionality. Audit logs are write-only and never deleted—I archive old entries to cold storage but retain them indefinitely for compliance. For GDPR, I pseudonymize user identifiers while maintaining the ability to reconstruct events.