query_owl 0.3.0 → 0.4.1

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: 5db617fa85bce1a43d851809139bd43015afd92263bd203ac0114921dd94c525
4
- data.tar.gz: 4ebf287c62f2779448c2277d416147d7b426332a2dd927f6a1a8485498c76638
3
+ metadata.gz: 1269c2c24be94cd4ebf8ef07d12fa3e5da1aa2c1573256779f641d3f0a089c6e
4
+ data.tar.gz: 891f6323d6a6ffd95b6e73e131ddbf2f556e90703f5aa67819d92a3bdf0b4ccb
5
5
  SHA512:
6
- metadata.gz: e3ec2642c611837a83dc96fdf8fc7eb669a99c6ae140fc3ad5bb1439a5b2db4349a9038a87a4af8e61fe1156e2d6fcbe43535641a2c51dd3bbcc41f9652ec88c
7
- data.tar.gz: 2b2fe57db0a34c3289ef76a6170c4ea0d36d441eb733571ce87d8b506e1772b54d1e63841a8719d009a7ffd52a708b8ce5ad19c36e22839ceb3d33111700ae8b
6
+ metadata.gz: 6fe7f83cbb596f8616ffdd11ccea40c4c2c485fa16b303f783a0b6b3a551eef4b8313e353585c8990061832227926738931fa582f9d2de8860b819afad301b96
7
+ data.tar.gz: 87244c9aba6eff9996e97d1f5a3727b8fbf283d6b10647c05fb903d4d7833bc464fdd0c26db84895f8ed7dbc7e290bc6c22f5f225f01360649d03859b9e2dfb7
@@ -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>
@@ -0,0 +1,15 @@
1
+ require "rails/generators"
2
+
3
+ module QueryOwl
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ desc "Creates a QueryOwl initializer in config/initializers."
9
+
10
+ def copy_initializer
11
+ template "initializer.rb", "config/initializers/query_owl.rb"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,42 @@
1
+ QueryOwl.configure do |config|
2
+ # Enable or disable all QueryOwl tracking.
3
+ # Defaults to true in development, false elsewhere.
4
+ # config.enabled = Rails.env.development?
5
+
6
+ # Flag N+1 queries when the same SQL pattern fires this many times per request.
7
+ # config.n_plus_one_threshold = 2
8
+
9
+ # Flag individual queries that take longer than this (in milliseconds).
10
+ # config.slow_query_threshold_ms = 100
11
+
12
+ # Log level for QueryOwl warnings (:debug, :info, or :warn).
13
+ # config.log_level = :warn
14
+
15
+ # Number of backtrace frames captured per query.
16
+ # config.backtrace_lines = 5
17
+
18
+ # Custom backtrace filter — a callable that receives a line and returns true to keep it.
19
+ # Defaults to stripping gem paths and QueryOwl internals.
20
+ # config.backtrace_filter = ->(line) { line.start_with?("app/") }
21
+
22
+ # Raise QueryOwl::NPlusOneError instead of logging when an N+1 is detected.
23
+ # Useful in CI test suites where silent warnings are easy to miss.
24
+ # config.raise_on_n_plus_one = false
25
+
26
+ # Maximum number of events retained in the in-memory ring buffer.
27
+ # config.event_store_size = 100
28
+
29
+ # Enable the HTML dashboard at GET /slow_queries (when the engine is mounted).
30
+ # Defaults to true in development, false elsewhere.
31
+ # config.dashboard_enabled = Rails.env.development?
32
+
33
+ # Append each detected event as a JSON line to this file path.
34
+ # Disabled by default (nil). Useful for persistence across restarts.
35
+ # config.log_file = Rails.root.join("log/query_owl.log").to_s
36
+
37
+ # Notifiers receive each detected event via #call(event).
38
+ # Defaults to [QueryOwl::Notifiers::Logger] which writes to Rails.logger.
39
+ # Use Console for TTY-aware colorized output (yellow: N+1, red: slow query).
40
+ # Use Stdout for non-request contexts (jobs, Rake tasks).
41
+ # config.notifiers = [QueryOwl::Notifiers::Console.new]
42
+ end
@@ -5,7 +5,20 @@ 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
8
+ :raise_on_n_plus_one, :event_store_size, :dashboard_enabled, :log_file
9
+
10
+ def notifiers
11
+ @notifiers ||= [Notifiers::Logger.new]
12
+ end
13
+
14
+ def notifiers=(arr)
15
+ arr.each do |notifier|
16
+ unless notifier.respond_to?(:call)
17
+ raise ArgumentError, "notifiers must respond to #call (#{notifier.class} does not)"
18
+ end
19
+ end
20
+ @notifiers = arr
21
+ end
9
22
 
10
23
  def initialize
11
24
  @enabled = Rails.env.development?
@@ -17,6 +30,7 @@ module QueryOwl
17
30
  @raise_on_n_plus_one = false
18
31
  @event_store_size = 100
19
32
  @dashboard_enabled = Rails.env.development?
33
+ @log_file = nil
20
34
  end
21
35
 
22
36
  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
