Class: RSpecTelemetry::Subscribers::Sql

Inherits:
Object
  • Object
show all
Defined in:
lib/rspec_telemetry/subscribers/sql.rb

Overview

Aggregates sql.active_record per example instead of writing one event per query: a request spec can fire thousands of queries per example, which would bloat the NDJSON file and slow the suite down. Queries are grouped by payload name ("Merchant Load", ...) and flushed as one sql.group event per (example, name) when the lifecycle calls #example_finished.

Constant Summary collapse

STACK_KEY =
:rspec_telemetry_sql_stack
FB_STACK_KEY =

Mirrors Subscribers::FactoryBot::STACK_KEY without forcing that file to load; a non-empty stack means this query runs inside a factory.

:rspec_telemetry_fb_stack
SKIP_NAMES =

SCHEMA is Rails introspection noise; TRANSACTION (BEGIN/COMMIT) mostly measures the test's own transaction wrapping. Nameless queries (raw #execute, and BEGIN/COMMIT on Rails < 7.1) are skipped too, because they cannot be told apart from transaction management reliably.

%w[SCHEMA TRANSACTION].freeze
SQL_TRUNCATE =
500

Instance Method Summary collapse

Constructor Details

#initialize(recorder) ⇒ Sql

Returns a new instance of Sql.



35
36
37
38
39
40
41
42
# File 'lib/rspec_telemetry/subscribers/sql.rb', line 35

def initialize(recorder)
  @recorder = recorder
  @subscription = nil
  # Queries can arrive from threads other than the example's (e.g. a
  # Capybara app server), so buckets are shared and keyed by example_id.
  @mutex = Mutex.new
  @buckets = {}
end

Instance Method Details

#example_finishedObject

Called by the lifecycle at example end, while the example id is still set, so the flushed sql.group events attach to the right example.



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/rspec_telemetry/subscribers/sql.rb', line 83

def example_finished
  RSpecTelemetry.safely("sql#example_finished") do
    example_id = Thread.current[Recorder::EXAMPLE_ID]
    groups = @mutex.synchronize { @buckets.delete(example_id) }
    next unless groups

    groups.each do |name, g|
      @recorder.record(
        "sql.group",
        name: name,
        count: g[:count],
        total_ms: g[:total_ms].round(3),
        max_ms: g[:max_ms].round(3),
        in_factory_count: g[:in_factory_count],
        in_factory_ms: g[:in_factory_ms].round(3),
        cached_count: g[:cached_count]
      )
    end
  end
end

#finish(_name, _id, payload) ⇒ Object



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/rspec_telemetry/subscribers/sql.rb', line 59

def finish(_name, _id, payload)
  RSpecTelemetry.safely("sql#finish") do
    started = stack.pop
    next unless started

    config = @recorder.config
    next unless config.enabled && config.capture_sql

    name = payload[:name]
    next if name.nil? || SKIP_NAMES.include?(name)

    now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
    ms = (now - started) * 1000.0
    cached = payload[:cached] ? true : false
    fb_stack = Thread.current[FB_STACK_KEY]
    in_factory = fb_stack && !fb_stack.empty?

    accumulate(name, ms, cached: cached, in_factory: in_factory)
    record_slow_query(name, ms, payload, in_factory) unless cached
  end
end

#start(_name, _id, _payload) ⇒ Object



53
54
55
56
57
# File 'lib/rspec_telemetry/subscribers/sql.rb', line 53

def start(_name, _id, _payload)
  RSpecTelemetry.safely("sql#start") do
    stack.push(Process.clock_gettime(Process::CLOCK_MONOTONIC))
  end
end

#subscribeObject



44
45
46
# File 'lib/rspec_telemetry/subscribers/sql.rb', line 44

def subscribe
  @subscription = ActiveSupport::Notifications.subscribe("sql.active_record", self)
end

#unsubscribeObject



48
49
50
51
# File 'lib/rspec_telemetry/subscribers/sql.rb', line 48

def unsubscribe
  ActiveSupport::Notifications.unsubscribe(@subscription) if @subscription
  @subscription = nil
end