audit_log_rails 0.1.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/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AuditLogger
4
+ class ActorContextResolver
5
+ def self.call(configuration)
6
+ new(configuration).call
7
+ end
8
+
9
+ def initialize(configuration)
10
+ @configuration = configuration
11
+ end
12
+
13
+ # Resolve todos os campos de contexto do ator em um unico ponto.
14
+ def call
15
+ {
16
+ changed_by_id: call_resolver(:changed_by_id, configuration.changed_by_id_resolver),
17
+ changed_by_type: call_resolver(:changed_by_type, configuration.changed_by_type_resolver),
18
+ changed_by_other: normalize_json_object(call_resolver(:changed_by_other, configuration.changed_by_other_resolver)),
19
+ uuid: resolve_uuid,
20
+ ip_remote: call_resolver(:ip_remote, configuration.ip_resolver)
21
+ }
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :configuration
27
+
28
+ # Executa o resolver apenas quando ele foi configurado e isola falhas.
29
+ def call_resolver(label, resolver)
30
+ return nil if resolver.nil?
31
+
32
+ resolver.call
33
+ rescue StandardError => e
34
+ log_warning(label, e)
35
+ nil
36
+ end
37
+
38
+ # Garante que o metadado adicional sempre seja persistido como objeto JSON.
39
+ def normalize_json_object(value)
40
+ return {} if value.nil?
41
+ return value if value.is_a?(Hash)
42
+
43
+ { value: value }
44
+ end
45
+
46
+ # Mantem um fallback seguro para correlacao quando o app nao informar uuid.
47
+ def resolve_uuid
48
+ value = call_resolver(:uuid, configuration.uuid_resolver)
49
+ return value if value && !value.to_s.empty?
50
+
51
+ SecureRandom.uuid
52
+ end
53
+
54
+ # Evita que falhas de contexto derrubem a auditoria inteira da requisicao.
55
+ def log_warning(label, error)
56
+ message = "[AuditLogger] Falha ao resolver #{label}: #{error.class} - #{error.message}"
57
+
58
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
59
+ Rails.logger.warn(message)
60
+ else
61
+ warn(message)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AuditLogger
4
+ class AuditLog < ActiveRecord::Base
5
+ self.table_name = "audit_logs"
6
+
7
+ JSON_FIELDS = %w[changed_by_other audited_changes audited_changes_humanize].freeze
8
+
9
+ # Mantem compatibilidade entre bancos nativos JSON/JSONB e colunas texto sem
10
+ # depender de conexao ativa no carregamento da classe.
11
+ JSON_FIELDS.each do |field_name|
12
+ define_method("#{field_name}=") do |value|
13
+ super(normalize_json_assignment(field_name, value))
14
+ end
15
+
16
+ define_method(field_name) do
17
+ deserialize_json_value(super())
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ # Em bancos sem suporte nativo a JSON, persiste como string JSON.
24
+ def normalize_json_assignment(field_name, value)
25
+ return value if value.nil? || json_like_column?(field_name)
26
+ return value if value.is_a?(String)
27
+
28
+ JSON.generate(value)
29
+ end
30
+
31
+ # Ao ler colunas texto, reconstrói o Hash/Array original para o restante da gem.
32
+ def deserialize_json_value(value)
33
+ return value unless value.is_a?(String)
34
+
35
+ JSON.parse(value)
36
+ rescue JSON::ParserError
37
+ value
38
+ end
39
+
40
+ # A verificacao do tipo fica em runtime, quando a conexao ja deve estar disponivel.
41
+ def json_like_column?(field_name)
42
+ self.class.type_for_attribute(field_name.to_s).type.in?([ :json, :jsonb ])
43
+ rescue ActiveRecord::ConnectionNotDefined, ActiveRecord::NoDatabaseError
44
+ false
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AuditLogger
4
+ module Auditable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ # Guarda a configuracao local da model sem poluir instancias.
9
+ class_attribute :audit_logger_model_config, instance_accessor: false, default: ModelConfig.new
10
+ # Evita registrar os mesmos callbacks mais de uma vez.
11
+ class_attribute :audit_logger_callbacks_registered, instance_accessor: false, default: false
12
+ end
13
+
14
+ class_methods do
15
+ # DSL principal da gem usada pelas models do projeto cliente.
16
+ def auditable(**options)
17
+ # Mantem a configuracao por model isolada do restante da gem.
18
+ self.audit_logger_model_config = ModelConfig.new(**options)
19
+
20
+ # Expoe uma associacao simples para consultar os logs desta model.
21
+ define_audit_logs_association!
22
+
23
+ return if audit_logger_callbacks_registered
24
+
25
+ # Usa callbacks de commit para evitar auditoria falsa em caso de rollback.
26
+ after_create_commit { AuditLogger::RecordAuditEntry.call(self, action: :create) }
27
+ after_update_commit { AuditLogger::RecordAuditEntry.call(self, action: :update) }
28
+ after_destroy_commit { AuditLogger::RecordAuditEntry.call(self, action: :destroy) }
29
+
30
+ self.audit_logger_callbacks_registered = true
31
+ end
32
+
33
+ private
34
+
35
+ # Define a associacao apenas uma vez para evitar conflitos com reload da classe.
36
+ def define_audit_logs_association!
37
+ return if reflect_on_association(:audit_logs)
38
+
39
+ has_many :audit_logs,
40
+ ->(record) { where(model_class_name: record.class.name) },
41
+ class_name: "AuditLogger::AuditLog",
42
+ foreign_key: :id_object,
43
+ primary_key: :id
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AuditLogger
4
+ class ChangeExtractor
5
+ def initialize(record:, action:, model_config:, configuration:)
6
+ @record = record
7
+ @action = action.to_s
8
+ @model_config = model_config
9
+ @configuration = configuration
10
+ end
11
+
12
+ # Retorna o payload bruto no formato padronizado da gem.
13
+ def call
14
+ {
15
+ "type" => action,
16
+ "fields" => extract_fields
17
+ }
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :record, :action, :model_config, :configuration
23
+
24
+ # Direciona a extracao conforme o tipo da acao auditada.
25
+ def extract_fields
26
+ case action
27
+ when "create"
28
+ snapshot_fields
29
+ when "update"
30
+ update_fields
31
+ when "destroy"
32
+ snapshot_fields
33
+ else
34
+ {}
35
+ end
36
+ end
37
+
38
+ # Usa snapshot completo em create e destroy para preservar contexto.
39
+ def snapshot_fields
40
+ filtered_attributes.each_with_object({}) do |(attribute, value), result|
41
+ result[attribute] = { "value" => value }
42
+ end
43
+ end
44
+
45
+ # Em update salva apenas delta com old/new para reduzir ruido.
46
+ def update_fields
47
+ filtered_previous_changes.each_with_object({}) do |(attribute, values), result|
48
+ old_value, new_value = values
49
+ result[attribute] = {
50
+ "old_value" => old_value,
51
+ "new_value" => new_value
52
+ }
53
+ end
54
+ end
55
+
56
+ # Remove do snapshot os atributos que nao devem ser auditados.
57
+ def filtered_attributes
58
+ record.attributes.reject do |attribute, _value|
59
+ ignored_attributes.include?(attribute)
60
+ end
61
+ end
62
+
63
+ # Filtra as mudancas do ActiveRecord antes de montar o delta final.
64
+ def filtered_previous_changes
65
+ record.previous_changes.reject do |attribute, _value|
66
+ ignored_attributes.include?(attribute)
67
+ end
68
+ end
69
+
70
+ # Memoiza a lista final de ignored attributes para nao recalcular varias vezes.
71
+ def ignored_attributes
72
+ @ignored_attributes ||= model_config.effective_ignored_attributes(configuration)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AuditLogger
4
+ class ConfigValidator
5
+ # Campos que aceitam estrategia configuravel via Proc/Lambda.
6
+ CALLABLE_FIELDS = %i[
7
+ changed_by_id_resolver
8
+ changed_by_type_resolver
9
+ changed_by_other_resolver
10
+ uuid_resolver
11
+ ip_resolver
12
+ humanizer
13
+ ].freeze
14
+
15
+ def self.validate!(configuration)
16
+ new(configuration).validate!
17
+ end
18
+
19
+ def initialize(configuration)
20
+ @configuration = configuration
21
+ end
22
+
23
+ # Centraliza a validacao para falhar cedo no boot da aplicacao.
24
+ def validate!
25
+ validate_callable_fields!
26
+ validate_humanize_by_default!
27
+ validate_i18n_scopes!
28
+ validate_ignored_attributes!
29
+
30
+ configuration
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :configuration
36
+
37
+ # Garante que os pontos extensivos da gem sejam sempre chamaveis.
38
+ def validate_callable_fields!
39
+ CALLABLE_FIELDS.each do |field_name|
40
+ value = configuration.public_send(field_name)
41
+ next if value.nil? || value.is_a?(Proc)
42
+
43
+ raise ConfigurationError,
44
+ "#{field_name} deve ser um Proc/Lambda ou nil"
45
+ end
46
+ end
47
+
48
+ # Evita estados ambiguos de configuracao para humanizacao.
49
+ def validate_humanize_by_default!
50
+ return if [true, false].include?(configuration.humanize_by_default)
51
+
52
+ raise ConfigurationError,
53
+ "humanize_by_default deve ser true ou false"
54
+ end
55
+
56
+ # Obriga escopos consistentes para o fallback de traducao.
57
+ def validate_i18n_scopes!
58
+ value = configuration.i18n_scopes
59
+
60
+ unless value.is_a?(Array) && value.all? { |item| item.is_a?(String) && !item.empty? }
61
+ raise ConfigurationError,
62
+ "i18n_scopes deve ser um Array de strings nao vazias"
63
+ end
64
+ end
65
+
66
+ # Mantem a comparacao de atributos ignorados previsivel.
67
+ def validate_ignored_attributes!
68
+ value = configuration.ignored_attributes
69
+
70
+ unless value.is_a?(Array) && value.all? { |item| item.is_a?(String) || item.is_a?(Symbol) }
71
+ raise ConfigurationError,
72
+ "ignored_attributes deve ser um Array de strings ou symbols"
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AuditLogger
4
+ class Configuration
5
+ # Cada accessor representa um ponto configuravel da gem.
6
+ attr_accessor :changed_by_id_resolver,
7
+ :changed_by_type_resolver,
8
+ :changed_by_other_resolver,
9
+ :uuid_resolver,
10
+ :ip_resolver,
11
+ :humanize_by_default,
12
+ :i18n_scopes,
13
+ :ignored_attributes,
14
+ :humanizer
15
+
16
+ def initialize
17
+ reset!
18
+ end
19
+
20
+ # Restaura todos os defaults para um estado conhecido e previsivel.
21
+ def reset!
22
+ # Defaults simples para que a gem funcione sem obrigar configuracao imediata.
23
+ self.changed_by_id_resolver = nil
24
+ self.changed_by_type_resolver = nil
25
+ self.changed_by_other_resolver = -> { {} }
26
+ self.uuid_resolver = nil
27
+ self.ip_resolver = nil
28
+ self.humanize_by_default = true
29
+ self.i18n_scopes = default_i18n_scopes
30
+ self.ignored_attributes = default_ignored_attributes
31
+ self.humanizer = nil
32
+
33
+ self
34
+ end
35
+
36
+ private
37
+
38
+ # Escopos padrao usados para localizar labels de atributos.
39
+ def default_i18n_scopes
40
+ ["activerecord.attributes", "attributes"]
41
+ end
42
+
43
+ # Campos normalmente ruidosos que nao agregam valor na auditoria.
44
+ def default_ignored_attributes
45
+ %i[created_at updated_at lock_version]
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AuditLogger
4
+ class Humanizer
5
+ def initialize(record:, payload:, model_config:, configuration:)
6
+ @record = record
7
+ @payload = payload
8
+ @model_config = model_config
9
+ @configuration = configuration
10
+ end
11
+
12
+ # Gera o payload humanizado mantendo a mesma estrutura do payload bruto.
13
+ def call
14
+ return empty_payload unless model_config.humanize_enabled?(configuration)
15
+
16
+ {
17
+ "type" => payload["type"],
18
+ "fields" => humanized_fields
19
+ }
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :record, :payload, :model_config, :configuration
25
+
26
+ # Quando a humanizacao estiver desabilitada, preserva apenas o tipo da acao.
27
+ def empty_payload
28
+ {
29
+ "type" => payload["type"],
30
+ "fields" => {}
31
+ }
32
+ end
33
+
34
+ # Humaniza campo por campo para permitir fallback individual.
35
+ def humanized_fields
36
+ payload.fetch("fields", {}).each_with_object({}) do |(attribute, values), result|
37
+ result[attribute] = build_field_payload(attribute, values)
38
+ end
39
+ end
40
+
41
+ # Tenta humanizer customizado primeiro e cai no comportamento padrao.
42
+ def build_field_payload(attribute, values)
43
+ custom_result = call_custom_humanizer(attribute, values)
44
+ return normalize_custom_result(attribute, values, custom_result) unless custom_result.nil?
45
+
46
+ default_field_payload(attribute, values)
47
+ end
48
+
49
+ # O humanizer recebe model, atributo e valores bruto para maximo contexto.
50
+ def call_custom_humanizer(attribute, values)
51
+ humanizer = model_config.effective_humanizer(configuration)
52
+ return nil if humanizer.nil?
53
+
54
+ humanizer.call(record.class, attribute, values["old_value"], values["new_value"] || values["value"])
55
+ rescue StandardError => e
56
+ log_warning(attribute, e)
57
+ nil
58
+ end
59
+
60
+ # Aceita retorno em Hash para sobrescrita rica ou valor simples para casos basicos.
61
+ def normalize_custom_result(attribute, values, custom_result)
62
+ return default_field_payload(attribute, values).merge(stringify_keys(custom_result)) if custom_result.is_a?(Hash)
63
+
64
+ default_field_payload(attribute, values).merge("value" => custom_result)
65
+ end
66
+
67
+ # Mantem o label traduzido e preserva os dados do payload bruto.
68
+ def default_field_payload(attribute, values)
69
+ result = { "label" => translate_attribute(attribute) }
70
+
71
+ values.each do |key, value|
72
+ result[key] = value
73
+ end
74
+
75
+ result
76
+ end
77
+
78
+ # Faz o fallback de traducao por escopo ate encontrar um label valido.
79
+ def translate_attribute(attribute)
80
+ scopes = model_config.effective_i18n_scopes(configuration)
81
+ model_key = record.class.model_name.i18n_key
82
+
83
+ scopes.each do |scope|
84
+ translation = I18n.t("#{scope}.#{model_key}.#{attribute}", default: nil)
85
+ return translation if translation
86
+
87
+ fallback = I18n.t("#{scope}.#{attribute}", default: nil)
88
+ return fallback if fallback
89
+ end
90
+
91
+ attribute.to_s.humanize
92
+ end
93
+
94
+ # Normaliza chaves vindas do humanizer customizado para JSON consistente.
95
+ def stringify_keys(hash)
96
+ hash.each_with_object({}) do |(key, value), result|
97
+ result[key.to_s] = value
98
+ end
99
+ end
100
+
101
+ # Evita que falhas no humanizer customizado interrompam a gravacao da auditoria.
102
+ def log_warning(attribute, error)
103
+ message = "[AuditLogger] Falha ao humanizar #{record.class.name}##{attribute}: #{error.class} - #{error.message}"
104
+
105
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
106
+ Rails.logger.warn(message)
107
+ else
108
+ warn(message)
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AuditLogger
4
+ class ModelConfig
5
+ # Guarda apenas a configuracao local da model, sem misturar com a global.
6
+ attr_reader :humanize, :i18n_scopes, :humanizer, :ignored_attributes
7
+
8
+ def initialize(humanize: nil, i18n_scopes: nil, humanizer: nil, ignored_attributes: [])
9
+ @humanize = humanize
10
+ @i18n_scopes = i18n_scopes
11
+ @humanizer = humanizer
12
+ @ignored_attributes = Array(ignored_attributes)
13
+
14
+ validate!
15
+ end
16
+
17
+ # Resolve se a humanizacao ficara ativa considerando override local.
18
+ def humanize_enabled?(global_configuration)
19
+ return global_configuration.humanize_by_default if humanize.nil?
20
+
21
+ humanize
22
+ end
23
+
24
+ # Retorna os escopos efetivos usados para traducao desta model.
25
+ def effective_i18n_scopes(global_configuration)
26
+ return global_configuration.i18n_scopes if i18n_scopes.nil? || i18n_scopes.empty?
27
+
28
+ i18n_scopes
29
+ end
30
+
31
+ # Prioriza o humanizer da model e cai no global quando necessario.
32
+ def effective_humanizer(global_configuration)
33
+ humanizer || global_configuration.humanizer
34
+ end
35
+
36
+ # Combina ignored attributes globais e locais em uma lista unica.
37
+ def effective_ignored_attributes(global_configuration)
38
+ (global_configuration.ignored_attributes + ignored_attributes).map(&:to_s).uniq
39
+ end
40
+
41
+ private
42
+
43
+ # Concentra as validacoes do contrato aceito pela DSL `auditable`.
44
+ def validate!
45
+ validate_humanize!
46
+ validate_i18n_scopes!
47
+ validate_humanizer!
48
+ validate_ignored_attributes!
49
+ end
50
+
51
+ # Limita o override de humanizacao a valores simples e previsiveis.
52
+ def validate_humanize!
53
+ return if humanize.nil? || [true, false].include?(humanize)
54
+
55
+ raise ConfigurationError, "humanize da model deve ser true, false ou nil"
56
+ end
57
+
58
+ # Garante que o fallback de I18n nao receba dados inconsistentes.
59
+ def validate_i18n_scopes!
60
+ return if i18n_scopes.nil?
61
+ return if i18n_scopes.is_a?(Array) && i18n_scopes.all? { |item| item.is_a?(String) && !item.empty? }
62
+
63
+ raise ConfigurationError, "i18n_scopes da model deve ser um Array de strings nao vazias"
64
+ end
65
+
66
+ # Mantem o ponto de extensao da model com a mesma regra do config global.
67
+ def validate_humanizer!
68
+ return if humanizer.nil? || humanizer.is_a?(Proc)
69
+
70
+ raise ConfigurationError, "humanizer da model deve ser um Proc/Lambda ou nil"
71
+ end
72
+
73
+ # Normaliza a entrada para facilitar o filtro de atributos depois.
74
+ def validate_ignored_attributes!
75
+ return if ignored_attributes.all? { |item| item.is_a?(String) || item.is_a?(Symbol) }
76
+
77
+ raise ConfigurationError, "ignored_attributes da model deve ser um Array de strings ou symbols"
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module AuditLogger
6
+ class Railtie < Rails::Railtie
7
+ initializer "audit_logger.active_record" do
8
+ ActiveSupport.on_load(:active_record) do
9
+ include AuditLogger::Auditable
10
+ end
11
+ end
12
+
13
+ generators do
14
+ require_relative "../generators/audit_logger/install_generator"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AuditLogger
4
+ class RecordAuditEntry
5
+ def self.call(record, action:)
6
+ new(record, action: action).call
7
+ end
8
+
9
+ def initialize(record, action:)
10
+ @record = record
11
+ @action = action.to_s
12
+ @configuration = AuditLogger.configuration
13
+ @model_config = record.class.audit_logger_model_config
14
+ end
15
+
16
+ # Orquestra toda a montagem do log e persiste o resultado final.
17
+ def call
18
+ payload = ChangeExtractor.new(
19
+ record: record,
20
+ action: action,
21
+ model_config: model_config,
22
+ configuration: configuration
23
+ ).call
24
+
25
+ return if payload.fetch("fields", {}).empty?
26
+
27
+ # Resolve o contexto do ator e gera a versao amigavel do payload antes de persistir.
28
+ actor_context = ActorContextResolver.call(configuration)
29
+ humanized_payload = Humanizer.new(
30
+ record: record,
31
+ payload: payload,
32
+ model_config: model_config,
33
+ configuration: configuration
34
+ ).call
35
+
36
+ AuditLog.create!(
37
+ model_class_name: record.class.name,
38
+ id_object: resolve_record_id,
39
+ 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
+ audited_changes: payload,
45
+ audited_changes_humanize: humanized_payload,
46
+ ip_remote: actor_context[:ip_remote],
47
+ created_at: Time.current,
48
+ updated_at: Time.current
49
+ )
50
+ 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
+ end
61
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AuditLogger
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "audit_logger/version"
4
+ require "active_support"
5
+ require "active_support/concern"
6
+ require "active_record"
7
+ require "i18n"
8
+ require "json"
9
+ require "securerandom"
10
+ require_relative "audit_logger/configuration"
11
+ require_relative "audit_logger/config_validator"
12
+ require_relative "audit_logger/model_config"
13
+ require_relative "audit_logger/audit_log"
14
+ require_relative "audit_logger/actor_context_resolver"
15
+ require_relative "audit_logger/change_extractor"
16
+ require_relative "audit_logger/humanizer"
17
+ require_relative "audit_logger/record_audit_entry"
18
+ require_relative "audit_logger/auditable"
19
+
20
+ module AuditLogger
21
+ class Error < StandardError; end
22
+ class ConfigurationError < Error; end
23
+
24
+ class << self
25
+ # Retorna a configuracao global da gem, instanciando-a sob demanda.
26
+ def configuration
27
+ @configuration ||= Configuration.new
28
+ end
29
+
30
+ # Permite configurar a gem via bloco e valida o resultado no final.
31
+ def configure
32
+ yield(configuration)
33
+ ConfigValidator.validate!(configuration)
34
+ configuration
35
+ end
36
+
37
+ # Facilita testes e reconfiguracao do estado global da gem.
38
+ def reset_configuration!
39
+ @configuration = Configuration.new
40
+ end
41
+ end
42
+ end
43
+
44
+ require_relative "audit_logger/railtie" if defined?(Rails::Railtie)