findbug 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 +4 -4
- data/README.md +45 -8
- data/app/models/findbug/error_event.rb +34 -6
- data/app/models/findbug/performance_event.rb +38 -16
- data/docs/docs.html +976 -0
- data/docs/index.html +594 -8
- data/lib/findbug/adapter_helper.rb +74 -0
- data/lib/findbug/version.rb +1 -1
- data/lib/findbug.rb +1 -0
- data/lib/generators/findbug/templates/create_findbug_error_events.rb +3 -3
- data/lib/generators/findbug/templates/create_findbug_performance_events.rb +3 -3
- metadata +8 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bc983a190a0ac08221e7bbaf108e52d55ecc20a28712f5bdf380acf61a27e43d
|
|
4
|
+
data.tar.gz: 77bc4565c0686c4d60646b4e7d9dc65f4faae482c83d8df6b98a4c02f540c162
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c6d0d26042c62b4ea65bd5f574e6c43d588fdfa83914b416a44638dae85c97d3fa12d74d4ffa6bb08b8575d96daa11ed87aeaefc314ee319353cb4acf961a0d6
|
|
7
|
+
data.tar.gz: 3d8ef1a2aece7c33efb7b629aa5b19013fe96da2b6ec3ec741eba8447b2fa80b36a5c3b6f1be926c4f19cda0ff44388eebc0a6eede1ce42d4e5fe199b448a5b2
|
data/README.md
CHANGED
|
@@ -8,16 +8,20 @@
|
|
|
8
8
|
|
|
9
9
|
FindBug provides Sentry-like functionality with all data stored on your own infrastructure using Redis and your database. Zero external dependencies, full data ownership.
|
|
10
10
|
|
|
11
|
+
📚 **Full documentation:** [findbug.dev/docs](https://findbug.dev/docs)
|
|
12
|
+
|
|
11
13
|
## Features
|
|
12
14
|
|
|
13
15
|
- **Error Tracking** - Capture exceptions with full context, stack traces, and request data
|
|
14
16
|
- **Performance Monitoring** - Track request timing, SQL queries, and automatic N+1 detection
|
|
15
|
-
- **Self-Hosted** - All data stays on your infrastructure (Redis +
|
|
17
|
+
- **Self-Hosted** - All data stays on your infrastructure (Redis + your existing database)
|
|
18
|
+
- **Database-Agnostic** - Works on PostgreSQL, MySQL, and SQLite — adapter detected automatically
|
|
16
19
|
- **Zero Performance Impact** - Async writes via Redis buffer, never blocks your requests
|
|
17
20
|
- **Built-in Dashboard** - Beautiful web UI for viewing errors and performance metrics
|
|
18
21
|
- **Multi-channel Alerts** - Email, Slack, Discord, and custom webhooks
|
|
19
22
|
- **Works Out of the Box** - Built-in background persister, no job scheduler required
|
|
20
23
|
- **Rails 7+ Native** - Designed for modern Rails applications
|
|
24
|
+
- **Tested** - Comprehensive RSpec suite covering every adapter path
|
|
21
25
|
|
|
22
26
|
## Why FindBug?
|
|
23
27
|
|
|
@@ -34,7 +38,7 @@ FindBug provides Sentry-like functionality with all data stored on your own infr
|
|
|
34
38
|
- Ruby 3.1+
|
|
35
39
|
- Rails 7.0+
|
|
36
40
|
- Redis 4.0+
|
|
37
|
-
- PostgreSQL or
|
|
41
|
+
- A relational database — PostgreSQL, MySQL, or SQLite
|
|
38
42
|
|
|
39
43
|
## Installation
|
|
40
44
|
|
|
@@ -240,7 +244,7 @@ end
|
|
|
240
244
|
│ └────────┬─────────┘ │
|
|
241
245
|
│ │ │
|
|
242
246
|
│ ▼ │
|
|
243
|
-
│ Dashboard ◄──────────────────── Database (PostgreSQL/MySQL)
|
|
247
|
+
│ Dashboard ◄──────────────────── Database (PostgreSQL/MySQL/SQLite) │
|
|
244
248
|
│ (/findbug) │
|
|
245
249
|
│ │
|
|
246
250
|
└─────────────────────────────────────────────────────────────────┘
|
|
@@ -274,11 +278,44 @@ rails findbug:clear_buffers
|
|
|
274
278
|
rails findbug:db:stats
|
|
275
279
|
```
|
|
276
280
|
|
|
277
|
-
##
|
|
281
|
+
## Database Support
|
|
282
|
+
|
|
283
|
+
As of v0.5.0 FindBug is database-agnostic. The install generator's migrations and the model layer detect your `ActiveRecord::Base.connection.adapter_name` at runtime and pick the right column types and SQL functions automatically.
|
|
284
|
+
|
|
285
|
+
| Adapter | JSON column | Time bucketing SQL |
|
|
286
|
+
|--------------------|-------------|------------------------------------|
|
|
287
|
+
| PostgreSQL/PostGIS | `jsonb` | `date_trunc(...)` |
|
|
288
|
+
| MySQL/Mysql2 | `json` | `DATE_FORMAT(...)` / `DATE(...)` |
|
|
289
|
+
| SQLite | `text` (with JSON serialisation in the model) | `strftime(...)` / `DATE(...)` |
|
|
290
|
+
|
|
291
|
+
The JSON accessors on the models normalise reads to a native Ruby `Hash` / `Array` regardless of the underlying column type, so your application code is identical across adapters.
|
|
292
|
+
|
|
293
|
+
If you're writing your own migrations against the same multi-DB strategy, the `Findbug::AdapterHelper` module is part of the public API:
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
Findbug::AdapterHelper.json_column_type # :jsonb / :json / :text
|
|
297
|
+
Findbug::AdapterHelper.json_default({}) # adapter-appropriate default
|
|
298
|
+
Findbug::AdapterHelper.date_trunc_sql("hour", "captured_at")
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Multi-Tenant Applications
|
|
302
|
+
|
|
303
|
+
Multi-tenant Rails apps are supported, but the setup depends on *how* your app isolates tenants. The matrix below shows what's possible per adapter:
|
|
304
|
+
|
|
305
|
+
| Adapter | Tenancy model | FindBug support |
|
|
306
|
+
|--------------------------|--------------------------------------------------------------|----------------------------------------------------------------------------------|
|
|
307
|
+
| PostgreSQL | Schema-per-tenant (Apartment's default) | ✅ First-class — keep FindBug tables in the `public` schema (see below). |
|
|
308
|
+
| PostgreSQL/MySQL/SQLite | Row-level (`tenant_id` column) | ✅ Nothing special — FindBug's tables aren't tenant-scoped. |
|
|
309
|
+
| MySQL | Database-per-tenant (Apartment with `use_schemas = false`) | ⚠️ Doable but awkward — point FindBug at a separate connection via `connects_to`. |
|
|
310
|
+
| SQLite | File-per-tenant | ❌ Not practical — use row-level scoping instead. |
|
|
311
|
+
|
|
312
|
+
For the full discussion (including MySQL setup options), see the [Multi-tenant section in the docs](https://findbug.dev/docs#multi-tenant).
|
|
313
|
+
|
|
314
|
+
### PostgreSQL + Apartment (schema-per-tenant)
|
|
278
315
|
|
|
279
|
-
If you're using [ros-apartment](https://github.com/rails-on-services/apartment)
|
|
316
|
+
If you're using [ros-apartment](https://github.com/rails-on-services/apartment) with PostgreSQL schemas, FindBug's tables need to stay in the public schema and the dashboard path should be excluded from tenant switching.
|
|
280
317
|
|
|
281
|
-
|
|
318
|
+
#### 1. Exclude FindBug Models
|
|
282
319
|
|
|
283
320
|
Add FindBug models to the `excluded_models` list in `config/initializers/apartment.rb`:
|
|
284
321
|
|
|
@@ -293,7 +330,7 @@ Apartment.configure do |config|
|
|
|
293
330
|
end
|
|
294
331
|
```
|
|
295
332
|
|
|
296
|
-
|
|
333
|
+
#### 2. Exclude FindBug Dashboard Path
|
|
297
334
|
|
|
298
335
|
Add `/findbug` to your tenant switching middleware's excluded paths:
|
|
299
336
|
|
|
@@ -317,7 +354,7 @@ class SwitchTenantMiddleware < Apartment::Elevators::Generic
|
|
|
317
354
|
end
|
|
318
355
|
```
|
|
319
356
|
|
|
320
|
-
|
|
357
|
+
#### 3. Run Migrations in Public Schema
|
|
321
358
|
|
|
322
359
|
Ensure FindBug migrations run in the public schema:
|
|
323
360
|
|
|
@@ -27,14 +27,15 @@ module Findbug
|
|
|
27
27
|
# t.timestamps
|
|
28
28
|
# end
|
|
29
29
|
#
|
|
30
|
-
# WHY
|
|
31
|
-
#
|
|
30
|
+
# WHY OVERRIDE JSON ACCESSORS?
|
|
31
|
+
# =============================
|
|
32
32
|
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
33
|
+
# The column type for context/request_data varies by adapter:
|
|
34
|
+
# PostgreSQL → jsonb (AR returns Hash natively)
|
|
35
|
+
# MySQL → json (AR returns Hash natively)
|
|
36
|
+
# SQLite → text (AR returns raw JSON String)
|
|
36
37
|
#
|
|
37
|
-
#
|
|
38
|
+
# The overrides below normalise both cases so callers always get a Hash.
|
|
38
39
|
#
|
|
39
40
|
class ErrorEvent < ActiveRecord::Base
|
|
40
41
|
self.table_name = "findbug_error_events"
|
|
@@ -55,6 +56,33 @@ module Findbug
|
|
|
55
56
|
validates :status, inclusion: { in: [STATUS_UNRESOLVED, STATUS_RESOLVED, STATUS_IGNORED] }
|
|
56
57
|
validates :severity, inclusion: { in: [SEVERITY_ERROR, SEVERITY_WARNING, SEVERITY_INFO] }
|
|
57
58
|
|
|
59
|
+
# JSON field accessors — normalise across adapters (jsonb/json/text).
|
|
60
|
+
# Reader always returns a Hash; writer stores a JSON string on text columns
|
|
61
|
+
# and the native object on json/jsonb columns.
|
|
62
|
+
%i[context request_data].each do |field|
|
|
63
|
+
define_method(field) do
|
|
64
|
+
val = read_attribute(field)
|
|
65
|
+
return {} if val.nil?
|
|
66
|
+
val.is_a?(String) ? JSON.parse(val) : val
|
|
67
|
+
rescue JSON::ParserError
|
|
68
|
+
{}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
define_method(:"#{field}=") do |val|
|
|
72
|
+
col_type = self.class.columns_hash[field.to_s]&.type
|
|
73
|
+
if col_type == :text
|
|
74
|
+
serialized = case val
|
|
75
|
+
when nil then nil
|
|
76
|
+
when String then val
|
|
77
|
+
else val.to_json
|
|
78
|
+
end
|
|
79
|
+
write_attribute(field, serialized)
|
|
80
|
+
else
|
|
81
|
+
write_attribute(field, val)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
58
86
|
# Scopes
|
|
59
87
|
scope :unresolved, -> { where(status: STATUS_UNRESOLVED) }
|
|
60
88
|
scope :resolved, -> { where(status: STATUS_RESOLVED) }
|
|
@@ -17,11 +17,11 @@ module Findbug
|
|
|
17
17
|
# t.float :db_time_ms, default: 0
|
|
18
18
|
# t.float :view_time_ms, default: 0
|
|
19
19
|
# t.integer :query_count, default: 0
|
|
20
|
-
# t.
|
|
21
|
-
# t.
|
|
20
|
+
# t.column :slow_queries, <json_type>, default: []
|
|
21
|
+
# t.column :n_plus_one_queries, <json_type>, default: []
|
|
22
22
|
# t.boolean :has_n_plus_one, default: false
|
|
23
23
|
# t.integer :view_count, default: 0
|
|
24
|
-
# t.
|
|
24
|
+
# t.column :context, <json_type>, default: {}
|
|
25
25
|
# t.string :environment
|
|
26
26
|
# t.string :release_version
|
|
27
27
|
# t.datetime :captured_at
|
|
@@ -48,6 +48,33 @@ module Findbug
|
|
|
48
48
|
TYPE_CUSTOM = "custom"
|
|
49
49
|
TYPE_JOB = "job"
|
|
50
50
|
|
|
51
|
+
# JSON field accessors — normalise across adapters (jsonb/json/text).
|
|
52
|
+
# Reader returns the native Array/Hash; writer serialises to JSON string
|
|
53
|
+
# for text columns and passes through to AR's type system otherwise.
|
|
54
|
+
{ slow_queries: [], n_plus_one_queries: [], context: {} }.each do |field, empty|
|
|
55
|
+
define_method(field) do
|
|
56
|
+
val = read_attribute(field)
|
|
57
|
+
return empty.dup if val.nil?
|
|
58
|
+
val.is_a?(String) ? JSON.parse(val) : val
|
|
59
|
+
rescue JSON::ParserError
|
|
60
|
+
empty.dup
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
define_method(:"#{field}=") do |val|
|
|
64
|
+
col_type = self.class.columns_hash[field.to_s]&.type
|
|
65
|
+
if col_type == :text
|
|
66
|
+
serialized = case val
|
|
67
|
+
when nil then nil
|
|
68
|
+
when String then val
|
|
69
|
+
else val.to_json
|
|
70
|
+
end
|
|
71
|
+
write_attribute(field, serialized)
|
|
72
|
+
else
|
|
73
|
+
write_attribute(field, val)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
51
78
|
# Validations
|
|
52
79
|
validates :transaction_name, presence: true
|
|
53
80
|
validates :duration_ms, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
|
@@ -181,28 +208,23 @@ module Findbug
|
|
|
181
208
|
# @return [Array<Hash>] time series data
|
|
182
209
|
#
|
|
183
210
|
def self.throughput_over_time(since: 24.hours.ago, interval: "hour")
|
|
184
|
-
|
|
185
|
-
# Works with PostgreSQL; adjust for other databases
|
|
186
|
-
time_column = case interval
|
|
187
|
-
when "minute" then "date_trunc('minute', captured_at)"
|
|
188
|
-
when "hour" then "date_trunc('hour', captured_at)"
|
|
189
|
-
when "day" then "date_trunc('day', captured_at)"
|
|
190
|
-
else "date_trunc('hour', captured_at)"
|
|
191
|
-
end
|
|
211
|
+
time_sql = Findbug::AdapterHelper.date_trunc_sql(interval, "captured_at")
|
|
192
212
|
|
|
193
213
|
where("captured_at >= ?", since)
|
|
194
|
-
.group(Arel.sql(
|
|
214
|
+
.group(Arel.sql(time_sql))
|
|
195
215
|
.select(
|
|
196
|
-
Arel.sql("#{
|
|
216
|
+
Arel.sql("#{time_sql} as time_bucket"),
|
|
197
217
|
"COUNT(*) as request_count",
|
|
198
218
|
"AVG(duration_ms) as avg_duration"
|
|
199
219
|
)
|
|
200
|
-
.order(Arel.sql(
|
|
220
|
+
.order(Arel.sql(time_sql))
|
|
201
221
|
.map do |row|
|
|
222
|
+
time = row.time_bucket
|
|
223
|
+
time = Time.parse(time.to_s) unless time.respond_to?(:strftime)
|
|
202
224
|
{
|
|
203
|
-
time:
|
|
225
|
+
time: time,
|
|
204
226
|
count: row.request_count,
|
|
205
|
-
avg_duration_ms: row.avg_duration
|
|
227
|
+
avg_duration_ms: row.avg_duration&.round(2) || 0
|
|
206
228
|
}
|
|
207
229
|
end
|
|
208
230
|
end
|