active_record_query_counter 1.1.2 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29ed527d87037456aa27022d13ea436b462945329de3b343a9391da90db88f90
4
- data.tar.gz: 22edd4498820f0452999cbffbad2532b534d791c8b41f0039599c0407bcc2279
3
+ metadata.gz: 9d653eb9665511008bc78023c7e39ae8eb189e051847d83e16244424b5cbe595
4
+ data.tar.gz: 6e9643c840a635228ea0a04cb02277b91c34b1ea5e69bd5b93753066be8ab4fb
5
5
  SHA512:
6
- metadata.gz: df174f4eb9e9b882c57fdae34456c36bff95581497cafcb628b57a42d06eefe3b784d7f7454367d36cb0d39726cfc141c0d808e9a14648fea898cc84603d651f
7
- data.tar.gz: a0ba3580554d39fe487fbc2eb5a9d1a7db59ad0d964b6f4f17e3bc2bdd642c7b46728d9e7130d015f14486901b5db224c6af4c6039d9c127c33d97fecb3a18e8
6
+ metadata.gz: df192239b02e3ed039eac91a64393d5332cad36dce242e9e02948b3ec0cbe23e7ae5d4b23dc58ebf01158fa885680135e6719f444a871abd37779e85a9f85d2d
7
+ data.tar.gz: '069e6959ba3f7ead49824ae91d35774b3b336afe18248c84800eaca65115daa040bdf5871d0c3e591b648cce2b0d5414078fe51ef39bef8d9f0e777be8e7330d'
data/CHANGELOG.md CHANGED
@@ -4,20 +4,50 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 2.1.0
8
+
9
+ ### Added
10
+
11
+ - Added count of queries that hit the query cache instead of being sent to the database.
12
+
13
+ ### Removed
14
+
15
+ - Dropped support for ActiveRecord 5.0.
16
+
17
+ ## 2.0.0
18
+
19
+ ### Added
20
+
21
+ - Added capability to send ActiveSupport notifications when query thresholds are exceeded.
22
+
23
+ ### Changed
24
+
25
+ - Calculate elapsed time using monotonic time rather than wall clock time.
26
+ - Schema queries to get the table structure and explain plan queries are no longer counted.
27
+ - **Breaking change**: transaction information is now returned in an array of `ActiveRecordQueryCounter::TransactionInfo` objects.
28
+ - **Breaking change**: internal API for tracking queries and transactions has changed
29
+
7
30
  ## 1.1.2
31
+
8
32
  ### Added
9
- * Ruby 3.0 compatibility
33
+
34
+ - Ruby 3.0 compatibility
35
+
10
36
  ### Removed
11
- * Dropped support for ActiveRecord 4.2
37
+
38
+ - Dropped support for ActiveRecord 4.2
12
39
 
13
40
  ## 1.1.1
14
41
  ### Added
15
- * Expose stack traces where transactions are being committed.
42
+
43
+ - Expose stack traces where transactions are being committed.
16
44
 
17
45
  ## 1.1.0
18
46
  ### Added
19
- * Add number of transactions to statistics being tracked.
47
+
48
+ - Add number of transactions to statistics being tracked.
20
49
 
21
50
  ## 1.0.0
22
51
  ### Added
23
- * Track stats about queries run by ActiveRecord within a block.
52
+
53
+ - Track stats about queries run by ActiveRecord within a block.
data/README.md CHANGED
@@ -1,13 +1,19 @@
1
1
  # ActiveRecordQueryCounter
2
2
 
