Module: ActiveRecordQueryCounter

Defined in:
lib/active_record_query_counter.rb,
lib/active_record_query_counter/counter.rb,
lib/active_record_query_counter/thresholds.rb,
lib/active_record_query_counter/rack_middleware.rb,
lib/active_record_query_counter/transaction_info.rb,
lib/active_record_query_counter/sidekiq_middleware.rb,
lib/active_record_query_counter/transaction_extension.rb,
lib/active_record_query_counter/connection_adapter_extension.rb

Overview

Everything you need to count ActiveRecord queries and row counts within a block.

Examples:


ActiveRecordQueryCounter.count_queries do
  yield
  puts ActiveRecordQueryCounter.query_count
  puts ActiveRecordQueryCounter.row_count
end

Defined Under Namespace

Modules: ConnectionAdapterExtension, TransactionExtension Classes: Counter, RackMiddleware, SidekiqMiddleware, Thresholds, TransactionInfo

Constant Summary collapse

VERSION =
File.read(File.join(__dir__, "..", "VERSION")).strip

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.default_thresholdsActiveRecordQueryCounter::Thresholds (readonly)

The global notification thresholds for sending notifications. The values set in these thresholds are used as the default values.



319
320
321
# File 'lib/active_record_query_counter.rb', line 319

def default_thresholds
  @default_thresholds
end

Class Method Details

.add_query(sql, name, binds, row_count, start_time, end_time, gc_time, cpu_time, connection_time = 0.0) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Increment the query counters.

The reported query time is the wall clock time spent executing the query with the GC time, Ruby thread CPU time, and connection setup time subtracted out so that it reflects the time actually spent waiting on the database as closely as possible (see database_query_time). This query time, rather than the raw wall clock time, is what is accumulated, compared against the threshold, and used as the duration of the emitted notification.

Parameters:

  • sql (String)

    the SQL statement that was executed

  • name (String, nil)

    the name of the query

  • binds (Array)

    the bind parameters

  • row_count (Integer)

    the number of rows returned by the query

  • start_time (Float)

    the monotonic time when the query started

  • end_time (Float)

    the monotonic time when the query ended

  • gc_time (Float)

    the GC time in seconds that elapsed while the query ran

  • cpu_time (Float)

    the thread CPU time in seconds spent while the query ran

  • connection_time (Float) (defaults to: 0.0)

    the time in seconds spent establishing, verifying, or reconnecting the database connection while the query ran



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/active_record_query_counter.rb', line 93

def add_query(sql, name, binds, row_count, start_time, end_time, gc_time, cpu_time, connection_time = 0.0)
  return if IGNORED_STATEMENTS.include?(name)

  counter = current_counter
  return unless counter.is_a?(Counter)

  elapsed_time = end_time - start_time
  query_time = database_query_time(elapsed_time, gc_time, cpu_time, connection_time)
  counter.query_count += 1
  counter.row_count += row_count
  counter.query_time += query_time

  # The notification duration is the database query time, so the event ends that long after
  # it started rather than at the raw wall clock end time.
  notification_end_time = start_time + query_time

  trace = nil
  query_time_threshold = counter.thresholds.query_time || -1
  if query_time_threshold.between?(0, query_time)
    trace = backtrace
    payload = notification_payload(sql: sql, binds: binds, row_count: row_count, trace: trace, elapsed_time: elapsed_time, gc_time: gc_time, cpu_time: cpu_time, connection_time: connection_time)
    send_notification("query_time", start_time, notification_end_time, **payload)
  end

  row_count_threshold = counter.thresholds.row_count || -1
  if row_count_threshold.between?(0, row_count)
    trace ||= backtrace
    payload = notification_payload(sql: sql, binds: binds, row_count: row_count, trace: trace, elapsed_time: elapsed_time, gc_time: gc_time, cpu_time: cpu_time, connection_time: connection_time)
    send_notification("row_count", start_time, notification_end_time, **payload)
  end
end

.add_transaction(start_time, end_time) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Increment the transaction counters.

Parameters:

  • start_time (Float)

    the time the transaction started

  • end_time (Float)

    the time the transaction ended



131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/active_record_query_counter.rb', line 131

def add_transaction(start_time, end_time)
  counter = current_counter
  return unless counter.is_a?(Counter)

  trace = backtrace
  counter.add_transaction(trace: trace, start_time: start_time, end_time: end_time)

  transaction_time_threshold = counter.thresholds.transaction_time || -1
  if transaction_time_threshold.between?(0, end_time - start_time)
    send_notification("transaction_time", start_time, end_time, trace: backtrace)
  end
