Module: Sixty::Instrument::Pg

Defined in:
lib/sixty/instrument/pg.rb

Overview

The pg gem, patched directly.

── Who this is for ───────────────────────────────────────────────────────

A Rails application is covered by sql.active_record, which is a public interface every adapter emits and every version has — strictly better than patching, so when ActiveRecord is in the process this is not installed at all. What it covers is everything else: Sinatra, Roda, a Sidekiq worker that talks to Postgres directly, Sequel and ROM (both of which run their queries through this class), and scripts.

── What is captured beyond timing ───────────────────────────────────────

rows   : `ntuples` for a read, `cmd_tuples` for a write. The 30 → 30,000
       signal, and the one thing a slow-query log cannot show you.
fields : column count, which is how `select *` creeping into a hot path
       becomes visible.
bytes  : a sampled estimate of the result's size.

Never the SQL text, and never a parameter: only the normalized shape.

── No query plans on this path ──────────────────────────────────────────

The ActiveRecord instrumentation captures plans because it can borrow a connection from a pool that is designed to be borrowed from. Here there is no pool — only the caller's own PG::Connection, which is not thread-safe and may be mid-query on another thread when the flush thread wakes up. The alternatives are worse: running the EXPLAIN inline puts a second round trip in front of a user, and opening a connection of our own means an observability agent quietly consuming a slot in somebody's connection limit. So plans are a Rails feature for now, and this path reports everything else.

Defined Under Namespace

Modules: Patch

Constant Summary collapse

TEXT_FIRST =

exec, query and async_exec are the same method under three names in modern pg, and super resolves by name — so a call to exec reaches the original exec rather than this module's async_exec, and nothing is counted twice. sync_* are separate implementations and need their own wrapper.

%i[exec query async_exec sync_exec exec_params async_exec_params
sync_exec_params].freeze
MAX_PREPARED =

Prepared statements name a query somewhere other than the call site. exec_prepared('find_user', [id]) has no SQL in it at all, so the text is remembered when the statement is prepared and looked up when it is run. Bounded, because a process that has prepared a thousand distinct statements is not going to learn much from the next one.

500

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.configObject (readonly)

Returns the value of attribute config.



64
65
66
# File 'lib/sixty/instrument/pg.rb', line 64

def config
  @config
end

Class Method Details

.install(config = nil) ⇒ Object



51
52
53
54
55
56
57
58
# File 'lib/sixty/instrument/pg.rb', line 51

def install(config = nil)
  return false if @installed
  return false unless defined?(::PG::Connection)

  @config = config
  ::PG::Connection.prepend(Patch)
  @installed = true
end

.installed?Boolean

Returns:

  • (Boolean)


60
61
62
# File 'lib/sixty/instrument/pg.rb', line 60

def installed?
  @installed == true
end

.measure(sql) ⇒ Object

One span for one statement. Shared by every wrapper below so that the rules — never raise, always emit, capture frames once — live in one place rather than in seven.



93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/sixty/instrument/pg.rb', line 93

def measure(sql)
  return yield unless Sixty.enabled? && sql.is_a?(String) && !sql.empty?

  normalized = Sql.normalize_sql(sql)
  started = Tracer.monotonic_ms
  begin
    result = yield
  rescue StandardError => e
    record(sql, normalized, Tracer.monotonic_ms - started, nil, e)
    raise
  end
  record(sql, normalized, Tracer.monotonic_ms - started, result, nil)
  result
end

.prepared_sql(name) ⇒ Object



81
82
83
# File 'lib/sixty/instrument/pg.rb', line 81

def prepared_sql(name)
  @prepared&.[](name)
end

.remember_prepared(name, sql) ⇒ Object



73
74
75
76
77
78
79
# File 'lib/sixty/instrument/pg.rb', line 73

def remember_prepared(name, sql)
  return unless name.is_a?(String) && sql.is_a?(String)

  @prepared ||= {}
  @prepared.clear if @prepared.size >= MAX_PREPARED
  @prepared[name] = sql
end

.reset!Object



85
86
87
88
# File 'lib/sixty/instrument/pg.rb', line 85

def reset!
  @installed = false
  @prepared = nil
end