AuditLogger
AuditLogger e uma gem Rails para auditar alteracoes em models ActiveRecord com:
- tabela propria de auditoria
- integracao simples por
auditable - payload bruto e payload humanizado
- configuracao global por initializer
- override por model
Compatibilidade
No estado atual da gem, as versoes suportadas sao:
- Ruby
>= 3.1.0 - Rails / ActiveRecord
>= 7.0e< 9.0
Em outras palavras, a gem foi preparada para funcionar com projetos Rails 7 e Rails 8, desde que a versao do Ruby seja compativel.
Visao Geral
Ao adicionar auditable em uma model, a gem registra eventos de:
createupdatedestroy
Os registros sao persistidos na tabela audit_logs com:
- identificacao da model auditada
- tipo da acao
- uuid de correlacao
- dados do ator que executou a acao
- payload bruto da alteracao
- payload humanizado para exibicao
Instalacao Passo A Passo
Se esta for a primeira vez que voce vai usar a gem, siga exatamente esta ordem.
Passo 1 - Adicionar a gem no projeto Rails
No Gemfile da aplicacao cliente, adicione:
gem "audit_log_rails"
Observacao importante:
- o nome publicado da gem e
audit_log_rails - o namespace Ruby continua sendo
AuditLogger - por isso, no
Gemfilevoce instalaaudit_log_rails, mas no codigo continua usandoAuditLogger
Depois rode:
bundle install
Passo 2 - Gerar os arquivos iniciais da gem
Depois que a gem estiver instalada, rode:
bin/rails generate audit_logger:install
Esse comando e importante porque ele cria automaticamente os arquivos base que o projeto precisa para comecar.
Passo 3 - Entender o que a gem criou
Ao rodar bin/rails generate audit_logger:install, a gem cria:
db/migrate/..._create_audit_logs.rbconfig/initializers/audit_logger.rb
Ou seja:
- voce nao precisa criar manualmente o initializer da gem do zero
- voce nao precisa escrever manualmente a migration base do zero
- a gem ja entrega esses arquivos como ponto de partida
Passo 4 - Rodar a migration
Depois que os arquivos forem gerados, rode:
bin/rails db:migrate
Esse comando cria a tabela audit_logs no banco da aplicacao.
Passo 5 - Ajustar o initializer ao seu projeto
Depois que o arquivo config/initializers/audit_logger.rb for criado, voce deve abrir esse arquivo e adaptar os resolvers para a realidade do seu sistema.
Em outras palavras:
- a gem cria o arquivo para voce
- mas voce precisa ajustar o conteudo conforme seu
Current.user,Current.request, sessao, perfis, tenants e assim por diante
O Que O Generator Cria Na Pratica
Migration
A migration criada pela gem prepara a tabela audit_logs, onde os registros de auditoria serao persistidos.
Initializer
O initializer criado pela gem e o lugar onde voce diz:
- quem e o usuario atual
- qual id deve ser salvo em
changed_by_id - qual tipo deve ser salvo em
changed_by_type - quais metadados vao para
changed_by_other - como o
uuidsera resolvido - como o IP sera obtido
- quais atributos devem ser ignorados
- como a humanizacao deve funcionar
Estrutura Da Tabela
A migration inicial cria a tabela audit_logs com os campos:
model_class_nameid_objectactionuuidchanged_by_idchanged_by_typechanged_by_otheraudited_changesaudited_changes_humanizeip_remotecreated_atupdated_atdeleted_at
Configuracao Global Explicada
Depois de rodar o generator, a gem cria automaticamente o arquivo:
config/initializers/audit_logger.rb
Voce nao precisa criar esse arquivo manualmente do zero.
O papel desse arquivo e ensinar a gem a descobrir informacoes do seu sistema.
Importante:
- esse arquivo e criado automaticamente pela gem quando voce roda o generator
- os exemplos relacionados a
Current.user,Current.requesteCurrent.audit_uuidsao apenas sugestoes - voce precisa descomentar e adaptar somente o que fizer sentido no seu projeto
Se o seu sistema usa outro contexto, por exemplo Current.admin, Current.account, session[:jwt] ou qualquer outro objeto, basta trocar nos resolvers.
Exemplo do initializer:
AuditLogger.configure do |config|
# Descomente apenas o que fizer sentido no seu projeto.
# A gem nao tem como adivinhar sozinha onde voce guarda usuario logado,
# request, tenant, sessao ou token.
# config.changed_by_id_resolver = -> { Current.user&.id }
# config.changed_by_type_resolver = -> { Current.user&.class&.name }
#
# config.changed_by_other_resolver = lambda do
# {
# name: Current.user&.name,
# email: Current.user&.email,
# request_id: Current.request_id
# }.compact
# end
#
# config.uuid_resolver = -> { Current.audit_uuid }
# config.ip_resolver = -> { Current.request&.remote_ip }
# humanize serve para utilizar traduções do i18n de forma que os attributes do model seja traduzidos humanizando a leitura.
config.humanize_by_default = true
# Configura o padrão do scope do i18n para a humanização dos atributos do model.
config.i18n_scopes = ["activerecord.attributes", "attributes"]
# Atributos que devem ser ignorados na auditoria.
# Por exemplo, `created_at`, `updated_at`, `lock_version` e outros campos de auditoria.
config.ignored_attributes = [:created_at, :updated_at, :lock_version]
# Configura a humanização dos atributos do model.
config.humanizer = ->(_model_klass, _attribute, _old_value, _new_value) { nil }
end
O Que Esse Arquivo Faz
Esse bloco:
AuditLogger.configure do |config|
...
end
serve para configurar o comportamento global da gem no projeto inteiro.
Tudo que estiver aqui vira o comportamento padrao da auditoria, a menos que uma model sobrescreva algo localmente.
Explicando Cada Configuracao
config.changed_by_id_resolver
Exemplo:
config.changed_by_id_resolver = -> { Current.user&.id }
Esse resolver diz para a gem qual valor deve ser salvo na coluna changed_by_id.
Na pratica:
- se o usuario logado for
Current.user - a gem vai salvar
Current.user.id
Se no seu sistema o ator principal for outro, voce troca aqui.
Exemplo:
config.changed_by_id_resolver = -> { Current.admin&.id }
config.changed_by_type_resolver
Exemplo:
config.changed_by_type_resolver = -> { Current.user&.class&.name }
Esse resolver diz qual valor sera salvo em changed_by_type.
Voce pode usar isso para salvar:
- nome da classe
- perfil
- role
- tipo de usuario
Exemplo:
config.changed_by_type_resolver = -> { Current.user&.profile }
config.changed_by_other_resolver
Exemplo:
config.changed_by_other_resolver = lambda do
{
name: Current.user&.name,
email: Current.user&.email,
request_id: Current.request_id
}.compact
end
Esse resolver preenche o campo changed_by_other, que e um JSON livre.
Use esse campo quando quiser guardar metadados extras, por exemplo:
- nome do usuario
- request id
- tenant
- school_id
- url
- user_agent
Esse campo existe justamente para voce nao ficar preso so a changed_by_id e changed_by_type.
config.uuid_resolver
Exemplo:
config.uuid_resolver = -> { Current.audit_uuid }
Esse resolver define o uuid do log.
Esse uuid e util para correlacionar varios logs da mesma sessao ou do mesmo fluxo.
Se esse resolver nao retornar valor, a gem gera um UUID automaticamente.
config.ip_resolver
Exemplo:
config.ip_resolver = -> { Current.request&.remote_ip }
Esse resolver informa qual IP deve ser salvo em ip_remote.
Se voce nao quiser salvar IP, pode deixar nil.
config.humanize_by_default
Exemplo:
config.humanize_by_default = true
Se estiver true, a gem tenta gerar audited_changes_humanize por padrao.
Se estiver false, a gem continua gravando o payload bruto, mas nao humaniza automaticamente.
config.i18n_scopes
Exemplo:
config.i18n_scopes = ["activerecord.attributes", "attributes"]
Esses sao os escopos que a gem usa para tentar traduzir o nome dos atributos.
Por exemplo, ao auditar Student.status, a gem tenta procurar traducoes nesses caminhos.
config.ignored_attributes
Exemplo:
config.ignored_attributes = [:created_at, :updated_at, :lock_version]
Aqui voce informa quais atributos nao devem entrar na auditoria por padrao.
Isso e util para evitar ruido.
config.humanizer
Exemplo:
config.humanizer = ->(_model_klass, _attribute, _old_value, _new_value) { nil }
Esse e um humanizer global opcional.
Ele existe para casos em que voce quer controlar manualmente como um campo sera apresentado.
Importante:
- se ele retornar
nil, a gem usa o fallback padrao com I18n - se ele retornar um
Hash, esse hash e usado para montar o payload humanizado - se ele retornar um valor simples, a gem usa esse valor como representacao humanizada
Exemplo Real Com Current
Uma forma comum de integrar a gem no app cliente e usar CurrentAttributes.
Se o seu projeto ainda nao tiver um Current, voce pode criar algo assim:
class Current < ActiveSupport::CurrentAttributes
attribute :user, :request, :audit_uuid, :request_id
end
Esse objeto serve como um lugar central para guardar o contexto atual da requisicao.
Depois, no controller base da aplicacao, voce pode preencher esses dados:
class ApplicationController < ActionController::Base
before_action :store_current_context
private
def store_current_context
Current.user = current_user
Current.request = request
Current.request_id = request.request_id
Current.audit_uuid ||= session[:audit_uuid] ||= SecureRandom.uuid
end
end
O objetivo desse passo e simples:
- deixar
Current.userdisponivel para a gem - deixar
Current.requestdisponivel para a gem - manter um
audit_uuidestavel dentro da sessao
Se o seu sistema ja tiver outra estrategia para isso, nao precisa copiar exatamente esse exemplo.
Ativando A Auditoria Na Model
Depois da instalacao e da configuracao global, voce ativa a auditoria na model com auditable.
Exemplo:
class Student < ApplicationRecord
auditable
end
So isso ja faz a gem registrar:
- criacao
- atualizacao
- remocao
Consultando Os Logs Da Model
Ao usar auditable, a gem tambem define automaticamente a associacao:
student.audit_logs
Ou seja, voce nao precisa criar manualmente um has_many :audit_logs basico para comecar.
Exemplo:
student = Student.find(1)
student.audit_logs
Essa associacao ja filtra:
model_class_nameid_object
Assim, cada model passa a enxergar apenas os logs que pertencem a ela.
Exemplo Pratico
student = Student.create!(name: "Joao")
student.update!(name: "Maria")
student.audit_logs.count
# => 2
Como A Associacao Funciona
A gem grava na tabela:
model_class_name: nome da classe auditada, por exemploStudentid_object: id do registro auditado, salvo como string
Com isso, a associacao consegue ligar corretamente:
- a model atual
- ao id correto do objeto auditado
Se no futuro voce quiser um nome diferente para a associacao ou um escopo proprio, ainda pode sobrescrever isso manualmente no projeto cliente.
Preciso Criar Uma Model No Projeto Cliente?
Para o uso basico, nao.
A gem ja fornece a model:
AuditLogger::AuditLog
Entao, para comecar, voce pode usar:
self.audit_logsstudent.audit_logsAuditLogger::AuditLog.all
Uso Simples Com O Proprio Objeto
Se voce ja tem o objeto em maos e quer acessar os logs dele, o jeito mais simples e:
self.audit_logs
Exemplo:
student = Student.find(1)
student.audit_logs
student.audit_logs.where(action: "update")
student.audit_logs.order(created_at: :desc)
Esse e o caminho ideal quando voce quer:
- mostrar historico dentro da tela do proprio registro
- montar uma aba de auditoria no detalhe do objeto
- buscar apenas os logs daquele registro especifico
Uso Avancado Para Relatorios, Rota E Ransack
Se voce quiser montar:
- relatorios gerais
- controllers e rotas proprias
- filtros com
ransack - telas administrativas
- scopes personalizados
ai vale a pena criar uma model no projeto cliente como wrapper da model da gem.
Exemplo:
class AuditLog < AuditLogger::AuditLog
end
Isso nao substitui a model da gem. Isso apenas cria um ponto de entrada mais natural dentro da sua aplicacao.
Exemplo Com Scopes
class AuditLog < AuditLogger::AuditLog
scope :recent, -> { order(created_at: :desc) }
scope :from_model, ->(model_name) { where(model_class_name: model_name) }
end
Exemplo De Controller
class AuditLogsController < ApplicationController
def index
@q = AuditLog.ransack(params[:q])
@audit_logs = @q.result.order(created_at: :desc)
end
end
Exemplo De Rotas
Rails.application.routes.draw do
resources :audit_logs, only: [:index, :show]
end
Exemplo De Consulta Geral
AuditLog.where(model_class_name: "Student")
AuditLog.where(action: "update")
AuditLog.order(created_at: :desc)
Regra Pratica
Se a necessidade for:
- historico do proprio registro: use
self.audit_logs - relatorio geral do sistema: use
AuditLogger::AuditLog - tela administrativa, filtro,
ransack, rota propria: crieAuditLog < AuditLogger::AuditLog
Sobrescrevendo Configuracao Em Uma Model
Se uma model precisar de comportamento diferente do padrao global, voce pode sobrescrever localmente.
Exemplo:
class Student < ApplicationRecord
auditable \
humanize: true,
i18n_scopes: ["school.student", "activerecord.attributes"],
ignored_attributes: [:updated_at],
humanizer: ->(_model_klass, attribute, old_value, new_value) do
{
label: "Campo #{attribute}",
old_value: old_value,
new_value: new_value
}
end
end
Nesse caso:
- a model usa
i18n_scopesproprios - ignora
updated_atlocalmente - pode ter um humanizer proprio so dela
Como A Auditoria Funciona
A gem usa callbacks de commit:
after_create_commitafter_update_commitafter_destroy_commit
Isso significa que o log so e gravado quando a transacao foi confirmada com sucesso. Se houver rollback, a auditoria nao e persistida.
Contrato Do Payload Bruto
Create
Em create, a gem grava um snapshot completo dos atributos auditaveis:
{
"type": "create",
"fields": {
"name": {
"value": "Joao"
},
"status": {
"value": "active"
}
}
}
Update
Em update, a gem grava apenas os campos alterados:
{
"type": "update",
"fields": {
"status": {
"old_value": "active",
"new_value": "inactive"
}
}
}
Destroy
Em destroy, a gem grava um snapshot antes da remocao:
{
"type": "destroy",
"fields": {
"name": {
"value": "Joao"
},
"status": {
"value": "inactive"
}
}
}
Contrato Do Payload Humanizado
O campo audited_changes_humanize segue a mesma estrutura do payload bruto, mas adiciona labels amigaveis:
{
"type": "update",
"fields": {
"status": {
"label": "Situacao",
"old_value": "active",
"new_value": "inactive"
}
}
}
Humanizacao Com I18n
Por padrao, a gem tenta buscar labels nesta ordem:
activerecord.attributes.<model>.<attribute>attributes.<attribute>- fallback para
attribute.humanize
Exemplo:
pt-BR:
activerecord:
attributes:
student:
name: "Nome"
status: "Situacao"
attributes:
status: "Status"
Metadados Extras Do Ator
O campo changed_by_other foi pensado para guardar metadados livres do ator e da requisicao, por exemplo:
config.changed_by_other_resolver = lambda do
{
name: Current.user&.name,
email: Current.user&.email,
url: Current.request&.original_url,
user_agent: Current.request&.user_agent,
school_id: Current.school&.id
}.compact
end
Defaults Atuais Da Gem
Se voce nao configurar nada, a gem usa:
changed_by_other_resolver = -> { {} }humanize_by_default = truei18n_scopes = ["activerecord.attributes", "attributes"]ignored_attributes = [:created_at, :updated_at, :lock_version]
Se uuid_resolver nao retornar valor, a gem gera um UUID automaticamente.
Limitacoes Atuais
- a migration usa
jsonb, entao o uso esperado em producao e com PostgreSQL - nos testes da gem, esses campos foram simulados com
textem SQLite em memoria urleuser_agentainda nao possuem colunas proprias; hoje o recomendado e armazenar isso emchanged_by_other
Desenvolvimento
Depois de clonar o repositorio:
bin/setup
bundle install
bundle exec rake test
Testes
A gem usa:
MinitestActiveRecordSQLiteem memoria para a suite local
Para rodar os testes:
bundle exec rake test
Proximos Passos Sugeridos
- refinar README com mais exemplos por dominio
- adicionar testes especificos de rollback
- documentar estrategias para soft delete
- melhorar metadata publica da gem no
gemspec
License
The gem is available as open source under the terms of the MIT License.