end

.cached_query_countInteger?

Return the number of queries that hit the query cache and were not sent to the database that have been counted within the current block. Returns nil if not inside a block where queries are being counted.

Returns:

  • (Integer, nil)


217
218
219
220
# File 'lib/active_record_query_counter.rb', line 217

def cached_query_count
  counter = current_counter
  counter.cached_query_count if counter.is_a?(Counter)
end

.count_queriesObject

Enable query counting within a block.

Returns:

  • (Object)

    the result of the block



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/active_record_query_counter.rb', line 36

def count_queries
  save_counter = current_counter
  begin
    counter = Counter.new
    self.current_counter = counter

    retval = yield

    transaction_count = counter.transaction_count
    if transaction_count > 0
      transaction_threshold = counter.thresholds.transaction_count || -1
      if transaction_threshold.between?(0, transaction_count)
        send_notification("transaction_count", counter.first_transaction_start_time, counter.last_transaction_end_time, transactions: counter.transactions)
      end
    end

    retval
  ensure
    self.current_counter = save_counter
  end
end

.disable(&block) ⇒ Object

Disable query counting in a block. Any queries or transactions inside the block will not be counted.

Returns:

  • (Object)

    the return value of the block



62
63
64
65
66
67
68
69
70
# File 'lib/active_record_query_counter.rb', line 62

def disable(&block)
  counter = current_counter
  begin
    self.current_counter = nil
    yield
  ensure
    self.current_counter = counter
  end
end

.enable!(connection_class) ⇒ void

This method returns an undefined value.

Enable the query counting behavior on a connection adapter class.

Parameters:

  • connection_class (Class)

    the connection adapter class to extend



331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# File 'lib/active_record_query_counter.rb', line 331

def enable!(connection_class)
  ActiveSupport.on_load(:active_record) do
    ConnectionAdapterExtension.inject(connection_class)
    TransactionExtension.inject(ActiveRecord::ConnectionAdapters::RealTransaction)
  end

  @lock.synchronize do
    @cache_subscription ||= ActiveSupport::Notifications.subscribe("sql.active_record") do |_name, _start_time, _end_time, _id, payload|
      if payload[:cached] && !IGNORED_STATEMENTS.include?(payload[:name])
        counter = current_counter
        counter.cached_query_count += 1 if counter.is_a?(Counter)
      end
    end
  end
end

.first_transaction_start_timeFloat?

Return the time when the first transaction began within the current block. Returns nil if not inside a block where queries are being counted or there are no transactions.

Returns:

  • (Float, nil)

    the monotonic time when the first transaction began,



262
263
264
265
# File 'lib/active_record_query_counter.rb', line 262

def first_transaction_start_time
  counter = current_counter
  counter.first_transaction_start_time if counter.is_a?(Counter)
end

.increment_rollbacksInteger?

Return the number of rollbacks that have been counted within the current block. Returns nil if not inside a block where queries are being counted.

Returns:

  • (Integer, nil)


148
149
150
151
152
153
# File 'lib/active_record_query_counter.rb', line 148

def increment_rollbacks
  counter = current_counter
  return unless counter.is_a?(Counter)

  counter.rollback_count += 1
end

.infoHash?

Return the query info as a hash with keys :query_count, :row_count, :query_time :transaction_count, and :transaction_type or nil if not inside a block where queries are being counted.

Returns:

  • (Hash, nil)


299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/active_record_query_counter.rb', line 299

def info
  counter = current_counter
  if counter.is_a?(Counter)
    {
      query_count: counter.query_count,
      row_count: counter.row_count,
      query_time: counter.query_time,
      cached_query_count: counter.cached_query_count,
      cache_hit_rate: counter.cache_hit_rate,
      transaction_count: counter.transaction_count,
      transaction_time: counter.transaction_time,
      rollback_count: counter.rollback_count
    }
  end
end

.last_transaction_end_timeFloat?

Return the time when the last transaction ended within the current block. Returns nil if not inside a block where queries are being counted or there are no transactions.

Returns:

  • (Float, nil)

    the monotonic time when the last transaction ended,



271
272
273
274
# File 'lib/active_record_query_counter.rb', line 271

