Module: Flare
- Defined in:
- lib/flare.rb,
lib/flare/cli.rb,
lib/flare/engine.rb,
lib/flare/marker.rb,
lib/flare/sampler.rb,
lib/flare/storage.rb,
lib/flare/version.rb,
lib/flare/cli/output.rb,
lib/flare/metric_key.rb,
lib/flare/trace_blob.rb,
lib/flare/rule_manager.rb,
lib/flare/configuration.rb,
lib/flare/backoff_policy.rb,
lib/flare/http_transport.rb,
lib/flare/metric_counter.rb,
lib/flare/metric_flusher.rb,
lib/flare/metric_storage.rb,
lib/flare/storage/sqlite.rb,
lib/flare/trace_exporter.rb,
lib/flare/source_location.rb,
lib/flare/sqlite_exporter.rb,
lib/flare/upload_url_pool.rb,
lib/flare/metric_submitter.rb,
lib/flare/cli/setup_command.rb,
lib/flare/cli/doctor_command.rb,
lib/flare/cli/status_command.rb,
lib/flare/http_metrics_config.rb,
lib/flare/metric_span_processor.rb,
lib/flare/trace_health_reporter.rb,
lib/flare/web_marker_subscriber.rb,
lib/flare/filtering_span_processor.rb,
app/helpers/flare/application_helper.rb,
app/controllers/flare/jobs_controller.rb,
app/controllers/flare/spans_controller.rb,
app/controllers/flare/requests_controller.rb,
app/controllers/flare/application_controller.rb
Defined Under Namespace
Modules: ApplicationHelper, CLI, SourceLocation, Storage
Classes: AlwaysRecordOnly, ApplicationController, BackoffPolicy, Configuration, DoctorCommand, Engine, Error, FilteringSpanProcessor, HttpMetricsConfig, HttpTransport, JobsController, Marker, MetricCounter, MetricFlusher, MetricKey, MetricSpanProcessor, MetricStorage, MetricSubmitter, RequestsController, RuleManager, SQLiteExporter, Sampler, SetupCommand, SpansController, StatusCommand, TraceBlob, TraceExporter, TraceHealthReporter, UploadUrlPool, WebMarkerSubscriber
Constant Summary
collapse
- MISSING_PARENT_ID =
"0000000000000000"
- TRANSACTION_NAME_ATTRIBUTE =
"flare.transaction_name"
- NOTIFICATION_TRANSFORMERS =
Payload transformers for different notification types
{
"sql.active_record" => ->(payload) {
attrs = {}
attrs["db.system"] = payload[:connection]&.adapter_name&.downcase rescue nil
attrs["db.statement"] = payload[:sql] if payload[:sql]
attrs["name"] = payload[:name] if payload[:name]
attrs["db.name"] = payload[:connection]&.pool&.db_config&.name rescue nil
SourceLocation.add_to_attributes(attrs)
attrs
},
"instantiation.active_record" => ->(payload) {
attrs = {}
attrs["record_count"] = payload[:record_count] if payload[:record_count]
attrs["class_name"] = payload[:class_name] if payload[:class_name]
attrs
},
"cache_read.active_support" => ->(payload) {
store = payload[:store]
store_name = store.is_a?(String) ? store : store&.class&.name
{ "key" => payload[:key]&.to_s, "hit" => payload[:hit], "store" => store_name }
},
"cache_write.active_support" => ->(payload) {
store = payload[:store]
store_name = store.is_a?(String) ? store : store&.class&.name
{ "key" => payload[:key]&.to_s, "store" => store_name }
},
"cache_delete.active_support" => ->(payload) {
store = payload[:store]
store_name = store.is_a?(String) ? store : store&.class&.name
{ "key" => payload[:key]&.to_s, "store" => store_name }
},
"cache_exist?.active_support" => ->(payload) {
store = payload[:store]
store_name = store.is_a?(String) ? store : store&.class&.name
{ "key" => payload[:key]&.to_s, "exist" => payload[:exist], "store" => store_name }
},
"cache_fetch_hit.active_support" => ->(payload) {
store = payload[:store]
store_name = store.is_a?(String) ? store : store&.class&.name
{ "key" => payload[:key]&.to_s, "store" => store_name }
},
"deliver.action_mailer" => ->(payload) {
attrs = {}
attrs["mailer"] = payload[:mailer] if payload[:mailer]
attrs["message_id"] = payload[:message_id] if payload[:message_id]
attrs["to"] = Array(payload[:to]).join(", ") if payload[:to]
attrs["subject"] = payload[:subject] if payload[:subject]
attrs
},
"process.action_mailer" => ->(payload) {
attrs = {}
attrs["mailer"] = payload[:mailer] if payload[:mailer]
attrs["action"] = payload[:action] if payload[:action]
attrs
}
}.freeze
- ALWAYS_RECORD_ONLY =
AlwaysRecordOnly.new
- VERSION =
"0.3.0"
Class Method Summary
collapse
Class Method Details
.after_fork ⇒ Object
Re-initialize background threads after fork. Call this from Puma/Unicorn after_fork hooks.
160
161
162
163
|
# File 'lib/flare.rb', line 160
def after_fork
@metric_flusher&.after_fork
@rule_manager&.after_fork
end
|
.configuration ⇒ Object
33
34
35
|
# File 'lib/flare.rb', line 33
def configuration
@configuration ||= Configuration.new
end
|
37
38
39
|
# File 'lib/flare.rb', line 37
def configure
yield(configuration) if block_given?
end
|
Configure OpenTelemetry SDK and instrumentations. Must run before the middleware stack is built so Rack/ActionPack can insert their middleware. Note: metrics flusher is started separately via start_metrics_flusher after user initializers have run.
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
|
# File 'lib/flare.rb', line 169
def configure_opentelemetry
return if @otel_configured
OpenTelemetry.logger = Logger.new(STDOUT, level: Logger::WARN)
service_name = service_name_for_app
require "opentelemetry-instrumentation-rack"
require "opentelemetry-instrumentation-net_http"
require "opentelemetry-instrumentation-active_support"
require "opentelemetry/instrumentation/active_support/span_subscriber"
require "opentelemetry-instrumentation-action_pack" if defined?(ActionController)
require "opentelemetry-instrumentation-action_view" if defined?(ActionView)
require "opentelemetry-instrumentation-active_job" if defined?(ActiveJob)
ENV["OTEL_TRACES_EXPORTER"] ||= "none"
log "Configuring OpenTelemetry (service=#{service_name})"
OpenTelemetry::SDK.configure do |c|
c.service_name = service_name
if configuration.spans_enabled && exporter
c.add_span_processor(span_processor)
log "Spans enabled (database=#{configuration.database_path})"
end
c.use_all(
"OpenTelemetry::Instrumentation::Rack" => {
untraced_requests: ->(env) {
request = Rack::Request.new(env)
return true if request.path.start_with?("/flare")
configuration.ignore_request.call(request)
}
},
"OpenTelemetry::Instrumentation::Sidekiq" => {
span_naming: :job_class,
}
)
end
if configuration.spans_enabled || configuration.metrics_enabled
subscribe_to_notifications
end
at_exit do
log "Shutting down..."
if configuration.spans_enabled && @span_processor
span_processor.force_flush
span_processor.shutdown
log "Span processor flushed and stopped"
end
if @trace_span_processor
@trace_span_processor.force_flush
@trace_span_processor.shutdown
log "Trace span processor flushed and stopped"
end
log "Shutdown complete"
end
@otel_configured = true
end
|
.enabled? ⇒ Boolean
41
42
43
|
# File 'lib/flare.rb', line 41
def enabled?
configuration.enabled
end
|
.exporter ⇒ Object
74
75
76
77
78
79
80
81
82
83
|
# File 'lib/flare.rb', line 74
def exporter
@exporter ||= begin
require_relative "flare/sqlite_exporter"
SQLiteExporter.new(configuration.database_path)
rescue LoadError
warn "[Flare] sqlite3 gem not found. Spans are disabled. Add `gem 'sqlite3'` to your Gemfile to enable the development dashboard."
configuration.spans_enabled = false
nil
end
end
|
.exporter=(exporter) ⇒ Object
85
86
87
|
# File 'lib/flare.rb', line 85
def exporter=(exporter)
@exporter = exporter
end
|
.flush_metrics ⇒ Object
Manually flush metrics (useful for testing or forced flushes).
135
136
137
|
# File 'lib/flare.rb', line 135
def flush_metrics
@metric_flusher&.flush_now || 0
end
|
.instrument(name, attributes = {}) { ... } ⇒ Object
Instrument a block of code, creating a span that shows up in Flare
NOTE: This method only works when Flare is loaded (typically development). For instrumentation that works in all environments, use ActiveSupport::Notifications directly and subscribe with Flare.subscribe in your initializer.
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
|
# File 'lib/flare.rb', line 496
def instrument(name, attributes = {}, &block)
return yield unless enabled?
location = SourceLocation.find
if location
attributes["code.filepath"] = location[:filepath]
attributes["code.lineno"] = location[:lineno]
attributes["code.function"] = location[:function] if location[:function]
end
tracer.in_span(name, attributes: attributes, kind: :internal) do |span|
yield span
end
end
|
.log(message) ⇒ Object
68
69
70
71
72
|
# File 'lib/flare.rb', line 68
def log(message)
return unless configuration.debug
logger.info("[Flare] #{message}")
end
|
.logger ⇒ Object
60
61
62
|
# File 'lib/flare.rb', line 60
def logger
@logger ||= Logger.new(STDOUT)
end
|
.logger=(logger) ⇒ Object
64
65
66
|
# File 'lib/flare.rb', line 64
def logger=(logger)
@logger = logger
end
|
.marker ⇒ Object
128
|
# File 'lib/flare.rb', line 128
def marker = @marker
|
.metric_flusher ⇒ Object
118
119
120
|
# File 'lib/flare.rb', line 118
def metric_flusher
@metric_flusher
end
|
.metric_flusher=(flusher) ⇒ Object
122
123
124
|
# File 'lib/flare.rb', line 122
def metric_flusher=(flusher)
@metric_flusher = flusher
end
|
.metric_storage ⇒ Object
110
111
112
|
# File 'lib/flare.rb', line 110
def metric_storage
@metric_storage
end
|
.metric_storage=(storage) ⇒ Object
114
115
116
|
# File 'lib/flare.rb', line 114
def metric_storage=(storage)
@metric_storage = storage
end
|
.rails_env_name ⇒ Object
150
151
152
153
154
155
156
|
# File 'lib/flare.rb', line 150
def rails_env_name
if defined?(Rails) && Rails.respond_to?(:env)
Rails.env.to_s
else
ENV.fetch("RACK_ENV", "development")
end
end
|
.reset! ⇒ Object
527
528
529
530
531
532
533
534
535
536
537
|
# File 'lib/flare.rb', line 527
def reset!
@configuration = nil
@exporter = nil
@span_processor = nil
@tracer = nil
@storage = nil
@metric_flusher&.stop
@metric_flusher = nil
@metric_storage = nil
@otel_configured = false
end
|
.reset_storage! ⇒ Object
523
524
525
|
# File 'lib/flare.rb', line 523
def reset_storage!
@storage = nil
end
|
.rule_manager ⇒ Object
130
|
# File 'lib/flare.rb', line 130
def rule_manager = @rule_manager
|
.sampler ⇒ Object
Trace-sampling components, exposed for tests + manual after_fork wiring.
127
|
# File 'lib/flare.rb', line 127
def sampler = @sampler
|
.service_name_for_app ⇒ Object
Default project key, derived from the host Rails app’s module name. Customers can override by configuring something else once we expose configuration.project; for v0.3 this matches MetricSubmitter’s behavior.
142
143
144
145
146
147
148
|
# File 'lib/flare.rb', line 142
def service_name_for_app
if defined?(Rails) && Rails.respond_to?(:application) && Rails.application
Rails.application.class.module_parent_name.underscore rescue "rails_app"
else
"app"
end
end
|
.setup_tracing_components ⇒ Object
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
|
# File 'lib/flare.rb', line 275
def setup_tracing_components
return if @trace_span_processor
@sampler = Sampler.new
@marker = Marker.new
@upload_url_pool = UploadUrlPool.new
OpenTelemetry.tracer_provider.sampler =
OpenTelemetry::SDK::Trace::Samplers.parent_based(
root: @sampler,
remote_parent_sampled: ALWAYS_RECORD_ONLY,
remote_parent_not_sampled: ALWAYS_RECORD_ONLY,
local_parent_not_sampled: ALWAYS_RECORD_ONLY
)
@trace_exporter = TraceExporter.new(
pool: @upload_url_pool,
notify_url: "#{configuration.url.to_s.chomp('/')}/api/traces",
api_key: configuration.key,
project: service_name_for_app,
environment: rails_env_name
)
@trace_span_processor = FilteringSpanProcessor.new(
exporter: @trace_exporter,
marker: @marker,
max_queue: configuration.tracing_max_queue
)
OpenTelemetry.tracer_provider.add_span_processor(@trace_span_processor)
@trace_health_reporter = TraceHealthReporter.new(
processor: @trace_span_processor,
pool: @upload_url_pool,
exporter: @trace_exporter
)
if defined?(ActiveSupport::Notifications)
@web_marker_subscriber = WebMarkerSubscriber.new(sampler: @sampler, marker: @marker).start
end
log "Tracing enabled (poll=#{configuration.tracing_poll_interval}s)"
end
|
.span_processor ⇒ Object
89
90
91
92
93
94
95
96
|
# File 'lib/flare.rb', line 89
def span_processor
@span_processor ||= OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
exporter,
max_queue_size: 1000,
max_export_batch_size: 100,
schedule_delay: 1000 )
end
|
.span_processor=(span_processor) ⇒ Object
98
99
100
|
# File 'lib/flare.rb', line 98
def span_processor=(span_processor)
@span_processor = span_processor
end
|
.start_metrics_flusher ⇒ Object
Start the metrics flusher. Called from config.after_initialize so user configuration (metrics_enabled, flush_interval, etc.) is applied.
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
|
# File 'lib/flare.rb', line 331
def start_metrics_flusher
return unless configuration.metrics_enabled
@metric_storage ||= MetricStorage.new
metric_processor = MetricSpanProcessor.new(
storage: @metric_storage,
http_metrics_config: configuration.http_metrics_config
)
OpenTelemetry.tracer_provider.add_span_processor(metric_processor)
log "Metrics enabled (endpoint=#{configuration.url} key=#{configuration.key ? 'present' : 'missing'})"
if configuration.metrics_submission_configured?
submitter = MetricSubmitter.new(
endpoint: configuration.url,
api_key: configuration.key
)
@metric_flusher = MetricFlusher.new(
storage: @metric_storage,
submitter: submitter,
interval: configuration.metrics_flush_interval,
health_reporters: @trace_health_reporter ? [@trace_health_reporter] : []
)
@metric_flusher.start
log "Metrics flusher started (interval=#{configuration.metrics_flush_interval}s)"
at_exit { @metric_flusher&.stop }
else
log "Metrics submission not configured (missing url or key)"
end
end
|
.start_rule_manager ⇒ Object
Start the trace-rules poller. Polls GET /api/rules every tracing_poll_interval (default 30s) so the in-process sampler + URL pool stay current. Called from config.after_initialize – after the user’s configure block has run – so configuration.url / .key / .tracing_enabled are settled.
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
|
# File 'lib/flare.rb', line 253
def start_rule_manager
return unless configuration.tracing_submission_configured?
setup_tracing_components
return unless @sampler && @marker && @upload_url_pool
@rule_manager = RuleManager.new(
sampler: @sampler,
marker: @marker,
pool: @upload_url_pool,
base_url: configuration.url,
api_key: configuration.key,
project: service_name_for_app,
environment: rails_env_name,
interval: configuration.tracing_poll_interval
)
@rule_manager.start
log "Rule manager started (poll=#{configuration.tracing_poll_interval}s)"
at_exit { @rule_manager&.stop }
end
|
.storage ⇒ Object
512
513
514
515
516
517
518
519
520
521
|
# File 'lib/flare.rb', line 512
def storage
@storage ||= begin
require_relative "flare/storage/sqlite"
Storage::SQLite.new(configuration.database_path)
rescue LoadError
warn "[Flare] sqlite3 gem not found. Dashboard is disabled. Add `gem 'sqlite3'` to your Gemfile to enable it."
configuration.spans_enabled = false
nil
end
end
|
.subscribe(pattern, &transformer) ⇒ Object
Subscribe to any ActiveSupport::Notification and create spans for it
463
464
465
466
467
468
469
|
# File 'lib/flare.rb', line 463
def subscribe(pattern, &transformer)
transformer ||= ->(payload) {
payload.transform_keys(&:to_s).transform_values(&:to_s)
}
OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, pattern, transformer)
end
|
.subscribe_to_custom_patterns ⇒ Object
434
435
436
437
438
439
440
441
442
443
444
445
446
447
|
# File 'lib/flare.rb', line 434
def subscribe_to_custom_patterns
configuration.subscribe_patterns.each do |prefix|
pattern = /\A#{Regexp.escape(prefix)}/
default_transformer = ->(payload) {
attrs = payload.transform_keys(&:to_s).select { |_, v|
v.is_a?(String) || v.is_a?(Numeric) || v.is_a?(TrueClass) || v.is_a?(FalseClass)
}
SourceLocation.add_to_attributes(attrs)
attrs
}
OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, pattern, default_transformer)
end
end
|
.subscribe_to_notifications ⇒ Object
422
423
424
425
426
427
428
429
430
431
432
|
# File 'lib/flare.rb', line 422
def subscribe_to_notifications
NOTIFICATION_TRANSFORMERS.each do |pattern, transformer|
OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, pattern, transformer)
rescue
end
subscribe_to_custom_patterns
end
|
.trace_health_reporter ⇒ Object
132
|
# File 'lib/flare.rb', line 132
def trace_health_reporter = @trace_health_reporter
|
.trace_span_processor ⇒ Object
131
|
# File 'lib/flare.rb', line 131
def trace_span_processor = @trace_span_processor
|
.tracer ⇒ Object
102
103
104
|
# File 'lib/flare.rb', line 102
def tracer
@tracer ||= OpenTelemetry.tracer_provider.tracer("Flare", Flare::VERSION)
end
|
.transaction_name(name) ⇒ Object
Set the transaction name for the current span. This overrides the default name derived from Rails controller/action or job class.
Useful for Rack middleware, mounted apps, or any request that doesn’t go through the Rails router.
Flare.transaction_name("RestApi::Routes::Audits#get")
53
54
55
56
57
58
|
# File 'lib/flare.rb', line 53
def transaction_name(name)
span = OpenTelemetry::Trace.current_span
return unless span.respond_to?(:set_attribute)
span.set_attribute(TRANSACTION_NAME_ATTRIBUTE, name)
end
|
.untraced(&block) ⇒ Object
106
107
108
|
# File 'lib/flare.rb', line 106
def untraced(&block)
OpenTelemetry::Common::Utilities.untraced(&block)
end
|
.upload_url_pool ⇒ Object
129
|
# File 'lib/flare.rb', line 129
def upload_url_pool = @upload_url_pool
|