Class: Errorgap::SpanCollector
- Inherits:
-
Object
- Object
- Errorgap::SpanCollector
- Defined in:
- lib/errorgap/span_collector.rb
Constant Summary collapse
- THREAD_KEY =
:errorgap_spans- NORMALIZE_PATTERN =
Quoted strings and standalone numeric literals → ?
/ '(?:[^'\\]|\\.)*' # single-quoted string literals | \b\d+(?:\.\d+)?\b # integer and float literals /x- SKIP_NAMES =
%w[SCHEMA EXPLAIN CACHE].freeze
- LIB_ROOT =
File.("..", __dir__)
- GEM_PATH_MARKERS =
["/gems/", "/rubygems/", "/lib/ruby/", "/bundler/"].freeze
Class Method Summary collapse
-
.app_call_site ⇒ Object
The first backtrace frame belonging to the application — skipping this gem, other gems, and the Ruby standard library — so a query can be attributed to the app code that ran it.
- .flush ⇒ Object
-
.install ⇒ Object
Call once at boot (from Railtie).
- .normalize_sql(sql) ⇒ Object
- .start ⇒ Object
- .store ⇒ Object
Class Method Details
.app_call_site ⇒ Object
The first backtrace frame belonging to the application — skipping this gem, other gems, and the Ruby standard library — so a query can be attributed to the app code that ran it. Returns [file, line, fn_name] with the file relative to Rails.root when available, or nils when no app frame is present (e.g. queries run from a console or gem).
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/errorgap/span_collector.rb', line 81 def app_call_site root = defined?(Rails) && Rails.respond_to?(:root) && Rails.root ? Rails.root.to_s : nil location = caller_locations(1, 60)&.find do |loc| path = loc.absolute_path || loc.path next false unless path next false if path.start_with?(LIB_ROOT) next false if GEM_PATH_MARKERS.any? { |marker| path.include?(marker) } root ? path.start_with?(root) : true end return [nil, nil, nil] unless location path = location.absolute_path || location.path path = path.delete_prefix("#{root}/") if root [path, location.lineno, location.label] end |
.flush ⇒ Object
62 63 64 65 66 |
# File 'lib/errorgap/span_collector.rb', line 62 def flush spans = Thread.current[THREAD_KEY] || [] Thread.current[THREAD_KEY] = nil spans end |
.install ⇒ Object
Call once at boot (from Railtie). Subscribers only record while a transaction is being collected (the middleware starts one per request when APM is enabled), so installing unconditionally is safe — and required, since app initializers that enable APM run after railtie initializers.
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/errorgap/span_collector.rb', line 22 def install return unless defined?(ActiveSupport::Notifications) return if @installed @installed = true # The 5-arg block form receives the event's start/finish times; the # sql.active_record payload itself carries no duration key. ActiveSupport::Notifications.subscribe("sql.active_record") do |_name, started, finished, _id, payload| next unless Thread.current[THREAD_KEY] next if SKIP_NAMES.any? { |n| payload[:name]&.start_with?(n) } next if payload[:cached] duration_ms = ((finished - started) * 1000.0).to_f sql = normalize_sql(payload[:sql].to_s) file, line, fn_name = app_call_site store << Span.new( kind: "db", sql: sql, file: file, line: line, fn_name: fn_name, duration_ms: duration_ms.round(3) ) end # Rails reports view_runtime with database time already subtracted, so # this does not double count the db spans above. ActiveSupport::Notifications.subscribe("process_action.action_controller") do |*, payload| next unless Thread.current[THREAD_KEY] view_ms = payload[:view_runtime] next unless view_ms&.positive? store << Span.new(kind: "view", duration_ms: view_ms.round(3)) end end |
.normalize_sql(sql) ⇒ Object
72 73 74 |
# File 'lib/errorgap/span_collector.rb', line 72 def normalize_sql(sql) sql.gsub(NORMALIZE_PATTERN, "?").gsub(/\s+/, " ").strip end |
.start ⇒ Object
58 59 60 |
# File 'lib/errorgap/span_collector.rb', line 58 def start Thread.current[THREAD_KEY] = [] end |
.store ⇒ Object
68 69 70 |
# File 'lib/errorgap/span_collector.rb', line 68 def store Thread.current[THREAD_KEY] ||= [] end |