query_guard 0.4.2 โ 0.5.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 +4 -4
- data/CHANGELOG.md +89 -1
- data/DESIGN.md +420 -0
- data/INDEX.md +309 -0
- data/README.md +579 -30
- data/exe/queryguard +23 -0
- data/lib/query_guard/action_controller_subscriber.rb +27 -0
- data/lib/query_guard/analysis/query_risk_classifier.rb +124 -0
- data/lib/query_guard/analysis/risk_detectors.rb +258 -0
- data/lib/query_guard/analysis/risk_level.rb +35 -0
- data/lib/query_guard/analyzers/base.rb +30 -0
- data/lib/query_guard/analyzers/query_count_analyzer.rb +31 -0
- data/lib/query_guard/analyzers/query_risk_analyzer.rb +146 -0
- data/lib/query_guard/analyzers/registry.rb +57 -0
- data/lib/query_guard/analyzers/select_star_analyzer.rb +42 -0
- data/lib/query_guard/analyzers/slow_query_analyzer.rb +39 -0
- data/lib/query_guard/budget.rb +148 -0
- data/lib/query_guard/cli/batch_report_formatter.rb +129 -0
- data/lib/query_guard/cli/command.rb +93 -0
- data/lib/query_guard/cli/commands/analyze.rb +52 -0
- data/lib/query_guard/cli/commands/check.rb +58 -0
- data/lib/query_guard/cli/formatter.rb +278 -0
- data/lib/query_guard/cli/json_reporter.rb +247 -0
- data/lib/query_guard/cli/paged_report_formatter.rb +137 -0
- data/lib/query_guard/cli/source_metadata_collector.rb +297 -0
- data/lib/query_guard/cli.rb +197 -0
- data/lib/query_guard/client.rb +4 -6
- data/lib/query_guard/config.rb +145 -6
- data/lib/query_guard/core/context.rb +80 -0
- data/lib/query_guard/core/finding.rb +162 -0
- data/lib/query_guard/core/finding_builders.rb +152 -0
- data/lib/query_guard/core/query.rb +40 -0
- data/lib/query_guard/explain/adapter_interface.rb +89 -0
- data/lib/query_guard/explain/explain_enricher.rb +367 -0
- data/lib/query_guard/explain/plan_signals.rb +385 -0
- data/lib/query_guard/explain/postgresql_adapter.rb +208 -0
- data/lib/query_guard/exporter.rb +124 -0
- data/lib/query_guard/fingerprint.rb +96 -0
- data/lib/query_guard/middleware.rb +101 -15
- data/lib/query_guard/migrations/database_adapter.rb +88 -0
- data/lib/query_guard/migrations/migration_analyzer.rb +100 -0
- data/lib/query_guard/migrations/migration_risk_detectors.rb +390 -0
- data/lib/query_guard/migrations/postgresql_adapter.rb +157 -0
- data/lib/query_guard/migrations/table_risk_analyzer.rb +154 -0
- data/lib/query_guard/migrations/table_size_resolver.rb +152 -0
- data/lib/query_guard/publish.rb +38 -0
- data/lib/query_guard/rspec.rb +119 -0
- data/lib/query_guard/security.rb +99 -0
- data/lib/query_guard/store.rb +38 -0
- data/lib/query_guard/subscriber.rb +46 -15
- data/lib/query_guard/suggest/index_suggester.rb +176 -0
- data/lib/query_guard/suggest/pattern_extractors.rb +137 -0
- data/lib/query_guard/trace.rb +106 -0
- data/lib/query_guard/uploader/http_uploader.rb +166 -0
- data/lib/query_guard/uploader/interface.rb +79 -0
- data/lib/query_guard/uploader/no_op_uploader.rb +46 -0
- data/lib/query_guard/uploader/registry.rb +37 -0
- data/lib/query_guard/uploader/upload_service.rb +80 -0
- data/lib/query_guard/version.rb +1 -1
- data/lib/query_guard.rb +54 -7
- metadata +78 -10
- data/.rspec +0 -3
- data/Rakefile +0 -21
- data/config/initializers/query_guard.rb +0 -9
data/README.md
CHANGED
|
@@ -1,63 +1,612 @@
|
|
|
1
|
-
#
|
|
1
|
+
# QueryGuard: Migration Safety for Rails
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Catch risky database changes before they reach production.**
|
|
4
|
+
|
|
5
|
+
QueryGuard automatically analyzes your Rails migrations in CI to detect safety issues, preventing schema problems from entering your codebase. Built with deep query intelligence and security features for comprehensive database safety.
|
|
6
|
+
|
|
7
|
+
## ๐ฏ What Makes QueryGuard Different?
|
|
8
|
+
|
|
9
|
+
**v1.0 Focus: Migration Safety + Query Security**
|
|
10
|
+
|
|
11
|
+
QueryGuard runs in your CI pipeline to:
|
|
12
|
+
- **Analyze Migrations**: Detect risky patterns like non-nullable columns without defaults, missing rollbacks, performance issues
|
|
13
|
+
- **Detect Security Issues**: Monitor for SQL injection, unusual query patterns, data exfiltration, mass assignment attacks
|
|
14
|
+
- **Monitor Query Performance**: Track slow queries and unusual database activity
|
|
4
15
|
|
|
5
16
|
## Installation
|
|
6
17
|
|
|
7
|
-
Add to your Gemfile:
|
|
18
|
+
Add QueryGuard to your Gemfile:
|
|
8
19
|
|
|
9
20
|
```ruby
|
|
10
|
-
|
|
21
|
+
group :development, :test do
|
|
22
|
+
gem 'query_guard'
|
|
23
|
+
end
|
|
11
24
|
```
|
|
12
25
|
|
|
13
|
-
|
|
26
|
+
Then install and generate the initializer:
|
|
14
27
|
|
|
15
|
-
|
|
16
|
-
|
|
28
|
+
```bash
|
|
29
|
+
bundle install
|
|
30
|
+
bundle exec rails generate query_guard:install
|
|
31
|
+
```
|
|
17
32
|
|
|
18
|
-
|
|
33
|
+
This creates `config/initializers/query_guard.rb` with essential configuration.
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
The default configuration works for most Rails applications. Edit `config/initializers/query_guard.rb` to customize:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
QueryGuard.configure do |config|
|
|
41
|
+
# Directory where migrations live (default: db/migrate)
|
|
42
|
+
config.migrations_directory = 'db/migrate'
|
|
43
|
+
|
|
44
|
+
# Environments to analyze (default: [:development, :test])
|
|
45
|
+
config.enabled_environments = [:development, :test]
|
|
46
|
+
|
|
47
|
+
# Max queries to check per request (prevent timeouts)
|
|
48
|
+
config.max_queries_per_request = 100
|
|
49
|
+
|
|
50
|
+
# Max query duration in ms before flagging as slow
|
|
51
|
+
config.max_duration_ms_per_query = 5000
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Using QueryGuard
|
|
56
|
+
|
|
57
|
+
### In CI (Recommended)
|
|
58
|
+
|
|
59
|
+
Add to your CI workflow to check migrations automatically:
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
# .github/workflows/db-safety.yml
|
|
63
|
+
name: Database Safety
|
|
64
|
+
on: [pull_request]
|
|
19
65
|
|
|
20
|
-
|
|
66
|
+
jobs:
|
|
67
|
+
analyze:
|
|
68
|
+
runs-on: ubuntu-latest
|
|
69
|
+
services:
|
|
70
|
+
postgres:
|
|
71
|
+
image: postgres:14
|
|
72
|
+
options: >-
|
|
73
|
+
--health-cmd pg_isready
|
|
74
|
+
--health-interval 10s
|
|
75
|
+
--health-timeout 5s
|
|
76
|
+
--health-retries 5
|
|
77
|
+
env:
|
|
78
|
+
POSTGRES_PASSWORD: postgres
|
|
21
79
|
|
|
22
|
-
|
|
80
|
+
steps:
|
|
81
|
+
- uses: actions/checkout@v3
|
|
82
|
+
- uses: ruby/setup-ruby@v1
|
|
83
|
+
with:
|
|
84
|
+
ruby-version: 3.3.0
|
|
85
|
+
bundler-cache: true
|
|
86
|
+
|
|
87
|
+
- name: Analyze migrations
|
|
88
|
+
run: bundle exec queryguard analyze
|
|
89
|
+
env:
|
|
90
|
+
DATABASE_URL: postgres://postgres:postgres@localhost/test_db
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Locally
|
|
23
94
|
|
|
24
95
|
```bash
|
|
25
|
-
|
|
96
|
+
# Analyze migrations in your current environment
|
|
97
|
+
bundle exec queryguard analyze
|
|
98
|
+
|
|
99
|
+
# Get JSON output for integration with CI dashboards
|
|
100
|
+
bundle exec queryguard analyze --format json
|
|
26
101
|
```
|
|
27
102
|
|
|
28
|
-
|
|
103
|
+
## JSON Output
|
|
104
|
+
|
|
105
|
+
QueryGuard can output analysis results as JSON for integration with CI dashboards, security tools, or custom workflows:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
bundle exec queryguard analyze --format json
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### JSON Schema
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"status": "success",
|
|
116
|
+
"findings": [
|
|
117
|
+
{
|
|
118
|
+
"type": "risky_migration",
|
|
119
|
+
"severity": "high",
|
|
120
|
+
"message": "Adding non-nullable column without default",
|
|
121
|
+
"file": "db/migrate/20240101120000_add_user_status.rb",
|
|
122
|
+
"line": 4,
|
|
123
|
+
"context": {
|
|
124
|
+
"migration_name": "AddUserStatus",
|
|
125
|
+
"environment": "test"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
],
|
|
129
|
+
"metadata": {
|
|
130
|
+
"environment": "test",
|
|
131
|
+
"database": "postgresql",
|
|
132
|
+
"schema_version": "20240101120000"
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## What's Coming Next
|
|
138
|
+
|
|
139
|
+
**v2.0 (Future)**
|
|
140
|
+
- Query analysis for N+1 problems and missing indexes
|
|
141
|
+
- Live application query monitoring
|
|
142
|
+
- Performance recommendations based on actual query patterns
|
|
143
|
+
- SaaS dashboard integration
|
|
144
|
+
|
|
145
|
+
**v1.0** focuses exclusively on migration safety because:
|
|
146
|
+
1. Migrations are deterministic and analyzable in CI
|
|
147
|
+
2. Migration problems are expensive to fix in production
|
|
148
|
+
3. This gives you immediate, measurable value
|
|
149
|
+
|
|
150
|
+
For query monitoring in your application, see QueryGuard's query analysis (coming v2.0) or integrate with tools like:
|
|
151
|
+
- [New Relic APM](https://newrelic.com)
|
|
152
|
+
- [DataDog APM](https://www.datadoghq.com)
|
|
153
|
+
- [Sentry Performance](https://sentry.io)
|
|
154
|
+
|
|
155
|
+
## Development
|
|
156
|
+
|
|
157
|
+
To set up the development environment:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
git clone https://github.com/yourusername/query_guard.git
|
|
161
|
+
cd query_guard
|
|
162
|
+
bundle install
|
|
163
|
+
bundle exec rake spec
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
All 186+ tests should pass. QueryGuard maintains 100% test coverage for core analysis logic.
|
|
167
|
+
|
|
168
|
+
## Contributing
|
|
169
|
+
|
|
170
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines.
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
MIT License - See [LICENSE.txt](LICENSE.txt) for details.
|
|
175
|
+
|
|
176
|
+
## Support
|
|
177
|
+
|
|
178
|
+
- **Issues**: [GitHub Issues](https://github.com/yourusername/query_guard/issues)
|
|
179
|
+
- **Discussions**: [GitHub Discussions](https://github.com/yourusername/query_guard/discussions)
|
|
180
|
+
- **Docs**: See [INDEX.md](INDEX.md) for complete documentation
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
### Why QueryGuard?
|
|
185
|
+
|
|
186
|
+
Database migrations are a critical part of your deployment pipeline. A single unsafe migration can:
|
|
187
|
+
- Lock production tables for hours
|
|
188
|
+
- Cause deployment failures requiring emergency rollbacks
|
|
189
|
+
- Add months of technical debt to your schema
|
|
190
|
+
|
|
191
|
+
QueryGuard catches these issues in CI, before they reach production.
|
|
192
|
+
|
|
193
|
+
**Stop deploying database surprises.** Start using QueryGuard.
|
|
194
|
+
=======
|
|
195
|
+
QueryGuard fills a unique niche between traditional APMs and Rails profilers:
|
|
196
|
+
|
|
197
|
+
- **vs Datadog APM**: Datadog excels at distributed tracing, logs/metrics correlation, and infrastructure monitoring, but is generic across languages. QueryGuard is **deeply Rails and ActiveRecord-aware**, providing schema intelligence and query budgets that Datadog doesn't offer.
|
|
198
|
+
|
|
199
|
+
- **vs Skylight**: Skylight is an excellent Rails profiler with request timelines, deploy tracking, and background job support. QueryGuard complements this by adding **query budgets/SLOs per endpoint**, **schema-aware linting** (missing indexes, wide scans), and **first-class test/CI support** via RSpec matchers.
|
|
200
|
+
|
|
201
|
+
- **vs Grafana**: Grafana provides powerful multi-signal visualization and metricsโtraces workflows via exemplars, but isn't Rails-specific. QueryGuard offers **Rails-native tooling** that works in development, tests, and production with minimal configuration.
|
|
202
|
+
|
|
203
|
+
**QueryGuard's Focus**: SQL + Schema + Budget enforcement + Developer UX (console, tests, CI).
|
|
204
|
+
|
|
205
|
+
## ๐ฆ Installation
|
|
206
|
+
|
|
207
|
+
Add to your Gemfile:
|
|
29
208
|
|
|
30
209
|
```ruby
|
|
31
|
-
|
|
210
|
+
gem "query_guard"
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Then run:
|
|
32
214
|
|
|
33
|
-
|
|
215
|
+
```bash
|
|
216
|
+
bundle install
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## โ๏ธ Configuration
|
|
220
|
+
|
|
221
|
+
Create an initializer at `config/initializers/query_guard.rb`:
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
34
224
|
QueryGuard.configure do |config|
|
|
35
225
|
# Environments where QueryGuard should be active
|
|
36
|
-
|
|
37
|
-
config.enabled_environments = %i[development test]
|
|
226
|
+
config.enabled_environments = %i[development test production]
|
|
38
227
|
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
|
|
228
|
+
# === Budget System (New in v2) ===
|
|
229
|
+
# Define query budgets/SLOs for specific endpoints or jobs
|
|
230
|
+
|
|
231
|
+
# Controller actions
|
|
232
|
+
config.budget.for("users#index", count: 10, duration_ms: 500)
|
|
233
|
+
config.budget.for("posts#show", count: 5, duration_ms: 200)
|
|
234
|
+
config.budget.for("admin/reports#dashboard", count: 50, duration_ms: 2000)
|
|
235
|
+
|
|
236
|
+
# Background jobs
|
|
237
|
+
config.budget.for_job("EmailJob", count: 20, duration_ms: 1000)
|
|
238
|
+
config.budget.for_job("DataExportJob", count: 100, duration_ms: 5000)
|
|
239
|
+
|
|
240
|
+
# Budget enforcement mode
|
|
241
|
+
config.budget.mode = :log # :log (warn only), :notify (callback), :raise (exception)
|
|
242
|
+
|
|
243
|
+
# Optional: callback for :notify mode
|
|
244
|
+
config.budget.on_violation = ->(key, violation) {
|
|
245
|
+
# Send to error tracker, metrics service, etc.
|
|
246
|
+
Honeybadger.notify("Budget violation", context: { key: key, violation: violation })
|
|
247
|
+
}
|
|
42
248
|
|
|
43
|
-
#
|
|
44
|
-
|
|
249
|
+
# === Legacy Limits (Still Supported) ===
|
|
250
|
+
config.max_queries_per_request = 100
|
|
45
251
|
config.max_duration_ms_per_query = 100.0
|
|
46
|
-
|
|
47
|
-
# Whether to flag or block SELECT * statements
|
|
48
252
|
config.block_select_star = true
|
|
49
|
-
|
|
50
|
-
# Ignore certain SQL patterns
|
|
253
|
+
|
|
254
|
+
# Ignore certain SQL patterns
|
|
51
255
|
config.ignored_sql = [
|
|
52
|
-
/^PRAGMA /i,
|
|
256
|
+
/^PRAGMA /i,
|
|
53
257
|
/^BEGIN/i,
|
|
54
|
-
/^COMMIT/i
|
|
258
|
+
/^COMMIT/i,
|
|
259
|
+
/^SHOW /i
|
|
55
260
|
]
|
|
56
261
|
|
|
57
|
-
#
|
|
58
|
-
config.
|
|
262
|
+
# === Security Features ===
|
|
263
|
+
config.enable_security = true
|
|
264
|
+
config.detect_sql_injection = true
|
|
265
|
+
config.detect_unusual_query_pattern = true
|
|
266
|
+
config.detect_data_exfiltration = true
|
|
267
|
+
config.detect_mass_assignment = true
|
|
59
268
|
|
|
60
|
-
#
|
|
269
|
+
# === Export Configuration ===
|
|
270
|
+
config.base_url = ENV["QUERY_GUARD_API_URL"]
|
|
271
|
+
config.api_key = ENV["QUERY_GUARD_API_KEY"]
|
|
272
|
+
config.project = "my_app"
|
|
273
|
+
config.env = Rails.env
|
|
274
|
+
|
|
275
|
+
# Logging
|
|
276
|
+
config.raise_on_violation = false # Set to true in CI
|
|
61
277
|
config.log_prefix = "[QueryGuard]"
|
|
62
278
|
end
|
|
63
279
|
```
|
|
280
|
+
|
|
281
|
+
## ๐ Features
|
|
282
|
+
|
|
283
|
+
### 1. Query Budgets & SLOs
|
|
284
|
+
|
|
285
|
+
Define query budgets for specific endpoints or background jobs:
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
# In config/initializers/query_guard.rb
|
|
289
|
+
QueryGuard.configure do |config|
|
|
290
|
+
# Set budgets for controller actions
|
|
291
|
+
config.budget.for("users#index", count: 10, duration_ms: 500)
|
|
292
|
+
config.budget.for("posts#show", count: 5, duration_ms: 200)
|
|
293
|
+
|
|
294
|
+
# Set budgets for background jobs
|
|
295
|
+
config.budget.for_job("EmailJob", count: 20)
|
|
296
|
+
config.budget.for_job("ReportJob", count: 100, duration_ms: 5000)
|
|
297
|
+
|
|
298
|
+
# Choose enforcement mode
|
|
299
|
+
config.budget.mode = :log # Options: :log, :notify, :raise
|
|
300
|
+
end
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**Enforcement Modes**:
|
|
304
|
+
|
|
305
|
+
- `:log` - Logs warnings when budgets are exceeded (default, safe for production)
|
|
306
|
+
- `:notify` - Calls a custom callback for integration with error trackers
|
|
307
|
+
- `:raise` - Raises an exception (useful in test/CI environments)
|
|
308
|
+
|
|
309
|
+
**With Callbacks**:
|
|
310
|
+
|
|
311
|
+
```ruby
|
|
312
|
+
config.budget.mode = :notify
|
|
313
|
+
config.budget.on_violation = ->(key, violation) {
|
|
314
|
+
# Send to your monitoring service
|
|
315
|
+
Datadog::Statsd.new.increment("query_guard.budget.exceeded", tags: ["endpoint:#{key}"])
|
|
316
|
+
|
|
317
|
+
# Or send to error tracker
|
|
318
|
+
Sentry.capture_message("Budget exceeded", extra: { key: key, violation: violation })
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### 2. Trace API (Console & Testing)
|
|
323
|
+
|
|
324
|
+
Manually trace query performance in any context:
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
# In Rails console or tests
|
|
328
|
+
result, report = QueryGuard.trace("load active users") do
|
|
329
|
+
User.where(active: true).limit(100).to_a
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
puts "Queries executed: #{report.query_count}"
|
|
333
|
+
puts "Total duration: #{report.total_duration_ms}ms"
|
|
334
|
+
puts "Violations: #{report.violations.inspect}"
|
|
335
|
+
puts "Queries:"
|
|
336
|
+
report.queries.each do |q|
|
|
337
|
+
puts " #{q[:duration_ms]}ms: #{q[:sql]}"
|
|
338
|
+
end
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
**With Context**:
|
|
342
|
+
|
|
343
|
+
```ruby
|
|
344
|
+
result, report = QueryGuard.trace("process batch", context: { batch_id: 123, user_id: 456 }) do
|
|
345
|
+
Batch.find(123).process!
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Context is included in the report for correlation
|
|
349
|
+
puts report.context # => { batch_id: 123, user_id: 456 }
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### 3. RSpec Matchers
|
|
353
|
+
|
|
354
|
+
Test query performance in your specs:
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
require "query_guard/rspec"
|
|
358
|
+
|
|
359
|
+
RSpec.describe UsersController, type: :controller do
|
|
360
|
+
describe "GET #index" do
|
|
361
|
+
it "stays within query budget" do
|
|
362
|
+
expect {
|
|
363
|
+
get :index
|
|
364
|
+
}.to_not exceed_query_budget(count: 10, duration_ms: 500)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Or use named budgets defined in config
|
|
368
|
+
it "respects users#index budget" do
|
|
369
|
+
expect {
|
|
370
|
+
get :index
|
|
371
|
+
}.to_not exceed_query_budget("users#index")
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
**Helper Method**:
|
|
378
|
+
|
|
379
|
+
```ruby
|
|
380
|
+
RSpec.describe "batch processing" do
|
|
381
|
+
it "processes batch efficiently" do
|
|
382
|
+
report = within_query_budget(count: 50, duration_ms: 2000) do
|
|
383
|
+
Batch.process_all
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
expect(report.query_count).to be < 50
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### 4. SQL Fingerprinting & Statistics
|
|
392
|
+
|
|
393
|
+
Track query patterns across your application:
|
|
394
|
+
|
|
395
|
+
```ruby
|
|
396
|
+
# In console or background job
|
|
397
|
+
QueryGuard::Fingerprint.record("SELECT * FROM users WHERE id = 123", 45.2)
|
|
398
|
+
QueryGuard::Fingerprint.record("SELECT * FROM users WHERE id = 456", 32.1)
|
|
399
|
+
|
|
400
|
+
# Get stats for a specific fingerprint
|
|
401
|
+
fp = QueryGuard::Fingerprint.generate("SELECT * FROM users WHERE id = ?")
|
|
402
|
+
stats = QueryGuard::Fingerprint.stats_for(fp)
|
|
403
|
+
|
|
404
|
+
puts stats[:count] # => 2
|
|
405
|
+
puts stats[:total_duration_ms] # => 77.3
|
|
406
|
+
puts stats[:min_duration_ms] # => 32.1
|
|
407
|
+
puts stats[:max_duration_ms] # => 45.2
|
|
408
|
+
puts stats[:first_seen_at]
|
|
409
|
+
puts stats[:last_seen_at]
|
|
410
|
+
|
|
411
|
+
# Get top queries by various metrics
|
|
412
|
+
QueryGuard::Fingerprint.top_by_count(10) # Most frequently executed
|
|
413
|
+
QueryGuard::Fingerprint.top_by_duration(10) # Highest total time
|
|
414
|
+
QueryGuard::Fingerprint.top_by_avg_duration(10) # Slowest on average
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**Fingerprinting normalizes SQL**:
|
|
418
|
+
- Removes string and numeric literals
|
|
419
|
+
- Collapses whitespace
|
|
420
|
+
- Normalizes `IN (...)` lists
|
|
421
|
+
- Returns consistent SHA1 hash
|
|
422
|
+
|
|
423
|
+
```ruby
|
|
424
|
+
# These all produce the same fingerprint:
|
|
425
|
+
QueryGuard::Fingerprint.generate("SELECT * FROM users WHERE id = 1")
|
|
426
|
+
QueryGuard::Fingerprint.generate("SELECT * FROM users WHERE id = 999")
|
|
427
|
+
QueryGuard::Fingerprint.generate("SELECT * FROM users WHERE id = 42")
|
|
428
|
+
# All normalize to: "select * from users where id = ?"
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### 5. Security Features (Existing)
|
|
432
|
+
|
|
433
|
+
QueryGuard includes built-in security detection:
|
|
434
|
+
|
|
435
|
+
- **SQL Injection Detection**: Flags suspicious patterns (OR 1=1, UNION SELECT, etc.)
|
|
436
|
+
- **Unusual Query Patterns**: Rate limiting per actor (IP/user)
|
|
437
|
+
- **Data Exfiltration**: Monitors large responses and suspicious endpoints
|
|
438
|
+
- **Mass Assignment**: Detects unpermitted parameters
|
|
439
|
+
|
|
440
|
+
```ruby
|
|
441
|
+
config.enable_security = true
|
|
442
|
+
config.detect_sql_injection = true
|
|
443
|
+
config.detect_unusual_query_pattern = true
|
|
444
|
+
config.max_queries_per_minute_per_actor = 300
|
|
445
|
+
|
|
446
|
+
# Custom actor resolver
|
|
447
|
+
config.actor_resolver = ->(env) {
|
|
448
|
+
env["warden"].user&.id || env["action_dispatch.remote_ip"]
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### 6. Export & Monitoring
|
|
453
|
+
|
|
454
|
+
Export query data to external services:
|
|
455
|
+
|
|
456
|
+
```ruby
|
|
457
|
+
config.base_url = "https://your-monitoring-service.com"
|
|
458
|
+
config.api_key = ENV["MONITORING_API_KEY"]
|
|
459
|
+
config.project = "my_rails_app"
|
|
460
|
+
config.env = Rails.env
|
|
461
|
+
config.export_mode = :async # Don't block requests
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
Exported data includes:
|
|
465
|
+
- Query statements with fingerprints
|
|
466
|
+
- Durations and timestamps
|
|
467
|
+
- Budget violations
|
|
468
|
+
- Security threat events
|
|
469
|
+
- Request context (controller, action, user, etc.)
|
|
470
|
+
|
|
471
|
+
## ๐ Use Cases
|
|
472
|
+
|
|
473
|
+
### Development
|
|
474
|
+
|
|
475
|
+
```ruby
|
|
476
|
+
# In Rails console
|
|
477
|
+
result, report = QueryGuard.trace("diagnose N+1") do
|
|
478
|
+
Post.limit(10).each { |post| post.comments.to_a }
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
puts "Queries: #{report.query_count}" # Spot N+1 problems immediately
|
|
482
|
+
report.queries.each { |q| puts q[:sql] }
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### Testing (CI)
|
|
486
|
+
|
|
487
|
+
```ruby
|
|
488
|
+
# spec/support/query_guard.rb
|
|
489
|
+
RSpec.configure do |config|
|
|
490
|
+
config.around(:each, :query_budget) do |example|
|
|
491
|
+
metadata = example.metadata
|
|
492
|
+
budget = metadata[:query_budget]
|
|
493
|
+
|
|
494
|
+
expect {
|
|
495
|
+
example.run
|
|
496
|
+
}.to_not exceed_query_budget(**budget)
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# spec/controllers/users_controller_spec.rb
|
|
501
|
+
RSpec.describe UsersController do
|
|
502
|
+
describe "GET #index", :query_budget, query_budget: { count: 5, duration_ms: 200 } do
|
|
503
|
+
it "loads users" do
|
|
504
|
+
get :index
|
|
505
|
+
expect(response).to be_successful
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### Production Monitoring
|
|
512
|
+
|
|
513
|
+
```ruby
|
|
514
|
+
# config/initializers/query_guard.rb
|
|
515
|
+
QueryGuard.configure do |config|
|
|
516
|
+
config.enabled_environments = [:production]
|
|
517
|
+
config.budget.mode = :notify
|
|
518
|
+
|
|
519
|
+
config.budget.for("api/v1/users#index", count: 10, duration_ms: 100)
|
|
520
|
+
config.budget.for("api/v1/posts#feed", count: 15, duration_ms: 150)
|
|
521
|
+
|
|
522
|
+
config.budget.on_violation = ->(key, violation) {
|
|
523
|
+
# Alert when budgets exceeded in production
|
|
524
|
+
Datadog::Statsd.new.increment("query.budget.exceeded", tags: [
|
|
525
|
+
"endpoint:#{key}",
|
|
526
|
+
"type:#{violation[:type]}"
|
|
527
|
+
])
|
|
528
|
+
}
|
|
529
|
+
end
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
## ๐ง API Reference
|
|
533
|
+
|
|
534
|
+
### QueryGuard.configure
|
|
535
|
+
|
|
536
|
+
Configure QueryGuard settings. See Configuration section above.
|
|
537
|
+
|
|
538
|
+
### QueryGuard.trace(label, context: {}, &block)
|
|
539
|
+
|
|
540
|
+
Trace a block of code and capture query statistics.
|
|
541
|
+
|
|
542
|
+
**Arguments**:
|
|
543
|
+
- `label` (String): Descriptive label for the trace
|
|
544
|
+
- `context` (Hash): Additional context (user_id, batch_id, etc.)
|
|
545
|
+
- `&block`: Code to trace
|
|
546
|
+
|
|
547
|
+
**Returns**: `[result, report]` tuple
|
|
548
|
+
|
|
549
|
+
### QueryGuard::Budget
|
|
550
|
+
|
|
551
|
+
**Methods**:
|
|
552
|
+
- `.for(key, **limits)`: Define budget for controller action
|
|
553
|
+
- `.for_job(job, **limits)`: Define budget for background job
|
|
554
|
+
- `.mode=`: Set enforcement mode (`:log`, `:notify`, `:raise`)
|
|
555
|
+
- `.on_violation=`: Set callback for `:notify` mode
|
|
556
|
+
|
|
557
|
+
### QueryGuard::Fingerprint
|
|
558
|
+
|
|
559
|
+
**Methods**:
|
|
560
|
+
- `.generate(sql)`: Generate fingerprint for SQL
|
|
561
|
+
- `.normalize(sql)`: Normalize SQL query
|
|
562
|
+
- `.record(sql, duration_ms)`: Record query execution
|
|
563
|
+
- `.stats_for(fingerprint)`: Get stats for fingerprint
|
|
564
|
+
- `.top_by_count(limit)`: Top queries by count
|
|
565
|
+
- `.top_by_duration(limit)`: Top queries by total duration
|
|
566
|
+
- `.top_by_avg_duration(limit)`: Top queries by average duration
|
|
567
|
+
- `.reset!`: Clear all stats
|
|
568
|
+
|
|
569
|
+
### RSpec Matchers
|
|
570
|
+
|
|
571
|
+
```ruby
|
|
572
|
+
require "query_guard/rspec"
|
|
573
|
+
|
|
574
|
+
expect { code }.to_not exceed_query_budget(count: 10)
|
|
575
|
+
expect { code }.to_not exceed_query_budget(count: 10, duration_ms: 500)
|
|
576
|
+
expect { code }.to_not exceed_query_budget("users#index")
|
|
577
|
+
|
|
578
|
+
report = within_query_budget(count: 10) { code }
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
## ๐งช Testing
|
|
582
|
+
|
|
583
|
+
Run the test suite:
|
|
584
|
+
|
|
585
|
+
```bash
|
|
586
|
+
bundle exec rspec
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
## ๐ License
|
|
590
|
+
|
|
591
|
+
MIT License. See [LICENSE.txt](LICENSE.txt) for details.
|
|
592
|
+
|
|
593
|
+
## ๐ Contributing
|
|
594
|
+
|
|
595
|
+
Contributions welcome! Please:
|
|
596
|
+
|
|
597
|
+
1. Fork the repository
|
|
598
|
+
2. Create a feature branch
|
|
599
|
+
3. Write tests for your changes
|
|
600
|
+
4. Submit a pull request
|
|
601
|
+
|
|
602
|
+
## ๐ฎ Support
|
|
603
|
+
|
|
604
|
+
- Issues: [GitHub Issues](https://github.com/Chitradevi36/query_guard/issues)
|
|
605
|
+
- Documentation: This README
|
|
606
|
+
- Example Rails App: [Coming soon]
|
|
607
|
+
|
|
608
|
+
---
|
|
609
|
+
|
|
610
|
+
Built with โค๏ธ to make Rails query performance monitoring delightful.
|
|
611
|
+
>>>>>>> 2fd3b3b87eaac58f779bc220c6bff2389c1b1c7b
|
|
612
|
+
|
data/exe/queryguard
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Handle --version early (before loading Rails)
|
|
5
|
+
if ARGV.first == '--version' || ARGV.first == '-v'
|
|
6
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
|
7
|
+
require 'query_guard/version'
|
|
8
|
+
puts "QueryGuard v#{QueryGuard::VERSION}"
|
|
9
|
+
exit 0
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Now load everything else
|
|
13
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
|
14
|
+
require 'query_guard'
|
|
15
|
+
require 'query_guard/cli'
|
|
16
|
+
|
|
17
|
+
begin
|
|
18
|
+
QueryGuard::CLI.run(ARGV)
|
|
19
|
+
rescue => e
|
|
20
|
+
puts "Error: #{e.message}"
|
|
21
|
+
puts e.backtrace.join("\n") if ENV['DEBUG']
|
|
22
|
+
exit 1
|
|
23
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "active_support/notifications"
|
|
3
|
+
|
|
4
|
+
module QueryGuard
|
|
5
|
+
module ActionControllerSubscriber
|
|
6
|
+
EVENT = "unpermitted_parameters.action_controller"
|
|
7
|
+
|
|
8
|
+
def self.install!(config)
|
|
9
|
+
return if @installed
|
|
10
|
+
@subscriber = ActiveSupport::Notifications.subscribe(EVENT) do |_name, _start, _finish, _id, payload|
|
|
11
|
+
stats = Thread.current[:query_guard_stats]
|
|
12
|
+
next unless stats
|
|
13
|
+
next unless config.enable_security && config.detect_mass_assignment
|
|
14
|
+
|
|
15
|
+
keys = Array(payload[:keys]).map(&:to_s)
|
|
16
|
+
sensitive = keys & Array(config.sensitive_param_keys)
|
|
17
|
+
|
|
18
|
+
stats[:violations] << {
|
|
19
|
+
type: :mass_assignment_unpermitted_params,
|
|
20
|
+
keys: keys.take(50),
|
|
21
|
+
sensitive_keys: sensitive.take(50)
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
@installed = true
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|