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/client_headers.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, ClientHeaders, 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.1"
Class Method Summary
collapse
Class Method Details
.after_fork ⇒ Object
Re-initialize background threads after fork. Call this from Puma/Unicorn after_fork hooks.
161
162
163
164
|
# File 'lib/flare.rb', line 161
def after_fork
@metric_flusher&.after_fork
@rule_manager&.after_fork
end
|
.configuration ⇒ Object
34
35
36
|
# File 'lib/flare.rb', line 34
def configuration
@configuration ||= Configuration.new
end
|
38
39
40
|
# File 'lib/flare.rb', line 38
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.
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
247
|
# File 'lib/flare.rb', line 170
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
42
43
44
|
# File 'lib/flare.rb', line 42
def enabled?
configuration.enabled
end
|
.exporter ⇒ Object
75
76
77
78
79
80
81
82
83
84
|
# File 'lib/flare.rb', line 75
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
86
87
88
|
# File 'lib/flare.rb', line 86
def exporter=(exporter)
@exporter = exporter
end
|
.flush_metrics ⇒ Object
Manually flush metrics (useful for testing or forced flushes).
136
137
138
|
# File 'lib/flare.rb', line 136
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.
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
|
# File 'lib/flare.rb', line 497
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
69
70
71
72
73
|
# File 'lib/flare.rb', line 69
def log(message)
return unless configuration.debug
logger.info("[Flare] #{message}")
end
|
.logger ⇒ Object
61
62
63
|
# File 'lib/flare.rb', line 61
def logger
@logger ||= Logger.new(STDOUT)
end
|
.logger=(logger) ⇒ Object
65
66
67
|
# File 'lib/flare.rb', line 65
def logger=(logger)
@logger = logger
end
|
.marker ⇒ Object
129
|
# File 'lib/flare.rb', line 129
def marker = @marker
|
.metric_flusher ⇒ Object
119
120
121
|
# File 'lib/flare.rb', line 119
def metric_flusher
@metric_flusher
end
|
.metric_flusher=(flusher) ⇒ Object
123
124
125
|
# File 'lib/flare.rb', line 123
def metric_flusher=(flusher)
@metric_flusher = flusher
end
|
.metric_storage ⇒ Object
111
112
113
|
# File 'lib/flare.rb', line 111
def metric_storage
@metric_storage
end
|
.metric_storage=(storage) ⇒ Object
115
116
117
|
# File 'lib/flare.rb', line 115
def metric_storage=(storage)
@metric_storage = storage
end
|
.rails_env_name ⇒ Object
151
152
153
154
155
156
157
|
# File 'lib/flare.rb', line 151
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
528
529
530
531
532
533
534
535
536
537
538
|
# File 'lib/flare.rb', line 528
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
524
525
526
|
# File 'lib/flare.rb', line 524
def reset_storage!
@storage = nil
end
|
.rule_manager ⇒ Object
131
|
# File 'lib/flare.rb', line 131
def rule_manager = @rule_manager
|
.sampler ⇒ Object
Trace-sampling components, exposed for tests + manual after_fork wiring.
128
|
# File 'lib/flare.rb', line 128
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.
143
144
145
146
147
148
149
|
# File 'lib/flare.rb', line 143
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
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
328
|
# File 'lib/flare.rb', line 276
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
90
91
92
93
94
95
96
97
|
# File 'lib/flare.rb', line 90
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
99
100
101
|
# File 'lib/flare.rb', line 99
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.
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
362
|
# File 'lib/flare.rb', line 332
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.
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
|
# File 'lib/flare.rb', line 254
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
513
514
515
516
517
518
519
520
521
522
|
# File 'lib/flare.rb', line 513
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
464
465
466
467
468
469
470
|
# File 'lib/flare.rb', line 464
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
435
436
437
438
439
440
441
442
443
444
445
446
447
448
|
# File 'lib/flare.rb', line 435
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
423
424
425
426
427
428
429
430
431
432
433
|
# File 'lib/flare.rb', line 423
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
133
|
# File 'lib/flare.rb', line 133
def trace_health_reporter = @trace_health_reporter
|
.trace_span_processor ⇒ Object
132
|
# File 'lib/flare.rb', line 132
def trace_span_processor = @trace_span_processor
|
.tracer ⇒ Object
103
104
105
|
# File 'lib/flare.rb', line 103
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")
54
55
56
57
58
59
|
# File 'lib/flare.rb', line 54
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
107
108
109
|
# File 'lib/flare.rb', line 107
def untraced(&block)
OpenTelemetry::Common::Utilities.untraced(&block)
end
|
.upload_url_pool ⇒ Object
130
|
# File 'lib/flare.rb', line 130
def upload_url_pool = @upload_url_pool
|