query_owl 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1269c2c24be94cd4ebf8ef07d12fa3e5da1aa2c1573256779f641d3f0a089c6e
4
- data.tar.gz: 891f6323d6a6ffd95b6e73e131ddbf2f556e90703f5aa67819d92a3bdf0b4ccb
3
+ metadata.gz: 9d6d1c90011594e84ad366b86242261eb507c646bbbd99567ee68e764f79fed3
4
+ data.tar.gz: 6b3d04a4eeba7e6cd61f77ac1c86d9267490cb68bc4bee375bf3fca39db21c48
5
5
  SHA512:
6
- metadata.gz: 6fe7f83cbb596f8616ffdd11ccea40c4c2c485fa16b303f783a0b6b3a551eef4b8313e353585c8990061832227926738931fa582f9d2de8860b819afad301b96
7
- data.tar.gz: 87244c9aba6eff9996e97d1f5a3727b8fbf283d6b10647c05fb903d4d7833bc464fdd0c26db84895f8ed7dbc7e290bc6c22f5f225f01360649d03859b9e2dfb7
6
+ metadata.gz: 48dd728246c8226b5fc04bf819e85712e8eaf5453d1fe77ca1ce3ba012f7399a52af50cffacb652c5958f2ab086c35270930fed9c093971b3e446bc3846f1756
7
+ data.tar.gz: 1f26cb6d3c28dccf5708446b4c9e17ce2f334af66ae8b0ed6c6d92eb36440818b4c1feff3cf4ef124f455e54e050641eadc8952aa0a8d59c5b2a4ed50672a8cf
@@ -34,6 +34,26 @@ QueryOwl.configure do |config|
34
34
  # Disabled by default (nil). Useful for persistence across restarts.
35
35
  # config.log_file = Rails.root.join("log/query_owl.log").to_s
36
36
 
37
+ # Paths to skip entirely — accepts strings (prefix match) or regexes.
38
+ # Useful for health check endpoints and other high-frequency low-value paths.
39
+ # config.ignore_paths = ["/up", "/healthz", %r{^/assets/}]
40
+
41
+ # Controllers to skip — matched against the Rails controller name (e.g. "rails/health").
42
+ # config.ignore_controllers = ["rails/health", "admin/metrics"]
43
+
44
+ # Test helper — opt-in RSpec matchers and Minitest assertions.
45
+ # Add to spec/rails_helper.rb (or test/test_helper.rb for Minitest):
46
+ #
47
+ # require "query_owl/test_helper"
48
+ # RSpec.configure { |c| c.include QueryOwl::TestHelper }
49
+ # # or: class ActiveSupport::TestCase; include QueryOwl::TestHelper; end
50
+ #
51
+ # Then use: expect { }.not_to trigger_n_plus_one
52
+ # expect { }.not_to trigger_slow_query
53
+ # expect { }.not_to trigger_unused_eager_load
54
+ # assert_no_n_plus_one { }
55
+ # assert_no_slow_query { }
56
+
37
57
  # Notifiers receive each detected event via #call(event).
38
58
  # Defaults to [QueryOwl::Notifiers::Logger] which writes to Rails.logger.
39
59
  # Use Console for TTY-aware colorized output (yellow: N+1, red: slow query).
@@ -5,7 +5,8 @@ module QueryOwl
5
5
 
6
6
  attr_reader :log_level, :backtrace_filter
7
7
  attr_accessor :enabled, :slow_query_threshold_ms, :n_plus_one_threshold, :backtrace_lines,
8
- :raise_on_n_plus_one, :event_store_size, :dashboard_enabled, :log_file
8
+ :raise_on_n_plus_one, :event_store_size, :dashboard_enabled, :log_file,
9
+ :ignore_paths, :ignore_controllers
9
10
 
10
11
  def notifiers
11
12
  @notifiers ||= [Notifiers::Logger.new]
@@ -31,6 +32,8 @@ module QueryOwl
31
32
  @event_store_size = 100
32
33
  @dashboard_enabled = Rails.env.development?
33
34
  @log_file = nil
