audit_log_rails 0.1.0 → 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.
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Mantem compatibilidade entre o nome publicado da gem e o namespace interno.
4
+ require_relative "audit_logger"
@@ -6,6 +6,18 @@ module AuditLogger
6
6
 
7
7
  JSON_FIELDS = %w[changed_by_other audited_changes audited_changes_humanize].freeze
8
8
 
9
+ # Historico de um agregado: tudo o que tem o registro como dono, do mais recente ao mais antigo.
10
+ scope :of_owner, lambda { |owner|
11
+ where(OwnerResolver.attributes_for(owner)).order(created_at: :desc, id: :desc)
12
+ }
13
+
14
+ # As colunas de dono chegaram na 0.2.0; a migration de upgrade cria as duas.
15
+ def self.owner_columns?
16
+ column_names.include?("owner_type") && column_names.include?("owner_id")
17
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::NoDatabaseError
18
+ false
19
+ end
20
+
9
21
  # Mantem compatibilidade entre bancos nativos JSON/JSONB e colunas texto sem
10
22
  # depender de conexao ativa no carregamento da classe.
11
23
  JSON_FIELDS.each do |field_name|
@@ -39,7 +51,7 @@ module AuditLogger
39
51
 
40
52
  # A verificacao do tipo fica em runtime, quando a conexao ja deve estar disponivel.
41
53
  def json_like_column?(field_name)
42
- self.class.type_for_attribute(field_name.to_s).type.in?([ :json, :jsonb ])
54
+ self.class.type_for_attribute(field_name.to_s).type.in?(%i[json jsonb])
43
55
  rescue ActiveRecord::ConnectionNotDefined, ActiveRecord::NoDatabaseError
44
56
  false
45
57
  end
@@ -38,21 +38,29 @@ module AuditLogger
38
38
  # Usa snapshot completo em create e destroy para preservar contexto.
39
39
  def snapshot_fields
40
40
  filtered_attributes.each_with_object({}) do |(attribute, value), result|
41
- result[attribute] = { "value" => value }
41
+ result[attribute] = { "value" => mask(attribute, value) }
42
42
  end
43
43
  end
44
44
 
45
45
  # Em update salva apenas delta com old/new para reduzir ruido.
46
+ # Atributo mascarado entra no delta (a troca de senha fica registrada), mas sem os valores.
46
47
  def update_fields
47
48
  filtered_previous_changes.each_with_object({}) do |(attribute, values), result|
48
49
  old_value, new_value = values
49
50
  result[attribute] = {
50
- "old_value" => old_value,
51
- "new_value" => new_value
51
+ "old_value" => mask(attribute, old_value),
52
+ "new_value" => mask(attribute, new_value)
52
53
  }
53
54
  end
54
55
  end
55
56
 
57
+ # Valor nulo continua nulo: mostra que o campo estava vazio sem revelar nada.
58
+ def mask(attribute, value)
59
+ return value if value.nil? || !masked_attributes.include?(attribute)
60
+
61
+ configuration.mask_value
62
+ end
63
+
56
64
  # Remove do snapshot os atributos que nao devem ser auditados.
57
65
  def filtered_attributes
58
66
  record.attributes.reject do |attribute, _value|
@@ -71,5 +79,9 @@ module AuditLogger
71
79
  def ignored_attributes
72
80
  @ignored_attributes ||= model_config.effective_ignored_attributes(configuration)
73
81
  end
82
+
83
+ def masked_attributes
84
+ @masked_attributes ||= model_config.effective_masked_attributes(configuration)
85
+ end
74
86
  end
75
87
  end
@@ -10,6 +10,7 @@ module AuditLogger
10
10
  uuid_resolver
11
11
  ip_resolver
12
12
  humanizer
13
+ error_handler
13
14
  ].freeze
14
15
 
15
16
  def self.validate!(configuration)
@@ -26,6 +27,8 @@ module AuditLogger
26
27
  validate_humanize_by_default!
