audit_log_rails 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -1,776 +1,418 @@
1
- # AuditLogger
1
+ # audit_log_rails
2
2
 
3
- `AuditLogger` e uma gem Rails para auditar alteracoes em models ActiveRecord com:
3
+ [![Gem Version](https://badge.fury.io/rb/audit_log_rails.svg)](https://rubygems.org/gems/audit_log_rails)
4
4
 
5
- - tabela propria de auditoria
6
- - integracao simples por `auditable`
7
- - payload bruto e payload humanizado
8
- - configuracao global por initializer
9
- - override por model
10
-
11
- ## Compatibilidade
12
-
13
- No estado atual da gem, as versoes suportadas sao:
14
-
15
- - Ruby `>= 3.1.0`
16
- - Rails / ActiveRecord `>= 7.0` e `< 9.0`
17
-
18
- Em outras palavras, a gem foi preparada para funcionar com projetos Rails 7 e Rails 8, desde que a versao do Ruby seja compativel.
19
-
20
- ## Visao Geral
21
-
22
- Ao adicionar `auditable` em uma model, a gem registra eventos de:
23
-
24
- - `create`
25
- - `update`
26
- - `destroy`
27
-
28
- Os registros sao persistidos na tabela `audit_logs` com:
29
-
30
- - identificacao da model auditada
31
- - tipo da acao
32
- - uuid de correlacao
33
- - dados do ator que executou a acao
34
- - payload bruto da alteracao
35
- - payload humanizado para exibicao
36
-
37
- ## Instalacao Passo A Passo
38
-
39
- Se esta for a primeira vez que voce vai usar a gem, siga exatamente esta ordem.
40
-
41
- ### Passo 1 - Adicionar a gem no projeto Rails
42
-
43
- No `Gemfile` da aplicacao cliente, adicione:
5
+ Auditoria de models ActiveRecord para Rails: registra quem criou, alterou ou excluiu cada registro, quando e o que mudou (valor antigo e valor novo), numa tabela própria, com uma versão já traduzida para exibir na tela.
44
6
 
45
7
  ```ruby
46
- gem "audit_log_rails"
47
- ```
48
-
49
- Observacao importante:
50
-
51
- - o nome publicado da gem e `audit_log_rails`
52
- - o namespace Ruby continua sendo `AuditLogger`
53
- - por isso, no `Gemfile` voce instala `audit_log_rails`, mas no codigo continua usando `AuditLogger`
54
-
55
- Depois rode:
56
-
57
- ```bash
58
- bundle install
59
- ```
60
-
61
- ### Passo 2 - Gerar os arquivos iniciais da gem
62
-
63
- Depois que a gem estiver instalada, rode:
64
-
65
- ```bash
66
- bin/rails generate audit_logger:install
67
- ```
68
-
69
- Esse comando e importante porque ele cria automaticamente os arquivos base que o projeto precisa para comecar.
70
-
71
- ### Passo 3 - Entender o que a gem criou
72
-
73
- Ao rodar `bin/rails generate audit_logger:install`, a gem cria:
74
-
75
- - `db/migrate/..._create_audit_logs.rb`
76
- - `config/initializers/audit_logger.rb`
77
-
78
- Ou seja:
79
-
80
- - voce **nao precisa criar manualmente** o initializer da gem do zero
81
- - voce **nao precisa escrever manualmente** a migration base do zero
82
- - a gem ja entrega esses arquivos como ponto de partida
83
-
84
- ### Passo 4 - Rodar a migration
85
-
86
- Depois que os arquivos forem gerados, rode:
87
-
88
- ```bash
89
- bin/rails db:migrate
90
- ```
91
-
92
- Esse comando cria a tabela `audit_logs` no banco da aplicacao.
93
-
94
- ### Passo 5 - Ajustar o initializer ao seu projeto
95
-
96
- 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.
8
+ class Contrato < ApplicationRecord
9
+ auditable owner: :pessoa
10
+ end
97
11
 
98
- Em outras palavras:
12
+ contrato.update!(cargo: assessor)
13
+ contrato.audit_logs.last.audited_changes_humanize
14
+ # => { "type" => "update",
15
+ # "fields" => { "cargo_id" => { "label" => "Cargo", "old_value" => 3, "new_value" => 7,
16
+ # "old_value_label" => "PROFESSOR", "new_value_label" => "ASSESSOR" } } }
17
+ ```
18
+
19
+ ## Sumário
20
+
21
+ - [Recursos](#recursos)
22
+ - [Compatibilidade](#compatibilidade)
23
+ - [Instalação](#instalação)
24
+ - [Configuração](#configuração)
25
+ - [Quem fez a alteração (contexto do ator)](#quem-fez-a-alteração-contexto-do-ator)
26
+ - [Auditando uma model](#auditando-uma-model)
27
+ - [Consultando os logs](#consultando-os-logs)
28
+ - [Campos sensíveis](#campos-sensíveis)
29
+ - [Histórico de um agregado (dono)](#histórico-de-um-agregado-dono)
30
+ - [Rótulo de associações](#rótulo-de-associações)
31
+ - [Eventos manuais](#eventos-manuais)
32
+ - [Falhas não derrubam a requisição](#falhas-não-derrubam-a-requisição)
33
+ - [Formato dos dados](#formato-dos-dados)
34
+ - [Tradução dos campos (I18n)](#tradução-dos-campos-i18n)
35
+ - [Atualizando da 0.1.x](#atualizando-da-01x)
36
+ - [Limitações e cuidados](#limitações-e-cuidados)
37
+ - [Desenvolvimento](#desenvolvimento)
38
+
39
+ ## Recursos
40
+
41
+ - **`auditable` na model:** registra `create`, `update` e `destroy` depois do commit. Se houver rollback, nada é gravado.
42
+ - **Só o que mudou:** em `update`, guarda apenas os campos alterados, com `old_value` e `new_value`.
43
+ - **Dois formatos:** o bruto (`audited_changes`) e o humanizado (`audited_changes_humanize`), com o nome traduzido de cada campo.
44
+ - **Campos sensíveis mascarados:** a troca de senha ou token fica registrada, sem os valores.
45
+ - **Rótulo de associações:** em `belongs_to`, o humanizado traz o nome do registro ("PROFESSOR → ASSESSOR"), e não só o id.
46
+ - **Dono do registro:** junta num só histórico a pessoa, os contratos e os dependentes dela.
47
+ - **Eventos manuais:** para o que não passa por callback, como login, pedido de reset de senha e `update_all`.
48
+ - **Falha segura:** se gravar o log falhar, a requisição do usuário não quebra.
49
+ - **Contexto do ator:** usuário, tipo, metadados, IP e um uuid que agrupa as alterações da mesma requisição.
99
50
 
100
- - a gem cria o arquivo para voce
101
- - mas voce precisa ajustar o conteudo conforme seu `Current.user`, `Current.request`, sessao, perfis, tenants e assim por diante
51
+ ## Compatibilidade
102
52
 
103
- ## O Que O Generator Cria Na Pratica
53
+ | | Versão |
54
+ |---|---|
55
+ | Ruby | `>= 3.1` |
56
+ | Rails / ActiveRecord | `>= 7.0` e `< 9.0` |
57
+ | Banco | PostgreSQL (a migration usa `jsonb`). Em outros bancos, troque as colunas JSON por `text`: a gem serializa sozinha. |
104
58
 
105
- ### Migration
59
+ A gem se chama `audit_log_rails`, mas o namespace Ruby é `AuditLogger`.
106
60
 
107
- A migration criada pela gem prepara a tabela `audit_logs`, onde os registros de auditoria serao persistidos.
61
+ ## Instalação
108
62
 
109
- ### Initializer
63
+ 1. Adicione a gem ao `Gemfile` e instale:
110
64
 
111
- O initializer criado pela gem e o lugar onde voce diz:
65
+ ```ruby
66
+ gem "audit_log_rails"
67
+ ```
112
68
 
113
- - quem e o usuario atual
114
- - qual id deve ser salvo em `changed_by_id`
115
- - qual tipo deve ser salvo em `changed_by_type`
116
- - quais metadados vao para `changed_by_other`
117
- - como o `uuid` sera resolvido
118
- - como o IP sera obtido
119
- - quais atributos devem ser ignorados
120
- - como a humanizacao deve funcionar
69
+ ```bash
70
+ bundle install
71
+ ```
121
72
 
122
- ## Estrutura Da Tabela
73
+ 2. Gere a migration e o initializer:
123
74
 
124
- A migration inicial cria a tabela `audit_logs` com os campos:
75
+ ```bash
76
+ bin/rails generate audit_logger:install
77
+ ```
125
78
 
126
- - `model_class_name`
127
- - `id_object`
128
- - `action`
129
- - `uuid`
130
- - `changed_by_id`
131
- - `changed_by_type`
132
- - `changed_by_other`
133
- - `audited_changes`
134
- - `audited_changes_humanize`
135
- - `ip_remote`
136
- - `created_at`
137
- - `updated_at`
138
- - `deleted_at`
79
+ O comando cria:
80
+ - `db/migrate/<timestamp>_create_audit_logs.rb`: a tabela `audit_logs`;
81
+ - `config/initializers/audit_logger.rb`: a configuração, com os exemplos comentados.
139
82
 
140
- ## Configuracao Global Explicada
83
+ 3. Rode a migration:
141
84
 
142
- Depois de rodar o generator, a gem cria automaticamente o arquivo:
85
+ ```bash
86
+ bin/rails db:migrate
87
+ ```
143
88
 
144
- ```text
145
- config/initializers/audit_logger.rb
146
- ```
89
+ 4. Ajuste o initializer para o seu app: quem é o usuário atual, o IP e assim por diante. Veja [Configuração](#configuração).
147
90
 
148
- Voce nao precisa criar esse arquivo manualmente do zero.
91
+ 5. Coloque `auditable` nas models que quiser auditar.
149
92
 
150
- O papel desse arquivo e ensinar a gem a descobrir informacoes do seu sistema.
93
+ ### A tabela `audit_logs`
151
94
 
152
- Importante:
95
+ | Coluna | Conteúdo |
96
+ |---|---|
97
+ | `model_class_name` | Classe auditada, ex.: `Contrato` |
98
+ | `id_object` | Id do registro auditado (texto) |
99
+ | `action` | `create`, `update`, `destroy` ou o nome do evento manual |
100
+ | `uuid` | Agrupa os logs da mesma requisição ou fluxo |
101
+ | `changed_by_id` / `changed_by_type` | Quem fez a alteração |
102
+ | `changed_by_other` | JSON livre com metadados do ator (nome, e-mail, request id...) |
103
+ | `audited_changes` | Payload bruto |
104
+ | `audited_changes_humanize` | Payload humanizado |
105
+ | `ip_remote` | IP de origem |
106
+ | `owner_type` / `owner_id` | Registro "dono" do log (desde a 0.2.0) |
107
+ | `created_at`, `updated_at`, `deleted_at` | Datas |
153
108
 
154
- - esse arquivo e criado automaticamente pela gem quando voce roda o generator
155
- - os exemplos relacionados a `Current.user`, `Current.request` e `Current.audit_uuid` sao apenas sugestoes
156
- - voce precisa descomentar e adaptar somente o que fizer sentido no seu projeto
109
+ A migration também cria os índices de consulta: por model e id, uuid, ação, ator, dono e data.
157
110
 
158
- Se o seu sistema usa outro contexto, por exemplo `Current.admin`, `Current.account`, `session[:jwt]` ou qualquer outro objeto, basta trocar nos resolvers.
111
+ ## Configuração
159
112
 
160
- Exemplo do initializer:
113
+ Tudo fica em `config/initializers/audit_logger.rb`:
161
114
 
162
115
  ```ruby
163
116
  AuditLogger.configure do |config|
164
- # Descomente apenas o que fizer sentido no seu projeto.
165
- # A gem nao tem como adivinhar sozinha onde voce guarda usuario logado,
166
- # request, tenant, sessao ou token.
167
-
168
- # config.changed_by_id_resolver = -> { Current.user&.id }
169
- # config.changed_by_type_resolver = -> { Current.user&.class&.name }
170
- #
171
- # config.changed_by_other_resolver = lambda do
172
- # {
173
- # name: Current.user&.name,
174
- # email: Current.user&.email,
175
- # request_id: Current.request_id
176
- # }.compact
177
- # end
178
- #
179
- # config.uuid_resolver = -> { Current.audit_uuid }
180
- # config.ip_resolver = -> { Current.request&.remote_ip }
181
-
182
- # humanize serve para utilizar traduções do i18n de forma que os attributes do model seja traduzidos humanizando a leitura.
183
- config.humanize_by_default = true
184
-
185
- # Configura o padrão do scope do i18n para a humanização dos atributos do model.
186
- config.i18n_scopes = ["activerecord.attributes", "attributes"]
187
-
188
- # Atributos que devem ser ignorados na auditoria.
189
- # Por exemplo, `created_at`, `updated_at`, `lock_version` e outros campos de auditoria.
190
- config.ignored_attributes = [:created_at, :updated_at, :lock_version]
191
-
192
- # Configura a humanização dos atributos do model.
193
- config.humanizer = ->(_model_klass, _attribute, _old_value, _new_value) { nil }
194
- end
195
- ```
196
-
197
- ### O Que Esse Arquivo Faz
117
+ # Quem fez a alteração.
118
+ config.changed_by_id_resolver = -> { Current.user&.id }
119
+ config.changed_by_type_resolver = -> { Current.user&.class&.name }
120
+ config.changed_by_other_resolver = -> { { nome: Current.user&.nome, request_id: Current.request_id }.compact }
198
121
 
199
- Esse bloco:
200
-
201
- ```ruby
202
- AuditLogger.configure do |config|
203
- ...
204
- end
205
- ```
122
+ # Agrupamento e origem.
123
+ config.uuid_resolver = -> { Current.request_id }
124
+ config.ip_resolver = -> { Current.ip }
206
125
 
207
- serve para configurar o comportamento global da gem no projeto inteiro.
126
+ # O que entra no log.
127
+ config.ignored_attributes = %i[created_at updated_at lock_version]
128
+ config.masked_attributes += %i[token_api]
129
+ config.mask_value = "[FILTRADO]"
208
130
 
209
- Tudo que estiver aqui vira o comportamento padrao da auditoria, a menos que uma model sobrescreva algo localmente.
210
-
211
- ### Explicando Cada Configuracao
212
-
213
- #### `config.changed_by_id_resolver`
214
-
215
- Exemplo:
216
-
217
- ```ruby
218
- config.changed_by_id_resolver = -> { Current.user&.id }
219
- ```
220
-
221
- Esse resolver diz para a gem qual valor deve ser salvo na coluna `changed_by_id`.
222
-
223
- Na pratica:
224
-
225
- - se o usuario logado for `Current.user`
226
- - a gem vai salvar `Current.user.id`
227
-
228
- Se no seu sistema o ator principal for outro, voce troca aqui.
229
-
230
- Exemplo:
231
-
232
- ```ruby
233
- config.changed_by_id_resolver = -> { Current.admin&.id }
234
- ```
235
-
236
- #### `config.changed_by_type_resolver`
237
-
238
- Exemplo:
239
-
240
- ```ruby
241
- config.changed_by_type_resolver = -> { Current.user&.class&.name }
242
- ```
243
-
244
- Esse resolver diz qual valor sera salvo em `changed_by_type`.
245
-
246
- Voce pode usar isso para salvar:
247
-
248
- - nome da classe
249
- - perfil
250
- - role
251
- - tipo de usuario
252
-
253
- Exemplo:
254
-
255
- ```ruby
256
- config.changed_by_type_resolver = -> { Current.user&.profile }
257
- ```
258
-
259
- #### `config.changed_by_other_resolver`
260
-
261
- Exemplo:
131
+ # Humanização.
132
+ config.humanize_by_default = true
133
+ config.i18n_scopes = ["activerecord.attributes", "attributes"]
134
+ config.resolve_associations = true
135
+ config.humanizer = nil
262
136
 
263
- ```ruby
264
- config.changed_by_other_resolver = lambda do
265
- {
266
- name: Current.user&.name,
267
- email: Current.user&.email,
268
- request_id: Current.request_id
269
- }.compact
137
+ # Falhas.
138
+ config.raise_errors = Rails.env.test?
139
+ config.error_handler = ->(error, record, action) { Rails.error.report(error, context: { action: action, record: record&.class&.name }) }
270
140
  end
271
141
  ```
272
142
 
273
- Esse resolver preenche o campo `changed_by_other`, que e um JSON livre.
274
-
275
- Use esse campo quando quiser guardar metadados extras, por exemplo:
276
-
277
- - nome do usuario
278
- - email
279
- - request id
280
- - tenant
281
- - school_id
282
- - url
283
- - user_agent
284
-
285
- Esse campo existe justamente para voce nao ficar preso so a `changed_by_id` e `changed_by_type`.
286
-
287
- #### `config.uuid_resolver`
288
-
289
- Exemplo:
290
-
291
- ```ruby
292
- config.uuid_resolver = -> { Current.audit_uuid }
293
- ```
294
-
295
- Esse resolver define o `uuid` do log.
296
-
297
- Esse `uuid` e util para correlacionar varios logs da mesma sessao ou do mesmo fluxo.
298
-
299
- Se esse resolver nao retornar valor, a gem gera um UUID automaticamente.
300
-
301
- #### `config.ip_resolver`
302
-
303
- Exemplo:
304
-
305
- ```ruby
306
- config.ip_resolver = -> { Current.request&.remote_ip }
307
- ```
308
-
309
- Esse resolver informa qual IP deve ser salvo em `ip_remote`.
310
-
311
- Se voce nao quiser salvar IP, pode deixar `nil`.
312
-
313
- #### `config.humanize_by_default`
314
-
315
- Exemplo:
316
-
317
- ```ruby
318
- config.humanize_by_default = true
319
- ```
320
-
321
- Se estiver `true`, a gem tenta gerar `audited_changes_humanize` por padrao.
143
+ | Opção | Padrão | O que faz |
144
+ |---|---|---|
145
+ | `changed_by_id_resolver` | `nil` | Valor gravado em `changed_by_id` |
146
+ | `changed_by_type_resolver` | `nil` | Valor gravado em `changed_by_type` (classe, perfil, papel...) |
147
+ | `changed_by_other_resolver` | `-> { {} }` | Hash gravado em `changed_by_other` |
148
+ | `uuid_resolver` | `nil` | Uuid de correlação; sem valor, a gem gera um por log |
149
+ | `ip_resolver` | `nil` | Valor gravado em `ip_remote` |
150
+ | `ignored_attributes` | `created_at`, `updated_at`, `lock_version` | Campos que nunca entram no log |
151
+ | `masked_attributes` | `password`, `password_digest`, `encrypted_password`, `reset_password_token`, `confirmation_token`, `unlock_token` | Campos registrados sem o valor ([Campos sensíveis](#campos-sensíveis)) |
152
+ | `mask_value` | `"[FILTRADO]"` | Texto que substitui o valor mascarado |
153
+ | `humanize_by_default` | `true` | Gera o `audited_changes_humanize` |
154
+ | `i18n_scopes` | `["activerecord.attributes", "attributes"]` | Onde procurar a tradução dos campos |
155
+ | `resolve_associations` | `true` | Rótulo do registro em `belongs_to` ([Rótulo de associações](#rótulo-de-associações)) |
156
+ | `humanizer` | `nil` | Humanização personalizada ([Tradução dos campos](#tradução-dos-campos-i18n)) |
157
+ | `raise_errors` | `false` | Deixa o erro de gravação do log subir (útil em teste) |
158
+ | `error_handler` | `nil` | `->(error, record, action)` chamado quando gravar o log falha |
322
159
 
323
- Se estiver `false`, a gem continua gravando o payload bruto, mas nao humaniza automaticamente.
160
+ Todo resolver é um lambda. Se um resolver lançar erro, a gem registra um aviso e grava o campo vazio; o log não se perde por isso. Uma configuração inválida, como um valor que não é lambda, gera `AuditLogger::ConfigurationError` na subida do app.
324
161
 
325
- #### `config.i18n_scopes`
162
+ ## Quem fez a alteração (contexto do ator)
326
163
 
327
- Exemplo:
328
-
329
- ```ruby
330
- config.i18n_scopes = ["activerecord.attributes", "attributes"]
331
- ```
332
-
333
- Esses sao os escopos que a gem usa para tentar traduzir o nome dos atributos.
334
-
335
- Por exemplo, ao auditar `Student.status`, a gem tenta procurar traducoes nesses caminhos.
336
-
337
- #### `config.ignored_attributes`
338
-
339
- Exemplo:
340
-
341
- ```ruby
342
- config.ignored_attributes = [:created_at, :updated_at, :lock_version]
343
- ```
344
-
345
- Aqui voce informa quais atributos nao devem entrar na auditoria por padrao.
346
-
347
- Isso e util para evitar ruido.
348
-
349
- #### `config.humanizer`
350
-
351
- Exemplo:
352
-
353
- ```ruby
354
- config.humanizer = ->(_model_klass, _attribute, _old_value, _new_value) { nil }
355
- ```
356
-
357
- Esse e um humanizer global opcional.
358
-
359
- Ele existe para casos em que voce quer controlar manualmente como um campo sera apresentado.
360
-
361
- Importante:
362
-
363
- - se ele retornar `nil`, a gem usa o fallback padrao com I18n
364
- - se ele retornar um `Hash`, esse hash e usado para montar o payload humanizado
365
- - se ele retornar um valor simples, a gem usa esse valor como representacao humanizada
366
-
367
- ## Exemplo Real Com `Current`
368
-
369
- Uma forma comum de integrar a gem no app cliente e usar `CurrentAttributes`.
370
-
371
- Se o seu projeto ainda nao tiver um `Current`, voce pode criar algo assim:
164
+ A gem não sabe sozinha quem está logado. O jeito mais comum é usar `ActiveSupport::CurrentAttributes`:
372
165
 
373
166
  ```ruby
167
+ # app/models/current.rb
374
168
  class Current < ActiveSupport::CurrentAttributes
375
- attribute :user, :request, :audit_uuid, :request_id
169
+ attribute :user, :request_id, :ip
376
170
  end
377
171
  ```
378
172
 
379
- Esse objeto serve como um lugar central para guardar o contexto atual da requisicao.
380
-
381
- Depois, no controller base da aplicacao, voce pode preencher esses dados:
382
-
383
173
  ```ruby
384
- class ApplicationController < ActionController::Base
385
- before_action :store_current_context
386
-
387
- private
388
-
389
- def store_current_context
390
- Current.user = current_user
391
- Current.request = request
174
+ # app/controllers/application_controller.rb
175
+ class ApplicationController < ActionController::Base # ou ActionController::API
176
+ before_action do
177
+ Current.user = current_user # na API com JWT, o usuário que o token identificou
392
178
  Current.request_id = request.request_id
393
- Current.audit_uuid ||= session[:audit_uuid] ||= SecureRandom.uuid
179
+ Current.ip = request.remote_ip
394
180
  end
395
181
  end
396
182
  ```
397
183
 
398
- O objetivo desse passo e simples:
184
+ Com `uuid_resolver = -> { Current.request_id }`, todos os logs de uma mesma requisição ficam com o mesmo uuid. Por exemplo, salvar a pessoa e os contratos dela num formulário só.
399
185
 
400
- - deixar `Current.user` disponivel para a gem
401
- - deixar `Current.request` disponivel para a gem
402
- - manter um `audit_uuid` estavel dentro da sessao
186
+ Fora de requisição (jobs, rake, console), não há `Current.user`. O log é gravado com o ator vazio. Se quiser identificar a origem, preencha o `Current` no job.
403
187
 
404
- Se o seu sistema ja tiver outra estrategia para isso, nao precisa copiar exatamente esse exemplo.
405
-
406
- ## Ativando A Auditoria Na Model
407
-
408
- Depois da instalacao e da configuracao global, voce ativa a auditoria na model com `auditable`.
409
-
410
- Exemplo:
188
+ ## Auditando uma model
411
189
 
412
190
  ```ruby
413
- class Student < ApplicationRecord
191
+ class Pessoa < ApplicationRecord
414
192
  auditable
415
193
  end
416
194
  ```
417
195
 
418
- So isso ja faz a gem registrar:
419
-
420
- - criacao
421
- - atualizacao
422
- - remocao
423
-
424
- ## Consultando Os Logs Da Model
425
-
426
- Ao usar `auditable`, a gem tambem define automaticamente a associacao:
427
-
428
- ```ruby
429
- student.audit_logs
430
- ```
431
-
432
- Ou seja, voce nao precisa criar manualmente um `has_many :audit_logs` basico para comecar.
196
+ Opções, todas opcionais e válidas só para a model:
433
197
 
434
- Exemplo:
198
+ | Opção | Exemplo | O que faz |
199
+ |---|---|---|
200
+ | `ignored_attributes` | `[:sincronizado_em]` | Soma aos ignorados globais |
201
+ | `masked_attributes` | `[:token_generate]` | Soma aos mascarados globais |
202
+ | `owner` | `:pessoa` ou `->(r) { r.contrato&.pessoa }` | Registro dono do log ([Histórico de um agregado](#histórico-de-um-agregado-dono)) |
203
+ | `humanize` | `false` | Liga ou desliga a humanização nesta model |
204
+ | `i18n_scopes` | `["meu_app.campos", "activerecord.attributes"]` | Escopos de tradução próprios |
205
+ | `humanizer` | `->(klass, campo, antigo, novo) { ... }` | Humanização própria da model |
435
206
 
436
207
  ```ruby
437
- student = Student.find(1)
438
- student.audit_logs
208
+ class User < ApplicationRecord
209
+ auditable masked_attributes: [:token_generate], ignored_attributes: [:last_sign_in_at]
210
+ end
439
211
  ```
440
212
 
441
- Essa associacao ja filtra:
442
-
443
- - `model_class_name`
444
- - `id_object`
445
-
446
- Assim, cada model passa a enxergar apenas os logs que pertencem a ela.
447
-
448
- ### Exemplo Pratico
213
+ ## Consultando os logs
449
214
 
450
215
  ```ruby
451
- student = Student.create!(name: "Joao")
216
+ pessoa.audit_logs # logs do próprio registro
217
+ pessoa.audit_logs.where(action: "update").order(created_at: :desc)
452
218
 
453
- student.update!(name: "Maria")
454
-
455
- student.audit_logs.count
456
- # => 2
219
+ AuditLogger::AuditLog.of_owner(pessoa) # pessoa + registros que têm ela como dona
220
+ AuditLogger::AuditLog.where(model_class_name: "Contrato", action: "destroy")
221
+ AuditLogger::AuditLog.where(changed_by_id: user.id)
457
222
  ```
458
223
 
459
- ### Como A Associacao Funciona
460
-
461
- A gem grava na tabela:
462
-
463
- - `model_class_name`: nome da classe auditada, por exemplo `Student`
464
- - `id_object`: id do registro auditado, salvo como string
465
-
466
- Com isso, a associacao consegue ligar corretamente:
467
-
468
- - a model atual
469
- - ao id correto do objeto auditado
470
-
471
- Se no futuro voce quiser um nome diferente para a associacao ou um escopo proprio, ainda pode sobrescrever isso manualmente no projeto cliente.
472
-
473
- ## Preciso Criar Uma Model No Projeto Cliente?
474
-
475
- Para o uso basico, nao.
476
-
477
- A gem ja fornece a model:
224
+ Para telas administrativas, filtros com Ransack ou scopes próprios, crie uma model no app que herda da model da gem:
478
225
 
479
226
  ```ruby
480
- AuditLogger::AuditLog
481
- ```
482
-
483
- Entao, para comecar, voce pode usar:
484
-
485
- - `self.audit_logs`
486
- - `student.audit_logs`
487
- - `AuditLogger::AuditLog.all`
488
-
489
- ## Uso Simples Com O Proprio Objeto
490
-
491
- Se voce ja tem o objeto em maos e quer acessar os logs dele, o jeito mais simples e:
227
+ class AuditLog < AuditLogger::AuditLog
228
+ scope :recentes, -> { order(created_at: :desc) }
492
229
 
493
- ```ruby
494
- self.audit_logs
230
+ def self.ransackable_attributes(_auth = nil) = %w[model_class_name action changed_by_id created_at]
231
+ end
495
232
  ```
496
233
 
497
- Exemplo:
234
+ ## Campos sensíveis
498
235
 
499
- ```ruby
500
- student = Student.find(1)
236
+ Pôr a senha em `ignored_attributes` faz a troca de senha sumir do histórico. Em `masked_attributes`, a alteração aparece, mas sem os valores:
501
237
 
502
- student.audit_logs
503
- student.audit_logs.where(action: "update")
504
- student.audit_logs.order(created_at: :desc)
238
+ ```json
239
+ { "type": "update", "fields": { "encrypted_password": { "old_value": "[FILTRADO]", "new_value": "[FILTRADO]" } } }
505
240
  ```
506
241
 
507
- Esse e o caminho ideal quando voce quer:
508
-
509
- - mostrar historico dentro da tela do proprio registro
510
- - montar uma aba de auditoria no detalhe do objeto
511
- - buscar apenas os logs daquele registro especifico
512
-
513
- ## Uso Avancado Para Relatorios, Rota E Ransack
242
+ - O valor nulo continua nulo, para mostrar que o campo estava vazio.
243
+ - No `create`, um campo mascarado preenchido aparece como `"[FILTRADO]"`.
244
+ - Se um campo estiver nos ignorados e nos mascarados ao mesmo tempo, vale o ignorado.
514
245
 
515
- Se voce quiser montar:
246
+ ## Histórico de um agregado (dono)
516
247
 
517
- - relatorios gerais
518
- - controllers e rotas proprias
519
- - filtros com `ransack`
520
- - telas administrativas
521
- - scopes personalizados
522
-
523
- ai vale a pena criar uma model no projeto cliente como wrapper da model da gem.
524
-
525
- Exemplo:
248
+ Cada log guarda um registro "dono" (`owner_type`/`owner_id`). Sem configuração, o dono é o próprio registro. Para juntar o histórico de várias models num lugar só:
526
249
 
527
250
  ```ruby
528
- class AuditLog < AuditLogger::AuditLog
251
+ class Pessoa < ApplicationRecord
252
+ auditable # dona de si mesma
529
253
  end
530
- ```
531
254
 
532
- Isso nao substitui a model da gem.
533
- Isso apenas cria um ponto de entrada mais natural dentro da sua aplicacao.
534
-
535
- ### Exemplo Com Scopes
536
-
537
- ```ruby
538
- class AuditLog < AuditLogger::AuditLog
539
- scope :recent, -> { order(created_at: :desc) }
540
- scope :from_model, ->(model_name) { where(model_class_name: model_name) }
255
+ class Contrato < ApplicationRecord
256
+ belongs_to :pessoa
257
+ auditable owner: :pessoa # associação
541
258
  end
542
- ```
543
259
 
544
- ### Exemplo De Controller
545
-
546
- ```ruby
547
- class AuditLogsController < ApplicationController
548
- def index
549
- @q = AuditLog.ransack(params[:q])
550
- @audit_logs = @q.result.order(created_at: :desc)
551
- end
260
+ class Dependente < ApplicationRecord
261
+ belongs_to :contrato
262
+ auditable owner: ->(dependente) { dependente.contrato&.pessoa } # caminho mais longo
552
263
  end
553
- ```
554
-
555
- ### Exemplo De Rotas
556
264
 
557
- ```ruby
558
- Rails.application.routes.draw do
559
- resources :audit_logs, only: [:index, :show]
560
- end
265
+ AuditLogger::AuditLog.of_owner(pessoa) # tudo da pessoa, do mais recente ao mais antigo
561
266
  ```
562
267
 
563
- ### Exemplo De Consulta Geral
268
+ Se a associação do dono estiver vazia, o dono passa a ser o próprio registro. Com STI, o `owner_type` é a classe base.
564
269
 
565
- ```ruby
566
- AuditLog.where(model_class_name: "Student")
567
- AuditLog.where(action: "update")
568
- AuditLog.order(created_at: :desc)
569
- ```
270
+ ## Rótulo de associações
570
271
 
571
- ## Regra Pratica
272
+ Com `resolve_associations = true`, a troca de um `belongs_to` ganha no formato humanizado o nome do registro antigo e do novo:
572
273
 
573
- Se a necessidade for:
274
+ ```json
275
+ { "label": "Cargo", "old_value": 3, "new_value": 7, "old_value_label": "PROFESSOR", "new_value_label": "ASSESSOR" }
276
+ ```
574
277
 
575
- - historico do proprio registro: use `self.audit_logs`
576
- - relatorio geral do sistema: use `AuditLogger::AuditLog`
577
- - tela administrativa, filtro, `ransack`, rota propria: crie `AuditLog < AuditLogger::AuditLog`
278
+ No `create` e no `destroy`, o rótulo vai em `value_label`. Ele vem do `to_s` definido na model associada; se ela não tiver um, de `display_name`, `descricao`, `nome`, `name` ou `title`. A busca inclui registros excluídos por soft delete (`with_deleted`), porque o histórico precisa do nome antigo.
578
279
 
579
- ## Sobrescrevendo Configuracao Em Uma Model
280
+ Cada rótulo custa uma consulta ao banco. Em tabelas com muitas associações e muita escrita, avalie desligar a opção na model (`humanize: false`) ou no global.
580
281
 
581
- Se uma model precisar de comportamento diferente do padrao global, voce pode sobrescrever localmente.
282
+ ## Eventos manuais
582
283
 
583
- Exemplo:
284
+ O que não passa pelos callbacks da model é registrado com `AuditLogger.record_event`. É o caso do `update_all`, do login e do pedido de reset de senha.
584
285
 
585
286
  ```ruby
586
- class Student < ApplicationRecord
587
- auditable \
588
- humanize: true,
589
- i18n_scopes: ["school.student", "activerecord.attributes"],
590
- ignored_attributes: [:updated_at],
591
- humanizer: ->(_model_klass, attribute, old_value, new_value) do
592
- {
593
- label: "Campo #{attribute}",
594
- old_value: old_value,
595
- new_value: new_value
596
- }
597
- end
598
- end
599
- ```
600
-
601
- Nesse caso:
602
-
603
- - a model usa `i18n_scopes` proprios
604
- - ignora `updated_at` localmente
605
- - pode ter um humanizer proprio so dela
606
-
607
- ## Como A Auditoria Funciona
287
+ # Com um registro: usa a humanização e o dono da model dele.
288
+ AuditLogger.record_event(
289
+ action: "password_reset_requested",
290
+ record: user,
291
+ fields: { encrypted_password: { old_value: "[FILTRADO]", new_value: "[FILTRADO]" } },
292
+ metadata: { origem: "esqueci_minha_senha" }
293
+ )
608
294
 
609
- A gem usa callbacks de commit:
295
+ # Sem registro (ex.: login de CPF que não existe).
296
+ AuditLogger.record_event(action: "login_failed", model_class_name: "User", id_object: cpf, metadata: { motivo: "senha" })
610
297
 
611
- - `after_create_commit`
612
- - `after_update_commit`
613
- - `after_destroy_commit`
614
-
615
- Isso significa que o log so e gravado quando a transacao foi confirmada com sucesso.
616
- Se houver rollback, a auditoria nao e persistida.
298
+ # Alteração em massa, com dono explícito.
299
+ ids = perfil.users.where(id: user_ids).pluck(:id)
300
+ perfil.users.where(id: ids).update_all(g_perfil_id: nil)
301
+ AuditLogger.record_event(action: "usuarios_desvinculados", record: perfil, metadata: { user_ids: ids })
302
+ ```
617
303
 
618
- ## Contrato Do Payload Bruto
304
+ | Parâmetro | Obrigatório | Conteúdo |
305
+ |---|---|---|
306
+ | `action` | sim | Nome do evento |
307
+ | `record` | não | Registro relacionado; define model, id e dono |
308
+ | `model_class_name`, `id_object` | não | Usados quando não há `record` |
309
+ | `owner` | não | Dono explícito (sobrepõe o da model) |
310
+ | `fields` | não | Mesmo formato do log automático |
311
+ | `metadata` | não | Hash livre, gravado em `audited_changes["metadata"]` |
619
312
 
620
- ### Create
313
+ ## Falhas não derrubam a requisição
621
314
 
622
- Em `create`, a gem grava um snapshot completo dos atributos auditaveis:
315
+ O log é gravado depois do commit, quando o dado do usuário já foi salvo. Se a gravação do log falhar (banco fora, JSON inválido, erro no humanizer), a gem:
623
316
 
624
- ```json
625
- {
626
- "type": "create",
627
- "fields": {
628
- "name": {
629
- "value": "Joao"
630
- },
631
- "status": {
632
- "value": "active"
633
- }
634
- }
635
- }
636
- ```
317
+ 1. registra o erro com `Rails.logger.error`;
318
+ 2. chama o `error_handler`, se houver;
319
+ 3. segue sem lançar exceção. A requisição não vira erro 500.
637
320
 
638
- ### Update
321
+ Em teste, use `config.raise_errors = true` para o erro aparecer.
639
322
 
640
- Em `update`, a gem grava apenas os campos alterados:
323
+ ## Formato dos dados
641
324
 
642
- ```json
643
- {
644
- "type": "update",
645
- "fields": {
646
- "status": {
647
- "old_value": "active",
648
- "new_value": "inactive"
649
- }
650
- }
651
- }
652
- ```
325
+ `audited_changes` (bruto) e `audited_changes_humanize` (humanizado) têm a mesma estrutura:
653
326
 
654
- ### Destroy
327
+ ```jsonc
328
+ // create e destroy: retrato completo
329
+ { "type": "create", "fields": { "nome": { "value": "Ana" }, "cargo_id": { "value": 3 } } }
655
330
 
656
- Em `destroy`, a gem grava um snapshot antes da remocao:
331
+ // update: só o que mudou
332
+ { "type": "update", "fields": { "nome": { "old_value": "Ana", "new_value": "Ana Maria" } } }
657
333
 
658
- ```json
659
- {
660
- "type": "destroy",
661
- "fields": {
662
- "name": {
663
- "value": "Joao"
664
- },
665
- "status": {
666
- "value": "inactive"
667
- }
668
- }
669
- }
334
+ // evento manual
335
+ { "type": "login_failed", "fields": {}, "metadata": { "motivo": "senha" } }
670
336
  ```
671
337
 
672
- ## Contrato Do Payload Humanizado
338
+ O humanizado acrescenta em cada campo:
339
+ - `label`: o nome traduzido;
340
+ - `old_value_label`, `new_value_label` ou `value_label`: o rótulo, nos campos de `belongs_to`.
673
341
 
674
- O campo `audited_changes_humanize` segue a mesma estrutura do payload bruto, mas adiciona labels amigaveis:
675
-
676
- ```json
677
- {
678
- "type": "update",
679
- "fields": {
680
- "status": {
681
- "label": "Situacao",
682
- "old_value": "active",
683
- "new_value": "inactive"
684
- }
685
- }
686
- }
687
- ```
342
+ ## Tradução dos campos (I18n)
688
343
 
689
- ## Humanizacao Com I18n
344
+ O `label` é procurado nesta ordem, para cada escopo de `i18n_scopes`:
690
345
 
691
- Por padrao, a gem tenta buscar labels nesta ordem:
346
+ 1. `<escopo>.<model>.<campo>`, ex.: `activerecord.attributes.pessoa.nome`;
347
+ 2. `<escopo>.<campo>`, ex.: `attributes.nome`.
692
348
 
693
- 1. `activerecord.attributes.<model>.<attribute>`
694
- 2. `attributes.<attribute>`
695
- 3. fallback para `attribute.humanize`
349
+ Se não achar nada, usa `campo.humanize`.
696
350
 
697
- Exemplo:
698
-
699
- ```yml
351
+ ```yaml
700
352
  pt-BR:
701
353
  activerecord:
702
354
  attributes:
703
- student:
704
- name: "Nome"
705
- status: "Situacao"
706
- attributes:
707
- status: "Status"
355
+ pessoa:
356
+ nome: "Nome"
357
+ cargo_id: "Cargo"
708
358
  ```
709
359
 
710
- ## Metadados Extras Do Ator
711
-
712
- O campo `changed_by_other` foi pensado para guardar metadados livres do ator e da requisicao, por exemplo:
360
+ Para controlar a apresentação de um campo, use um `humanizer`, global ou na model. Ele pode devolver:
361
+ - `nil`: usa a tradução padrão;
362
+ - um `Hash`: as chaves são mescladas ao campo (ex.: `{ label:, old_value:, new_value: }`);
363
+ - um valor simples: vira o `value` do campo.
713
364
 
714
365
  ```ruby
715
- config.changed_by_other_resolver = lambda do
716
- {
717
- name: Current.user&.name,
718
- email: Current.user&.email,
719
- url: Current.request&.original_url,
720
- user_agent: Current.request&.user_agent,
721
- school_id: Current.school&.id
722
- }.compact
366
+ auditable humanizer: ->(_klass, campo, antigo, novo) do
367
+ next unless campo == "situacao"
368
+
369
+ { label: "Situação", old_value: SITUACOES[antigo], new_value: SITUACOES[novo] }
723
370
  end
724
371
  ```
725
372
 
726
- ## Defaults Atuais Da Gem
727
-
728
- Se voce nao configurar nada, a gem usa:
373
+ Uma falha no humanizer é registrada como aviso, e o campo cai na tradução padrão.
729
374
 
730
- - `changed_by_other_resolver = -> { {} }`
731
- - `humanize_by_default = true`
732
- - `i18n_scopes = ["activerecord.attributes", "attributes"]`
733
- - `ignored_attributes = [:created_at, :updated_at, :lock_version]`
375
+ ## Atualizando da 0.1.x
734
376
 
735
- Se `uuid_resolver` nao retornar valor, a gem gera um UUID automaticamente.
736
-
737
- ## Limitacoes Atuais
738
-
739
- - a migration usa `jsonb`, entao o uso esperado em producao e com PostgreSQL
740
- - nos testes da gem, esses campos foram simulados com `text` em SQLite em memoria
741
- - `url` e `user_agent` ainda nao possuem colunas proprias; hoje o recomendado e armazenar isso em `changed_by_other`
742
-
743
- ## Desenvolvimento
744
-
745
- Depois de clonar o repositorio:
377
+ A 0.2.0 traz as colunas `owner_type` e `owner_id`. Para criá-las:
746
378
 
747
379
  ```bash
748
- bin/setup
749
- bundle install
750
- bundle exec rake test
380
+ bin/rails generate audit_logger:upgrade
381
+ bin/rails db:migrate
751
382
  ```
752
383
 
753
- ## Testes
384
+ Sem as colunas a gem continua funcionando; só não grava o dono. As demais novidades estão no [CHANGELOG](CHANGELOG.md):
385
+ - campos mascarados;
386
+ - falha segura;
387
+ - rótulos de associação;
388
+ - eventos manuais.
754
389
 
755
- A gem usa:
390
+ ## Limitações e cuidados
756
391
 
757
- - `Minitest`
758
- - `ActiveRecord`
759
- - `SQLite` em memoria para a suite local
392
+ - **Operações em massa não disparam callbacks.** `update_all`, `delete_all`, `insert_all`, `upsert_all` e `update_column(s)` não geram log; registre com `AuditLogger.record_event`.
393
+ - **Soft delete:**
394
+ - com `paranoia`, o `destroy` dispara os callbacks de destroy, então confira no seu app se o `after_destroy_commit` é chamado;
395
+ - com `discard`, a exclusão é um `update` e aparece como tal, com `discarded_at` mudando.
396
+ - **PostgreSQL:** a migration gerada usa `jsonb`. Nos testes da gem, as colunas são `text` em SQLite.
397
+ - **`url` e `user_agent`** não têm coluna própria: guarde em `changed_by_other`.
760
398
 
761
- Para rodar os testes:
399
+ ## Desenvolvimento
762
400
 
763
401
  ```bash
764
- bundle exec rake test
402
+ bin/setup
403
+ bundle exec rake test # Minitest com SQLite em memória
404
+ bundle exec rubocop
405
+ bundle-audit check # dependências com CVE
765
406
  ```
766
407
 
767
- ## Proximos Passos Sugeridos
408
+ Para publicar uma versão:
768
409
 
769
- - refinar README com mais exemplos por dominio
770
- - adicionar testes especificos de rollback
771
- - documentar estrategias para soft delete
772
- - melhorar metadata publica da gem no `gemspec`
410
+ 1. Atualize `lib/audit_logger/version.rb` e o `CHANGELOG.md`.
411
+ 2. Rode `bundle exec rake test`.
412
+ 3. Gere o pacote com `gem build audit_logger.gemspec`.
413
+ 4. Publique com `gem push audit_log_rails-<versão>.gem` (pede login e o código de verificação do RubyGems).
414
+ 5. Crie a tag `v<versão>` no Git e envie: `git tag v<versão> && git push origin main --tags`.
773
415
 
774
- ## License
416
+ ## Licença
775
417
 
776
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
418
+ Código aberto sob a [licença MIT](LICENSE.txt).