35
+ @ignore_paths = []
36
+ @ignore_controllers = []
34
37
  end
35
38
 
36
39
  def log_level=(level)
@@ -4,41 +4,63 @@ module QueryOwl
4
4
  @app = app
5
5
  end
6
6
 
7
- def raise_on_n_plus_one!(events)
8
- event = events.find { |e| e[:type] == :n_plus_one }
9
- return unless event
10
-
11
- raise NPlusOneError, "N+1 detected: #{event[:sql]} (#{event[:count]} times) #{event[:backtrace].first}"
12
- end
13
-
14
7
  def call(env)
8
+ tracking = false
15
9
  return @app.call(env) unless QueryOwl.config.enabled
10
+ return @app.call(env) if ignored_path?(env["PATH_INFO"])
16
11
 
12
+ tracking = true
17
13
  QueryTracker.start!
18
14
  EagerLoadTracker.start!
19
15
  @app.call(env)
20
16
  ensure
21
- params = env["action_dispatch.request.path_parameters"] || {}
22
- RequestContext.set(controller: params[:controller], action: params[:action], path: env["PATH_INFO"])
23
- queries = QueryTracker.stop!
24
- eager_data = EagerLoadTracker.stop!
25
- context = RequestContext.current
26
- RequestContext.clear
27
- events = (Detector.detect_n_plus_one(queries) +
17
+ if tracking
18
+ params = env["action_dispatch.request.path_parameters"] || {}
19
+ RequestContext.set(controller: params[:controller], action: params[:action], path: env["PATH_INFO"])
20
+ queries = QueryTracker.stop!
21
+ eager_data = EagerLoadTracker.stop!
22
+ context = RequestContext.current
23
+ RequestContext.clear
24
+
25
+ unless ignored_controller?(context[:controller])
26
+ events = (Detector.detect_n_plus_one(queries) +
28
27
  Detector.detect_slow_queries(queries) +
29
28
  Detector.detect_unused_eager_loads(eager_data))
30
29
  .map { |e| e.merge(context) }
31
- events.each do |event|
32
- QueryOwl.config.notifiers.each do |notifier|
33
- notifier.call(event)
34
- rescue => e
35
- Rails.logger.error "[QueryOwl] Notifier #{notifier.class} raised: #{e.message}"
30
+ events.each do |event|
31
+ QueryOwl.config.notifiers.each do |notifier|
32
+ notifier.call(event)
33
+ rescue => e
34
+ Rails.logger.error "[QueryOwl] Notifier #{notifier.class} raised: #{e.message}"
35
+ end
36
+ end
37
+ Logger.log_summary(events)
38
+ events.each { |e| EventStore.push(e) }
39
+ FileLogger.append(events)
40
+ raise_on_n_plus_one!(events) if QueryOwl.config.raise_on_n_plus_one
36
41
  end
37
42
  end
38
- Logger.log_summary(events)
39
- events.each { |e| EventStore.push(e) }
40
- FileLogger.append(events)
41
- raise_on_n_plus_one!(events) if QueryOwl.config.raise_on_n_plus_one
42
43
  end
44
+
45
+ def raise_on_n_plus_one!(events)
46
+ event = events.find { |e| e[:type] == :n_plus_one }
47
+ return unless event
48
+
49
+ raise NPlusOneError, "N+1 detected: #{event[:sql]} (#{event[:count]} times) #{event[:backtrace].first}"
50
+ end
51
+
52
+ private
53
+
54
+ def ignored_path?(path)
55
+ QueryOwl.config.ignore_paths.any? do |pattern|
56
+ pattern.is_a?(Regexp) ? pattern.match?(path) : path.start_with?(pattern.to_s)
57
+ end
58
+ end
59
+
60
+ def ignored_controller?(controller)
61
+ return false unless controller
62
+
63
+ QueryOwl.config.ignore_controllers.any? { |name| name.to_s == controller }
64
+ end
43
65
  end
44
66
  end
@@ -0,0 +1,85 @@
1
+ require "query_owl"
2
+
3
+ module QueryOwl
4
+ module TestHelper
5
+ # Runs block with QueryOwl's trackers active and returns detected events.
6
+ # Isolated from config.enabled and config.raise_on_n_plus_one.
7
+ def self.capture_events
8
+ QueryTracker.start!
9
+ EagerLoadTracker.start!
10
+ yield
11
+ queries = QueryTracker.stop!
12
+ eager_data = EagerLoadTracker.stop!
13
+ Detector.detect_n_plus_one(queries) +
14
+ Detector.detect_slow_queries(queries) +
15
+ Detector.detect_unused_eager_loads(eager_data)
16
+ rescue
17
+ QueryTracker.stop!
18
+ EagerLoadTracker.stop!
19
+ raise
20
+ end
21
+
22
+ # RSpec block matchers — use with expect { }.to / not_to
23
+
24
+ def trigger_n_plus_one
25
+ EventTypeMatcher.new(:n_plus_one)
26
+ end
27
+
28
+ def trigger_slow_query
29
+ EventTypeMatcher.new(:slow_query)
30
+ end
31
+
32
+ def trigger_unused_eager_load
33
+ EventTypeMatcher.new(:unused_eager_load)
34
+ end
35
+
36
+ # Minitest assertions — call assert_no_n_plus_one { } inside a test method
37
+
38
+ def assert_no_n_plus_one(msg = nil, &block)
39
+ events = QueryOwl::TestHelper.capture_events(&block)
40
+ count = events.count { |e| e[:type] == :n_plus_one }
41
+ assert count.zero?, msg || "Expected no N+1 queries, but #{count} detected"
42
+ end
43
+
44
+ def assert_no_slow_query(msg = nil, &block)
45
+ events = QueryOwl::TestHelper.capture_events(&block)
46
+ count = events.count { |e| e[:type] == :slow_query }
47
+ assert count.zero?, msg || "Expected no slow queries, but #{count} detected"
48
+ end
49
+
50
+ class EventTypeMatcher
51
+ def initialize(type)
52
+ @type = type
53
+ @events = []
54
+ end
55
+
56
+ def matches?(block)
57
+ @events = QueryOwl::TestHelper.capture_events(&block)
58
+ @events.any? { |e| e[:type] == @type }
59
+ end
60
+
61
+ def does_not_match?(block)
62
+ !matches?(block)
63
+ end
64
+
65
+ def supports_block_expectations?
66
+ true
67
+ end
68
+
69
+ def failure_message
70
+ "expected block to trigger #{label} but none were detected"
71
+ end
72
+
73
+ def failure_message_when_negated
74
+ n = @events.count { |e| e[:type] == @type }
75
+ "expected block not to trigger #{label} but #{n} detected"
76
+ end
77
+
78
+ private
79
+
80
+ def label
81
+ @type.to_s.tr("_", " ")
82
+ end
83
+ end
84
+ end
85
+ end
@@ -1,3 +1,3 @@
1
1
  module QueryOwl
2
- VERSION = "0.4.1"
2
+ VERSION = "0.6.0"
3
3
  end
@@ -1,4 +1,8 @@
1
- # desc "Explaining what the task does"
2
- # task :query_owl do
3
- # # Task goes here
4
- # end
1
+ namespace :query_owl do
2
+ desc "Clear all events from the QueryOwl in-memory event store"
3
+ task clear: :environment do
4
+ count = QueryOwl::EventStore.size
5
+ QueryOwl::EventStore.clear
6
+ puts "[QueryOwl] Event store cleared (#{count} event#{"s" if count != 1} removed)."
7
+ end
8
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: query_owl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -63,6 +63,7 @@ files:
63
63
  - lib/query_owl/notifiers/stdout.rb
64
64
  - lib/query_owl/query_tracker.rb
65
65
  - lib/query_owl/request_context.rb
66
+ - lib/query_owl/test_helper.rb
66
67
  - lib/query_owl/version.rb
67
68
  - lib/tasks/query_owl_tasks.rake
68
69
  homepage: https://github.com/eclectic-coding/query_owl