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 +4 -4
- data/lib/generators/query_owl/install/templates/initializer.rb +20 -0
- data/lib/query_owl/configuration.rb +4 -1
- data/lib/query_owl/middleware.rb +45 -23
- data/lib/query_owl/test_helper.rb +85 -0
- data/lib/query_owl/version.rb +1 -1
- data/lib/tasks/query_owl_tasks.rake +8 -4
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9d6d1c90011594e84ad366b86242261eb507c646bbbd99567ee68e764f79fed3
|
|
4
|
+
data.tar.gz: 6b3d04a4eeba7e6cd61f77ac1c86d9267490cb68bc4bee375bf3fca39db21c48
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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)
|
data/lib/query_owl/middleware.rb
CHANGED
|
@@ -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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
data/lib/query_owl/version.rb
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
+
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
|