Class: Wurk::Metrics::Prometheus
- Inherits:
-
Object
- Object
- Wurk::Metrics::Prometheus
- Includes:
- Exposition
- Defined in:
- lib/wurk/metrics/prometheus.rb
Overview
Prometheus text exposition (format 0.0.4) served by the health listener at
GET /metrics. Reads only keys Sidekiq already writes — no new Redis keys.
Two families, and the difference matters when aggregating:
* cluster gauges/counters (`wurk_queue_*`, `wurk_processed_total`, …)
come straight from shared Redis, so every scrape target reports the
same value. Aggregate with `max`, never `sum`.
* `wurk_process_*` / `wurk_swarm_*` describe THIS host's process group:
heartbeat identities sharing this process's nonce (the swarm parent
stamps one nonce that every child inherits), so a scrape of one pod
covers all of its children no matter which child owns the port.
`wurk_leader` is the same: 1 on every scrape target in the group that
holds the lease.
* `wurk_fetch_errors_total`, `wurk_reaper_reclaimed_total` and the other
ProcessCounters families, the Redis pool gauges and
`wurk_job_duration_seconds` count in the memory of the process that
owns the port, so under a swarm they are a sample of one child. `rate`
them; a child restart resets them.
* `wurk_swarm_child_restarts_total` / `wurk_swarm_crash_streak` are the
swarm parent's, read through Swarm::RestartCounters, so they survive
child restarts; present only inside a swarm child.
One Redis checkout per refresh — two pipelines, because the second needs
the queue and process names the first returns — and the snapshot is cached
for ttl seconds, so a scrape storm costs at most one refresh per window.
A failed refresh is cached for the same window: a dead Redis is not
hammered once per scrape.
Defined Under Namespace
Classes: ProcessRow, QueueRow, Snapshot
Constant Summary collapse
- CONTENT_TYPE =
'text/plain; version=0.0.4; charset=utf-8'- TTL =
1.0- PROCESS_FIELDS =
%w[busy concurrency beat rss quiet info rtt_us].freeze
- RUN_AT_RE =
The last field of a heartbeat work entry (
{"queue":…,"payload":…,"run_at":…}); anchored at the end so arun_atinside the escaped payload never matches. /"run_at":(\d+(?:\.\d+)?)\}\z/- PSS_FIELDS =
%w[Pss].freeze
- SCALARS =
{ processed: ['GET', Keys::STAT_PROCESSED], failed: ['GET', 'stat:failed'], scheduled: ['ZCARD', Keys::SCHEDULE], retries: ['ZCARD', Keys::RETRY], dead: ['ZCARD', Keys::DEAD] }.freeze
- SET_HEADS =
Score of each set's earliest member: past due by now - score when the scheduler is behind.
Scheduled::SETS.map { |set| ['ZRANGE', set, 0, 0, 'WITHSCORES'] }.freeze
- FIRST_PASS =
Names come last: the detail pass is built from them.
[*SCALARS.values, *SET_HEADS, ['GET', Wurk::Leader::DEFAULT_KEY], ['SMEMBERS', Keys::QUEUES_SET], ['SMEMBERS', Keys::PROCESSES]].freeze
- QUEUE_GAUGES =
[ ['wurk_queue_size', 'Jobs enqueued per queue.', :depth.to_proc], ['wurk_queue_latency_seconds', 'Age of the oldest job per queue.', :latency.to_proc], ['wurk_queue_consumers', 'Live processes subscribed to the queue, quiet ones included.', :consumers.to_proc] ].freeze
- CLUSTER_GAUGES =
[ ['wurk_scheduled_size', 'Jobs in the schedule set.', :scheduled.to_proc], ['wurk_retry_size', 'Jobs in the retry set.', :retries.to_proc], ['wurk_dead_size', 'Jobs in the dead set.', :dead.to_proc], ['wurk_processes', 'Live worker processes, cluster-wide.', ->(snap) { snap.processes.size }], ['wurk_busy', 'Jobs executing right now, cluster-wide.', ->(snap) { snap.processes.sum(&:busy) }], ['wurk_concurrency', 'Worker threads, cluster-wide.', ->(snap) { snap.processes.sum(&:concurrency) }] ].freeze
- PROCESS_GAUGES =
[ ['wurk_process_busy', 'Jobs executing in this local process.', ->(row, _now) { row.busy }], ['wurk_process_concurrency', 'Worker threads in this local process.', ->(row, _now) { row.concurrency }], ['wurk_process_rss_bytes', 'Resident set size reported by the heartbeat.', ->(row, _now) { row.rss_kb * 1024 }], ['wurk_process_heartbeat_age_seconds', 'Seconds since the last heartbeat.', ->(row, now) { row.beat ? [now - row.beat, 0.0].max : 0.0 }], ['wurk_process_quiet', '1 when the process is quiet (TSTP).', ->(row, _now) { row.quiet ? 1 : 0 }], ['wurk_process_redis_rtt_seconds', 'Round trip of the last heartbeat write.', ->(row, _now) { row.rtt_us / 1_000_000.0 }], ['wurk_process_oldest_job_seconds', 'Age of the longest-running job in the process; 0 when idle.', ->(row, now) { row.oldest_run_at ? [now - row.oldest_run_at, 0.0].max : 0.0 }], ['wurk_process_pss_bytes', 'Proportional set size from /proc/<pid>/smaps_rollup (Linux only).', ->(row, _now) { pss_bytes(row.pid) }] ].freeze
Class Method Summary collapse
Instance Method Summary collapse
-
#fresh_local(snap, window, now = ::Time.now.to_f) ⇒ Object
Local processes whose heartbeat landed within
windowseconds. -
#initialize(config, ttl: TTL, nonce: Component::PROCESS_NONCE, hostname: Component.hostname, clock: -> { ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) }) ⇒ Prometheus
constructor
A new instance of Prometheus.
-
#local_processes(snap) ⇒ Object
This host's process group (see class doc), from a Snapshot.
-
#render(expected_children: nil, fresh_window: 30, swarm: nil) ⇒ Object
swarm:is a Swarm::RestartCounters::Snapshot, nil outside a swarm child. -
#snapshot ⇒ Object
Cached Snapshot;
okfalse when Redis could not be read.
Constructor Details
#initialize(config, ttl: TTL, nonce: Component::PROCESS_NONCE, hostname: Component.hostname, clock: -> { ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) }) ⇒ Prometheus
Returns a new instance of Prometheus.
103 104 105 106 107 108 109 110 111 112 |
# File 'lib/wurk/metrics/prometheus.rb', line 103 def initialize(config, ttl: TTL, nonce: Component::PROCESS_NONCE, hostname: Component.hostname, clock: -> { ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) }) @config = config @ttl = ttl @local_suffix = ":#{nonce}" @local_prefix = "#{hostname}:" @clock = clock @lock = ::Mutex.new @snapshot = nil end |
Class Method Details
.pss_bytes(pid) ⇒ Object
90 91 92 93 94 95 |
# File 'lib/wurk/metrics/prometheus.rb', line 90 def self.pss_bytes(pid) kb = Heartbeat.smaps_kb(pid, PSS_FIELDS) kb && (kb * 1024) rescue ::SystemCallError, ::IOError nil end |
Instance Method Details
#fresh_local(snap, window, now = ::Time.now.to_f) ⇒ Object
Local processes whose heartbeat landed within window seconds.
129 130 131 |
# File 'lib/wurk/metrics/prometheus.rb', line 129 def fresh_local(snap, window, now = ::Time.now.to_f) local_processes(snap).select { |row| row.beat && now - row.beat < window } end |
#local_processes(snap) ⇒ Object
This host's process group (see class doc), from a Snapshot.
124 125 126 |
# File 'lib/wurk/metrics/prometheus.rb', line 124 def local_processes(snap) snap.processes.select { |row| local?(row.identity) } end |
#render(expected_children: nil, fresh_window: 30, swarm: nil) ⇒ Object
swarm: is a Swarm::RestartCounters::Snapshot, nil outside a swarm child.
134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/wurk/metrics/prometheus.rb', line 134 def render(expected_children: nil, fresh_window: 30, swarm: nil) snap = snapshot out = +'' build_info(out) gauge(out, 'wurk_redis_up', 'Whether the last metrics refresh could read Redis.', [[nil, snap.ok ? 1 : 0]]) ProcessFamilies.render(out, swarm: swarm) if snap.ok cluster(out, snap) local(out, snap, expected_children, fresh_window) end out end |
#snapshot ⇒ Object
Cached Snapshot; ok false when Redis could not be read.
115 116 117 118 119 120 121 |
# File 'lib/wurk/metrics/prometheus.rb', line 115 def snapshot @lock.synchronize do now = @clock.call @snapshot = fetch(now) if @snapshot.nil? || now - @snapshot.taken_at >= @ttl @snapshot end end |