query_owl 0.4.0 → 0.5.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: 9dd0e38c57b1b0c94a99a0ffb22f6ecc71f0e28c6d4d6d041dac57bc6e5bf626
4
- data.tar.gz: dd6c94c94314dc38da3a757d0e4147dc349ae8842f10e0e29f9fa7fc2e0727ed
3
+ metadata.gz: 2654c621778910c1a0b1926398480e21b967f86c63ff45b642e635d80bfebcd0
4
+ data.tar.gz: 54245db5c8aa3a64869f3ce18b9c95b662d01ba58a042150ea551f19e1bf7299
5
5
  SHA512:
6
- metadata.gz: be09a0350d981ffcd6e2b5b4e57825daf3d4aca7a0747d0f3579188df22ddc5f26babeab619c24e1d47c0e86ebda1dc1dce99ee65ce32cc01ff1cef1f997a95e
7
- data.tar.gz: 6d499e2d7972b3252f6eccaae8e415f742e2e28b579c08d9e53f2c7377a998a6c5b7be1a46bc3f4189d353258fe7dc0dd01f5df29eb4986d7851c07ad1179fca
6
+ metadata.gz: 5d1a2a46359810b4f5e482097264c6d1d402c4344f5cf29b672d89a9c834c04e81936f07f3111454abf549e12c5a30614e6829c5d62563860de6b625220ce234
7
+ data.tar.gz: 41561e881c73aac6ed2ce8b7d3fc4f1a9f83d35fe92f4473713d274978451d9d7cd4ae7fada1af25739d3222457216ae13fdfb015b41bc8cf87116ee8aaa9b1a
@@ -26,4 +26,9 @@
26
26
  }
27
27
 
28
28
  .qo-table tr:last-child td { border-bottom: none; }
29
- .qo-table tr:hover td { background: #fafafa; }
29
+ .qo-table tr:hover td { background: #fafafa; }
30
+
31
+ .qo-table td:nth-child(2) { max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
32
+
33
+ .qo-request { white-space: nowrap; }
34
+ .qo-path { font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; font-size: 11px; opacity: .7; }
@@ -12,6 +12,7 @@
12
12
  <th>Type</th>
13
13
  <th>SQL / Details</th>
14
14
  <th>Info</th>
15
+ <th>Request</th>
15
16
  <th>Recorded At</th>
16
17
  <th>Backtrace</th>
17
18
  </tr>
@@ -25,6 +26,14 @@
25
26
  <% if event[:count] %>count: <%= event[:count] %><% end %>
26
27
  <% if event[:duration_ms] %><%= event[:duration_ms] %>ms<% end %>
27
28
  </td>
29
+ <td class="qo-request qo-muted">
30
+ <% if event[:controller] || event[:action] %>
31
+ <span class="qo-monospace"><%= [event[:controller], event[:action]].compact.join("#") %></span>
32
+ <% end %>
33
+ <% if event[:path] %>
34
+ <br><span class="qo-path"><%= event[:path] %></span>
35
+ <% end %>
36
+ </td>
28
37
  <td class="qo-muted"><%= event[:recorded_at]&.strftime("%H:%M:%S") %></td>
29
38
  <td class="qo-monospace qo-muted"><%= Array(event[:backtrace]).first %></td>
30
39
  </tr>
@@ -34,6 +34,13 @@ 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
+
37
44
  # Notifiers receive each detected event via #call(event).
38
45
  # Defaults to [QueryOwl::Notifiers::Logger] which writes to Rails.logger.
39
46
  # Use Console for TTY-aware colorized output (yellow: N+1, red: slow query).
@@ -5,13 +5,19 @@ 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]
12
13
  end
13
14
 
14
15
  def notifiers=(arr)
16
+ arr.each do |notifier|
17
+ unless notifier.respond_to?(:call)
18
+ raise ArgumentError, "notifiers must respond to #call (#{notifier.class} does not)"
19
+ end
20
+ end
15
21
  @notifiers = arr
16
22
  end
17
23
 
@@ -26,6 +32,8 @@ module QueryOwl
26
32
  @event_store_size = 100
27
33
  @dashboard_enabled = Rails.env.development?
28
34
  @log_file = nil
35
+ @ignore_paths = []
36
+ @ignore_controllers = []
29
37
  end
30
38
 
31
39
  def log_level=(level)
@@ -4,7 +4,7 @@ module QueryOwl
4
4
  def push(event)
5
5
  mutex.synchronize do
6
6
  ensure_buffer_size
7
- buffer[@write_pos] = event.merge(recorded_at: Time.now)
7
+ buffer[@write_pos] = event.merge(recorded_at: Time.current)
8
8
  @write_pos = (@write_pos + 1) % capacity
9
9
  @stored = [@stored + 1, capacity].min
10
10
  end
@@ -1,4 +1,5 @@
1
1
  require "json"
2
+ require "fileutils"
2
3
 
3
4
  module QueryOwl
4
5
  class FileLogger
@@ -9,9 +10,12 @@ module QueryOwl
9
10
  path = QueryOwl.config.log_file
10
11
  return unless path
11
12
 
13
+ FileUtils.mkdir_p(File.dirname(path))
12
14
  File.open(path, "a") do |f|
13
15
  events.each { |e| f.puts(JSON.generate(serializable(e))) }
14
16
  end
17
+ rescue => e
18
+ Rails.logger.error "[QueryOwl] FileLogger failed: #{e.message}"
15
19
  end
16
20
 
17
21
  private
@@ -4,35 +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 { |event| QueryOwl.config.notifiers.each { |notifier| notifier.call(event) } }
32
- Logger.log_summary(events)
33
- events.each { |e| EventStore.push(e) }
34
- FileLogger.append(events)
35
- raise_on_n_plus_one!(events) if QueryOwl.config.raise_on_n_plus_one
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
41
+ end
42
+ end
36
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
37
65
  end
38
66
  end
@@ -1,3 +1,3 @@
1
1
  module QueryOwl
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.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.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith