Class: EzLogsAgent::Capturers::DatabaseCapturer

Inherits:
Object
  • Object
show all
Defined in:
lib/ez_logs_agent/capturers/database_capturer.rb

Overview

Captures database operations via ActiveRecord model lifecycle callbacks.

This capturer:

  • Installs after_create, after_update, after_destroy callbacks on ActiveRecord::Base

  • Captures model class, record id, and operation type

  • Extracts resource_ids from the model instance

  • For updates, extracts curated business-relevant change context

  • Preserves correlation_id from current context

  • Never crashes the host application (fail-open)

  • Respects capture_database configuration flag

What This Capturer Does NOT Do

  • Parse SQL queries

  • Dump full attribute diffs

  • Include sensitive data

  • Guess actors

  • Act as an audit log

Event Shape

Produces events with:

  • source_type: :database_callback

  • source_data: { model_class: “User”, operation: “create|update|destroy” }

  • outcome: :success

  • correlation_id: EzLogsAgent::Correlation.current (if present)

  • resource_ids: [{ resource_type: “User”, resource_id: “123” }]

  • context: { changes: [{ attribute: “status”, from: “pending”, to: “shipped” }, …] } (updates only, if meaningful)

Constant Summary collapse

IGNORED_ATTRIBUTES =

Attributes to always ignore when detecting business changes

%w[
  id
  created_at
  updated_at
  lock_version
  encrypted_password
  reset_password_token
  reset_password_sent_at
  remember_created_at
  confirmation_token
  confirmed_at
  confirmation_sent_at
  unconfirmed_email
  unlock_token
  locked_at
  sign_in_count
  current_sign_in_at
  last_sign_in_at
  current_sign_in_ip
  last_sign_in_ip
].freeze
SENSITIVE_PATTERNS =

Patterns for sensitive data to ignore.

The first source of truth is ‘record.class.encrypted_attributes` (Rails 7+ `encrypts :foo` declaration) — see encrypted_attribute?. If the host app encrypted it, we never capture it.

This list is the secondary defense: column names that frequently carry sensitive material even when the host app didn’t declare ‘encrypts` (legacy code, manual hashing, externally-generated material). Matching is substring + case-insensitive.

%w[
  password
  token
  secret
  api_key
  credit_card
  ssn
  social_security
  encrypted
  private_key
  public_key
  signing_key
  pem
  cipher
  nonce
  salt
  digest
  signature
  hmac
].freeze

Class Method Summary collapse

Class Method Details

.handle_create(model) ⇒ void

This method returns an undefined value.

Handles after_create callback

Parameters:

  • model (ActiveRecord::Base)

    The created model instance



129
130
131
132
133
134
135
136
137
# File 'lib/ez_logs_agent/capturers/database_capturer.rb', line 129

def handle_create(model)
  return unless capture_enabled?

  context = extract_initial_attributes(model) || {}
  context[:display_name] = resolve_display_name(model)
  capture_event(model, "create", context: context.presence)
rescue StandardError => e
  EzLogsAgent::Logger.error("[DatabaseCapturer] handle_create failed: #{e.class} - #{e.message}")
end

.handle_destroy(model) ⇒ void

This method returns an undefined value.

Handles after_destroy callback

Parameters:

  • model (ActiveRecord::Base)

    The destroyed model instance



157
158
159
160
161
162
163
164
# File 'lib/ez_logs_agent/capturers/database_capturer.rb', line 157

def handle_destroy(model)
  return unless capture_enabled?

  context = { display_name: resolve_display_name(model) }
  capture_event(model, "destroy", context: context.presence)
rescue StandardError => e
  EzLogsAgent::Logger.error("[DatabaseCapturer] handle_destroy failed: #{e.class} - #{e.message}")
end

.handle_update(model) ⇒ void

This method returns an undefined value.

Handles after_update callback

Parameters:

  • model (ActiveRecord::Base)

    The updated model instance



143
144
145
146
147
148
149
150
151
# File 'lib/ez_logs_agent/capturers/database_capturer.rb', line 143

def handle_update(model)
  return unless capture_enabled?

  context = extract_change_context(model) || {}
  context[:display_name] = resolve_display_name(model)
  capture_event(model, "update", context: context.presence)
rescue StandardError => e
  EzLogsAgent::Logger.error("[DatabaseCapturer] handle_update failed: #{e.class} - #{e.message}")
end

.installvoid

This method returns an undefined value.

Installs ActiveRecord lifecycle callbacks for database capture.

This method is idempotent and can be called multiple times safely. Only installs if ActiveRecord is present.



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/ez_logs_agent/capturers/database_capturer.rb', line 105

def install
  return unless defined?(ActiveRecord::Base)
  return if @installed

  # Only register callbacks once per Ruby process
  unless @callbacks_registered
    ActiveRecord::Base.class_eval do
      after_create { |model| EzLogsAgent::Capturers::DatabaseCapturer.handle_create(model) }
      after_update { |model| EzLogsAgent::Capturers::DatabaseCapturer.handle_update(model) }
      after_destroy { |model| EzLogsAgent::Capturers::DatabaseCapturer.handle_destroy(model) }
    end
    @callbacks_registered = true
  end

  @installed = true
  EzLogsAgent::Logger.debug("[DatabaseCapturer] Installed")
rescue StandardError => e
  EzLogsAgent::Logger.error("[DatabaseCapturer] Installation failed: #{e.class} - #{e.message}")
end