27
28
  validate_i18n_scopes!
28
29
  validate_ignored_attributes!
30
+ validate_masked_attributes!
31
+ validate_booleans!
29
32
 
30
33
  configuration
31
34
  end
@@ -63,6 +66,25 @@ module AuditLogger
63
66
  end
64
67
  end
65
68
 
69
+ # Mesma regra dos ignorados: nomes de atributo como string ou symbol.
70
+ def validate_masked_attributes!
71
+ value = configuration.masked_attributes
72
+
73
+ unless value.is_a?(Array) && value.all? { |item| item.is_a?(String) || item.is_a?(Symbol) }
74
+ raise ConfigurationError,
75
+ "masked_attributes deve ser um Array de strings ou symbols"
76
+ end
77
+ end
78
+
79
+ # Flags simples nao aceitam valores "truthy" ambiguos.
80
+ def validate_booleans!
81
+ %i[resolve_associations raise_errors].each do |field_name|
82
+ next if [true, false].include?(configuration.public_send(field_name))
83
+
84
+ raise ConfigurationError, "#{field_name} deve ser true ou false"
85
+ end
86
+ end
87
+
66
88
  # Mantem a comparacao de atributos ignorados previsivel.
67
89
  def validate_ignored_attributes!
68
90
  value = configuration.ignored_attributes
@@ -11,7 +11,12 @@ module AuditLogger
11
11
  :humanize_by_default,
12
12
  :i18n_scopes,
13
13
  :ignored_attributes,
14
- :humanizer
14
+ :masked_attributes,
15
+ :mask_value,
16
+ :humanizer,
17
+ :resolve_associations,
18
+ :raise_errors,
19
+ :error_handler
15
20
 
16
21
  def initialize
17
22
  reset!
@@ -28,7 +33,15 @@ module AuditLogger
28
33
  self.humanize_by_default = true
29
34
  self.i18n_scopes = default_i18n_scopes
30
35
  self.ignored_attributes = default_ignored_attributes
36
+ # Atributos sensiveis: a alteracao e registrada, mas o valor vira mask_value.
37
+ self.masked_attributes = default_masked_attributes
38
+ self.mask_value = "[FILTRADO]"
31
39
  self.humanizer = nil
40
+ # Troca o id de belongs_to pelo rotulo do registro associado no payload humanizado.
41
+ self.resolve_associations = true
42
+ # Por padrao a auditoria nunca derruba a operacao principal; nos testes vale ligar.
43
+ self.raise_errors = false
44
+ self.error_handler = nil
32
45
 
33
46
  self
34
47
  end
@@ -44,5 +57,10 @@ module AuditLogger
44
57
  def default_ignored_attributes
45
58
  %i[created_at updated_at lock_version]
46
59
  end
60
+
61
+ # Senhas e tokens comuns em apps Rails (Devise, has_secure_password, tokens de API).
62
+ def default_masked_attributes
63
+ %i[password password_digest encrypted_password reset_password_token confirmation_token unlock_token]
64
+ end
47
65
  end
48
66
  end
@@ -65,6 +65,8 @@ module AuditLogger
65
65
  end
66
66
 
67
67
  # Mantem o label traduzido e preserva os dados do payload bruto.
68
+ # Em belongs_to, acrescenta o rotulo do registro associado: "r_cargo_id: 3 -> 7" vira
69
+ # old_value_label "PROFESSOR" e new_value_label "ASSESSOR".
68
70
  def default_field_payload(attribute, values)
69
71
  result = { "label" => translate_attribute(attribute) }
70
72
 
@@ -72,7 +74,51 @@ module AuditLogger
72
74
  result[key] = value
73
75
  end
74
76
 
