flare 0.1.0 → 0.2.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: 8eb495874fecf193c205e02c4254d2229ad264cc3e640bba5239940ac3de8e5d
4
- data.tar.gz: 71218afa6c8972dcf6cf76dc3dddc059a5197a7542a05cf8d0ad8a8f964c290f
3
+ metadata.gz: c659b175126da90baf0738ea221b54c25d9939ffbca3883b7756b2972574d0b6
4
+ data.tar.gz: 1b5d1f9f146a91ac1e7fc7a605fa2a3e3f22d28b906b92939362f99d9c63361d
5
5
  SHA512:
6
- metadata.gz: '035922584796dd1033486f6723cf859ae0d9f8dd169221817c120f66d3352ad5bbea6793f154d2963476a77e58b7f1c5a3635bf965821d245176271bd4d752de'
7
- data.tar.gz: 72338f6d8503a8b464f86de6e5397023716d55a810623eefe1d9b81cfdeae4ef3829f15b1228ccb1bdbacf0d957938d54a9e345a5c08f6ba09a7c68cd4b2624e
6
+ metadata.gz: 2662255ed5b8c4b77315e348c6c334f02df3a0879fdba62b64bd9d20ead9c78add8d88847dd945c9a276138ebdaa222352e64b2f2d1b054acb91bb23e3c67da4
7
+ data.tar.gz: 6374272263432f9f3a75d7d291fb7099bc7bcd8c8bfe87b8d3eb1b6c1a7ff79d39d226e49d20f88018f17f0f1411df6849c7ae65faa73f15350612fe1a03880c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2026-04-23
4
+
5
+ - Auto-detect OTel instrumentation gems via `use_all`
6
+ - Name Sidekiq job spans by worker class (via `job_class` attribute)
7
+ - Support both old and new OTel semantic convention property keys in dashboard queries
8
+ - Guard dashboard routes when sqlite3 is missing
9
+
10
+ ## [0.1.1] - 2025-12-17
11
+
12
+ - Rename gem from caboose to flare
13
+ - Warn instead of raising when sqlite3 gem is missing
14
+ - Make sqlite3 an optional dependency for production compatibility
15
+
3
16
  ## [0.1.0] - 2025-12-17
4
17
 
5
18
  - Initial release
@@ -6,10 +6,18 @@ module Flare
6
6
 
7
7
  layout "flare/application"
8
8
 
9
+ before_action :require_storage
10
+
9
11
  helper_method :show_redis_tab?
10
12
 
11
13
  private
12
14
 
15
+ def require_storage
16
+ return if Flare.storage
17
+
18
+ render plain: "Flare dashboard requires the sqlite3 gem. Add `gem 'sqlite3'` to your Gemfile.", status: :service_unavailable
19
+ end
20
+
13
21
  # Only show the Redis tab if:
14
22
  # 1. The Redis client library is loaded
15
23
  # 2. There are Redis spans in the database
@@ -53,8 +53,8 @@ module Flare
53
53
  { primary: primary, secondary: nil }
54
54
  when "http"
55
55
  full_url = props["http.url"] || ""
56
- target = props["http.target"] || ""
57
- host = props["http.host"] || props["net.peer.name"] || props["peer.service"]
56
+ target = props["url.path"] || props["http.target"] || ""
57
+ host = props["server.address"] || props["http.host"] || props["net.peer.name"] || props["peer.service"]
58
58
  uri = URI.parse(full_url) rescue nil
59
59
  if uri && uri.host
60
60
  domain = uri.host
@@ -64,8 +64,8 @@ module Flare
64
64
  domain = host
65
65
  path = target.presence || full_url
66
66
  end
67
- method = props["http.method"]
68
- status = props["http.status_code"]
67
+ method = props["http.request.method"] || props["http.method"]
68
+ status = props["http.response.status_code"] || props["http.status_code"]
69
69
  { primary: path.to_s.truncate(100), secondary: domain, http_method: method, http_status: status }
70
70
  when "mail"
71
71
  mailer = props["mailer"]
@@ -220,8 +220,8 @@
220
220
  span[:name]
221
221
  end
222
222
  elsif category == "http"
223
- url = props["http.url"] || props["http.target"]
224
- method = props["http.method"]
223
+ url = props["http.url"] || props["url.path"]
224
+ method = props["http.request.method"]
225
225
  "#{method} #{url}".strip.presence || span[:name]
226
226
  elsif category == "controller"
227
227
  ns = props["code.namespace"]
@@ -1,9 +1,9 @@
1
1
  <%
2
2
  # Get root span properties
3
3
  root_props = @root_span ? @root_span[:properties] : {}