@@ -0,0 +1,28 @@
1
+ require "json"
2
+ require "fileutils"
3
+
4
+ module QueryOwl
5
+ class FileLogger
6
+ class << self
7
+ def append(events)
8
+ return if events.empty?
9
+
10
+ path = QueryOwl.config.log_file
11
+ return unless path
12
+
13
+ FileUtils.mkdir_p(File.dirname(path))
14
+ File.open(path, "a") do |f|
15
+ events.each { |e| f.puts(JSON.generate(serializable(e))) }
16
+ end
17
+ rescue => e
18
+ Rails.logger.error "[QueryOwl] FileLogger failed: #{e.message}"
19
+ end
20
+
21
+ private
22
+
23
+ def serializable(event)
24
+ event.transform_values { |v| v.is_a?(Symbol) ? v.to_s : v }
25
+ end
26
+ end
27
+ end
28
+ end
@@ -18,14 +18,26 @@ module QueryOwl
18
18
  EagerLoadTracker.start!
19
19
  @app.call(env)
20
20
  ensure
21
- queries = QueryTracker.stop!
22
- eager_data = EagerLoadTracker.stop!
23
- events = Detector.detect_n_plus_one(queries) +
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) +
24
28
  Detector.detect_slow_queries(queries) +
25
- Detector.detect_unused_eager_loads(eager_data)
26
- Logger.log_events(events)
29
+ Detector.detect_unused_eager_loads(eager_data))
30
+ .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}"
36
+ end
37
+ end
27
38
  Logger.log_summary(events)
28
39
  events.each { |e| EventStore.push(e) }
40
+ FileLogger.append(events)
29
41
  raise_on_n_plus_one!(events) if QueryOwl.config.raise_on_n_plus_one
30
42
  end
31
43
  end
@@ -0,0 +1,38 @@
1
+ module QueryOwl
2
+ module Notifiers
3
+ class Console
4
+ YELLOW = "\e[33m"
5
+ RED = "\e[31m"
6
+ RESET = "\e[0m"
7
+
8
+ def call(event)
9
+ line = format(event)
10
+ line = apply_color(event[:type], line) if $stdout.tty?
11
+ $stdout.puts line
12
+ end
13
+
14
+ private
15
+
16
+ def format(event)
17
+ case event[:type]
18
+ when :n_plus_one
19
+ "#{QueryOwl::Logger::PREFIX} n_plus_one #{event[:sql]} ×#{event[:count]}"
20
+ when :slow_query
21
+ "#{QueryOwl::Logger::PREFIX} slow_query #{event[:sql]} #{event[:duration_ms]}ms"
22
+ when :unused_eager_load
23
+ "#{QueryOwl::Logger::PREFIX} unused_eager_load #{event[:model]}##{event[:association]}"
24
+ else
25
+ "#{QueryOwl::Logger::PREFIX} #{event[:type]}"
26
+ end
27
+ end
28
+
29
+ def apply_color(type, text)
30
+ case type
31
+ when :n_plus_one then "#{YELLOW}#{text}#{RESET}"
32
+ when :slow_query then "#{RED}#{text}#{RESET}"
33
+ else text
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,9 @@
1
+ module QueryOwl
2
+ module Notifiers
3
+ class Logger
4
+ def call(event)
5
+ ::Rails.logger.public_send(QueryOwl.config.log_level, "#{QueryOwl::Logger::PREFIX} #{event.to_json}")
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module QueryOwl
2
+ module Notifiers
3
+ class Stdout
4
+ def call(event)
5
+ $stdout.puts "#{QueryOwl::Logger::PREFIX} #{event.to_json}"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ module QueryOwl
2
+ module RequestContext
3
+ class << self
4
+ def set(controller:, action:, path:)
5
+ Thread.current[:query_owl_request_context] = { controller: controller, action: action, path: path }
6
+ end
7
+
8
+ def current
9
+ Thread.current[:query_owl_request_context] || {}
10
+ end
11
+
12
+ def clear
13
+ Thread.current[:query_owl_request_context] = nil
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,3 +1,3 @@
1
1
  module QueryOwl
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.1"
3
3
  end
data/lib/query_owl.rb CHANGED
@@ -5,6 +5,11 @@ require "query_owl/eager_load_tracker"
5
5
  require "query_owl/event_store"
6
6
  require "query_owl/detector"
7
7
  require "query_owl/logger"
8
+ require "query_owl/file_logger"
9
+ require "query_owl/notifiers/logger"
10
+ require "query_owl/notifiers/stdout"
11
+ require "query_owl/notifiers/console"
12
+ require "query_owl/request_context"
8
13
  require "query_owl/middleware"
9
14
  require "query_owl/engine"
10
15
 
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.3.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -47,15 +47,22 @@ files:
47
47
  - app/views/layouts/query_owl/application.html.erb
48
48
  - app/views/query_owl/slow_queries/index.html.erb
49
49
  - config/routes.rb
50
+ - lib/generators/query_owl/install/install_generator.rb
51
+ - lib/generators/query_owl/install/templates/initializer.rb
50
52
  - lib/query_owl.rb
51
53
  - lib/query_owl/configuration.rb
52
54
  - lib/query_owl/detector.rb
53
55
  - lib/query_owl/eager_load_tracker.rb
54
56
  - lib/query_owl/engine.rb
55
57
  - lib/query_owl/event_store.rb
58
+ - lib/query_owl/file_logger.rb
56
59
  - lib/query_owl/logger.rb
57
60
  - lib/query_owl/middleware.rb
61
+ - lib/query_owl/notifiers/console.rb
62
+ - lib/query_owl/notifiers/logger.rb
63
+ - lib/query_owl/notifiers/stdout.rb
58
64
  - lib/query_owl/query_tracker.rb
65
+ - lib/query_owl/request_context.rb
59
66
  - lib/query_owl/version.rb
60
67
  - lib/tasks/query_owl_tasks.rake
61
68
  homepage: https://github.com/eclectic-coding/query_owl