active_record_query_counter 1.1.1 → 2.0.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: 519c3f12525967d15b3c72c1d81f461812e4716420ab58f5360d6cc5f9876796
4
- data.tar.gz: 849e4d66eba96b9cf05268f0373315bc72f9285f14abb544f0cfe91ae1ecd3de
3
+ metadata.gz: eb19f1f7e8a7678f3cdf3079b05e9aa26353b33b809c240943e0899f584dac6a
4
+ data.tar.gz: 7bcbce0b2d06c4f871074898651071f1400cd87c7ee8ee514837ee80caac6774
5
5
  SHA512:
6
- metadata.gz: 41087b8c0ea5b7f0775f15477578f09f7ccfe26f3f6135ecbcd521f273401b7dc43d1657febffd4a79bda61ade46247302eb0c9df192f5be51abb0627c76b55f
7
- data.tar.gz: ced16b82fba34bc1a25c7912584ac12e56dc17a584d44c2f8faad1ea80cdd292dc2f1517fc14c32f03f13652309c69d4fbb9ea8bbad08c5bb386306ec6b1736e
6
+ metadata.gz: 80c468ec96c069bb4043a0e210d4fb4b1d82714d2aa0b69bd07ac9fd23bb04255ffcf4b175fcfa66b7aa0afd8434c211645e70f4fd8d05ad9c10faa4ee95c679
7
+ data.tar.gz: fb972d526b98c97f98230a29eb9412726746a58c3d4a05406ab9ccc046d6eb0ec5007fa8d52d26a40c9a966ad2af4c50c045c1d9257cd15ce90dd21c5dedbcfa
data/CHANGELOG.md ADDED
@@ -0,0 +1,43 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## 2.0.0
8
+
9
+ ### Added
10
+
11
+ - Added capability to send ActiveSupport notifications when query thresholds are exceeded.
12
+
13
+ ### Changed
14
+
15
+ - Calculate elapsed time using monotonic time rather than wall clock time.
16
+ - Schema queries to get the table structure and explain plan queries are no longer counted.
17
+ - **Breaking change**: transaction information is now returned in an array of `ActiveRecordQueryCounter::TransactionInfo` objects.
18
+ - **Breaking change**: internal API for tracking queries and transactions has changed
19
+
20
+ ## 1.1.2
21
+
22
+ ### Added
23
+
24
+ - Ruby 3.0 compatibility
25
+
26
+ ### Removed
27
+
28
+ - Dropped support for ActiveRecord 4.2
29
+
30
+ ## 1.1.1
31
+ ### Added
32
+
33
+ - Expose stack traces where transactions are being committed.
34
+
35
+ ## 1.1.0
36
+ ### Added
37
+
38
+ - Add number of transactions to statistics being tracked.
39
+
40
+ ## 1.0.0
41
+ ### Added
42
+
43
+ - Track stats about queries run by ActiveRecord within a block.
data/README.md CHANGED
@@ -1,12 +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
4
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
6
5
 
7
- 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.
8
7
 
9
- 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.
10
17
 
11
18
  ## Usage
12
19
 
@@ -37,9 +44,7 @@ ActiveRecordQueryCounter.count_queries do
37
44
  end
38
45
  ```
39
46
 
40
- This gem includes middleware for both Rack and Sidekiq that will enable query counting.
41
-
42
- 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.
43
48
 
44
49
  ```ruby
45
50
  ActiveSupport.on_load(:active_record) do
@@ -54,3 +59,145 @@ Sidekiq.configure_server do |config|
54
59
  end
55
60
  end
56
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.1
1
+ 2.0.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,9 +28,9 @@ Gem::Specification.new do |spec|
26
28
 
27
29
  spec.require_paths = ["lib"]
28
30
 
29
- spec.add_dependency "activerecord", ">= 4.2"
31
+ spec.add_dependency "activerecord", ">= 5.0"
30
32
 
31
- spec.add_development_dependency "bundler", "~>2.0"
32
- spec.add_development_dependency "rspec", ["~> 3.0"]
33
- spec.add_development_dependency "sqlite3"
33
+ spec.add_development_dependency "bundler"
34
+
35
+ spec.required_ruby_version = ">= 2.5"
34
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,75 @@
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
7
+ attr_reader :thresholds
8
+
9
+ def initialize
10
+ @query_count = 0
11
+ @row_count = 0
12
+ @query_time = 0.0
13
+ @transactions_hash = {}
14
+ @thresholds = ActiveRecordQueryCounter.default_thresholds.dup
15
+ end
16
+
17
+ # Return an array of transaction information for any transactions that have been tracked
18
+ # by the counter.
19
+ #
20
+ # @return [Array<ActiveRecordQueryCounter::TransactionInfo>]
21
+ def transactions
22
+ @transactions_hash.values.flatten.sort_by(&:start_time)
23
+ end
24
+
25
+ # Add a tracked transaction.
26
+ #
27
+ # @param trace [Array<String>] the trace of the transaction
28
+ # @param start_time [Float] the monotonic time when the transaction began
29
+ # @param end_time [Float] the monotonic time when the transaction ended
30
+ # @return [void]
31
+ # @api private
32
+ def add_transaction(trace:, start_time:, end_time:)
33
+ trace_transactions = @transactions_hash[trace]
34
+ if trace_transactions
35
+ # Memory optimization so that we don't store duplicate traces for every transaction in a loop.
36
+ trace = trace_transactions.first.trace
37
+ else
38
+ trace_transactions = []
39
+ @transactions_hash[trace] = trace_transactions
40
+ end
41
+
42
+ trace_transactions << TransactionInfo.new(start_time: start_time, end_time: end_time, trace: trace)
43
+ end
44
+
45
+ # Return the number of transactions that have been tracked by the counter.
46
+ #
47
+ # @return [Integer]
48
+ def transaction_count
49
+ @transactions_hash.values.flatten.size
50
+ end
51
+
52
+ # Return the total time spent in transactions that have been tracked by the counter.
53
+ #
54
+ # @return [Float]
55
+ def transaction_time
56
+ @transactions_hash.values.flatten.sum(&:elapsed_time)
57
+ end
58
+
59
+ # Get the time when the first transaction began.
60
+ #
61
+ # @return [Float, nil] the monotonic time when the first transaction began,
62
+ # or nil if no transactions have been tracked
63
+ def first_transaction_start_time
64
+ transactions.first&.start_time
65
+ end
66
+
67
+ # Get the time when the last transaction completed.
68
+ #
69
+ # @return [Float, nil] the monotonic time when the first transaction completed,
70
+ # or nil if no transactions have been tracked
71
+ def last_transaction_end_time
72
+ transactions.max_by(&:end_time)&.end_time
73
+ end
74
+ end
75
+ 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,104 +19,164 @@
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[CACHE 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 rows that have been counted within the current block.
111
+ # Returns nil if not inside a block where queries are being counted.
112
+ #
113
+ # @return [Integer, nil]
81
114
  def row_count
82
- counter = Thread.current[:database_query_counter]
115
+ counter = current_counter
83
116
  counter.row_count if counter.is_a?(Counter)
84
117
  end
85
118
 
119
+ # Return the total time spent executing queries within the current block.
120
+ # Returns nil if not inside a block where queries are being counted.
121
+ #
122
+ # @return [Float, nil]
86
123
  def query_time
87
- counter = Thread.current[:database_query_counter]
124
+ counter = current_counter
88
125
  counter.query_time if counter.is_a?(Counter)
89
126
  end
90
127
 
128
+ # Return the number of transactions that have been counted within the current block.
129
+ # Returns nil if not inside a block where queries are being counted.
130
+ #
131
+ # @return [Integer, nil]
91
132
  def transaction_count
92
- counter = Thread.current[:database_query_counter]
133
+ counter = current_counter
93
134
  counter.transaction_count if counter.is_a?(Counter)
94
135
  end
95
136
 
137
+ # Return the total time spent in transactions that have been counted within the current block.
138
+ # Returns nil if not inside a block where queries are being counted.
139
+ #
140
+ # @return [Float, nil]
96
141
  def transaction_time
97
- counter = Thread.current[:database_query_counter]
142
+ counter = current_counter
98
143
  counter.transaction_time if counter.is_a?(Counter)
99
144
  end
100
145
 
146
+ # Return the time when the first transaction began within the current block.
147
+ # Returns nil if not inside a block where queries are being counted or there are no transactions.
148
+ #
149
+ # @return [Float, nil] the monotonic time when the first transaction began,
150
+ def first_transaction_start_time
151
+ counter = current_counter
152
+ counter.first_transaction_start_time if counter.is_a?(Counter)
153
+ end
154
+
155
+ # Return the time when the last transaction ended within the current block.
156
+ # Returns nil if not inside a block where queries are being counted or there are no transactions.
157
+ #
158
+ # @return [Float, nil] the monotonic time when the last transaction ended,
159
+ def last_transaction_end_time
160
+ counter = current_counter
161
+ counter.transactions.last&.end_time if counter.is_a?(Counter)
162
+ end
163
+
164
+ # Return an array of transaction information for any transactions that have been counted
165
+ # within the current block. Returns nil if not inside a block where queries are being counted.
166
+ #
167
+ # @return [Array<ActiveRecordQueryCounter::TransactionInfo>, nil]
101
168
  def transactions
102
- counter = Thread.current[:database_query_counter]
103
- counter.transactions.dup if counter.is_a?(Counter)
169
+ counter = current_counter
170
+ counter.transactions if counter.is_a?(Counter)
104
171
  end
105
172
 
106
173
  # Return the query info as a hash with keys :query_count, :row_count, :query_time
107
174
  # :transaction_count, and :transaction_type or nil if not inside a block where queries
108
175
  # are being counted.
176
+ #
177
+ # @return [Hash, nil]
109
178
  def info
110
- counter = Thread.current[:database_query_counter]
179
+ counter = current_counter
111
180
  if counter.is_a?(Counter)
112
181
  {
113
182
  query_count: counter.query_count,
@@ -119,7 +188,24 @@ module ActiveRecordQueryCounter
119
188
  end
120
189
  end
121
190
 
191
+ # The global notification thresholds for sending notifications. The values set in these
192
+ # thresholds are used as the default values.
193
+ #
194
+ # @return [ActiveRecordQueryCounter::Thresholds]
195
+ def default_thresholds
196
+ @default_thresholds ||= Thresholds.new
197
+ end
198
+
199
+ # Get the current local notification thresholds. These thresholds are only used within
200
+ # the current `count_queries` block.
201
+ def thresholds
202
+ current_counter&.thresholds || default_thresholds.dup
203
+ end
204
+
122
205
  # Enable the query counting behavior on a connection adapter class.
206
+ #
207
+ # @param connection_class [Class] the connection adapter class to extend
208
+ # @return [void]
123
209
  def enable!(connection_class)
124
210
  unless connection_class.include?(ConnectionAdapterExtension)
125
211
  connection_class.prepend(ConnectionAdapterExtension)
@@ -128,60 +214,24 @@ module ActiveRecordQueryCounter
128
214
  ActiveRecord::ConnectionAdapters::TransactionManager.prepend(TransactionManagerExtension)
129
215
  end
130
216
  end
131
- end
132
217
 
133
- # Module to prepend to the connection adapter to inject the counting behavior.
134
- module ConnectionAdapterExtension
135
- def exec_query(*args)
136
- start_time = Time.now
137
- result = super
138
- if result.is_a?(ActiveRecord::Result)
139
- ActiveRecordQueryCounter.increment(result.length, Time.now - start_time)
140
- end
141
- result
142
- end
143
- end
144
-
145
- module TransactionManagerExtension
146
- def begin_transaction(*args)
147
- if open_transactions == 0
148
- @active_record_query_counter_transaction_start_time = Time.current
149
- end
150
- super
151
- end
152
-
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
159
- end
218
+ private
160
219
 
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
220
+ def current_counter
221
+ Thread.current[:active_record_query_counter]
167
222
  end
168
- end
169
223
 
170
- # Rack middleware to count queries on a request.
171
- class RackMiddleware
172
- def initialize(app)
173
- @app = app
224
+ def current_counter=(counter)
225
+ Thread.current[:active_record_query_counter] = counter
174
226
  end
175
227
 
176
- def call(env)
177
- ActiveRecordQueryCounter.count_queries { @app.call(env) }
228
+ def send_notification(name, start_time, end_time, payload = {})
229
+ id = "#{name}-#{SecureRandom.hex}"
230
+ ActiveSupport::Notifications.publish("active_record_query_counter.#{name}", start_time, end_time, id, payload)
178
231
  end
179
- end
180
232
 
181
- # Sidekiq middleware to count queries on a job.
182
- class SidekiqMiddleware
183
- def call(worker, job, queue, &block)
184
- ActiveRecordQueryCounter.count_queries(&block)
233
+ def backtrace
234
+ caller.reject { |line| line.start_with?(__dir__) }
185
235
  end
186
236
  end
187
237
  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.1
4
+ version: 2.0.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: 2020-11-05 00:00:00.000000000 Z
11
+ date: 2023-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,44 +16,16 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '4.2'
19
+ version: '5.0'
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: '4.2'
26
+ version: '5.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '2.0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '2.0'
41
- - !ruby/object:Gem::Dependency
42
- name: rspec
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '3.0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '3.0'
55
- - !ruby/object:Gem::Dependency
56
- name: sqlite3
57
29
  requirement: !ruby/object:Gem::Requirement
58
30
  requirements:
59
31
  - - ">="
@@ -66,27 +38,32 @@ dependencies:
66
38
  - - ">="
67
39
  - !ruby/object:Gem::Version
68
40
  version: '0'
69
- description:
41
+ description:
70
42
  email:
71
43
  - bbdurand@gmail.com
72
44
  executables: []
73
45
  extensions: []
74
46
  extra_rdoc_files: []
75
47
  files:
76
- - ".github/dependabot.yml"
77
- - ".github/workflows/continuous_integration.yml"
78
- - ".standard.yml"
79
- - CHANGE_LOG.md
48
+ - CHANGELOG.md
80
49
  - MIT_LICENSE
81
50
  - README.md
82
51
  - VERSION
83
52
  - active_record_query_counter.gemspec
84
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
85
62
  homepage: https://github.com/bdurand/active_record_query_counter
86
63
  licenses:
87
64
  - MIT
88
65
  metadata: {}
89
- post_install_message:
66
+ post_install_message:
90
67
  rdoc_options: []
91
68
  require_paths:
92
69
  - lib
@@ -94,15 +71,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
94
71
  requirements:
95
72
  - - ">="
96
73
  - !ruby/object:Gem::Version
97
- version: '0'
74
+ version: '2.5'
98
75
  required_rubygems_version: !ruby/object:Gem::Requirement
99
76
  requirements:
100
77
  - - ">="
101
78
  - !ruby/object:Gem::Version
102
79
  version: '0'
103
80
  requirements: []
104
- rubygems_version: 3.0.3
105
- signing_key:
81
+ rubygems_version: 3.4.10
82
+ signing_key:
106
83
  specification_version: 4
107
84
  summary: Count total number of ActiveRecord queries and row counts inside a block
108
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,51 +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
- jobs:
16
- specs:
17
- name: ${{ matrix.job }} ruby-${{ matrix.ruby }} ${{ matrix.activerecord && format('activerecord-{0}', matrix.activerecord) }}
18
- runs-on: ubuntu-latest
19
- strategy:
20
- fail-fast: false
21
- matrix:
22
- ruby: ["2.6"]
23
- activerecord: ["4.2", "5.0", "5.1", "5.2", "6.0"]
24
- job: [ rspec ]
25
- include:
26
- - ruby: "2.7"
27
- activerecord: original
28
- job: rspec
29
- - ruby: "2.7"
30
- job: standardrb
31
- steps:
32
- - name: checkout
33
- uses: actions/checkout@v2
34
- - name: set up Ruby
35
- uses: ruby/setup-ruby@v1
36
- with:
37
- ruby-version: ${{ matrix.ruby }}
38
- - name: inject activerecord ${{ matrix.activerecord }}
39
- if: matrix.activerecord != 'original' && matrix.activerecord != null
40
- run: | # inject a specific version of activerecord into the Gemfile
41
- bundle update
42
- bundle exec appraisal generate
43
- bundle config set gemfile "gemfiles/activerecord_${{ matrix.activerecord }}.gemfile"
44
- - name: install dependencies
45
- run: bundle install
46
- - name: specs
47
- if: matrix.job == 'rspec'
48
- run: bundle exec rake spec
49
- - name: standardrb
50
- if: matrix.job == 'standardrb'
51
- 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.4
4
-
5
- format: progress
6
-
7
- ignore:
8
- - '**/*':
9
- - Standard/SemanticBlocks
10
- - 'spec/**/*':
11
- - Lint/UselessAssignment
data/CHANGE_LOG.md DELETED
@@ -1,11 +0,0 @@
1
- # 1.1.1
2
-
3
- * Expose stack traces for where transactions are being committed.
4
-
5
- # 1.1.0
6
-
7
- * Add transaction counts.
8
-
9
- # 1.0.0
10
-
11
- * Initial release.