query_owl 0.1.0 → 0.3.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: 45b067e5caa42b01df6cb84cce7541b77ee9bbee1f9f6d919ed554fb2386fd0e
4
- data.tar.gz: 688d0b4f41edcf13e99bad7dbe6a4bdfe8ad40d458f908a06c2bc87b8ccbdbea
3
+ metadata.gz: 5db617fa85bce1a43d851809139bd43015afd92263bd203ac0114921dd94c525
4
+ data.tar.gz: 4ebf287c62f2779448c2277d416147d7b426332a2dd927f6a1a8485498c76638
5
5
  SHA512:
6
- metadata.gz: 960d4332a40bd8531ccdda7b06d8e6a947b048c3c5f758fba374e62759668bbe43ae335e7d5dc4fc394785e2ea5fa6e13e1f7f8ca9c4e6f035f52b380815c982
7
- data.tar.gz: 1764fbd57f9359a30866c2665a5698ede5bd4764fab90f10afa7984451086cc1d51fd8bf933828bfdff14a87631c753824b8cff7cd225a1474ca41b9efd5a0f3
6
+ metadata.gz: e3ec2642c611837a83dc96fdf8fc7eb669a99c6ae140fc3ad5bb1439a5b2db4349a9038a87a4af8e61fe1156e2d6fcbe43535641a2c51dd3bbcc41f9652ec88c
7
+ data.tar.gz: 2b2fe57db0a34c3289ef76a6170c4ea0d36d441eb733571ce87d8b506e1772b54d1e63841a8719d009a7ffd52a708b8ce5ad19c36e22839ceb3d33111700ae8b
data/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.3-ruby)](https://www.ruby-lang.org)
7
7
  [![codecov](https://codecov.io/gh/eclectic-coding/query_owl/branch/main/graph/badge.svg)](https://codecov.io/gh/eclectic-coding/query_owl)
8
8
 
9
- A leaner alternative to Bullet. QueryOwl detects N+1 queries and slow queries in development, logging structured warnings to your Rails logger — without the noise.
9
+ A leaner alternative to Bullet. QueryOwl detects N+1 queries, slow queries, and unused eager loads in development, logging structured warnings to your Rails logger — without the noise.
10
10
 
11
11
  ## Table of Contents
12
12
 
@@ -14,6 +14,7 @@ A leaner alternative to Bullet. QueryOwl detects N+1 queries and slow queries in
14
14
  - [Installation](#installation)
15
15
  - [Configuration](#configuration)
16
16
  - [Log Output](#log-output)
17
+ - [Dashboard Endpoint](#dashboard-endpoint)
17
18
  - [Manual Testing in the Dummy App](#manual-testing-in-the-dummy-app)
18
19
  - [Roadmap](#roadmap)
19
20
  - [Contributing](#contributing)
@@ -25,6 +26,9 @@ A leaner alternative to Bullet. QueryOwl detects N+1 queries and slow queries in
25
26
 
26
27
  - **N+1 detection** — flags when the same SQL pattern fires 2+ times in a single request
27
28
  - **Slow query detection** — flags queries exceeding a configurable threshold (default: 100ms)
29
+ - **Unused eager load detection** — flags associations preloaded via `includes`/`eager_load` that are never accessed during the request
30
+ - **Per-request summary** — single summary line at the end of each request with totals (e.g. `Request complete — 3 N+1s, 1 slow query`)
31
+ - **CI-friendly raise mode** — set `raise_on_n_plus_one: true` to raise `QueryOwl::NPlusOneError` instead of logging, making N+1s fail fast in test suites
28
32
  - **Structured log output** — JSON-style warnings via `Rails.logger` with SQL, duration, count, and filtered backtrace
29
33
  - **Zero overhead in production** — auto-enabled in development only
30
34
 
@@ -61,6 +65,11 @@ QueryOwl.configure do |config|
61
65
  config.slow_query_threshold_ms = 100 # flag queries slower than this
62
66
  config.n_plus_one_threshold = 2 # flag after this many repeated patterns
63
67
  config.log_level = :warn # :warn | :info | :debug
68
+ config.backtrace_lines = 5 # number of backtrace frames to capture
69
+ config.backtrace_filter = ->(line) { line.start_with?("app/") } # optional custom filter
70
+ config.raise_on_n_plus_one = false # set true in CI to raise instead of log
71
+ config.event_store_size = 100 # ring buffer capacity
72
+ config.dashboard_enabled = Rails.env.development? # HTML view on/off
64
73
  end
65
74
  ```
66
75
 
@@ -75,6 +84,47 @@ When a problem is detected, QueryOwl writes a structured line to `Rails.logger`:
75
84
  ```
76
85
  [QueryOwl] {"type":"n_plus_one","sql":"SELECT * FROM posts WHERE user_id = ?","count":10,"backtrace":["app/controllers/posts_controller.rb:12"]}
77
86
  [QueryOwl] {"type":"slow_query","sql":"SELECT * FROM reports WHERE ...","duration_ms":340}
87
+ [QueryOwl] {"type":"unused_eager_load","model":"Widget","association":"tags"}
88
+ [QueryOwl] Request complete — 10 N+1s, 1 slow query, 1 unused eager load
89
+ ```
90
+
91
+ [↑ Back to top](#table-of-contents)
92
+
93
+ ---
94
+
95
+ ## Dashboard Endpoint
96
+
97
+ Mount the engine in your host app's routes to enable the JSON endpoint:
98
+
99
+ ```ruby
100
+ # config/routes.rb
101
+ mount QueryOwl::Engine => "/rails"
102
+ ```
103
+
104
+ Then browse the HTML dashboard or query JSON at `GET /rails/slow_queries`:
105
+
106
+ ```
107
+ GET /rails/slow_queries # HTML dashboard (browser)
108
+ GET /rails/slow_queries.json # JSON array
109
+ GET /rails/slow_queries?type=n_plus_one
110
+ GET /rails/slow_queries?type=slow_query
111
+ GET /rails/slow_queries?type=unused_eager_load
112
+ ```
113
+
114
+ The HTML view is enabled when `config.dashboard_enabled` is `true` (default in development); returns `403` otherwise. The JSON endpoint is always available.
115
+
116
+ The JSON response is an array of event objects, newest first, up to `config.event_store_size` entries:
117
+
118
+ ```json
119
+ [
120
+ {
121
+ "type": "n_plus_one",
122
+ "sql": "SELECT * FROM posts WHERE user_id = ?",
123
+ "count": 5,
124
+ "backtrace": ["app/controllers/posts_controller.rb:12"],
125
+ "recorded_at": "2026-06-15T18:00:00.000Z"
126
+ }
127
+ ]
78
128
  ```
79
129
 
80
130
  [↑ Back to top](#table-of-contents)
@@ -116,15 +166,30 @@ QueryOwl::Logger.log_events(events)
116
166
  # => [QueryOwl] {"type":"slow_query","sql":"SELECT ...","duration_ms":...}
117
167
  ```
118
168
 
169
+ **Trigger unused eager load detection:**
170
+
171
+ ```ruby
172
+ QueryOwl.config.enabled = true
173
+ QueryOwl::EagerLoadTracker.start!
174
+ Widget.includes(:tags).map(&:name) # loads tags but never touches them
175
+ eager_data = QueryOwl::EagerLoadTracker.stop!
176
+ events = QueryOwl::Detector.detect_unused_eager_loads(eager_data)
177
+ QueryOwl::Logger.log_events(events)
178
+ # => [QueryOwl] {"type":"unused_eager_load","model":"Widget","association":"tags"}
179
+ ```
180
+
119
181
  **Full pipeline** (as it runs on every real HTTP request):
120
182
 
121
183
  ```ruby
122
184
  QueryOwl.config.slow_query_threshold_ms = 0
123
185
  QueryOwl::QueryTracker.start!
186
+ QueryOwl::EagerLoadTracker.start!
124
187
  Widget.all.each { |w| Widget.find(w.id) }
125
- queries = QueryOwl::QueryTracker.stop!
126
- events = QueryOwl::Detector.detect_n_plus_one(queries) +
127
- QueryOwl::Detector.detect_slow_queries(queries)
188
+ queries = QueryOwl::QueryTracker.stop!
189
+ eager_data = QueryOwl::EagerLoadTracker.stop!
190
+ events = QueryOwl::Detector.detect_n_plus_one(queries) +
191
+ QueryOwl::Detector.detect_slow_queries(queries) +
192
+ QueryOwl::Detector.detect_unused_eager_loads(eager_data)
128
193
  QueryOwl::Logger.log_events(events)
129
194
  ```
130
195
 
@@ -0,0 +1,23 @@
1
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
2
+
3
+ :root {
4
+ --bg: #f8f9fa;
5
+ --surface: #ffffff;
6
+ --border: #dee2e6;
7
+ --text: #212529;
8
+ --muted: #6c757d;
9
+ --radius: 6px;
10
+ --shadow: 0 1px 3px rgba(0,0,0,.08);
11
+ }
12
+
13
+ body {
14
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
15
+ font-size: 14px;
16
+ line-height: 1.5;
17
+ color: var(--text);
18
+ background: var(--bg);
19
+ padding: 2rem;
20
+ }
21
+
22
+ .qo-monospace { font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; font-size: 12px; }
23
+ .qo-muted { color: var(--muted); font-size: 12px; }
@@ -0,0 +1,5 @@
1
+ .qo-header { margin-bottom: 1.5rem; }
2
+ .qo-header h1 { font-size: 1.4rem; font-weight: 600; }
3
+ .qo-header p { color: var(--muted); margin-top: 0.2rem; }
4
+
5
+ .qo-empty { color: var(--muted); font-style: italic; margin-top: 1rem; }
@@ -0,0 +1,29 @@
1
+ .qo-table {
2
+ width: 100%;
3
+ border-collapse: collapse;
4
+ background: var(--surface);
5
+ border-radius: var(--radius);
6
+ overflow: hidden;
7
+ box-shadow: var(--shadow);
8
+ }
9
+
10
+ .qo-table th {
11
+ text-align: left;
12
+ padding: 0.6rem 1rem;
13
+ background: #f0f0f0;
14
+ font-weight: 600;
15
+ font-size: 11px;
16
+ text-transform: uppercase;
17
+ letter-spacing: .04em;
18
+ color: var(--muted);
19
+ border-bottom: 1px solid var(--border);
20
+ }
21
+
22
+ .qo-table td {
23
+ padding: 0.6rem 1rem;
24
+ border-bottom: 1px solid #eee;
25
+ vertical-align: top;
26
+ }
27
+
28
+ .qo-table tr:last-child td { border-bottom: none; }
29
+ .qo-table tr:hover td { background: #fafafa; }
@@ -0,0 +1,13 @@
1
+ .qo-badge {
2
+ display: inline-block;
3
+ padding: 2px 8px;
4
+ border-radius: 3px;
5
+ font-size: 11px;
6
+ font-weight: 600;
7
+ text-transform: uppercase;
8
+ white-space: nowrap;
9
+ }
10
+
11
+ .qo-badge--n_plus_one { background: #fde8e8; color: #c0392b; }
12
+ .qo-badge--slow_query { background: #fef3e2; color: #e67e22; }
13
+ .qo-badge--unused_eager_load { background: #e8f4fd; color: #2980b9; }
@@ -0,0 +1,28 @@
1
+ module QueryOwl
2
+ class SlowQueriesController < ActionController::Base
3
+ protect_from_forgery with: :null_session
4
+ layout "query_owl/application"
5
+ helper QueryOwl::ApplicationHelper
6
+
7
+ before_action :check_dashboard_enabled, if: -> { request.format.html? }
8
+
9
+ def index
10
+ filters = request.query_parameters
11
+ events = EventStore.all
12
+ events = events.select { |e| e[:type].to_s == filters["type"] } if filters["type"].present?
13
+ events = events.select { |e| e[:controller] == filters["controller"] } if filters["controller"].present?
14
+ events = events.select { |e| e[:action] == filters["action"] } if filters["action"].present?
15
+
16
+ respond_to do |format|
17
+ format.json { render json: events }
18
+ format.html { @events = events.reverse }
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def check_dashboard_enabled
25
+ head :forbidden unless QueryOwl.config.dashboard_enabled
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ module QueryOwl
2
+ module ApplicationHelper
3
+ def inline_styles
4
+ dir = QueryOwl::Engine.root.join("app/assets/stylesheets/query_owl")
5
+ css = dir.glob("_*.css").sort.map(&:read).join("\n")
6
+ content_tag(:style, css.html_safe)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>QueryOwl</title>
7
+ <link rel="icon" href="data:,">
8
+ <%= inline_styles %>
9
+ </head>
10
+ <body>
11
+ <%= yield %>
12
+ </body>
13
+ </html>
@@ -0,0 +1,34 @@
1
+ <div class="qo-header">
2
+ <h1>QueryOwl</h1>
3
+ <p>Last <%= @events.length %> detected event<%= "s" unless @events.length == 1 %> (newest first)</p>
4
+ </div>
5
+
6
+ <% if @events.empty? %>
7
+ <p class="qo-empty">No events detected yet.</p>
8
+ <% else %>
9
+ <table class="qo-table">
10
+ <thead>
11
+ <tr>
12
+ <th>Type</th>
13
+ <th>SQL / Details</th>
14
+ <th>Info</th>
15
+ <th>Recorded At</th>
16
+ <th>Backtrace</th>
17
+ </tr>
18
+ </thead>
19
+ <tbody>
20
+ <% @events.each do |event| %>
21
+ <tr>
22
+ <td><span class="qo-badge qo-badge--<%= event[:type] %>"><%= event[:type].to_s.tr("_", " ") %></span></td>
23
+ <td class="qo-monospace"><%= event[:sql] || "#{event[:model]}##{event[:association]}" %></td>
24
+ <td class="qo-muted">
25
+ <% if event[:count] %>count: <%= event[:count] %><% end %>
26
+ <% if event[:duration_ms] %><%= event[:duration_ms] %>ms<% end %>
27
+ </td>
28
+ <td class="qo-muted"><%= event[:recorded_at]&.strftime("%H:%M:%S") %></td>
29
+ <td class="qo-monospace qo-muted"><%= Array(event[:backtrace]).first %></td>
30
+ </tr>
31
+ <% end %>
32
+ </tbody>
33
+ </table>
34
+ <% end %>
data/config/routes.rb CHANGED
@@ -1,2 +1,3 @@
1
1
  QueryOwl::Engine.routes.draw do
2
+ get "slow_queries", to: "slow_queries#index"
2
3
  end
@@ -1,15 +1,22 @@
1
1
  module QueryOwl
2
2
  class Configuration
3
3
  VALID_LOG_LEVELS = %i[debug info warn].freeze
4
+ DEFAULT_BACKTRACE_FILTER = ->(line) { line !~ %r{/gems/|/rubygems/|/ruby/gems/|lib/query_owl/} }
4
5
 
5
- attr_reader :log_level
6
- attr_accessor :enabled, :slow_query_threshold_ms, :n_plus_one_threshold
6
+ attr_reader :log_level, :backtrace_filter
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
7
9
 
8
10
  def initialize
9
- @enabled = Rails.env.development?
11
+ @enabled = Rails.env.development?
10
12
  @slow_query_threshold_ms = 100
11
- @n_plus_one_threshold = 2
12
- @log_level = :warn
13
+ @n_plus_one_threshold = 2
14
+ @log_level = :warn
15
+ @backtrace_lines = 5
16
+ @backtrace_filter = DEFAULT_BACKTRACE_FILTER
17
+ @raise_on_n_plus_one = false
18
+ @event_store_size = 100
19
+ @dashboard_enabled = Rails.env.development?
13
20
  end
14
21
 
15
22
  def log_level=(level)
@@ -19,5 +26,11 @@ module QueryOwl
19
26
 
20
27
  @log_level = level
21
28
  end
29
+
30
+ def backtrace_filter=(filter)
31
+ raise ArgumentError, "backtrace_filter must respond to #call" unless filter.respond_to?(:call)
32
+
33
+ @backtrace_filter = filter
34
+ end
22
35
  end
23
36
  end
@@ -1,9 +1,13 @@
1
1
  module QueryOwl
2
2
  module Detector
3
- # Matches numeric literals, single-quoted strings, and IN-list contents.
4
3
  NORMALIZE_PATTERNS = [
5
4
  [/'[^']*'/, "?"],
6
- [/\b\d+\b/, "?"],
5
+ [/\b[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\b/i, "?"],
6
+ [/\$\d+/, "?"],
7
+ [/\b\d+\.?\d*\b/, "?"],
8
+ [/\bIN\s*\(\s*\?(?:\s*,\s*\?)*\s*\)/i, "IN (?)"],
9
+ [/"([^"]+)"/, '\1'],
10
+ [/`([^`]+)`/, '\1'],
7
11
  [/\s+/, " "]
8
12
  ].freeze
9
13
 
@@ -42,6 +46,16 @@ module QueryOwl
42
46
  end
43
47
  end
44
48
 
49
+ def detect_unused_eager_loads(eager_data)
50
+ preloaded = eager_data[:preloaded] || []
51
+ accessed = eager_data[:accessed] || Set.new
52
+
53
+ preloaded
54
+ .uniq { |e| "#{e[:model]}##{e[:association]}" }
55
+ .reject { |e| accessed.include?("#{e[:model]}##{e[:association]}") }
56
+ .map { |e| { type: :unused_eager_load, model: e[:model], association: e[:association] } }
57
+ end
58
+
45
59
  def normalize(sql)
46
60
  NORMALIZE_PATTERNS
47
61
  .reduce(sql.to_s) { |s, (pattern, replacement)| s.gsub(pattern, replacement) }
@@ -0,0 +1,43 @@
1
+ module QueryOwl
2
+ module EagerLoadTracker
3
+ class << self
4
+ def start!
5
+ Thread.current[:query_owl_preloaded] = []
6
+ Thread.current[:query_owl_el_accessed] = Set.new
7
+ end
8
+
9
+ def stop!
10
+ result = { preloaded: preloaded.dup, accessed: accessed.dup }
11
+ Thread.current[:query_owl_preloaded] = nil
12
+ Thread.current[:query_owl_el_accessed] = nil
13
+ result
14
+ end
15
+
16
+ def tracking?
17
+ !Thread.current[:query_owl_preloaded].nil?
18
+ end
19
+
20
+ def record_preload(model_name, association_name)
21
+ return unless tracking?
22
+
23
+ preloaded << { model: model_name.to_s, association: association_name.to_s }
24
+ end
25
+
26
+ def record_access(model_name, association_name)
27
+ return unless tracking?
28
+
29
+ accessed << "#{model_name}##{association_name}"
30
+ end
31
+
32
+ private
33
+
34
+ def preloaded
35
+ Thread.current[:query_owl_preloaded] ||= []
36
+ end
37
+
38
+ def accessed
39
+ Thread.current[:query_owl_el_accessed] ||= Set.new
40
+ end
41
+ end
42
+ end
43
+ end
@@ -15,5 +15,35 @@ module QueryOwl
15
15
  initializer "query_owl.request_tracking" do |app|
16
16
  app.middleware.use(Middleware)
17
17
  end
18
+
19
+ config.after_initialize do
20
+ ActiveRecord::Associations::Preloader.prepend(Module.new do
21
+ def initialize(records:, associations:, **kwargs)
22
+ if QueryOwl::EagerLoadTracker.tracking? && records.any?
23
+ model_name = records.first.class.name
24
+ Array(associations).each do |assoc|
25
+ QueryOwl::EagerLoadTracker.record_preload(model_name, assoc)
26
+ end
27
+ end
28
+ super
29
+ end
30
+
31
+ def call
32
+ Thread.current[:query_owl_preloading] = true
33
+ super
34
+ ensure
35
+ Thread.current[:query_owl_preloading] = false
36
+ end
37
+ end)
38
+
39
+ ActiveRecord::Base.prepend(Module.new do
40
+ def association(name)
41
+ unless Thread.current[:query_owl_preloading]
42
+ QueryOwl::EagerLoadTracker.record_access(self.class.name, name)
43
+ end
44
+ super
45
+ end
46
+ end)
47
+ end
18
48
  end
19
49
  end
@@ -0,0 +1,59 @@
1
+ module QueryOwl
2
+ module EventStore
3
+ class << self
4
+ def push(event)
5
+ mutex.synchronize do
6
+ ensure_buffer_size
7
+ buffer[@write_pos] = event.merge(recorded_at: Time.now)
8
+ @write_pos = (@write_pos + 1) % capacity
9
+ @stored = [@stored + 1, capacity].min
10
+ end
11
+ end
12
+
13
+ def all
14
+ mutex.synchronize do
15
+ stored = @stored || 0
16
+ return [] if stored.zero?
17
+
18
+ if stored < capacity
19
+ buffer.first(stored)
20
+ else
21
+ buffer[@write_pos..] + buffer[0...@write_pos]
22
+ end
23
+ end
24
+ end
25
+
26
+ def clear
27
+ mutex.synchronize { reset! }
28
+ end
29
+
30
+ def size
31
+ mutex.synchronize { @stored || 0 }
32
+ end
33
+
34
+ private
35
+
36
+ def mutex
37
+ @mutex ||= Mutex.new
38
+ end
39
+
40
+ def capacity
41
+ QueryOwl.config.event_store_size
42
+ end
43
+
44
+ def ensure_buffer_size
45
+ reset! if @write_pos.nil? || @buffer.nil? || @buffer.size != capacity
46
+ end
47
+
48
+ def buffer
49
+ @buffer ||= Array.new(capacity)
50
+ end
51
+
52
+ def reset!
53
+ @buffer = Array.new(capacity)
54
+ @write_pos = 0
55
+ @stored = 0
56
+ end
57
+ end
58
+ end
59
+ end
@@ -11,6 +11,18 @@ module QueryOwl
11
11
  events.each { |event| write(event) }
12
12
  end
13
13
 
14
+ def log_summary(events)
15
+ return if events.empty?
16
+
17
+ counts = events.group_by { |e| e[:type] }.transform_values(&:count)
18
+ parts = []
19
+ parts << "#{counts[:n_plus_one]} N+1#{"s" if counts[:n_plus_one] != 1}" if counts[:n_plus_one]
20
+ parts << "#{counts[:slow_query]} slow #{counts[:slow_query] == 1 ? "query" : "queries"}" if counts[:slow_query]
21
+ parts << "#{counts[:unused_eager_load]} unused eager load#{"s" if counts[:unused_eager_load] != 1}" if counts[:unused_eager_load]
22
+
23
+ Rails.logger.public_send(QueryOwl.config.log_level, "#{PREFIX} Request complete — #{parts.join(", ")}")
24
+ end
25
+
14
26
  private
15
27
 
16
28
  def write(event)
@@ -4,15 +4,29 @@ 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
+
7
14
  def call(env)
8
15
  return @app.call(env) unless QueryOwl.config.enabled
9
16
 
10
17
  QueryTracker.start!
18
+ EagerLoadTracker.start!
11
19
  @app.call(env)
12
20
  ensure
13
- queries = QueryTracker.stop!
14
- events = Detector.detect_n_plus_one(queries) + Detector.detect_slow_queries(queries)
21
+ queries = QueryTracker.stop!
22
+ eager_data = EagerLoadTracker.stop!
23
+ events = Detector.detect_n_plus_one(queries) +
24
+ Detector.detect_slow_queries(queries) +
25
+ Detector.detect_unused_eager_loads(eager_data)
15
26
  Logger.log_events(events)
27
+ Logger.log_summary(events)
28
+ events.each { |e| EventStore.push(e) }
29
+ raise_on_n_plus_one!(events) if QueryOwl.config.raise_on_n_plus_one
16
30
  end
17
31
  end
18
32
  end
@@ -37,7 +37,9 @@ module QueryOwl
37
37
  private
38
38
 
39
39
  def filtered_backtrace
40
- caller.grep_v(%r{/gems/|/rubygems/|/ruby/gems/|lib/query_owl/}).first(5)
40
+ filter = QueryOwl.config.backtrace_filter
41
+ lines = QueryOwl.config.backtrace_lines
42
+ caller.select { |line| filter.call(line) }.first(lines)
41
43
  end
42
44
  end
43
45
  end
@@ -1,3 +1,3 @@
1
1
  module QueryOwl
2
- VERSION = "0.1.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/query_owl.rb CHANGED
@@ -1,12 +1,16 @@
1
1
  require "query_owl/version"
2
2
  require "query_owl/configuration"
3
3
  require "query_owl/query_tracker"
4
+ require "query_owl/eager_load_tracker"
5
+ require "query_owl/event_store"
4
6
  require "query_owl/detector"
5
7
  require "query_owl/logger"
6
8
  require "query_owl/middleware"
7
9
  require "query_owl/engine"
8
10
 
9
11
  module QueryOwl
12
+ class NPlusOneError < StandardError; end
13
+
10
14
  class << self
11
15
  def configure
12
16
  yield config
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.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -34,15 +34,25 @@ files:
34
34
  - MIT-LICENSE
35
35
  - README.md
36
36
  - Rakefile
37
+ - app/assets/stylesheets/query_owl/_01_base.css
38
+ - app/assets/stylesheets/query_owl/_02_layout.css
39
+ - app/assets/stylesheets/query_owl/_03_table.css
40
+ - app/assets/stylesheets/query_owl/_04_badges.css
37
41
  - app/controllers/query_owl/application_controller.rb
42
+ - app/controllers/query_owl/slow_queries_controller.rb
43
+ - app/helpers/query_owl/application_helper.rb
38
44
  - app/jobs/query_owl/application_job.rb
39
45
  - app/mailers/query_owl/application_mailer.rb
40
46
  - app/models/query_owl/application_record.rb
47
+ - app/views/layouts/query_owl/application.html.erb
48
+ - app/views/query_owl/slow_queries/index.html.erb
41
49
  - config/routes.rb
42
50
  - lib/query_owl.rb
43
51
  - lib/query_owl/configuration.rb
44
52
  - lib/query_owl/detector.rb
53
+ - lib/query_owl/eager_load_tracker.rb
45
54
  - lib/query_owl/engine.rb
55
+ - lib/query_owl/event_store.rb
46
56
  - lib/query_owl/logger.rb
47
57
  - lib/query_owl/middleware.rb
48
58
  - lib/query_owl/query_tracker.rb