75
- result
77
+ result.merge(association_labels(attribute, values))
78
+ end
79
+
80
+ VALUE_KEYS = %w[old_value new_value value].freeze
81
+
82
+ def association_labels(attribute, values)
83
+ return {} unless configuration.resolve_associations
84
+
85
+ reflection = belongs_to_reflection(attribute)
86
+ return {} unless reflection
87
+
88
+ values.each_with_object({}) do |(key, value), labels|
89
+ next unless VALUE_KEYS.include?(key) && !value.nil?
90
+
91
+ label = association_label(reflection, value)
92
+ labels["#{key}_label"] = label unless label.nil?
93
+ end
94
+ rescue StandardError => e
95
+ log_warning(attribute, e)
96
+ {}
97
+ end
98
+
99
+ def belongs_to_reflection(attribute)
100
+ record.class.reflect_on_all_associations(:belongs_to).find do |reflection|
101
+ !reflection.polymorphic? && reflection.foreign_key.to_s == attribute.to_s
102
+ end
103
+ end
104
+
105
+ # Procura tambem registros excluidos (soft delete): o historico precisa do nome antigo.
106
+ def association_label(reflection, id)
107
+ klass = reflection.klass
108
+ scope = klass.respond_to?(:with_deleted) ? klass.with_deleted : klass.unscoped
109
+ associated = scope.find_by(reflection.association_primary_key => id)
110
+ associated && display_label(associated)
111
+ end
112
+
113
+ LABEL_METHODS = %i[display_name descricao nome name title].freeze
114
+
115
+ # Prefere um to_s definido na propria model; depois metodos comuns de rotulo.
116
+ def display_label(associated)
117
+ owner = associated.class.instance_method(:to_s).owner
118
+ return associated.to_s if owner.is_a?(Class) && owner < ActiveRecord::Base
119
+
120
+ method_name = LABEL_METHODS.find { |name| associated.respond_to?(name) }
121
+ method_name && associated.public_send(method_name)&.to_s
76
122
  end
77
123
 
78
124
  # Faz o fallback de traducao por escopo ate encontrar um label valido.
@@ -3,13 +3,19 @@
3
3
  module AuditLogger
4
4
  class ModelConfig
5
5
  # Guarda apenas a configuracao local da model, sem misturar com a global.
6
- attr_reader :humanize, :i18n_scopes, :humanizer, :ignored_attributes
6
+ attr_reader :humanize, :i18n_scopes, :humanizer, :ignored_attributes, :masked_attributes, :owner
7
7
 
8
- def initialize(humanize: nil, i18n_scopes: nil, humanizer: nil, ignored_attributes: [])
8
+ # owner: nome da associacao (Symbol) ou Proc(record) que devolve o registro "dono" do log,
9
+ # para consultar num lugar so o historico de um agregado (ex.: pessoa + contratos).
10
+ # Sem owner, o dono e o proprio registro.
11
+ def initialize(humanize: nil, i18n_scopes: nil, humanizer: nil, ignored_attributes: [], masked_attributes: [],
12
+ owner: nil)
9
13
  @humanize = humanize
10
14
  @i18n_scopes = i18n_scopes
11
15
  @humanizer = humanizer
12
16
  @ignored_attributes = Array(ignored_attributes)
17
+ @masked_attributes = Array(masked_attributes)
18
+ @owner = owner
13
19
 
14
20
  validate!
15
21
  end
@@ -38,6 +44,11 @@ module AuditLogger
38
44
  (global_configuration.ignored_attributes + ignored_attributes).map(&:to_s).uniq
39
45
  end
40
46
 
47
+ # Combina masked attributes globais e locais em uma lista unica.
48
+ def effective_masked_attributes(global_configuration)
49
+ (global_configuration.masked_attributes + masked_attributes).map(&:to_s).uniq
50
+ end
51
+
41
52
  private
42
53
 
43
54
  # Concentra as validacoes do contrato aceito pela DSL `auditable`.
@@ -46,6 +57,8 @@ module AuditLogger
46
57
  validate_i18n_scopes!
47
58
  validate_humanizer!