def last_transaction_end_time
  counter = current_counter
  counter.last_transaction_end_time if counter.is_a?(Counter)
end

.measure_connection_setup { ... } ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Measure the wall clock time a connection setup operation (connect, reconnect, or verify) takes and accumulate it onto the current query's connection timer. When no query is being measured, or when a connection setup operation is already being measured (for example when verify! delegates to reconnect!), the block is yielded without recording so the interval is only counted once.

Yields:

  • the connection setup operation

Returns:

  • (Object)

    the result of the block



189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/active_record_query_counter.rb', line 189

def measure_connection_setup
  timer = connection_timer
  return yield if timer.nil? || timer[:measuring]

  timer[:measuring] = true
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  begin
    yield
  ensure
    timer[:elapsed] += Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
    timer[:measuring] = false
  end
end

.query_countInteger?

Return the number of queries that have been counted within the current block. Returns nil if not inside a block where queries are being counted.

Returns:

  • (Integer, nil)


207
208
209
210
# File 'lib/active_record_query_counter.rb', line 207

def query_count
  counter = current_counter
  counter.query_count if counter.is_a?(Counter)
end

.query_timeFloat?

Return the total time spent executing queries within the current block. Returns nil if not inside a block where queries are being counted.

Returns:

  • (Float, nil)


235
236
237
238
# File 'lib/active_record_query_counter.rb', line 235

def query_time
  counter = current_counter
  counter.query_time if counter.is_a?(Counter)
end

.rollback_countInteger?

Return the number of transactions that have rolled back within the current block. Returns nil if not inside a block where queries are being counted.

Returns:

  • (Integer, nil)


289
290
291
292
# File 'lib/active_record_query_counter.rb', line 289

def rollback_count
  counter = current_counter
  counter.rollback_count if counter.is_a?(Counter)
end

.row_countInteger?

Return the number of rows that have been counted within the current block. Returns nil if not inside a block where queries are being counted.

Returns:

  • (Integer, nil)


226
227
228
229
# File 'lib/active_record_query_counter.rb', line 226

def row_count
  counter = current_counter
  counter.row_count if counter.is_a?(Counter)
end

.start_connection_timerObject?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Begin measuring the time spent establishing, verifying, or reconnecting the database connection for a single query. Returns the timer that was previously in effect so it can be restored by stop_connection_timer; this keeps nested queries (should they ever occur) from leaking connection time into one another.

Returns:

  • (Object, nil)

    the previous connection timer



162
163
164
165
166
# File 'lib/active_record_query_counter.rb', line 162

def start_connection_timer
  previous_timer = connection_timer
  self.connection_timer = {elapsed: 0.0, measuring: false}
  previous_timer
end

.stop_connection_timer(previous_timer) ⇒ Float

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Finish measuring connection setup time for the current query and restore the previously active timer.

Parameters:

Returns:

  • (Float)

    the connection setup time in seconds accumulated for the query



174
175
176
177
178
# File 'lib/active_record_query_counter.rb', line 174

def stop_connection_timer(previous_timer)
  timer = connection_timer
  self.connection_timer = previous_timer
  timer ? timer[:elapsed] : 0.0
end

.thresholdsObject

Get the current local notification thresholds. These thresholds are only used within the current count_queries block.



323
324
325
# File 'lib/active_record_query_counter.rb', line 323

def thresholds
  current_counter&.thresholds || default_thresholds.dup
end

.transaction_countInteger?

Return the number of transactions that have been counted within the current block. Returns nil if not inside a block where queries are being counted.

Returns:

  • (Integer, nil)


244
245
246
247
# File 'lib/active_record_query_counter.rb', line 244

def transaction_count
  counter = current_counter
  counter.transaction_count if counter.is_a?(Counter)
end

.transaction_timeFloat?

Return the total time spent in transactions that have been counted within the current block. Returns nil if not inside a block where queries are being counted.

Returns:

  • (Float, nil)


253
254
255
256
# File 'lib/active_record_query_counter.rb', line 253

def transaction_time
  counter = current_counter
  counter.transaction_time if counter.is_a?(Counter)
end

.transactionsArray<ActiveRecordQueryCounter::TransactionInfo>?

Return an array of transaction information for any transactions that have been counted within the current block. Returns nil if not inside a block where queries are being counted.



280
281
282
283
# File 'lib/active_record_query_counter.rb', line 280

def transactions
  counter = current_counter
  counter.transactions if counter.is_a?(Counter)
end