flare 0.4.0 → 1.0.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.
Potentially problematic release.
This version of flare might be problematic. Click here for more details.
- data/.document +5 -0
- data/.gitignore +22 -0
- data/LICENSE +20 -0
- data/README.rdoc +18 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/flare.gemspec +66 -0
- data/lib/flare/active_record.rb +102 -0
- data/lib/flare/collection.rb +47 -0
- data/lib/flare/configuration.rb +64 -123
- data/lib/flare/index_builder.rb +24 -0
- data/lib/flare/session.rb +142 -0
- data/lib/flare/tasks.rb +18 -0
- data/lib/flare.rb +19 -537
- data/test/helper.rb +10 -0
- data/test/test_flare.rb +7 -0
- metadata +89 -242
- checksums.yaml +0 -7
- data/CHANGELOG.md +0 -3
- data/LICENSE.txt +0 -21
- data/README.md +0 -148
- data/app/controllers/flare/application_controller.rb +0 -30
- data/app/controllers/flare/jobs_controller.rb +0 -55
- data/app/controllers/flare/requests_controller.rb +0 -73
- data/app/controllers/flare/spans_controller.rb +0 -101
- data/app/helpers/flare/application_helper.rb +0 -168
- data/app/views/flare/jobs/index.html.erb +0 -69
- data/app/views/flare/jobs/show.html.erb +0 -323
- data/app/views/flare/requests/index.html.erb +0 -120
- data/app/views/flare/requests/show.html.erb +0 -498
- data/app/views/flare/spans/index.html.erb +0 -112
- data/app/views/flare/spans/show.html.erb +0 -184
- data/app/views/layouts/flare/application.html.erb +0 -126
- data/config/routes.rb +0 -20
- data/exe/flare +0 -9
- data/lib/flare/backoff_policy.rb +0 -73
- data/lib/flare/cli/doctor_command.rb +0 -129
- data/lib/flare/cli/output.rb +0 -45
- data/lib/flare/cli/setup_command.rb +0 -404
- data/lib/flare/cli/status_command.rb +0 -47
- data/lib/flare/cli.rb +0 -50
- data/lib/flare/client_headers.rb +0 -39
- data/lib/flare/deadline.rb +0 -27
- data/lib/flare/engine.rb +0 -45
- data/lib/flare/filtering_span_processor.rb +0 -429
- data/lib/flare/http_metrics_config.rb +0 -101
- data/lib/flare/http_transport.rb +0 -76
- data/lib/flare/lifecycle.rb +0 -39
- data/lib/flare/marker.rb +0 -106
- data/lib/flare/metric_counter.rb +0 -51
- data/lib/flare/metric_flusher.rb +0 -279
- data/lib/flare/metric_key.rb +0 -42
- data/lib/flare/metric_span_processor.rb +0 -470
- data/lib/flare/metric_storage.rb +0 -67
- data/lib/flare/metric_submitter.rb +0 -239
- data/lib/flare/recording_batch_span_processor.rb +0 -238
- data/lib/flare/rule_manager.rb +0 -141
- data/lib/flare/sampler.rb +0 -130
- data/lib/flare/source_location.rb +0 -113
- data/lib/flare/sqlite_exporter.rb +0 -334
- data/lib/flare/storage/sqlite.rb +0 -794
- data/lib/flare/storage.rb +0 -54
- data/lib/flare/trace_blob.rb +0 -116
- data/lib/flare/trace_exporter.rb +0 -182
- data/lib/flare/trace_health_reporter.rb +0 -74
- data/lib/flare/upload_url_pool.rb +0 -108
- data/lib/flare/version.rb +0 -5
- data/lib/flare/web_marker_subscriber.rb +0 -76
- data/public/flare-assets/flare.css +0 -1245
- data/public/flare-assets/images/flipper.png +0 -0
data/lib/flare.rb
CHANGED
|
@@ -1,544 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
require
|
|
1
|
+
begin
|
|
2
|
+
# require 'time'
|
|
3
|
+
# require 'date'
|
|
4
|
+
require 'rsolr'
|
|
5
|
+
rescue LoadError
|
|
6
|
+
require 'rubygems'
|
|
7
|
+
require 'rsolr'
|
|
8
|
+
end
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
require_relative "flare/metric_storage"
|
|
12
|
-
require_relative "flare/metric_span_processor"
|
|
13
|
-
require_relative "flare/metric_flusher"
|
|
14
|
-
require_relative "flare/backoff_policy"
|
|
15
|
-
require_relative "flare/metric_submitter"
|
|
16
|
-
require_relative "flare/recording_batch_span_processor"
|
|
17
|
-
require_relative "flare/lifecycle"
|
|
10
|
+
%w(configuration collection index_builder session active_record).each do |file|
|
|
11
|
+
require File.join(File.dirname(__FILE__), 'flare', file)
|
|
12
|
+
end
|
|
18
13
|
|
|
19
|
-
|
|
20
|
-
require_relative "flare/marker"
|
|
21
|
-
require_relative "flare/web_marker_subscriber"
|
|
22
|
-
require_relative "flare/filtering_span_processor"
|
|
23
|
-
require_relative "flare/upload_url_pool"
|
|
24
|
-
require_relative "flare/trace_exporter"
|
|
25
|
-
require_relative "flare/rule_manager"
|
|
26
|
-
require_relative "flare/trace_health_reporter"
|
|
14
|
+
ActiveRecord::Base.send(:include, Flare::ActiveRecord)
|
|
27
15
|
|
|
28
16
|
module Flare
|
|
29
|
-
class
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
TRANSACTION_NAME_ATTRIBUTE = "flare.transaction_name" unless const_defined?(:TRANSACTION_NAME_ATTRIBUTE)
|
|
33
|
-
|
|
34
|
-
module_function
|
|
35
|
-
|
|
36
|
-
def configuration
|
|
37
|
-
@configuration ||= Configuration.new
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def configure
|
|
41
|
-
yield(configuration) if block_given?
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def enabled?
|
|
45
|
-
configuration.enabled
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Set the transaction name for the current span. This overrides the
|
|
49
|
-
# default name derived from Rails controller/action or job class.
|
|
50
|
-
#
|
|
51
|
-
# Useful for Rack middleware, mounted apps, or any request that
|
|
52
|
-
# doesn't go through the Rails router.
|
|
53
|
-
#
|
|
54
|
-
# Flare.transaction_name("RestApi::Routes::Audits#get")
|
|
55
|
-
#
|
|
56
|
-
def transaction_name(name)
|
|
57
|
-
span = OpenTelemetry::Trace.current_span
|
|
58
|
-
return unless span.respond_to?(:set_attribute)
|
|
59
|
-
|
|
60
|
-
span.set_attribute(TRANSACTION_NAME_ATTRIBUTE, name)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def logger
|
|
64
|
-
@logger ||= Logger.new(STDOUT)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def logger=(logger)
|
|
68
|
-
@logger = logger
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def log(message)
|
|
72
|
-
return unless configuration.debug
|
|
73
|
-
|
|
74
|
-
logger.info("[Flare] #{message}")
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def exporter
|
|
78
|
-
@exporter ||= begin
|
|
79
|
-
require_relative "flare/sqlite_exporter"
|
|
80
|
-
SQLiteExporter.new(configuration.database_path)
|
|
81
|
-
rescue LoadError
|
|
82
|
-
warn "[Flare] sqlite3 gem not found. Spans are disabled. Add `gem 'sqlite3'` to your Gemfile to enable the development dashboard."
|
|
83
|
-
configuration.spans_enabled = false
|
|
84
|
-
nil
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def exporter=(exporter)
|
|
89
|
-
@exporter = exporter
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def span_processor
|
|
93
|
-
@span_processor ||= RecordingBatchSpanProcessor.new(
|
|
94
|
-
exporter,
|
|
95
|
-
max_queue_size: 1000,
|
|
96
|
-
max_export_batch_size: 100,
|
|
97
|
-
schedule_delay: 1000 # 1 second
|
|
98
|
-
)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def span_processor=(span_processor)
|
|
102
|
-
@span_processor = span_processor
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def tracer
|
|
106
|
-
@tracer ||= OpenTelemetry.tracer_provider.tracer("Flare", Flare::VERSION)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def untraced(&block)
|
|
110
|
-
OpenTelemetry::Common::Utilities.untraced(&block)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def metric_storage
|
|
114
|
-
@metric_storage
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def metric_storage=(storage)
|
|
118
|
-
@metric_storage = storage
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def metric_flusher
|
|
122
|
-
@metric_flusher
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def metric_flusher=(flusher)
|
|
126
|
-
@metric_flusher = flusher
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
# Trace-sampling components, exposed for tests + manual after_fork wiring.
|
|
130
|
-
def sampler = @sampler
|
|
131
|
-
def marker = @marker
|
|
132
|
-
def upload_url_pool = @upload_url_pool
|
|
133
|
-
def rule_manager = @rule_manager
|
|
134
|
-
def trace_span_processor = @trace_span_processor
|
|
135
|
-
def trace_health_reporter = @trace_health_reporter
|
|
136
|
-
|
|
137
|
-
# Manually flush metrics (useful for testing or forced flushes).
|
|
138
|
-
def flush_metrics
|
|
139
|
-
@metric_flusher&.flush_now || 0
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# Default project key, derived from the host Rails app's module name.
|
|
143
|
-
# Customers can override by configuring something else once we expose
|
|
144
|
-
# configuration.project; for v0.3 this matches MetricSubmitter's behavior.
|
|
145
|
-
def service_name_for_app
|
|
146
|
-
if defined?(Rails) && Rails.respond_to?(:application) && Rails.application
|
|
147
|
-
Rails.application.class.module_parent_name.underscore rescue "rails_app"
|
|
148
|
-
else
|
|
149
|
-
"app"
|
|
17
|
+
class << self
|
|
18
|
+
def session
|
|
19
|
+
@session ||= Flare::Session.new
|
|
150
20
|
end
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if defined?(Rails) && Rails.respond_to?(:env)
|
|
155
|
-
Rails.env.to_s
|
|
156
|
-
else
|
|
157
|
-
ENV.fetch("RACK_ENV", "development")
|
|
21
|
+
|
|
22
|
+
def indexed_models
|
|
23
|
+
@@indexed_models ||= []
|
|
158
24
|
end
|
|
159
25
|
end
|
|
160
|
-
|
|
161
|
-
# Re-initialize background threads after fork.
|
|
162
|
-
# Call this from Puma/Unicorn after_fork hooks.
|
|
163
|
-
def after_fork
|
|
164
|
-
@metric_flusher&.after_fork
|
|
165
|
-
@rule_manager&.after_fork
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
# Configure OpenTelemetry SDK and instrumentations. Must run before the
|
|
169
|
-
# middleware stack is built so Rack/ActionPack can insert their middleware.
|
|
170
|
-
# Note: metrics flusher is started separately via start_metrics_flusher
|
|
171
|
-
# after user initializers have run.
|
|
172
|
-
def configure_opentelemetry
|
|
173
|
-
return if @otel_configured
|
|
174
|
-
|
|
175
|
-
# Suppress noisy OTel INFO logs
|
|
176
|
-
OpenTelemetry.logger = Logger.new(STDOUT, level: Logger::WARN)
|
|
177
|
-
|
|
178
|
-
service_name = service_name_for_app
|
|
179
|
-
|
|
180
|
-
# Require flare's bundled instrumentations
|
|
181
|
-
require "opentelemetry-instrumentation-rack"
|
|
182
|
-
require "opentelemetry-instrumentation-net_http"
|
|
183
|
-
require "opentelemetry-instrumentation-active_support"
|
|
184
|
-
require "opentelemetry/instrumentation/active_support/span_subscriber"
|
|
185
|
-
require "opentelemetry-instrumentation-action_pack" if defined?(ActionController)
|
|
186
|
-
require "opentelemetry-instrumentation-action_view" if defined?(ActionView)
|
|
187
|
-
require "opentelemetry-instrumentation-active_job" if defined?(ActiveJob)
|
|
188
|
-
|
|
189
|
-
# Tell the SDK not to try configuring OTLP from env vars.
|
|
190
|
-
# Flare manages its own exporters (SQLite for spans, HTTP for metrics).
|
|
191
|
-
ENV["OTEL_TRACES_EXPORTER"] ||= "none"
|
|
192
|
-
|
|
193
|
-
log "Configuring OpenTelemetry (service=#{service_name})"
|
|
194
|
-
|
|
195
|
-
OpenTelemetry::SDK.configure do |c|
|
|
196
|
-
c.service_name = service_name
|
|
197
|
-
|
|
198
|
-
# Spans: detailed trace data stored in SQLite
|
|
199
|
-
if configuration.spans_enabled && exporter
|
|
200
|
-
c.add_span_processor(span_processor)
|
|
201
|
-
log "Spans enabled (database=#{configuration.database_path})"
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
# Auto-detect and install all OTel instrumentation gems in the bundle.
|
|
205
|
-
# Apps can add gems like opentelemetry-instrumentation-sidekiq to their
|
|
206
|
-
# Gemfile and they'll be picked up automatically.
|
|
207
|
-
c.use_all(
|
|
208
|
-
"OpenTelemetry::Instrumentation::Rack" => {
|
|
209
|
-
untraced_requests: ->(env) {
|
|
210
|
-
request = Rack::Request.new(env)
|
|
211
|
-
return true if request.path.start_with?("/flare")
|
|
212
|
-
|
|
213
|
-
configuration.ignore_request.call(request)
|
|
214
|
-
}
|
|
215
|
-
},
|
|
216
|
-
# Name Sidekiq job spans after the worker class (e.g. "MyWorker
|
|
217
|
-
# process") instead of the upstream default of the queue name
|
|
218
|
-
# ("default process"), matching how ActiveJob spans are named.
|
|
219
|
-
"OpenTelemetry::Instrumentation::Sidekiq" => {
|
|
220
|
-
span_naming: :job_class,
|
|
221
|
-
}
|
|
222
|
-
)
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
# Subscribe to common ActiveSupport notification patterns
|
|
226
|
-
# This captures SQL, cache, mailer, and custom notifications.
|
|
227
|
-
# Required for both spans (detailed traces) and metrics (aggregated counters)
|
|
228
|
-
# because DB, cache, and mailer data flows through ActiveSupport notifications.
|
|
229
|
-
if configuration.spans_enabled || configuration.metrics_enabled
|
|
230
|
-
subscribe_to_notifications
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
at_exit do
|
|
234
|
-
log "Shutting down..."
|
|
235
|
-
if configuration.spans_enabled && @span_processor
|
|
236
|
-
span_processor.force_flush
|
|
237
|
-
span_processor.shutdown
|
|
238
|
-
log "Span processor flushed and stopped"
|
|
239
|
-
end
|
|
240
|
-
if @trace_span_processor
|
|
241
|
-
@trace_span_processor.force_flush
|
|
242
|
-
@trace_span_processor.shutdown
|
|
243
|
-
log "Trace span processor flushed and stopped"
|
|
244
|
-
end
|
|
245
|
-
log "Shutdown complete"
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
@otel_configured = true
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
# Start the trace-rules poller. Polls GET /api/rules every
|
|
252
|
-
# tracing_poll_interval (default 30s) so the in-process sampler + URL
|
|
253
|
-
# pool stay current. Called from config.after_initialize -- after the
|
|
254
|
-
# user's configure block has run -- so configuration.url / .key /
|
|
255
|
-
# .tracing_enabled are settled.
|
|
256
|
-
def start_rule_manager
|
|
257
|
-
return unless configuration.tracing_submission_configured?
|
|
258
|
-
|
|
259
|
-
setup_tracing_components
|
|
260
|
-
return unless @sampler && @marker && @upload_url_pool
|
|
261
|
-
|
|
262
|
-
@rule_manager = RuleManager.new(
|
|
263
|
-
sampler: @sampler,
|
|
264
|
-
marker: @marker,
|
|
265
|
-
pool: @upload_url_pool,
|
|
266
|
-
base_url: configuration.url,
|
|
267
|
-
api_key: configuration.key,
|
|
268
|
-
project: service_name_for_app,
|
|
269
|
-
environment: rails_env_name,
|
|
270
|
-
interval: configuration.tracing_poll_interval
|
|
271
|
-
)
|
|
272
|
-
@rule_manager.start
|
|
273
|
-
log "Rule manager started (poll=#{configuration.tracing_poll_interval}s)"
|
|
274
|
-
|
|
275
|
-
at_exit { @rule_manager&.stop }
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
def setup_tracing_components
|
|
279
|
-
return if @trace_span_processor
|
|
280
|
-
|
|
281
|
-
@sampler = Sampler.new
|
|
282
|
-
@marker = Marker.new
|
|
283
|
-
@upload_url_pool = UploadUrlPool.new
|
|
284
|
-
|
|
285
|
-
# Trace sampling: server-controlled per-route capture. The sampler runs
|
|
286
|
-
# at span start; for routes it can't decide there (Rails web spans get
|
|
287
|
-
# their controller#action attributes set post-routing) the marker +
|
|
288
|
-
# WebMarkerSubscriber handle it. The RECORD_ONLY delegates keep children
|
|
289
|
-
# of unsampled local and remote parents recording so processors still see
|
|
290
|
-
# web requests that arrive with an unsampled traceparent header.
|
|
291
|
-
#
|
|
292
|
-
# Sampler is set on the tracer_provider AFTER SDK.configure -- the SDK's
|
|
293
|
-
# Configurator block doesn't expose a `sampler=`; the provider does.
|
|
294
|
-
OpenTelemetry.tracer_provider.sampler =
|
|
295
|
-
OpenTelemetry::SDK::Trace::Samplers.parent_based(
|
|
296
|
-
root: @sampler,
|
|
297
|
-
remote_parent_sampled: ALWAYS_RECORD_ONLY,
|
|
298
|
-
remote_parent_not_sampled: ALWAYS_RECORD_ONLY,
|
|
299
|
-
local_parent_not_sampled: ALWAYS_RECORD_ONLY
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
@trace_exporter = TraceExporter.new(
|
|
303
|
-
pool: @upload_url_pool,
|
|
304
|
-
notify_url: "#{configuration.url.to_s.chomp('/')}/api/traces",
|
|
305
|
-
api_key: configuration.key,
|
|
306
|
-
project: service_name_for_app,
|
|
307
|
-
environment: rails_env_name
|
|
308
|
-
)
|
|
309
|
-
|
|
310
|
-
@trace_span_processor = FilteringSpanProcessor.new(
|
|
311
|
-
exporter: @trace_exporter,
|
|
312
|
-
marker: @marker,
|
|
313
|
-
max_queue: configuration.tracing_max_queue
|
|
314
|
-
)
|
|
315
|
-
OpenTelemetry.tracer_provider.add_span_processor(@trace_span_processor)
|
|
316
|
-
|
|
317
|
-
@trace_health_reporter = TraceHealthReporter.new(
|
|
318
|
-
processor: @trace_span_processor,
|
|
319
|
-
pool: @upload_url_pool,
|
|
320
|
-
exporter: @trace_exporter
|
|
321
|
-
)
|
|
322
|
-
|
|
323
|
-
# Path 2 trace marking. Rails-only -- in non-Rails contexts the
|
|
324
|
-
# subscriber would never fire but creating it is harmless.
|
|
325
|
-
if defined?(ActiveSupport::Notifications)
|
|
326
|
-
@web_marker_subscriber = WebMarkerSubscriber.new(sampler: @sampler, marker: @marker).start
|
|
327
|
-
end
|
|
328
|
-
|
|
329
|
-
log "Tracing enabled (poll=#{configuration.tracing_poll_interval}s)"
|
|
330
|
-
end
|
|
331
|
-
|
|
332
|
-
# Start the metrics flusher. Called from config.after_initialize so
|
|
333
|
-
# user configuration (metrics_enabled, flush_interval, etc.) is applied.
|
|
334
|
-
def start_metrics_flusher
|
|
335
|
-
return unless configuration.metrics_enabled
|
|
336
|
-
|
|
337
|
-
@metric_storage ||= MetricStorage.new
|
|
338
|
-
metric_processor = MetricSpanProcessor.new(
|
|
339
|
-
storage: @metric_storage,
|
|
340
|
-
http_metrics_config: configuration.http_metrics_config
|
|
341
|
-
)
|
|
342
|
-
OpenTelemetry.tracer_provider.add_span_processor(metric_processor)
|
|
343
|
-
|
|
344
|
-
log "Metrics enabled (endpoint=#{configuration.url} key=#{configuration.key ? 'present' : 'missing'})"
|
|
345
|
-
|
|
346
|
-
if configuration.metrics_submission_configured?
|
|
347
|
-
submitter = MetricSubmitter.new(
|
|
348
|
-
endpoint: configuration.url,
|
|
349
|
-
api_key: configuration.key
|
|
350
|
-
)
|
|
351
|
-
@metric_flusher = MetricFlusher.new(
|
|
352
|
-
storage: @metric_storage,
|
|
353
|
-
submitter: submitter,
|
|
354
|
-
interval: configuration.metrics_flush_interval,
|
|
355
|
-
health_reporters: @trace_health_reporter ? [@trace_health_reporter] : []
|
|
356
|
-
)
|
|
357
|
-
@metric_flusher.start
|
|
358
|
-
log "Metrics flusher started (interval=#{configuration.metrics_flush_interval}s)"
|
|
359
|
-
|
|
360
|
-
at_exit { @metric_flusher&.stop }
|
|
361
|
-
else
|
|
362
|
-
log "Metrics submission not configured (missing url or key)"
|
|
363
|
-
end
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
# Payload transformers for different notification types
|
|
367
|
-
NOTIFICATION_TRANSFORMERS = {
|
|
368
|
-
"sql.active_record" => ->(payload) {
|
|
369
|
-
attrs = {}
|
|
370
|
-
attrs["db.system"] = payload[:connection]&.adapter_name&.downcase rescue nil
|
|
371
|
-
attrs["db.statement"] = payload[:sql] if payload[:sql]
|
|
372
|
-
attrs["name"] = payload[:name] if payload[:name]
|
|
373
|
-
attrs["db.name"] = payload[:connection]&.pool&.db_config&.name rescue nil
|
|
374
|
-
# Capture source location (app code that triggered this query)
|
|
375
|
-
SourceLocation.add_to_attributes(attrs)
|
|
376
|
-
attrs
|
|
377
|
-
},
|
|
378
|
-
"instantiation.active_record" => ->(payload) {
|
|
379
|
-
attrs = {}
|
|
380
|
-
attrs["record_count"] = payload[:record_count] if payload[:record_count]
|
|
381
|
-
attrs["class_name"] = payload[:class_name] if payload[:class_name]
|
|
382
|
-
attrs
|
|
383
|
-
},
|
|
384
|
-
"cache_read.active_support" => ->(payload) {
|
|
385
|
-
store = payload[:store]
|
|
386
|
-
store_name = store.is_a?(String) ? store : store&.class&.name
|
|
387
|
-
{ "key" => payload[:key]&.to_s, "hit" => payload[:hit], "store" => store_name }
|
|
388
|
-
},
|
|
389
|
-
"cache_write.active_support" => ->(payload) {
|
|
390
|
-
store = payload[:store]
|
|
391
|
-
store_name = store.is_a?(String) ? store : store&.class&.name
|
|
392
|
-
{ "key" => payload[:key]&.to_s, "store" => store_name }
|
|
393
|
-
},
|
|
394
|
-
"cache_delete.active_support" => ->(payload) {
|
|
395
|
-
store = payload[:store]
|
|
396
|
-
store_name = store.is_a?(String) ? store : store&.class&.name
|
|
397
|
-
{ "key" => payload[:key]&.to_s, "store" => store_name }
|
|
398
|
-
},
|
|
399
|
-
"cache_exist?.active_support" => ->(payload) {
|
|
400
|
-
store = payload[:store]
|
|
401
|
-
store_name = store.is_a?(String) ? store : store&.class&.name
|
|
402
|
-
{ "key" => payload[:key]&.to_s, "exist" => payload[:exist], "store" => store_name }
|
|
403
|
-
},
|
|
404
|
-
"cache_fetch_hit.active_support" => ->(payload) {
|
|
405
|
-
store = payload[:store]
|
|
406
|
-
store_name = store.is_a?(String) ? store : store&.class&.name
|
|
407
|
-
{ "key" => payload[:key]&.to_s, "store" => store_name }
|
|
408
|
-
},
|
|
409
|
-
"deliver.action_mailer" => ->(payload) {
|
|
410
|
-
attrs = {}
|
|
411
|
-
attrs["mailer"] = payload[:mailer] if payload[:mailer]
|
|
412
|
-
attrs["message_id"] = payload[:message_id] if payload[:message_id]
|
|
413
|
-
attrs["to"] = Array(payload[:to]).join(", ") if payload[:to]
|
|
414
|
-
attrs["subject"] = payload[:subject] if payload[:subject]
|
|
415
|
-
attrs
|
|
416
|
-
},
|
|
417
|
-
"process.action_mailer" => ->(payload) {
|
|
418
|
-
attrs = {}
|
|
419
|
-
attrs["mailer"] = payload[:mailer] if payload[:mailer]
|
|
420
|
-
attrs["action"] = payload[:action] if payload[:action]
|
|
421
|
-
attrs
|
|
422
|
-
}
|
|
423
|
-
}.freeze
|
|
424
|
-
|
|
425
|
-
def subscribe_to_notifications
|
|
426
|
-
NOTIFICATION_TRANSFORMERS.each do |pattern, transformer|
|
|
427
|
-
OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, pattern, transformer)
|
|
428
|
-
rescue
|
|
429
|
-
# Ignore errors for patterns that don't exist
|
|
430
|
-
end
|
|
431
|
-
|
|
432
|
-
# Auto-subscribe to custom patterns (default: "app.*")
|
|
433
|
-
# This lets users just do: ActiveSupport::Notifications.instrument("app.whatever") { }
|
|
434
|
-
subscribe_to_custom_patterns
|
|
435
|
-
end
|
|
436
|
-
|
|
437
|
-
def subscribe_to_custom_patterns
|
|
438
|
-
configuration.subscribe_patterns.each do |prefix|
|
|
439
|
-
# Subscribe to all notifications starting with this prefix
|
|
440
|
-
pattern = /\A#{Regexp.escape(prefix)}/
|
|
441
|
-
default_transformer = ->(payload) {
|
|
442
|
-
attrs = payload.transform_keys(&:to_s).select { |_, v|
|
|
443
|
-
v.is_a?(String) || v.is_a?(Numeric) || v.is_a?(TrueClass) || v.is_a?(FalseClass)
|
|
444
|
-
}
|
|
445
|
-
SourceLocation.add_to_attributes(attrs)
|
|
446
|
-
attrs
|
|
447
|
-
}
|
|
448
|
-
OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, pattern, default_transformer)
|
|
449
|
-
end
|
|
450
|
-
end
|
|
451
|
-
|
|
452
|
-
# Subscribe to any ActiveSupport::Notification and create spans for it
|
|
453
|
-
#
|
|
454
|
-
# @param pattern [String, Regexp] The notification pattern to subscribe to
|
|
455
|
-
# @param transformer [Proc, nil] Optional proc to transform payload into span attributes
|
|
456
|
-
# If nil, all payload keys become span attributes
|
|
457
|
-
#
|
|
458
|
-
# @example Subscribe to a custom notification
|
|
459
|
-
# Flare.subscribe("my_service.call")
|
|
460
|
-
#
|
|
461
|
-
# @example Subscribe with custom attribute transformer
|
|
462
|
-
# Flare.subscribe("stripe.charge") do |payload|
|
|
463
|
-
# { "charge_id" => payload[:id], "amount" => payload[:amount] }
|
|
464
|
-
# end
|
|
465
|
-
#
|
|
466
|
-
def subscribe(pattern, &transformer)
|
|
467
|
-
transformer ||= ->(payload) {
|
|
468
|
-
# Default: convert all payload keys to string attributes
|
|
469
|
-
payload.transform_keys(&:to_s).transform_values(&:to_s)
|
|
470
|
-
}
|
|
471
|
-
OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, pattern, transformer)
|
|
472
|
-
end
|
|
473
|
-
|
|
474
|
-
# Instrument a block of code, creating a span that shows up in Flare
|
|
475
|
-
#
|
|
476
|
-
# NOTE: This method only works when Flare is loaded (typically development).
|
|
477
|
-
# For instrumentation that works in all environments, use ActiveSupport::Notifications
|
|
478
|
-
# directly and subscribe with Flare.subscribe in your initializer.
|
|
479
|
-
#
|
|
480
|
-
# @param name [String] The name of the span (e.g., "my_service.call", "external_api.fetch")
|
|
481
|
-
# @param attributes [Hash] Optional attributes to add to the span
|
|
482
|
-
# @yield The block to instrument
|
|
483
|
-
# @return The return value of the block
|
|
484
|
-
#
|
|
485
|
-
# @example Basic usage (dev only)
|
|
486
|
-
# Flare.instrument("geocoding.lookup") do
|
|
487
|
-
# geocoder.lookup(address)
|
|
488
|
-
# end
|
|
489
|
-
#
|
|
490
|
-
# @example For all environments, use ActiveSupport::Notifications instead:
|
|
491
|
-
# # In your app code (works everywhere):
|
|
492
|
-
# ActiveSupport::Notifications.instrument("myapp.geocoding", address: addr) do
|
|
493
|
-
# geocoder.lookup(addr)
|
|
494
|
-
# end
|
|
495
|
-
#
|
|
496
|
-
# # In config/initializers/flare.rb (only loaded in dev):
|
|
497
|
-
# Flare.subscribe("myapp.geocoding")
|
|
498
|
-
#
|
|
499
|
-
def instrument(name, attributes = {}, &block)
|
|
500
|
-
return yield unless enabled?
|
|
501
|
-
|
|
502
|
-
# Add source location
|
|
503
|
-
location = SourceLocation.find
|
|
504
|
-
if location
|
|
505
|
-
attributes["code.filepath"] = location[:filepath]
|
|
506
|
-
attributes["code.lineno"] = location[:lineno]
|
|
507
|
-
attributes["code.function"] = location[:function] if location[:function]
|
|
508
|
-
end
|
|
509
|
-
|
|
510
|
-
tracer.in_span(name, attributes: attributes, kind: :internal) do |span|
|
|
511
|
-
yield span
|
|
512
|
-
end
|
|
513
|
-
end
|
|
514
|
-
|
|
515
|
-
def storage
|
|
516
|
-
@storage ||= begin
|
|
517
|
-
require_relative "flare/storage/sqlite"
|
|
518
|
-
Storage::SQLite.new(configuration.database_path)
|
|
519
|
-
rescue LoadError
|
|
520
|
-
warn "[Flare] sqlite3 gem not found. Dashboard is disabled. Add `gem 'sqlite3'` to your Gemfile to enable it."
|
|
521
|
-
configuration.spans_enabled = false
|
|
522
|
-
nil
|
|
523
|
-
end
|
|
524
|
-
end
|
|
525
|
-
|
|
526
|
-
def reset_storage!
|
|
527
|
-
@storage = nil
|
|
528
|
-
end
|
|
529
|
-
|
|
530
|
-
def reset!
|
|
531
|
-
@configuration = nil
|
|
532
|
-
@exporter = nil
|
|
533
|
-
@span_processor = nil
|
|
534
|
-
@tracer = nil
|
|
535
|
-
@storage = nil
|
|
536
|
-
@metric_flusher&.stop
|
|
537
|
-
@metric_flusher = nil
|
|
538
|
-
@metric_storage = nil
|
|
539
|
-
@otel_configured = false
|
|
540
|
-
end
|
|
541
|
-
end
|
|
542
|
-
|
|
543
|
-
require_relative "flare/storage"
|
|
544
|
-
require_relative "flare/engine" if defined?(Rails)
|
|
26
|
+
end
|
data/test/helper.rb
ADDED