4
- http_method = root_props["http.method"]
5
- http_status = root_props["http.status_code"]
6
- http_target = root_props["http.target"]
4
+ http_method = root_props["http.request.method"]
5
+ http_status = root_props["http.response.status_code"]
6
+ http_target = root_props["url.path"]
7
7
  controller = root_props["code.namespace"]
8
8
  action = root_props["code.function"]
9
9
 
@@ -51,8 +51,8 @@
51
51
 
52
52
  # Properties to exclude from "other" section (already shown elsewhere)
53
53
  shown_keys = %w[
54
- http.method http.status_code http.target http.scheme http.host
55
- code.namespace code.function http.route http.user_agent
54
+ http.request.method http.response.status_code url.path url.scheme server.address
55
+ code.namespace code.function http.route user_agent.original
56
56
  ]
57
57
  other_props = root_props.reject { |k, v| shown_keys.include?(k) || v.is_a?(Hash) || v.is_a?(Array) }
58
58
  %>
@@ -260,8 +260,8 @@
260
260
  span[:name]
261
261
  end
262
262
  elsif category == "http"
263
- url = props["http.url"] || props["http.target"]
264
- method = props["http.method"]
263
+ url = props["http.url"] || props["url.path"]
264
+ method = props["http.request.method"]
265
265
  "#{method} #{url}".strip.presence || span[:name]
266
266
  elsif category == "controller"
267
267
  ns = props["code.namespace"]
@@ -387,7 +387,7 @@
387
387
  extra = props['identifier'].replace(/.*\/app\/views\//, '');
388
388
  } else if (category === 'http') {
389
389
  name = 'HTTP';
390
- extra = ((props['http.method'] || '') + ' ' + (props['http.url'] || props['http.target'] || '')).trim();
390
+ extra = ((props['http.request.method'] || '') + ' ' + (props['http.url'] || props['url.path'] || '')).trim();
391
391
  }
392
392
 
393
393
  let line = offset.padStart(8) + ' | ' + duration.padStart(8) + ' | ' + name;
@@ -156,7 +156,7 @@ module Flare
156
156
  bucket: bucket_time(span),
157
157
  namespace: "job",
158
158
  service: extract_job_system(span),
159
- target: transaction_name || span.attributes["code.namespace"] || span.attributes["messaging.destination"] || "unknown",
159
+ target: transaction_name || span.attributes["code.namespace"] || span.attributes["messaging.sidekiq.job_class"] || span.attributes["messaging.destination"] || "unknown",
160
160
  operation: transaction_name ? "perform" : (span.attributes["code.function"] || span.name)
161
161
  )
162
162
 
@@ -34,25 +34,25 @@ module Flare
34
34
  if status
35
35
  case status
36
36
  when "2xx"
37
- conditions << "status_prop.value LIKE ?"
37
+ conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
38
38
  values << "2%"
39
39
  when "3xx"
40
- conditions << "status_prop.value LIKE ?"
40
+ conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
41
41
  values << "3%"
42
42
  when "4xx"
43
- conditions << "status_prop.value LIKE ?"
43
+ conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
44
44
  values << "4%"
45
45
  when "5xx"
46
- conditions << "status_prop.value LIKE ?"
46
+ conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
47
47
  values << "5%"
48
48
  else
49
- conditions << "status_prop.value = ?"
49
+ conditions << "COALESCE(status_prop.value, status_prop_old.value) = ?"
50
50
  values << status.to_s
51
51
  end
52
52
  end
53
53
 
54
54
  if method
55
- conditions << "method_prop.value = ?"
55
+ conditions << "COALESCE(method_prop.value, method_prop_old.value) = ?"
56
56
  values << "\"#{method}\""
57
57
  end
58
58
 
@@ -79,19 +79,22 @@ module Flare
79
79
 
80
80
  rows = query_all(<<~SQL, values)
81
81
  SELECT s.*,
82
- method_prop.value as http_method,
83
- status_prop.value as http_status,
84
- target_prop.value as http_target,
82
+ COALESCE(method_prop.value, method_prop_old.value) as http_method,
83
+ COALESCE(status_prop.value, status_prop_old.value) as http_status,
84
+ COALESCE(target_prop.value, target_prop_old.value) as http_target,
85
85
  controller_prop.value as controller,
86
86
  action_prop.value as action
87
87
  FROM flare_spans s
