Module: LogStruct::Integrations::ActiveRecord

Extended by:
IntegrationInterface, T::Sig
Defined in:
lib/log_struct/integrations/active_record.rb

Overview

ActiveRecord Integration for SQL Query Logging

This integration captures and structures all SQL queries executed through ActiveRecord, providing detailed performance and debugging information in a structured format.

Features:

  • Captures all SQL queries with execution time
  • Safely filters sensitive data from bind parameters
  • Extracts database operation metadata
  • Provides connection pool monitoring information
  • Identifies query types and table names

Performance Considerations:

  • Minimal overhead on query execution
  • Async logging prevents I/O blocking
  • Configurable to disable in production if needed
  • Smart filtering reduces log volume for repetitive queries

Security:

  • SQL queries are always parameterized (safe)
  • Bind parameters filtered through LogStruct's param filters
  • Sensitive patterns automatically scrubbed

Configuration:

LogStruct.configure do |config|
  config.integrations.enable_sql_logging = true
  config.integrations.sql_slow_query_threshold = 100.0 # ms
  config.integrations.sql_log_bind_params = false # disable in production
end

Class Method Summary collapse

Methods included from IntegrationInterface

setup

Class Method Details

.extract_active_connections(payload) ⇒ Integer?

Extract active connection count

Parameters:

  • payload (Hash{Symbol => T.untyped})

Returns:

  • (Integer, nil)


187
188
189
190
191
192
193
194
195
# File 'lib/log_struct/integrations/active_record.rb', line 187

def self.extract_active_connections(payload)
  connection = payload[:connection]
  return nil unless connection

  pool = connection.pool if connection.respond_to?(:pool)
  pool&.stat&.[](:busy)
rescue
  nil
end

.extract_adapter_name(payload) ⇒ String?

Extract database adapter name

Parameters:

  • payload (Hash{Symbol => T.untyped})

Returns:

  • (String, nil)


135
136
137
138
139
140
141
# File 'lib/log_struct/integrations/active_record.rb', line 135

def self.extract_adapter_name(payload)
  connection = payload[:connection]
  return nil unless connection

  adapter_name = connection.class.name
  adapter_name&.split("::")&.last
end

.extract_and_filter_binds(payload) ⇒ Array<T.untyped>?

Extract and filter bind parameters

Parameters:

  • payload (Hash{Symbol => T.untyped})

Returns:

  • (Array<T.untyped>, nil)


145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/log_struct/integrations/active_record.rb', line 145

def self.extract_and_filter_binds(payload)
  return nil unless LogStruct.config.integrations.sql_log_bind_params

  # Prefer type_casted_binds as they're more readable
  binds = payload[:type_casted_binds] || payload[:binds]
  return nil unless binds

  # Filter sensitive data from bind parameters
  binds.map do |bind|
    filter_bind_parameter(bind)
  end
end

.extract_database_name(payload) ⇒ String?

Extract database name from connection

Parameters:

  • payload (Hash{Symbol => T.untyped})

Returns:

  • (String, nil)


160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/log_struct/integrations/active_record.rb', line 160

def self.extract_database_name(payload)
  connection = payload[:connection]
  return nil unless connection

  if connection.respond_to?(:current_database)
    connection.current_database
  elsif connection.respond_to?(:database)
    connection.database
  end
rescue
  nil
end

.extract_operation_type(payload) ⇒ String?

Extract SQL operation type (SELECT, INSERT, etc.)

Parameters:

  • payload (Hash{Symbol => T.untyped})

Returns:

  • (String, nil)


199
200
201
202
203
204
205
206
# File 'lib/log_struct/integrations/active_record.rb', line 199

def self.extract_operation_type(payload)
  sql = payload[:sql]
  return nil unless sql

  # Extract first word of SQL query
  match = sql.strip.match(/\A\s*(\w+)/i)
  match&.captures&.first&.upcase
end

.extract_pool_size(payload) ⇒ Integer?

Extract connection pool size

Parameters:

  • payload (Hash{Symbol => T.untyped})

Returns:

  • (Integer, nil)


175
176
177
178
179
180
181
182
183
# File 'lib/log_struct/integrations/active_record.rb', line 175

def self.extract_pool_size(payload)
  connection = payload[:connection]
  return nil unless connection

  pool = connection.pool if connection.respond_to?(:pool)
  pool&.size
rescue
  nil
end

.extract_row_count(payload) ⇒ Integer?

Extract row count from payload

Parameters:

  • payload (Hash{Symbol => T.untyped})

Returns:

  • (Integer, nil)


128
129
130
131
# File 'lib/log_struct/integrations/active_record.rb', line 128

def self.extract_row_count(payload)
  row_count = payload[:row_count]
  row_count.is_a?(Integer) ? row_count : nil
end

.extract_table_names(payload) ⇒ Array<String>?

Extract table names from SQL query

Parameters:

  • payload (Hash{Symbol => T.untyped})

Returns:

  • (Array<String>, nil)


210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/log_struct/integrations/active_record.rb', line 210