48
59
  validate_ignored_attributes!
60
+ validate_masked_attributes!
61
+ validate_owner!
49
62
  end
50
63
 
51
64
  # Limita o override de humanizacao a valores simples e previsiveis.
@@ -70,6 +83,18 @@ module AuditLogger
70
83
  raise ConfigurationError, "humanizer da model deve ser um Proc/Lambda ou nil"
71
84
  end
72
85
 
86
+ def validate_masked_attributes!
87
+ return if masked_attributes.all? { |item| item.is_a?(String) || item.is_a?(Symbol) }
88
+
89
+ raise ConfigurationError, "masked_attributes da model deve ser um Array de strings ou symbols"
90
+ end
91
+
92
+ def validate_owner!
93
+ return if owner.nil? || owner.is_a?(Symbol) || owner.is_a?(Proc)
94
+
95
+ raise ConfigurationError, "owner da model deve ser um Symbol (associacao), um Proc ou nil"
96
+ end
97
+
73
98
  # Normaliza a entrada para facilitar o filtro de atributos depois.
74
99
  def validate_ignored_attributes!
75
100
  return if ignored_attributes.all? { |item| item.is_a?(String) || item.is_a?(Symbol) }
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AuditLogger
4
+ # Descobre o registro "dono" do log, para consultar o historico de um agregado num lugar so
5
+ # (ex.: alteracoes da pessoa, dos contratos e dos dependentes dela).
6
+ class OwnerResolver
7
+ def self.call(record, model_config)
8
+ new(record, model_config).call
9
+ end
10
+
11
+ def initialize(record, model_config)
12
+ @record = record
13
+ @model_config = model_config
14
+ end
15
+
16
+ # Sem owner configurado, ou com associacao vazia, o dono e o proprio registro.
17
+ def call
18
+ attributes_for(resolve_owner || record)
19
+ end
20
+
21
+ # Atributos owner_type/owner_id de um registro qualquer (tambem usado por record_event).
22
+ def self.attributes_for(owner)
23
+ return {} if owner.nil?
24
+
25
+ klass = owner.class
26
+ type = klass.respond_to?(:base_class) ? klass.base_class.name : klass.name
27
+ { owner_type: type, owner_id: owner.id.to_s }
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :record, :model_config
33
+
34
+ def resolve_owner
35
+ case model_config.owner
36
+ when Symbol then record.public_send(model_config.owner)
37
+ when Proc then model_config.owner.call(record)
38
+ end
39
+ end
40
+
41
+ def attributes_for(owner)
42
+ self.class.attributes_for(owner)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AuditLogger
4
+ # Grava uma linha em audit_logs com o contexto do ator (usuario, uuid, ip).
5
+ # Compartilhado pelo log automatico (auditable) e pelos eventos manuais (record_event).
6
+ module Persistence
7
+ def self.create!(model_class_name:, id_object:, action:, audited_changes:, audited_changes_humanize:, owner: {})
8
+ actor_context = ActorContextResolver.call(AuditLogger.configuration)
9
+ now = Time.current
10
+ attributes = {
11
+ model_class_name: model_class_name,
12
+ id_object: id_object.to_s,
13
+ action: action.to_s,
14
+ uuid: actor_context[:uuid].to_s,
15
+ changed_by_id: actor_context[:changed_by_id],
16
+ changed_by_type: actor_context[:changed_by_type],
17
+ changed_by_other: actor_context[:changed_by_other],
18
+ audited_changes: audited_changes,
19
+ audited_changes_humanize: audited_changes_humanize,
20
+ ip_remote: actor_context[:ip_remote],
21
+ created_at: now,
22
+ updated_at: now
23
+ }
24
+ # Instalacoes da 0.1.x ainda sem as colunas de dono continuam funcionando.
25
+ attributes.merge!(owner) if AuditLog.owner_columns?
26
+
27
+ AuditLog.create!(attributes)
28
+ end
29
+ end
30
+ end
@@ -12,6 +12,7 @@ module AuditLogger
12
12
 
