Module: Flare
- Defined in:
- lib/flare.rb,
lib/flare/cli.rb,
lib/flare/engine.rb,
lib/flare/storage.rb,
lib/flare/version.rb,
lib/flare/cli/output.rb,
lib/flare/metric_key.rb,
lib/flare/configuration.rb,
lib/flare/backoff_policy.rb,
lib/flare/metric_counter.rb,
lib/flare/metric_flusher.rb,
lib/flare/metric_storage.rb,
lib/flare/storage/sqlite.rb,
lib/flare/source_location.rb,
lib/flare/sqlite_exporter.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,
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: ApplicationController, BackoffPolicy, Configuration, DoctorCommand, Engine, Error, HttpMetricsConfig, JobsController, MetricCounter, MetricFlusher, MetricKey, MetricSpanProcessor, MetricStorage, MetricSubmitter, RequestsController, SQLiteExporter, SetupCommand, SpansController, StatusCommand
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
- VERSION =
"0.2.0"
Class Method Summary
collapse
Class Method Details
.after_fork ⇒ Object
Re-initialize metric flusher after fork. Call this from Puma/Unicorn after_fork hooks.
124
125
126
|
# File 'lib/flare.rb', line 124
def after_fork
@metric_flusher&.after_fork
end
|
.configuration ⇒ Object
24
25
26
|
# File 'lib/flare.rb', line 24
def configuration
@configuration ||= Configuration.new
end
|
28
29
30
|
# File 'lib/flare.rb', line 28
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.
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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
|
# File 'lib/flare.rb', line 132
def configure_opentelemetry
return if @otel_configured
OpenTelemetry.logger = Logger.new(STDOUT, level: Logger::WARN)
service_name = if defined?(Rails) && Rails.application
Rails.application.class.module_parent_name.underscore rescue "rails_app"
else
"app"
end
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
log "Shutdown complete"
end
@otel_configured = true
end
|
.enabled? ⇒ Boolean
32
33
34
|
# File 'lib/flare.rb', line 32
def enabled?
configuration.enabled
end
|
.exporter ⇒ Object
65
66
67
68
69
70
71
72
73
74
|
# File 'lib/flare.rb', line 65
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
76
77
78
|
# File 'lib/flare.rb', line 76
def exporter=(exporter)
@exporter = exporter
end
|
.flush_metrics ⇒ Object
Manually flush metrics (useful for testing or forced flushes).
118
119
120
|
# File 'lib/flare.rb', line 118
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.
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
|
# File 'lib/flare.rb', line 376
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
59
60
61
62
63
|
# File 'lib/flare.rb', line 59
def log(message)
return unless configuration.debug
logger.info("[Flare] #{message}")
end
|
.logger ⇒ Object
51
52
53
|
# File 'lib/flare.rb', line 51
def logger
@logger ||= Logger.new(STDOUT)
end
|
.logger=(logger) ⇒ Object
55
56
57
|
# File 'lib/flare.rb', line 55
def logger=(logger)
@logger = logger
end
|
.metric_flusher ⇒ Object
109
110
111
|
# File 'lib/flare.rb', line 109
def metric_flusher
@metric_flusher
end
|
.metric_flusher=(flusher) ⇒ Object
113
114
115
|
# File 'lib/flare.rb', line 113
def metric_flusher=(flusher)
@metric_flusher = flusher
end
|
.metric_storage ⇒ Object
101
102
103
|
# File 'lib/flare.rb', line 101
def metric_storage
@metric_storage
end
|
.metric_storage=(storage) ⇒ Object
105
106
107
|
# File 'lib/flare.rb', line 105
def metric_storage=(storage)
@metric_storage = storage
end
|
.reset! ⇒ Object
407
408
409
410
411
412
413
414
415
416
417
|
# File 'lib/flare.rb', line 407
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
403
404
405
|
# File 'lib/flare.rb', line 403
def reset_storage!
@storage = nil
end
|
.span_processor ⇒ Object
80
81
82
83
84
85
86
87
|
# File 'lib/flare.rb', line 80
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
89
90
91
|
# File 'lib/flare.rb', line 89
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.
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
|
# File 'lib/flare.rb', line 212
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
)
@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
|
.storage ⇒ Object
392
393
394
395
396
397
398
399
400
401
|
# File 'lib/flare.rb', line 392
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
343
344
345
346
347
348
349
|
# File 'lib/flare.rb', line 343
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
314
315
316
317
318
319
320
321
322
323
324
325
326
327
|
# File 'lib/flare.rb', line 314
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
302
303
304
305
306
307
308
309
310
311
312
|
# File 'lib/flare.rb', line 302
def subscribe_to_notifications
NOTIFICATION_TRANSFORMERS.each do |pattern, transformer|
OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, pattern, transformer)
rescue
end
subscribe_to_custom_patterns
end
|
.tracer ⇒ Object
93
94
95
|
# File 'lib/flare.rb', line 93
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")
44
45
46
47
48
49
|
# File 'lib/flare.rb', line 44
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
97
98
99
|
# File 'lib/flare.rb', line 97
def untraced(&block)
OpenTelemetry::Common::Utilities.untraced(&block)
end
|