3
3
  ![Continuous Integration](https://github.com/bdurand/active_record_query_counter/workflows/Continuous%20Integration/badge.svg)
4
- [![Maintainability](https://api.codeclimate.com/v1/badges/21094ecec0c151983bb1/maintainability)](https://codeclimate.com/github/bdurand/active_record_query_counter/maintainability)
5
- [![Test Coverage](https://api.codeclimate.com/v1/badges/21094ecec0c151983bb1/test_coverage)](https://codeclimate.com/github/bdurand/active_record_query_counter/test_coverage)
6
4
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
7
5
 
8
- This gem injects itself into ActiveRecord to count the number of queries, the number of rows returned, the amount of time spent on queries, the number of transactions, and the amount of time spent inside transactions within a block.
6
+ This gem injects itself into ActiveRecord to give you insight into how your code is using the database.
9
7
 
10
- The intended use is to gather instrumentation stats for finding hot spots in your code.
8
+ Within a block of code, it will count:
9
+
10
+ - the number of queries
11
+ - the number of rows returned
12
+ - the amount of time spent on queries
13
+ - the number of transactions used
14
+ - the amount of time spent inside transactions
15
+
16
+ The intended use is to gather instrumentation stats for finding hot spots in your code that produce a lot of queries or slow queries or queries that return a lot of rows. It can also be used to find code that is not using transactions when making multiple updates to the database.
11
17
 
12
18
  ## Usage
13
19
 
@@ -38,9 +44,7 @@ ActiveRecordQueryCounter.count_queries do
38
44
  end
39
45
  ```
40
46
 
41
- This gem includes middleware for both Rack and Sidekiq that will enable query counting.
42
-
43
- If you are using Rails with Sidekiq, you can enable both with an initializer.
47
+ This gem includes middleware for both Rack and Sidekiq that will enable query counting on web requests and in workers. If you are using Rails with Sidekiq, you can enable both with an initializer.
44
48
 
45
49
  ```ruby
46
50
  ActiveSupport.on_load(:active_record) do
@@ -55,3 +59,145 @@ Sidekiq.configure_server do |config|
55
59
  end
56
60
  end
57
61
  ```
62
+
63
+ ### Notifications
64
+
65
+ You can also subscribe to ActiveSupport notifications to get notified when query thresholds are exceeded.
66
+
67
+ #### active_record_query_counter.query_time notification
68
+
69
+ This notification is triggered when a query takes longer than the `query_time` threshold. The payload contains the following keys:
70
+
71
+ - `:sql` - The SQL statement that was executed.
72
+ - `:binds` - The bind parameters that were used.
73
+ - `:row_count` - The number of rows returned.
74
+ - `:trace` - The stack trace of where the query was executed.
75
+
76
+ #### active_record_query_counter.row_count notification
77
+
78
+ This notification is triggered when a query returns more rows than the `row_count` threshold. The payload contains the following keys:
79
+
80
+ - `:sql` - The SQL statement that was executed.
81
+ - `:binds` - The bind parameters that were used.
82
+ - `:row_count` - The number of rows returned.
83
+ - `:trace` - The stack trace of where the query was executed.
84
+
85
+ #### active_record_query_counter.transaction_time notification
86
+
87
+ This notification is triggered when a transaction takes longer than the `transaction_time` threshold. The payload contains the following keys:
88
+
89
+ - `:trace` - The stack trace of where the transaction was completed.
90
+
91
+ #### active_record_query_counter.transaction_count notification
92
+
93
+ This notification is triggered when a transaction takes longer than the `transaction_count` threshold. The payload contains the following keys:
94
+
95
+ - `:transactions` - An array of `ActiveRecordQueryCounter::TransactionInfo` objects.
96
+
97
+ The duration of the notification event is the time between when the first transaction was started and the last transaction was completed.
98
+
99
+ #### Thresholds
100
+
101
+ The thresholds for triggering notifications can be set globally in an initializer:
102
+
103
+ ```ruby
104
+ ActiveRecordQueryCounter.default_thresholds.set(
105
+ query_time: 2.0,
106
+ row_count: 1000,
107
+ transaction_time: 5.0,
108
+ transaction_count: 2
109
+ )
110
+ ```
111
+
112
+ They can be set locally inside a `count_queries` block with the `thresholds` object. Local thresholds will override the global thresholds only inside the block and will not change any global state.
113
+
114
+ ```ruby
115
+ ActiveRecordQueryCounter.count_queries do
116
+ ActiveRecordQueryCounter.thresholds.set(
117
+ query_time: 1.0,
118
+ row_count: 100,
119
+ transaction_time: 2.0,
120
+ transaction_count: 1
121
+ )
122
+ end
123
+ ```
124
+
125
+ You can pass thresholds to individual Sidekiq workers via the `sidekiq_options` on the worker.
126
+
127
+ ```ruby
128
+ class MyWorker
129
+ include Sidekiq::Worker
130
+
131
+ sidekiq_options(
132
+ active_record_query_counter: {
133
+ thresholds: {
134
+ query_time: 1.0,
135
+ row_count: 100,
136
+ transaction_time: 2.0,
137
+ transaction_count: 1
138
+ }
139
+ }
140
+ )
141
+ # You can disable thresholds for the worker by setting `thresholds: false`.
142
+
143
+ def perform
144
+ do_something
145
+ end
146
+ end
147
+ ```
148
+
149
+ You can set separate thresholds on the Rack middleware when you install it.
150
+
151
+ ```ruby
152
+ Rails.application.config.middleware.use(ActiveRecordQueryCounter::RackMiddleware, thresholds: {
153
+ query_time: 1.0,
154
+ row_count: 100,
155
+ transaction_time: 2.0,
156
+ transaction_count: 1
157
+ })
158
+ ```
159
+
160
+ #### Example Notification Subscriptions
161
+
162
+ ```ruby
163
+ ActiveRecordQueryCounter.default_thresholds.query_time = 1.0
164
+ ActiveRecordQueryCounter.default_thresholds.row_count = 1000
165
+ ActiveRecordQueryCounter.default_thresholds.transaction_time = 2.0
166
+ ActiveRecordQueryCounter.default_thresholds.transaction_count = 1
167
+
168
+ ActiveSupport::Notifications.subscribe('active_record_query_counter.query_time') do |*args|
169
+ event = ActiveSupport::Notifications::Event.new(*args)
170
+ puts "Query time exceeded (#{event.duration}ms): #{event.payload[:sql]}"
171
+ puts event.payload[:trace].join("\n")
172
+ end
173
+
174
+ ActiveSupport::Notifications.subscribe('active_record_query_counter.row_count') do |*args|
175
+ event = ActiveSupport::Notifications::Event.new(*args)
176
+ puts "Row count exceeded (#{event.payload[:row_count]} rows): #{event.payload[:sql]}"
177
+ puts event.payload[:trace].join("\n")
178
+ end
179
+
180
+ ActiveSupport::Notifications.subscribe('active_record_query_counter.transaction_time') do |*args|
181
+ event = ActiveSupport::Notifications::Event.new(*args)
182
+ puts "Transaction time exceeded (#{event.duration}ms)"
183
+ puts event.payload[:trace].join("\n")
184
+ end
185
+
186
+ ActiveSupport::Notifications.subscribe('active_record_query_counter.transaction_count') do |*args|
187
+ event = ActiveSupport::Notifications::Event.new(*args)
188
+ puts "Transaction count exceeded (#{event.payload[:transactions].size} transactions in #{event.duration}ms)"
189
+ event.payload[:transactions].each do |info|
190
+ puts info.trace.join("\n")
191
+ end
192
+ end
193
+ ```
194
+
195
+ ## Contributing
196
+
197
+ Open a pull request on GitHub.
198
+
199
+ Please use the [standardrb](https://github.com/testdouble/standard) syntax and lint your code with `standardrb --fix` before submitting.
200
+
201
+ ## License
202
+
203
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.1.2
1
+ 2.1.0
@@ -11,12 +11,14 @@ Gem::Specification.new do |spec|
11
11
  # Specify which files should be added to the gem when it is released.
12
12
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
13
13
  ignore_files = %w[
14
- .gitignore
15
- .travis.yml
14
+ .
16
15
  Appraisals
17
16
  Gemfile
18
17
  Gemfile.lock
19
18
  Rakefile
19
+ config.ru
20
+ assets/
21
+ bin/
20
22
  gemfiles/
21
23
  spec/
22
24
  ]
@@ -26,7 +28,9 @@ Gem::Specification.new do |spec|
26
28
 
27
29
  spec.require_paths = ["lib"]
28
30
 
29
- spec.add_dependency "activerecord", ">= 5.0"
31
+ spec.add_dependency "activerecord", ">= 5.1"
30
32
 
31
33
  spec.add_development_dependency "bundler"
34
+
35
+ spec.required_ruby_version = ">= 2.5"
32
36
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordQueryCounter
4
+ # Module to prepend to the connection adapter to inject the counting behavior.
5
+ module ConnectionAdapterExtension
6
+ def exec_query(sql, name = nil, binds = [], *args, **kwargs)
7
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
8
+ result = super
9
+ if result.is_a?(ActiveRecord::Result)
10
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
11
+ ActiveRecordQueryCounter.add_query(sql, name, binds, result.length, start_time, end_time)
12
+ end
13
+ result
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordQueryCounter
4
+ # Data structure for storing query information encountered within a block.
5
+ class Counter
6
+ attr_accessor :query_count, :row_count, :query_time, :cached_query_count
7
+ attr_reader :thresholds
8
+
9
+ def initialize
10
+ @query_count = 0
11
+ @row_count = 0
12
+ @query_time = 0.0
13
+ @cached_query_count = 0
14
+ @transactions_hash = {}
15
+ @thresholds = ActiveRecordQueryCounter.default_thresholds.dup
16
+ end
17
+
18
+ # Return the percentage of queries that used the query cache instead of going to the database.
19
+ #
20
+ # @return [Float]
21
+ def cache_hit_rate
22
+ total_queries = query_count + cached_query_count
23
+ if total_queries > 0
24
+ (cached_query_count.to_f / total_queries)
25
+ else
26
+ 0.0
27
+ end
28
+ end
29
+
30
+ # Return an array of transaction information for any transactions that have been tracked
31
+ # by the counter.
32
+ #
33
+ # @return [Array<ActiveRecordQueryCounter::TransactionInfo>]
34
+ def transactions
35
+ @transactions_hash.values.flatten.sort_by(&:start_time)
36
+ end
37
+
38
+ # Add a tracked transaction.
39
+ #
40
+ # @param trace [Array<String>] the trace of the transaction
41
+ # @param start_time [Float] the monotonic time when the transaction began
42
+ # @param end_time [Float] the monotonic time when the transaction ended
43
+ # @return [void]
44
+ # @api private
45
+ def add_transaction(trace:, start_time:, end_time:)
46
+ trace_transactions = @transactions_hash[trace]
47
+ if trace_transactions
48
+ # Memory optimization so that we don't store duplicate traces for every transaction in a loop.
49
+ trace = trace_transactions.first.trace
50
+ else
51
+ trace_transactions = []
52
+ @transactions_hash[trace] = trace_transactions
53
+ end
54
+
55
+ trace_transactions << TransactionInfo.new(start_time: start_time, end_time: end_time, trace: trace)
56
+ end
57
+
58
+ # Return the number of transactions that have been tracked by the counter.
59
+ #
60
+ # @return [Integer]
61
+ def transaction_count
62
+ @transactions_hash.values.flatten.size
63
+ end
64
+
65
+ # Return the total time spent in transactions that have been tracked by the counter.
66
+ #
67
+ # @return [Float]
68
+ def transaction_time
69
+ @transactions_hash.values.flatten.sum(&:elapsed_time)
70
+ end
71
+
72
+ # Get the time when the first transaction began.
73
+ #
74
+ # @return [Float, nil] the monotonic time when the first transaction began,
75
+ # or nil if no transactions have been tracked
76
+ def first_transaction_start_time
77
+ transactions.first&.start_time
78
+ end
79
+
80
+ # Get the time when the last transaction completed.
81
+ #
82
+ # @return [Float, nil] the monotonic time when the first transaction completed,
83
+ # or nil if no transactions have been tracked
84
+ def last_transaction_end_time
85
+ transactions.max_by(&:end_time)&.end_time
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordQueryCounter
4
+ # Rack middleware to count queries on a request.
5
+ class RackMiddleware
6
+ # @param app [Object] The Rack application.
7
+ # @param thresholds [Hash] Options for the notification thresholds. Valid keys are:
8
+ # * `:query_time` - The minimum query time to send a notification for.
9
+ # * `:row_count` - The minimum row count to send a notification for.
10
+ # * `:transaction_time` - The minimum transaction time to send a notification for.
11
+ # * `:transaction_count` - The minimum transaction count to send a notification for.
12
+ def initialize(app, thresholds: nil)
13
+ @app = app
14
+ @thresholds = thresholds.dup.freeze if thresholds
15
+ end
16
+
17
+ def call(env)
18
+ ActiveRecordQueryCounter.count_queries do
19
+ ActiveRecordQueryCounter.thresholds.set(@thresholds) if @thresholds
20
+
21
+ @app.call(env)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordQueryCounter
4
+ # Sidekiq middleware to count queries on a job.
5
+ #
6
+ # Notification thresholds can be set per worker with the `active_record_query_counter.thresholds` key in the
7
+ # `sidekiq_options` hash. Valid keys are:
8
+ # * `:query_time` - The minimum query time to send a notification for.
9
+ # * `:row_count` - The minimum row count to send a notification for.
10
+ # * `:transaction_time` - The minimum transaction time to send a notification for.
11
+ # * `:transaction_count` - The minimum transaction count to send a notification for.
12
+ #
13
+ # Thresholds can be disabled for a worker by setting `active_record_query_counter.thresholds` to `false`.
14
+ #
15
+ # @example
16
+ #
17
+ # class MyWorker
18
+ # include Sidekiq::Worker
19
+ #
20
+ # sidekiq_options active_record_query_counter: {thresholds: {query_time: 1.5}}
21
+ #
22
+ # def perform
23
+ # # ...
24
+ # end
25
+ # end
26
+ class SidekiqMiddleware
27
+ if defined?(Sidekiq::ServerMiddleware)
28
+ include Sidekiq::ServerMiddleware
29
+ end
30
+
31
+ def call(job_instance, job_payload, queue)
32
+ ActiveRecordQueryCounter.count_queries do
33
+ thresholds = job_payload.dig("active_record_query_counter", "thresholds")
34
+ if thresholds.is_a?(Hash)
35
+ ActiveRecordQueryCounter.thresholds.set(thresholds)
36
+ elsif thresholds == false
37
+ ActiveRecordQueryCounter.thresholds.clear
38
+ end
39
+
40
+ yield
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordQueryCounter
4
+ # Thresholds for sending notifications based on query time, row count, transaction time, and
5
+ # transaction count.
6
+ class Thresholds
7
+ attr_reader :query_time, :row_count, :transaction_time, :transaction_count
8
+
9
+ def query_time=(value)
10
+ @query_time = value&.to_f
11
+ end
12
+
13
+ def row_count=(value)
14
+ @row_count = value&.to_i
15
+ end
16
+
17
+ def transaction_time=(value)
18
+ @transaction_time = value&.to_f
19
+ end
20
+
21
+ def transaction_count=(value)
22
+ @transaction_count = value&.to_i
23
+ end
24
+
25
+ # Set threshold values from a hash.
26
+ #
27
+ # @param attributes [Hash] the attributes to set
28
+ # @return [void]
29
+ def set(values)
30
+ values.each do |key, value|
31
+ setter = "#{key}="
32
+ if respond_to?(setter)
33
+ public_send("#{key}=", value)
34
+ else
35
+ raise ArgumentError, "Unknown threshold: #{key}"
36
+ end
37
+ end
38
+ end
39
+
40
+ # Clear all threshold values.
41
+ def clear
42
+ @query_time = nil
43
+ @row_count = nil
44
+ @transaction_time = nil
45
+ @transaction_count = nil
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordQueryCounter
4
+ # Data structure for storing information about a transaction. Note that the start and end
5
+ # times are monotonic time and not wall clock time.
6
+ class TransactionInfo
7
+ attr_reader :start_time, :end_time, :trace
8
+
9
+ def initialize(start_time:, end_time:, trace:)
10
+ @start_time = start_time
11
+ @end_time = end_time
12
+ @trace = trace
13
+ end
14
+
15
+ # Return the time spent in the transaction.
16
+ #
17
+ # @return [Float]
18
+ def elapsed_time
19
+ end_time - start_time
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordQueryCounter
4
+ # Extension to ActiveRecord::ConnectionAdapters::TransactionManager to count transactions.
5
+ module TransactionManagerExtension
6
+ def begin_transaction(*args, **kwargs)
7
+ if open_transactions == 0
8
+ @active_record_query_counter_transaction_start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
9
+ end
10
+ super
11
+ end
12
+
13
+ def commit_transaction(*args)
14
+ if @active_record_query_counter_transaction_start_time && open_transactions == 1
15
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
16
+ ActiveRecordQueryCounter.add_transaction(@active_record_query_counter_transaction_start_time, end_time)
17
+ @active_record_query_counter_transaction_start_time = nil
18
+ end
19
+ super
20
+ end
21
+
22
+ def rollback_transaction(*args)
23
+ if @active_record_query_counter_transaction_start_time && open_transactions == 1
24
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
25
+ ActiveRecordQueryCounter.add_transaction(@active_record_query_counter_transaction_start_time, end_time)
26
+ @active_record_query_counter_transaction_start_time = nil
27
+ end
28
+ super
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordQueryCounter
4
+ VERSION = File.read(File.join(__dir__, "..", "..", "VERSION")).strip
5
+ end
@@ -1,8 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "active_record_query_counter/connection_adapter_extension"
4
+ require_relative "active_record_query_counter/counter"
5
+ require_relative "active_record_query_counter/rack_middleware"
6
+ require_relative "active_record_query_counter/sidekiq_middleware"
7
+ require_relative "active_record_query_counter/thresholds"
8
+ require_relative "active_record_query_counter/transaction_info"
9
+ require_relative "active_record_query_counter/transaction_manager_extension"
10
+ require_relative "active_record_query_counter/version"
11
+
3
12
  # Everything you need to count ActiveRecord queries and row counts within a block.
4
13
  #
5
- # Usage:
14
+ # @example
6
15
  #
7
16
  # ActiveRecordQueryCounter.count_queries do
8
17
  # yield
@@ -10,116 +19,205 @@
10
19
  # puts ActiveRecordQueryCounter.row_count
11
20
  # end
12
21
  module ActiveRecordQueryCounter
13
- class Counter
14
- attr_accessor :query_count, :row_count, :query_time
15
- attr_reader :transactions
16
-
17
- def initialize
18
- @query_count = 0
19
- @row_count = 0
20
- @query_time = 0.0
21
- @transactions = {}
22
- end
23
-
24
- def transaction_count
25
- @transactions.size
26
- end
27
-
28
- def transaction_time
29
- @transactions.values.sum { |count, time| time }
30
- end
31
- end
22
+ IGNORED_STATEMENTS = %w[SCHEMA EXPLAIN].freeze
23
+ private_constant :IGNORED_STATEMENTS
32
24
 
33
25
  class << self
34
26
  # Enable query counting within a block.
27
+ #
28
+ # @return [Object] the result of the block
35
29
  def count_queries
36
- current = Thread.current[:database_query_counter]
30
+ save_counter = current_counter
37
31
  begin
38
- Thread.current[:database_query_counter] = Counter.new
39
- yield
32
+ counter = Counter.new
33
+ self.current_counter = counter
34
+
35
+ retval = yield
36
+
37
+ transaction_count = counter.transaction_count
38
+ if transaction_count > 0
39
+ transaction_threshold = (counter.thresholds.transaction_count || -1)
40
+ if transaction_threshold >= 0 && transaction_count >= transaction_threshold
41
+ send_notification("transaction_count", counter.first_transaction_start_time, counter.last_transaction_end_time, transactions: counter.transactions)
42
+ end
43
+ end
44
+
45
+ retval
40
46
  ensure
41
- Thread.current[:database_query_counter] = current
47
+ self.current_counter = save_counter
42
48
  end
43
49
  end
44
50
 
45
- # Increment the query counters
46
- def increment(row_count, elapsed_time)
47
- counter = Thread.current[:database_query_counter]
48
- if counter.is_a?(Counter)
49
- counter.query_count += 1
50
- counter.row_count += row_count
51
- counter.query_time += elapsed_time
51
+ # Increment the query counters.
52
+ #
53
+ # @param row_count [Integer] the number of rows returned by the query
54
+ # @param elapsed_time [Float] the time spent executing the query
55
+ # @return [void]
56
+ # @api private
57
+ def add_query(sql, name, binds, row_count, start_time, end_time)
58
+ return if IGNORED_STATEMENTS.include?(name)
59
+
60
+ counter = current_counter
61
+ return unless counter.is_a?(Counter)
62
+
63
+ elapsed_time = end_time - start_time
64
+ counter.query_count += 1
65
+ counter.row_count += row_count
66
+ counter.query_time += elapsed_time
67
+
68
+ trace = nil
69
+ query_time_threshold = (counter.thresholds.query_time || -1)
70
+ if query_time_threshold >= 0 && elapsed_time >= query_time_threshold
71
+ trace = backtrace
72
+ send_notification("query_time", start_time, end_time, sql: sql, binds: binds, row_count: row_count, trace: trace)
73
+ end
74
+
75
+ row_count_threshold = (counter.thresholds.row_count || -1)
76
+ if row_count_threshold >= 0 && row_count >= row_count_threshold
77
+ trace ||= backtrace
78
+ send_notification("row_count", start_time, end_time, sql: sql, binds: binds, row_count: row_count, trace: trace)
52
79
  end
53
80
  end
54
81
 
55
- def increment_transaction(elapsed_time)
56
- counter = Thread.current[:database_query_counter]
57
- if counter.is_a?(Counter)
58
- trace = caller
59
- index = 0
60
- caller.each do |line|
61
- break unless line.start_with?(__FILE__)
62
- index += 1
63
- end
64
- trace = trace[index, trace.length]
65
- info = counter.transactions[trace]
66
- if info
67
- info[0] += 1
68
- info[1] += elapsed_time
69
- else
70
- info = [1, elapsed_time]
71
- counter.transactions[trace] = info
72
- end
82
+ # Increment the transaction counters.
83
+ #
84
+ # @param start_time [Float] the time the transaction started
85
+ # @param end_time [Float] the time the transaction ended
86
+ # @return [void]
87
+ # @api private
88
+ def add_transaction(start_time, end_time)
89
+ counter = current_counter
90
+ return unless counter.is_a?(Counter)
91
+
92
+ trace = backtrace
93
+ counter.add_transaction(trace: trace, start_time: start_time, end_time: end_time)
94
+
95
+ transaction_time_threshold = (counter.thresholds.transaction_time || -1)
96
+ if transaction_time_threshold >= 0 && end_time - start_time >= transaction_time_threshold
97
+ send_notification("transaction_time", start_time, end_time, trace: backtrace)
73
98
  end
74
99
  end
75
100
 
101
+ # Return the number of queries that have been counted within the current block.
102
+ # Returns nil if not inside a block where queries are being counted.
103
+ #
104
+ # @return [Integer, nil]
76
105
  def query_count
77
- counter = Thread.current[:database_query_counter]
106
+ counter = current_counter
78
107
  counter.query_count if counter.is_a?(Counter)
79
108
  end
80
109
 
110
+ # Return the number of queries that hit the query cache and were not sent to the database
111
+ # that have been counted within the current block. Returns nil if not inside a block where
112
+ # queries are being counted.
113
+ #
114
+ # @return [Integer, nil]
115
+ def cached_query_count
116
+ counter = current_counter
117
+ counter.cached_query_count if counter.is_a?(Counter)
118
+ end
119
+
120
+ # Return the number of rows that have been counted within the current block.
121
+ # Returns nil if not inside a block where queries are being counted.
122
+ #
123
+ # @return [Integer, nil]
81
124
  def row_count
82
- counter = Thread.current[:database_query_counter]
125
+ counter = current_counter
83
126
  counter.row_count if counter.is_a?(Counter)
84
127
  end
85
128
 
129
+ # Return the total time spent executing queries within the current block.
130
+ # Returns nil if not inside a block where queries are being counted.
131
+ #
132
+ # @return [Float, nil]
86
133
  def query_time
87
- counter = Thread.current[:database_query_counter]
134
+ counter = current_counter
88
135
  counter.query_time if counter.is_a?(Counter)
89
136
  end
90
137
 
138
+ # Return the number of transactions that have been counted within the current block.
139
+ # Returns nil if not inside a block where queries are being counted.
140
+ #
141
+ # @return [Integer, nil]
91
142
  def transaction_count
92
- counter = Thread.current[:database_query_counter]
143
+ counter = current_counter
93
144
  counter.transaction_count if counter.is_a?(Counter)
94
145
  end
95
146
 
147
+ # Return the total time spent in transactions that have been counted within the current block.
148
+ # Returns nil if not inside a block where queries are being counted.
149
+ #
150
+ # @return [Float, nil]
96
151
  def transaction_time
97
- counter = Thread.current[:database_query_counter]
152
+ counter = current_counter
98
153
  counter.transaction_time if counter.is_a?(Counter)
99
154
  end
100
155
 
156
+ # Return the time when the first transaction began within the current block.
157
+ # Returns nil if not inside a block where queries are being counted or there are no transactions.
158
+ #
159
+ # @return [Float, nil] the monotonic time when the first transaction began,
160
+ def first_transaction_start_time
161
+ counter = current_counter
162
+ counter.first_transaction_start_time if counter.is_a?(Counter)
163
+ end
164
+
165
+ # Return the time when the last transaction ended within the current block.
166
+ # Returns nil if not inside a block where queries are being counted or there are no transactions.
167
+ #
168
+ # @return [Float, nil] the monotonic time when the last transaction ended,
169
+ def last_transaction_end_time
170
+ counter = current_counter
171
+ counter.transactions.last&.end_time if counter.is_a?(Counter)
172
+ end
173
+
174
+ # Return an array of transaction information for any transactions that have been counted
175
+ # within the current block. Returns nil if not inside a block where queries are being counted.
176
+ #
177
+ # @return [Array<ActiveRecordQueryCounter::TransactionInfo>, nil]
101
178
  def transactions
102
- counter = Thread.current[:database_query_counter]
103
- counter.transactions.dup if counter.is_a?(Counter)
179
+ counter = current_counter
180
+ counter.transactions if counter.is_a?(Counter)
104
181
  end
105
182
 
106
183
  # Return the query info as a hash with keys :query_count, :row_count, :query_time
107
184
  # :transaction_count, and :transaction_type or nil if not inside a block where queries
108
185
  # are being counted.
186
+ #
187
+ # @return [Hash, nil]
109
188
  def info
110
- counter = Thread.current[:database_query_counter]
189
+ counter = current_counter
111
190
  if counter.is_a?(Counter)
112
191
  {
113
192
  query_count: counter.query_count,
114
193
  row_count: counter.row_count,
115
194
  query_time: counter.query_time,
195
+ cached_query_count: counter.cached_query_count,
196
+ cache_hit_rate: counter.cache_hit_rate,
116
197
  transaction_count: counter.transaction_count,
117
198
  transaction_time: counter.transaction_time
118
199
  }
119
200
  end
120
201
  end
121
202
 
203
+ # The global notification thresholds for sending notifications. The values set in these
204
+ # thresholds are used as the default values.
205
+ #
206
+ # @return [ActiveRecordQueryCounter::Thresholds]
207
+ def default_thresholds
208
+ @default_thresholds ||= Thresholds.new
209
+ end
210
+
211
+ # Get the current local notification thresholds. These thresholds are only used within
212
+ # the current `count_queries` block.
213
+ def thresholds
214
+ current_counter&.thresholds || default_thresholds.dup
215
+ end
216
+
122
217
  # Enable the query counting behavior on a connection adapter class.
218
+ #
219
+ # @param connection_class [Class] the connection adapter class to extend
220
+ # @return [void]
123
221
  def enable!(connection_class)
124
222
  unless connection_class.include?(ConnectionAdapterExtension)
125
223
  connection_class.prepend(ConnectionAdapterExtension)
@@ -127,61 +225,32 @@ module ActiveRecordQueryCounter
127
225
  unless ActiveRecord::ConnectionAdapters::TransactionManager.include?(TransactionManagerExtension)
128
226
  ActiveRecord::ConnectionAdapters::TransactionManager.prepend(TransactionManagerExtension)
129
227
  end
130
- end
131
- end
132
228
 
133
- # Module to prepend to the connection adapter to inject the counting behavior.
134
- module ConnectionAdapterExtension
135
- def exec_query(*args, **kwargs)
136
- start_time = Time.now
137
- result = super
138
- if result.is_a?(ActiveRecord::Result)
139
- ActiveRecordQueryCounter.increment(result.length, Time.now - start_time)
229
+ @cache_subscription ||= ActiveSupport::Notifications.subscribe("sql.active_record") do |_name, _start_time, _end_time, _id, payload|
230
+ if payload[:cached]
231
+ counter = current_counter
232
+ counter.cached_query_count += 1 if counter
233
+ end
140
234
  end
141
- result
142
235
  end
143
- end
144
236
 
145
- module TransactionManagerExtension
146
- def begin_transaction(*args, **kwargs)
147
- if open_transactions == 0
148
- @active_record_query_counter_transaction_start_time = Time.current
149
- end
150
- super
151
- end
237
+ private
152
238
 
153
- def commit_transaction(*args)
154
- if @active_record_query_counter_transaction_start_time && open_transactions == 1
155
- ActiveRecordQueryCounter.increment_transaction(Time.current - @active_record_query_counter_transaction_start_time)
156
- @active_record_query_counter_transaction_start_time = nil
157
- end
158
- super
239
+ def current_counter
240
+ Thread.current[:active_record_query_counter]
159
241
  end
160
242
 
161
- def rollback_transaction(*args)
162
- if @active_record_query_counter_transaction_start_time && open_transactions == 1
163
- ActiveRecordQueryCounter.increment_transaction(Time.current - @active_record_query_counter_transaction_start_time)
164
- @active_record_query_counter_transaction_start_time = nil
165
- end
166
- super
243
+ def current_counter=(counter)
244
+ Thread.current[:active_record_query_counter] = counter
167
245
  end
168
- end
169
246
 
170
- # Rack middleware to count queries on a request.
171
- class RackMiddleware
172
- def initialize(app)
173
- @app = app
247
+ def send_notification(name, start_time, end_time, payload = {})
248
+ id = "#{name}-#{SecureRandom.hex}"
249
+ ActiveSupport::Notifications.publish("active_record_query_counter.#{name}", start_time, end_time, id, payload)
174
250
  end
175
251
 
176
- def call(env)
177
- ActiveRecordQueryCounter.count_queries { @app.call(env) }
178
- end
179
- end
180
-
181
- # Sidekiq middleware to count queries on a job.
182
- class SidekiqMiddleware
183
- def call(worker, job, queue, &block)
184
- ActiveRecordQueryCounter.count_queries(&block)
252
+ def backtrace
253
+ caller.reject { |line| line.start_with?(__dir__) }
185
254
  end
186
255
  end
187
256
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_record_query_counter
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-03 00:00:00.000000000 Z
11
+ date: 2023-09-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '5.0'
19
+ version: '5.1'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '5.0'
26
+ version: '5.1'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -38,27 +38,32 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
- description:
41
+ description:
42
42
  email:
43
43
  - bbdurand@gmail.com
44
44
  executables: []
45
45
  extensions: []
46
46
  extra_rdoc_files: []
47
47
  files:
48
- - ".github/dependabot.yml"
49
- - ".github/workflows/continuous_integration.yml"
50
- - ".standard.yml"
51
48
  - CHANGELOG.md
52
49
  - MIT_LICENSE
53
50
  - README.md
54
51
  - VERSION
55
52
  - active_record_query_counter.gemspec
56
53
  - lib/active_record_query_counter.rb
54
+ - lib/active_record_query_counter/connection_adapter_extension.rb
55
+ - lib/active_record_query_counter/counter.rb
56
+ - lib/active_record_query_counter/rack_middleware.rb
57
+ - lib/active_record_query_counter/sidekiq_middleware.rb
58
+ - lib/active_record_query_counter/thresholds.rb
59
+ - lib/active_record_query_counter/transaction_info.rb
60
+ - lib/active_record_query_counter/transaction_manager_extension.rb
61
+ - lib/active_record_query_counter/version.rb
57
62
  homepage: https://github.com/bdurand/active_record_query_counter
58
63
  licenses:
59
64
  - MIT
60
65
  metadata: {}
61
- post_install_message:
66
+ post_install_message:
62
67
  rdoc_options: []
63
68
  require_paths:
64
69
  - lib
@@ -66,15 +71,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
66
71
  requirements:
67
72
  - - ">="
68
73
  - !ruby/object:Gem::Version
69
- version: '0'
74
+ version: '2.5'
70
75
  required_rubygems_version: !ruby/object:Gem::Requirement
71
76
  requirements:
72
77
  - - ">="
73
78
  - !ruby/object:Gem::Version
74
79
  version: '0'
75
80
  requirements: []
76
- rubygems_version: 3.0.3
77
- signing_key:
81
+ rubygems_version: 3.4.10
82
+ signing_key:
78
83
  specification_version: 4
79
84
  summary: Count total number of ActiveRecord queries and row counts inside a block
80
85
  test_files: []
@@ -1,12 +0,0 @@
1
- # Dependabot update strategy
2
- version: 2
3
- updates:
4
- - package-ecosystem: bundler
5
- directory: "/"
6
- schedule:
7
- interval: daily
8
- allow:
9
- # Automatically keep all runtime dependencies updated
10
- - dependency-name: "*"
11
- dependency-type: "production"
12
- versioning-strategy: lockfile-only
@@ -1,73 +0,0 @@
1
- name: Continuous Integration
2
- on:
3
- push:
4
- branches:
5
- - master
6
- - actions-*
7
- tags:
8
- - v*
9
- pull_request:
10
- env:
11
- BUNDLE_CLEAN: "true"
12
- BUNDLE_PATH: vendor/bundle
13
- BUNDLE_JOBS: 3
14
- BUNDLE_RETRY: 3
15
- CC_TEST_REPORTER_ID: 8f360e59f6cb98313ccbb27fecf82f667455acec33cf23ae115c2db121852700
16
- jobs:
17
- specs:
18
- name: ${{ matrix.job }} ruby-${{ matrix.combo.ruby || matrix.ruby }} ${{ matrix.combo.activerecord && format('activerecord-{0}', matrix.combo.activerecord) }}
19
- runs-on: ubuntu-latest
20
- strategy:
21
- fail-fast: false
22
- matrix:
23
- combo:
24
- - activerecord: "latest"
25
- ruby: "3.0"
26
- coverage: true
27
- - activerecord: "6.1"
28
- ruby: "3.0"
29
- - activerecord: "6.0"
30
- ruby: "2.7"
31
- - activerecord: "5.2"
32
- ruby: "2.6"
33
- - activerecord: "5.1"
34
- ruby: "2.6"
35
- - activerecord: "5.0"
36
- ruby: "2.5"
37
- job: [ rspec ]
38
- include:
39
- - ruby: "3.0"
40
- activerecord: "latest"
41
- job: rspec
42
- - ruby: "2.7"
43
- activerecord: "latest"
44
- job: standardrb
45
- steps:
46
- - name: checkout
47
- uses: actions/checkout@v2
48
- - name: set up Ruby
49
- uses: ruby/setup-ruby@v1
50
- with:
51
- ruby-version: ${{ matrix.combo.ruby || matrix.ruby }}
52
- - name: setup bundler
53
- run: |
54
- if [ "${{ matrix.combo.bundler }}" != "" ]; then
55
- gem uninstall bundler --all
56
- gem install bundler --no-document --version ${{ matrix.combo.bundler }}
57
- fi
58
- if [ "${{ matrix.combo.activerecord || matrix.activerecord }}" != "latest" ]; then
59
- echo "using gemfile gemfiles/activerecord_${{ matrix.combo.activerecord || matrix.activerecord }}.gemfile"
60
- bundle config set gemfile "gemfiles/activerecord_${{ matrix.combo.activerecord || matrix.activerecord }}.gemfile"
61
- fi
62
- bundle update
63
- - name: install dependencies
64
- run: bundle install
65
- - name: specs
66
- if: matrix.job == 'rspec'
67
- run: bundle exec rake spec
68
- - name: code coverage
69
- if: matrix.coverage == true
70
- uses: paambaati/codeclimate-action@v2.7.5
71
- - name: standardrb
72
- if: matrix.job == 'standardrb'
73
- run: bundle exec rake standard
data/.standard.yml DELETED
@@ -1,11 +0,0 @@
1
- # I really just have issues with the automatic "semantic blocks"
2
-
3
- ruby_version: 2.5
4
-
5
- format: progress
6
-
7
- ignore:
8
- - '**/*':
9
- - Standard/SemanticBlocks
10
- - 'spec/**/*':
11
- - Lint/UselessAssignment