13
13
  generators do
14
14
  require_relative "../generators/audit_logger/install_generator"
15
+ require_relative "../generators/audit_logger/upgrade_generator"
15
16
  end
16
17
  end
17
18
  end
@@ -14,7 +14,16 @@ module AuditLogger
14
14
  end
15
15
 
16
16
  # Orquestra toda a montagem do log e persiste o resultado final.
17
+ # Roda depois do commit: uma falha aqui nao pode virar erro da requisicao que ja gravou.
17
18
  def call
19
+ AuditLogger.safely(record: record, action: action) { persist }
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :record, :action, :configuration, :model_config
25
+
26
+ def persist
18
27
  payload = ChangeExtractor.new(
19
28
  record: record,
20
29
  action: action,
@@ -24,8 +33,7 @@ module AuditLogger
24
33
 
25
34
  return if payload.fetch("fields", {}).empty?
26
35
 
27
- # Resolve o contexto do ator e gera a versao amigavel do payload antes de persistir.
28
- actor_context = ActorContextResolver.call(configuration)
36
+ # Gera a versao amigavel do payload antes de persistir.
29
37
  humanized_payload = Humanizer.new(
30
38
  record: record,
31
39
  payload: payload,
@@ -33,29 +41,14 @@ module AuditLogger
33
41
  configuration: configuration
34
42
  ).call
35
43
 
36
- AuditLog.create!(
44
+ AuditLogger::Persistence.create!(
37
45
  model_class_name: record.class.name,
38
- id_object: resolve_record_id,
46
+ id_object: record.id.to_s,
39
47
  action: action,
40
- uuid: actor_context[:uuid].to_s,
41
- changed_by_id: actor_context[:changed_by_id],
42
- changed_by_type: actor_context[:changed_by_type],
43
- changed_by_other: actor_context[:changed_by_other],
44
48
  audited_changes: payload,
45
49
  audited_changes_humanize: humanized_payload,
46
- ip_remote: actor_context[:ip_remote],
47
- created_at: Time.current,
48
- updated_at: Time.current
50
+ owner: OwnerResolver.call(record, model_config)
49
51
  )
50
52
  end
51
-
52
- private
53
-
54
- attr_reader :record, :action, :configuration, :model_config
55
-
56
- # Normaliza o id como string para manter consistencia entre tipos de PK.
57
- def resolve_record_id
58
- record.id.to_s
59
- end
60
53
  end
61
54
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AuditLogger
4
+ # Evento registrado a mao, para o que nao passa pelos callbacks do model:
5
+ # pedido de reset de senha, login, alteracao em massa (update_all) etc.
6
+ class RecordEvent
7
+ def self.call(**options)
8
+ new(**options).call
9
+ end
10
+
11
+ # fields usa o mesmo formato do log automatico: { "campo" => { "old_value" =>, "new_value" => } }
12
+ # ou { "campo" => { "value" => } }. metadata guarda contexto livre (origem, quantidade...).
13
+ def initialize(action:, record: nil, model_class_name: nil, id_object: nil, owner: nil, fields: {}, metadata: {})
14
+ @action = action.to_s
15
+ @record = record
16
+ @model_class_name = model_class_name
17
+ @id_object = id_object
18
+ @owner = owner
19
+ @fields = stringify(fields)
20
+ @metadata = metadata || {}
21
+ end
22
+
23
+ def call
24
+ AuditLogger.safely(record: record, action: action) { persist }
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :action, :record, :model_class_name, :id_object, :owner, :fields, :metadata
30
+
31
+ def persist
32
+ payload = { "type" => action, "fields" => fields }
33
+ payload["metadata"] = stringify(metadata) unless metadata.empty?
34
+
35
+ AuditLogger::Persistence.create!(
36
+ model_class_name: model_class_name || record&.class&.name || "AuditLogger::Event",
37
+ id_object: id_object || record&.id,
38
+ action: action,
39
+ audited_changes: payload,
40
+ audited_changes_humanize: humanize(payload),
41
+ owner: owner_attributes
42
+ )
43
+ end
44
+
45
+ # Com registro, reaproveita a humanizacao (labels e associacoes) da model dele.
46
+ def humanize(payload)
47
+ return payload unless record
48
+
49
+ humanized = Humanizer.new(
50
+ record: record, payload: payload, model_config: model_config, configuration: AuditLogger.configuration
51
+ ).call
52
+ humanized.merge(payload.slice("metadata"))
53
+ end
54
+
55
+ def owner_attributes
56
+ return OwnerResolver.attributes_for(owner) if owner
57
+ return OwnerResolver.call(record, model_config) if record
58
+
59
+ {}
60
+ end
61
+
62
+ def model_config
63
+ klass = record.class
64
+ klass.respond_to?(:audit_logger_model_config) ? klass.audit_logger_model_config : ModelConfig.new
65
+ end
66
+
67
+ def stringify(hash)
68
+ hash.to_h.each_with_object({}) do |(key, value), result|
69
+ result[key.to_s] = value.is_a?(Hash) ? stringify(value) : value
70
+ end
71
+ end
72
+ end
73
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AuditLogger
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/audit_logger.rb CHANGED
@@ -14,7 +14,10 @@ require_relative "audit_logger/audit_log"
14
14
  require_relative "audit_logger/actor_context_resolver"
15
15
  require_relative "audit_logger/change_extractor"
16
16
  require_relative "audit_logger/humanizer"
17
+ require_relative "audit_logger/owner_resolver"
18
+ require_relative "audit_logger/persistence"
17
19
  require_relative "audit_logger/record_audit_entry"
20
+ require_relative "audit_logger/record_event"
18
21
  require_relative "audit_logger/auditable"
19
22
 
20
23
  module AuditLogger
@@ -38,6 +41,38 @@ module AuditLogger
38
41
  def reset_configuration!
39
42
  @configuration = Configuration.new
40
43
  end
44
+
45
+ # Registra um evento que nao passa pelos callbacks (reset de senha, login, update_all).
46
+ # Ver AuditLogger::RecordEvent para as opcoes.
47
+ def record_event(**options)
48
+ RecordEvent.call(**options)
49
+ end
50
+
51
+ # A auditoria nunca derruba a operacao principal: registra o erro, chama o error_handler
52
+ # e segue. Com raise_errors = true (util em teste) o erro sobe.
53
+ def safely(record: nil, action: nil)
54
+ yield
55
+ rescue StandardError => e
56
+ raise if configuration.raise_errors
57
+
58
+ report_error(e, record, action)
59
+ nil
60
+ end
61
+
62
+ private
63
+
64
+ def report_error(error, record, action)
65
+ alvo = record ? " de #{record.class.name}##{record.id}" : ""
66
+ message = "[AuditLogger] Falha ao registrar #{action}#{alvo}: #{error.class} - #{error.message}"
67
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
68
+ Rails.logger.error(message)
69
+ else
70
+ warn(message)
71
+ end
72
+ configuration.error_handler&.call(error, record, action)
73
+ rescue StandardError
74
+ nil
75
+ end
41
76
  end
42
77
  end
43
78
 
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Upgrade da 0.1.x: colunas do registro "dono", para consultar o historico de um agregado.
4
+ class AddOwnerToAuditLogs < ActiveRecord::Migration<%= migration_version %>
5
+ def up
6
+ add_column :audit_logs, :owner_type, :string unless column_exists?(:audit_logs, :owner_type)
7
+ add_column :audit_logs, :owner_id, :string unless column_exists?(:audit_logs, :owner_id)
8
+ return if index_exists?(:audit_logs, [:owner_type, :owner_id, :created_at])
9
+
10
+ add_index :audit_logs, [:owner_type, :owner_id, :created_at]
11
+ end
12
+
13
+ def down
14
+ remove_index :audit_logs, [:owner_type, :owner_id, :created_at] if index_exists?(:audit_logs, [:owner_type, :owner_id, :created_at])
15
+ remove_column :audit_logs, :owner_id if column_exists?(:audit_logs, :owner_id)
16
+ remove_column :audit_logs, :owner_type if column_exists?(:audit_logs, :owner_type)
17
+ end
18
+ end
@@ -42,6 +42,19 @@ AuditLogger.configure do |config|
42
42
  # Campos que normalmente nao agregam valor na auditoria.
43
43
  config.ignored_attributes = [:created_at, :updated_at, :lock_version]
44
44
 
45
+ # Campos sensiveis: a alteracao e registrada (ex.: "senha alterada"), mas o valor vira mask_value.
46
+ # Acrescente tokens proprios do projeto. Por model: auditable masked_attributes: [:token]
47
+ config.masked_attributes = [:password, :password_digest, :encrypted_password, :reset_password_token, :confirmation_token, :unlock_token]
48
+ config.mask_value = "[FILTRADO]"
49
+
50
+ # Em belongs_to, grava no payload humanizado o rotulo do registro associado (old_value_label/new_value_label).
51
+ config.resolve_associations = true
52
+
53
+ # A auditoria roda depois do commit e nunca derruba a requisicao: falha vira log de erro.
54
+ # Ligue raise_errors no ambiente de teste para enxergar problemas.
55
+ config.raise_errors = false
56
+ # config.error_handler = ->(error, record, action) { Sentry.capture_exception(error) }
57
+
45
58
  # Humanizer opcional.
46
59
  # Se retornar nil, a gem usa o fallback padrao baseado em I18n.
47
60
  config.humanizer = ->(_model_klass, _attribute, _old_value, _new_value) { nil }
@@ -14,6 +14,8 @@ class CreateAuditLogs < ActiveRecord::Migration<%= migration_version %>
14
14
  t.jsonb :audited_changes, null: false, default: {}
15
15
  t.jsonb :audited_changes_humanize, null: false, default: {}
16
16
  t.string :ip_remote
17
+ t.string :owner_type
18
+ t.string :owner_id
17
19
  t.datetime :deleted_at
18
20
  t.timestamps
19
21
  end
@@ -21,6 +23,7 @@ class CreateAuditLogs < ActiveRecord::Migration<%= migration_version %>
21
23
  add_index :audit_logs, [:model_class_name, :id_object]
22
24
  add_index :audit_logs, :action
23
25
  add_index :audit_logs, [:changed_by_type, :changed_by_id]
26
+ add_index :audit_logs, [:owner_type, :owner_id, :created_at]
24
27
  add_index :audit_logs, :created_at
25
28
  add_index :audit_logs, :deleted_at
26
29
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module AuditLogger
7
+ module Generators
8
+ # Para quem instalou a 0.1.x: cria a migration das colunas owner_type/owner_id.
9
+ class UpgradeGenerator < Rails::Generators::Base
10
+ include ActiveRecord::Generators::Migration
11
+
12
+ source_root File.expand_path("templates", __dir__)
13
+ desc "Cria a migration que acrescenta owner_type/owner_id em audit_logs (upgrade da 0.1.x)."
14
+
15
+ def copy_migration
16
+ migration_template "add_owner_to_audit_logs.rb.tt", "db/migrate/add_owner_to_audit_logs.rb"
17
+ end
18
+
19
+ private
20
+
21
+ def migration_version
22
+ "[#{ActiveRecord::VERSION::STRING.to_f}]"
23
+ end
24
+
25
+ def self.next_migration_number(path)
26
+ ActiveRecord::Generators::Base.next_migration_number(path)
27
+ end
28
+ end
29
+ end
30
+ end