Class: NewRelic::Agent::PumaStatsSampler

Inherits:
Object
  • Object
show all
Defined in:
lib/new_relic/agent/puma_stats_sampler.rb

Overview

Samples Puma's clustered server statistics from the master process and records them as timeslice metrics under Ruby/Puma/*.

Only the master exposes cluster-wide stats (per-worker thread-pool backlog, running threads, pool_capacity, max_threads, requests_count), so sampling happens there.

This class restarts the harvest thread in the master via NewRelic::Agent.after_fork(force_reconnect: true) to deliver the sampled metrics. This starts an additional agent connection in the master.

Records only integer gauges/counters via NewRelic::Agent.record_metric (no request, query, or user data), so it is fully functional under High Security Mode.

Defined Under Namespace

Classes: UnrecognizedStatsError

Constant Summary collapse

METRIC_NAMESPACE =
'Ruby/Puma'
INSTRUMENTATION_NAME =
'Puma'
WORKER_STAT_KEYS =

Per-worker stat keys, summed across the cluster. requests_count is a cumulative counter (use rate() in NRQL); the rest are point-in-time gauges. pool_capacity is spare request capacity: idle threads plus unspawned threads still allowed up to max_threads.

%i[backlog running pool_capacity max_threads requests_count].freeze
RUNNER_INFO_KEYS =

Runner metadata reported by Puma::Runner#stats alongside (or, before the runner's Puma::Server exists, instead of) the per-worker keys.

%i[started_at versions].freeze
DEFAULT_SAMPLE_RATE =
60
LOG_FAILURE_INTERVAL =
10
LOG_FAILURE_TIME_FLOOR_SECONDS =

Caps how long a persistent failure can go unlogged: forces a re-log once ~5 min have elapsed, instead of waiting for the backed-off "every Nth" cadence (~55 min at the default sample rate).

300
BACKOFF_EXPONENT_CAP =

Caps backoff growth at 2**3 = 8 sample intervals.

3

Instance Method Summary collapse

Constructor Details

#initialize(stats_source) ⇒ PumaStatsSampler

Returns a new instance of PumaStatsSampler.



50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/new_relic/agent/puma_stats_sampler.rb', line 50

def initialize(stats_source)
  @stats_source = stats_source
  @sample_rate = resolve_sample_rate
  @lock = Mutex.new
  @stop_signal = ConditionVariable.new
  @running = false
  @stopped = false
  @consecutive_failures = 0
  @last_failure_logged_at = nil
  @consecutive_report_failures = 0
  @last_report_failure_logged_at = nil
end

Instance Method Details

#startObject

Runs the sampling loop on the background thread the instrumentation spawns in the master. Blocks until #stop is called from a Puma lifecycle event. Single-shot: the instrumentation builds one sampler per Puma boot, so #start is never re-entered on the same instance.



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/new_relic/agent/puma_stats_sampler.rb', line 67

def start
  return unless ensure_master_is_reporting

  NewRelic::Agent.record_instrumentation_invocation(INSTRUMENTATION_NAME)

  @lock.synchronize do
    return if @stopped

    @running = true
    begin
      while @running
        sample
        # Wait one interval (releases the lock); #stop wakes us early.
        # Re-check @running because the signal may have flipped it.
        @stop_signal.wait(@lock, next_wait_interval) if @running
      end
    ensure
      # Keep @running consistent if the loop aborts via an exception.
      @running = false
    end
  end
rescue => e
  # The instrumentation starts this loop on a bare +Thread.new+, so an
  # unhandled exception would silently kill the sampler thread. Log
  # StandardError; log and re-raise Exception so interrupts still propagate.
  ::NewRelic::Agent.logger.error('NewRelic Puma stats sampler thread exited with error', e)
rescue Exception => e
  ::NewRelic::Agent.logger.error('NewRelic Puma stats sampler thread exited with exception. Re-raising in case of interrupt.', e)
  raise
end

#stopObject

Thread-safe: #stop runs on the Puma event thread, #start on the background thread.



100
101
102
103
104
105
106
# File 'lib/new_relic/agent/puma_stats_sampler.rb', line 100

def stop
  @lock.synchronize do
    @running = false
    @stopped = true
    @stop_signal.signal
  end
end