def self.extract_table_names(payload)
  sql = payload[:sql]
  return nil unless sql

  # Simple regex to extract table names (basic implementation)
  # This covers most common cases but could be enhanced
  tables = []

  # Match FROM, JOIN, UPDATE, INSERT INTO, DELETE FROM patterns
  sql.scan(/(?:FROM|JOIN|UPDATE|INTO|DELETE\s+FROM)\s+["`]?(\w+)["`]?/i) do |match|
    table_name = match[0]
    tables << table_name unless tables.include?(table_name)
  end

  tables.empty? ? nil : tables
end

.filter_bind_parameter(value) ⇒ T.untyped

Filter individual bind parameter values to remove sensitive data

Parameters:

  • value (T.untyped)

Returns:

  • (T.untyped)


229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/log_struct/integrations/active_record.rb', line 229

def self.filter_bind_parameter(value)
  case value
  when String
    # Filter strings that look like passwords, tokens, secrets, etc.
    if looks_sensitive?(value)
      "[FILTERED]"
    else
      value
    end
  else
    value
  end
end

.format_sql_message(payload) ⇒ String

Format a readable message for the SQL log

Parameters:

  • payload (Hash{Symbol => T.untyped})

Returns:

  • (String)


121
122
123
124
# File 'lib/log_struct/integrations/active_record.rb', line 121

def self.format_sql_message(payload)
  operation_name = payload[:name] || "SQL Query"
  "#{operation_name} executed"
end

.handle_sql_event(name, start, finish, id, payload) ⇒ void

This method returns an undefined value.

Process SQL notification event and create structured log

Parameters:

  • name (String)
  • start (T.untyped)
  • finish (T.untyped)
  • id (String)
  • payload (Hash{Symbol => T.untyped})


67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/log_struct/integrations/active_record.rb', line 67

def self.handle_sql_event(name, start, finish, id, payload)
  # Skip schema queries and Rails internal queries
  return if skip_query?(payload)

  duration = ((finish - start) * 1000.0).round(2)

  # Skip fast queries if threshold is configured
  config = LogStruct.config
  if config.integrations.sql_slow_query_threshold&.positive?
    return if duration < config.integrations.sql_slow_query_threshold
  end

  sql_log = Log::SQL.new(
    message: format_sql_message(payload),
    source: Source::App,
    event: Event::Database,
    sql: payload[:sql]&.strip || "",
    name: payload[:name] || "SQL Query",
    duration: duration,
    row_count: extract_row_count(payload),
    connection_adapter: extract_adapter_name(payload),
    bind_params: extract_and_filter_binds(payload),
    database_name: extract_database_name(payload),
    connection_pool_size: extract_pool_size(payload),
    active_connections: extract_active_connections(payload),
    operation_type: extract_operation_type(payload),
    table_names: extract_table_names(payload)
  )

  LogStruct.info(sql_log)
end

.looks_sensitive?(value) ⇒ Boolean

Check if a string value looks sensitive and should be filtered

Parameters:

  • value (String)

Returns:

  • (Boolean)


245
246
247
248
249
250
251
252
253
254
255
# File 'lib/log_struct/integrations/active_record.rb', line 245

def self.looks_sensitive?(value)
  # Filter very long strings that might be tokens
  return true if value.length > 50

  # Filter strings that look like hashed passwords, API keys, tokens
  return true if value.match?(/\A[a-f0-9]{32,}\z/i)  # MD5, SHA, etc.
  return true if value.match?(/\A[A-Za-z0-9+\/]{20,}={0,2}\z/)  # Base64
  return true if value.match?(/(password|secret|token|key|auth)/i)

  false
end

.setup(config) ⇒ Boolean?

Set up SQL query logging integration

Parameters:

Returns:

  • (Boolean, nil)


45
46
47
48
49
50
51
# File 'lib/log_struct/integrations/active_record.rb', line 45

def self.setup(config)
  return nil unless config.integrations.enable_sql_logging
  return nil unless defined?(::ActiveRecord::Base)

  subscribe_to_sql_notifications
  true
end

.skip_query?(payload) ⇒ Boolean

Determine if query should be skipped from logging

Parameters:

  • payload (Hash{Symbol => T.untyped})

Returns:

  • (Boolean)


101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/log_struct/integrations/active_record.rb', line 101

def self.skip_query?(payload)
  query_name = payload[:name]
  sql = payload[:sql]

  # Skip Rails schema queries
  return true if query_name&.include?("SCHEMA")
  return true if query_name&.include?("CACHE")

  # Skip common Rails internal queries
  return true if sql&.include?("schema_migrations")
  return true if sql&.include?("ar_internal_metadata")

  # Skip SHOW/DESCRIBE queries
  return true if sql&.match?(/\A\s*(SHOW|DESCRIBE|EXPLAIN)\s/i)

  false
end

.subscribe_to_sql_notificationsvoid

This method returns an undefined value.

Subscribe to ActiveRecord's sql.active_record notifications



57
58
59
60
61
62
63
# File 'lib/log_struct/integrations/active_record.rb', line 57

def self.subscribe_to_sql_notifications
  ::ActiveSupport::Notifications.subscribe("sql.active_record") do |name, start, finish, id, payload|
    handle_sql_event(name, start, finish, id, payload)
  rescue => error
    LogStruct.handle_exception(error, source: LogStruct::Source::LogStruct)
  end
end