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.0 e < 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:

  • create
  • update
  • destroy

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 Gemfile voce instala audit_log_rails, mas no codigo continua usando AuditLogger

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.rb
  • config/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 uuid sera 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_name
  • id_object
  • action
  • uuid
  • changed_by_id
  • changed_by_type
  • changed_by_other
  • audited_changes
  • audited_changes_humanize
  • ip_remote
  • created_at
  • updated_at
  • deleted_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.request e Current.audit_uuid sao 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
  • email
  • 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.user disponivel para a gem
  • deixar Current.request disponivel para a gem
  • manter um audit_uuid estavel 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_name
  • id_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 exemplo Student
  • id_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_logs
  • student.audit_logs
  • AuditLogger::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: crie AuditLog < 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_scopes proprios
  • ignora updated_at localmente
  • pode ter um humanizer proprio so dela

Como A Auditoria Funciona

A gem usa callbacks de commit:

  • after_create_commit
  • after_update_commit
  • after_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:

  1. activerecord.attributes.<model>.<attribute>
  2. attributes.<attribute>
  3. 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 = true
  • i18n_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 text em SQLite em memoria
  • url e user_agent ainda nao possuem colunas proprias; hoje o recomendado e armazenar isso em changed_by_other

Desenvolvimento

Depois de clonar o repositorio:

bin/setup
bundle install
bundle exec rake test

Testes

A gem usa:

  • Minitest
  • ActiveRecord
  • SQLite em 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.