88
- LEFT JOIN flare_properties method_prop ON method_prop.owner_type = 'Flare::Span' AND method_prop.owner_id = s.id AND method_prop.key = 'http.method'
89
- LEFT JOIN flare_properties status_prop ON status_prop.owner_type = 'Flare::Span' AND status_prop.owner_id = s.id AND status_prop.key = 'http.status_code'
90
- LEFT JOIN flare_properties target_prop ON target_prop.owner_type = 'Flare::Span' AND target_prop.owner_id = s.id AND target_prop.key = 'http.target'
88
+ LEFT JOIN flare_properties method_prop ON method_prop.owner_type = 'Flare::Span' AND method_prop.owner_id = s.id AND method_prop.key = 'http.request.method'
89
+ LEFT JOIN flare_properties method_prop_old ON method_prop_old.owner_type = 'Flare::Span' AND method_prop_old.owner_id = s.id AND method_prop_old.key = 'http.method'
90
+ LEFT JOIN flare_properties status_prop ON status_prop.owner_type = 'Flare::Span' AND status_prop.owner_id = s.id AND status_prop.key = 'http.response.status_code'
91
+ LEFT JOIN flare_properties status_prop_old ON status_prop_old.owner_type = 'Flare::Span' AND status_prop_old.owner_id = s.id AND status_prop_old.key = 'http.status_code'
92
+ LEFT JOIN flare_properties target_prop ON target_prop.owner_type = 'Flare::Span' AND target_prop.owner_id = s.id AND target_prop.key = 'url.path'
93
+ LEFT JOIN flare_properties target_prop_old ON target_prop_old.owner_type = 'Flare::Span' AND target_prop_old.owner_id = s.id AND target_prop_old.key = 'http.target'
91
94
  LEFT JOIN flare_properties controller_prop ON controller_prop.owner_type = 'Flare::Span' AND controller_prop.owner_id = s.id AND controller_prop.key = 'code.namespace'
92
95
  LEFT JOIN flare_properties action_prop ON action_prop.owner_type = 'Flare::Span' AND action_prop.owner_id = s.id AND action_prop.key = 'code.function'
93
96
  #{where_clause}
94
- AND method_prop.value IS NOT NULL
97
+ AND COALESCE(method_prop.value, method_prop_old.value) IS NOT NULL
95
98
  ORDER BY s.created_at DESC
96
99
  LIMIT ? OFFSET ?
97
100
  SQL
@@ -352,25 +355,25 @@ module Flare
352
355
  if status
353
356
  case status
354
357
  when "2xx"
355
- conditions << "status_prop.value LIKE ?"
358
+ conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
356
359
  values << "2%"
357
360
  when "3xx"
358
- conditions << "status_prop.value LIKE ?"
361
+ conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
359
362
  values << "3%"
360
363
  when "4xx"
361
- conditions << "status_prop.value LIKE ?"
364
+ conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
362
365
  values << "4%"
363
366
  when "5xx"
364
- conditions << "status_prop.value LIKE ?"
367
+ conditions << "COALESCE(status_prop.value, status_prop_old.value) LIKE ?"
365
368
  values << "5%"
366
369
  else
367
- conditions << "status_prop.value = ?"
370
+ conditions << "COALESCE(status_prop.value, status_prop_old.value) = ?"
368
371
  values << status.to_s
369
372
  end
370
373
  end
371
374
 
372
375
  if method
373
- conditions << "method_prop.value = ?"
376
+ conditions << "COALESCE(method_prop.value, method_prop_old.value) = ?"
374
377
  values << "\"#{method}\""
375
378
  end
376
379
 
@@ -396,11 +399,13 @@ module Flare
396
399
  row = query_one(<<~SQL, values)
397
400
  SELECT COUNT(*) as count
398
401
  FROM flare_spans s
399
- LEFT JOIN flare_properties method_prop ON method_prop.owner_type = 'Flare::Span' AND method_prop.owner_id = s.id AND method_prop.key = 'http.method'
400
- LEFT JOIN flare_properties status_prop ON status_prop.owner_type = 'Flare::Span' AND status_prop.owner_id = s.id AND status_prop.key = 'http.status_code'
402
+ LEFT JOIN flare_properties method_prop ON method_prop.owner_type = 'Flare::Span' AND method_prop.owner_id = s.id AND method_prop.key = 'http.request.method'
403
+ LEFT JOIN flare_properties method_prop_old ON method_prop_old.owner_type = 'Flare::Span' AND method_prop_old.owner_id = s.id AND method_prop_old.key = 'http.method'
404
+ LEFT JOIN flare_properties status_prop ON status_prop.owner_type = 'Flare::Span' AND status_prop.owner_id = s.id AND status_prop.key = 'http.response.status_code'
405
+ LEFT JOIN flare_properties status_prop_old ON status_prop_old.owner_type = 'Flare::Span' AND status_prop_old.owner_id = s.id AND status_prop_old.key = 'http.status_code'
401
406
  LEFT JOIN flare_properties controller_prop ON controller_prop.owner_type = 'Flare::Span' AND controller_prop.owner_id = s.id AND controller_prop.key = 'code.namespace'
402
407
  #{where_clause}
403
- AND method_prop.value IS NOT NULL
408
+ AND COALESCE(method_prop.value, method_prop_old.value) IS NOT NULL
404
409
  SQL
405
410
 
406
411
  row ? row["count"] : 0
data/lib/flare/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Flare
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/flare.rb CHANGED
@@ -67,7 +67,9 @@ module Flare
67
67
  require_relative "flare/sqlite_exporter"
68
68
  SQLiteExporter.new(configuration.database_path)
69
69
  rescue LoadError
70
- raise LoadError, "Flare spans require the sqlite3 gem. Add `gem 'sqlite3'` to your Gemfile."
70
+ warn "[Flare] sqlite3 gem not found. Spans are disabled. Add `gem 'sqlite3'` to your Gemfile to enable the development dashboard."
71
+ configuration.spans_enabled = false
72
+ nil
71
73
  end
72
74
  end
73
75
 
@@ -139,7 +141,7 @@ module Flare
139
141
  "app"
140
142
  end
141
143
 
142
- # Require only the instrumentations we want
144
+ # Require flare's bundled instrumentations
143
145
  require "opentelemetry-instrumentation-rack"
144
146
  require "opentelemetry-instrumentation-net_http"
145
147
  require "opentelemetry-instrumentation-active_support"
@@ -158,24 +160,30 @@ module Flare
158
160
  c.service_name = service_name
159
161
 
160
162
  # Spans: detailed trace data stored in SQLite
161
- if configuration.spans_enabled
163
+ if configuration.spans_enabled && exporter
162
164
  c.add_span_processor(span_processor)
163
165
  log "Spans enabled (database=#{configuration.database_path})"
164
166
  end
165
167
 
166
- # Configure specific instrumentations
167
- c.use "OpenTelemetry::Instrumentation::Rack",
168
- untraced_requests: ->(env) {
169
- request = Rack::Request.new(env)
170
- return true if request.path.start_with?("/flare")
171
-
172
- configuration.ignore_request.call(request)
168
+ # Auto-detect and install all OTel instrumentation gems in the bundle.
169
+ # Apps can add gems like opentelemetry-instrumentation-sidekiq to their
170
+ # Gemfile and they'll be picked up automatically.
171
+ c.use_all(
172
+ "OpenTelemetry::Instrumentation::Rack" => {
173
+ untraced_requests: ->(env) {
174
+ request = Rack::Request.new(env)
175
+ return true if request.path.start_with?("/flare")
176
+
177
+ configuration.ignore_request.call(request)
178
+ }
179
+ },
180
+ # Name Sidekiq job spans after the worker class (e.g. "MyWorker
181
+ # process") instead of the upstream default of the queue name
182
+ # ("default process"), matching how ActiveJob spans are named.
183
+ "OpenTelemetry::Instrumentation::Sidekiq" => {
184
+ span_naming: :job_class,
173
185
  }
174
- c.use "OpenTelemetry::Instrumentation::Net::HTTP"
175
- c.use "OpenTelemetry::Instrumentation::ActiveSupport"
176
- c.use "OpenTelemetry::Instrumentation::ActionPack" if defined?(ActionController)
177
- c.use "OpenTelemetry::Instrumentation::ActionView" if defined?(ActionView)
178
- c.use "OpenTelemetry::Instrumentation::ActiveJob" if defined?(ActiveJob)
186
+ )
179
187
  end
180
188
 
181
189
  # Subscribe to common ActiveSupport notification patterns
@@ -188,7 +196,7 @@ module Flare
188
196
 
189
197
  at_exit do
190
198
  log "Shutting down..."
191
- if configuration.spans_enabled
199
+ if configuration.spans_enabled && @span_processor
192
200
  span_processor.force_flush
193
201
  span_processor.shutdown
194
202
  log "Span processor flushed and stopped"
@@ -386,7 +394,9 @@ module Flare
386
394
  require_relative "flare/storage/sqlite"
387
395
  Storage::SQLite.new(configuration.database_path)
388
396
  rescue LoadError
389
- raise LoadError, "Flare spans require the sqlite3 gem. Add `gem 'sqlite3'` to your Gemfile."
397
+ warn "[Flare] sqlite3 gem not found. Dashboard is disabled. Add `gem 'sqlite3'` to your Gemfile to enable it."
398
+ configuration.spans_enabled = false
399
+ nil
390
400
  end
391
401
  end
392
402
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flare
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker