pgbus 0.9.7 → 0.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/README.md +119 -1
- data/Rakefile +10 -1
- data/app/helpers/pgbus/application_helper.rb +37 -0
- data/app/views/pgbus/processes/_processes_table.html.erb +4 -1
- data/config/locales/da.yml +4 -0
- data/config/locales/de.yml +4 -0
- data/config/locales/en.yml +4 -0
- data/config/locales/es.yml +4 -0
- data/config/locales/fi.yml +4 -0
- data/config/locales/fr.yml +4 -0
- data/config/locales/it.yml +4 -0
- data/config/locales/ja.yml +4 -0
- data/config/locales/nb.yml +4 -0
- data/config/locales/nl.yml +4 -0
- data/config/locales/pt.yml +4 -0
- data/config/locales/sv.yml +4 -0
- data/lib/pgbus/active_job/executor.rb +25 -4
- data/lib/pgbus/cli/dlq.rb +164 -0
- data/lib/pgbus/cli.rb +18 -1
- data/lib/pgbus/client/connection_health.rb +194 -0
- data/lib/pgbus/client.rb +592 -73
- data/lib/pgbus/config_loader.rb +23 -4
- data/lib/pgbus/configuration.rb +98 -12
- data/lib/pgbus/dedup_cache.rb +8 -0
- data/lib/pgbus/doctor.rb +250 -0
- data/lib/pgbus/engine.rb +15 -0
- data/lib/pgbus/execution_pools/async_pool.rb +7 -0
- data/lib/pgbus/execution_pools/thread_pool.rb +7 -0
- data/lib/pgbus/instrumentation.rb +1 -0
- data/lib/pgbus/integrations/appsignal/probe.rb +23 -1
- data/lib/pgbus/metrics/backend.rb +38 -0
- data/lib/pgbus/metrics/backends/prometheus.rb +123 -0
- data/lib/pgbus/metrics/backends/statsd.rb +64 -0
- data/lib/pgbus/metrics/prometheus_exporter.rb +34 -0
- data/lib/pgbus/metrics/subscriber.rb +190 -0
- data/lib/pgbus/metrics.rb +42 -0
- data/lib/pgbus/process/consumer.rb +215 -8
- data/lib/pgbus/process/consumer_priority.rb +34 -0
- data/lib/pgbus/process/dispatcher.rb +265 -41
- data/lib/pgbus/process/heartbeat.rb +18 -5
- data/lib/pgbus/process/memory_usage.rb +48 -0
- data/lib/pgbus/process/notify_listener.rb +26 -7
- data/lib/pgbus/process/notify_probe.rb +96 -0
- data/lib/pgbus/process/primary_validator.rb +53 -0
- data/lib/pgbus/process/signal_handler.rb +6 -0
- data/lib/pgbus/process/supervisor.rb +396 -46
- data/lib/pgbus/process/worker.rb +298 -35
- data/lib/pgbus/recurring/scheduler.rb +15 -1
- data/lib/pgbus/streams/turbo_broadcastable.rb +7 -5
- data/lib/pgbus/table_maintenance.rb +13 -2
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +20 -4
- data/lib/pgbus/web/health_app.rb +102 -0
- data/lib/pgbus/web/health_server.rb +144 -0
- data/lib/pgbus/web/streamer/instance.rb +55 -1
- data/lib/pgbus/web/streamer/listener.rb +72 -9
- data/lib/pgbus.rb +37 -0
- data/lib/rubocop/cop/pgbus/no_ruby_timeout.rb +42 -0
- data/lib/rubocop/pgbus.rb +5 -0
- data/lib/tasks/pgbus_doctor.rake +12 -0
- metadata +18 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module Metrics
|
|
5
|
+
# Interface every metrics backend implements. The Subscriber only ever calls
|
|
6
|
+
# these three methods, so a backend that satisfies them plugs in directly as
|
|
7
|
+
# `config.metrics_backend`.
|
|
8
|
+
#
|
|
9
|
+
# increment(name, value = 1, tags = {}) — monotonic counter add
|
|
10
|
+
# gauge(name, value, tags = {}) — point-in-time value
|
|
11
|
+
# histogram(name, value, tags = {}) — timing/size distribution sample
|
|
12
|
+
#
|
|
13
|
+
# `name` is a `pgbus_`-prefixed String; `tags` is a Hash of low-cardinality
|
|
14
|
+
# label => value pairs (no msg_id, no event_id).
|
|
15
|
+
class Backend
|
|
16
|
+
def increment(_name, _value = 1, _tags = {})
|
|
17
|
+
raise NotImplementedError, "#{self.class}#increment"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def gauge(_name, _value, _tags = {})
|
|
21
|
+
raise NotImplementedError, "#{self.class}#gauge"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def histogram(_name, _value, _tags = {})
|
|
25
|
+
raise NotImplementedError, "#{self.class}#histogram"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# No-op backend. Used as the resolved backend for a nil configuration so
|
|
29
|
+
# callers never have to nil-check, though in practice the Subscriber is
|
|
30
|
+
# simply not installed when metrics_backend is nil.
|
|
31
|
+
class Null < Backend
|
|
32
|
+
def increment(_name, _value = 1, _tags = {}) = nil
|
|
33
|
+
def gauge(_name, _value, _tags = {}) = nil
|
|
34
|
+
def histogram(_name, _value, _tags = {}) = nil
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module Metrics
|
|
5
|
+
module Backends
|
|
6
|
+
# Dependency-free, in-process Prometheus registry.
|
|
7
|
+
#
|
|
8
|
+
# Counters and gauges are stored per (name, sorted-tags) series. Histograms
|
|
9
|
+
# are stored as a sum+count summary — cheap, dependency-free, and enough for
|
|
10
|
+
# rate/avg dashboards without pre-declaring buckets. State is Mutex-guarded
|
|
11
|
+
# (no unsynchronized shared state), since instrumentation fires from many
|
|
12
|
+
# worker threads while the exporter renders from a Rack thread.
|
|
13
|
+
#
|
|
14
|
+
# Render output is Prometheus text exposition format (v0.0.4), served by
|
|
15
|
+
# Pgbus::Metrics::PrometheusExporter.
|
|
16
|
+
class Prometheus < Backend
|
|
17
|
+
Series = Struct.new(:type, :samples) # samples: { tags_key => value }
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
super
|
|
21
|
+
@mutex = Mutex.new
|
|
22
|
+
# name => Series
|
|
23
|
+
@counters = {}
|
|
24
|
+
@gauges = {}
|
|
25
|
+
# name => { tags_key => { sum:, count:, tags: } }
|
|
26
|
+
@histograms = {}
|
|
27
|
+
# tags_key (sorted array) => original tags hash, for rendering labels
|
|
28
|
+
@tag_labels = {}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def increment(name, value = 1, tags = {})
|
|
32
|
+
key = tag_key(tags)
|
|
33
|
+
@mutex.synchronize do
|
|
34
|
+
series = (@counters[name] ||= {})
|
|
35
|
+
series[key] = (series[key] || 0) + value
|
|
36
|
+
end
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def gauge(name, value, tags = {})
|
|
41
|
+
key = tag_key(tags)
|
|
42
|
+
@mutex.synchronize do
|
|
43
|
+
series = (@gauges[name] ||= {})
|
|
44
|
+
series[key] = value
|
|
45
|
+
end
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def histogram(name, value, tags = {})
|
|
50
|
+
key = tag_key(tags)
|
|
51
|
+
@mutex.synchronize do
|
|
52
|
+
series = (@histograms[name] ||= {})
|
|
53
|
+
bucket = (series[key] ||= { sum: 0, count: 0 })
|
|
54
|
+
bucket[:sum] += value
|
|
55
|
+
bucket[:count] += 1
|
|
56
|
+
end
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Render the full registry as Prometheus text exposition format.
|
|
61
|
+
def render
|
|
62
|
+
@mutex.synchronize do
|
|
63
|
+
lines = []
|
|
64
|
+
@counters.each { |name, series| render_simple(lines, name, "counter", series) }
|
|
65
|
+
@gauges.each { |name, series| render_simple(lines, name, "gauge", series) }
|
|
66
|
+
@histograms.each { |name, series| render_summary(lines, name, series) }
|
|
67
|
+
"#{lines.join("\n")}\n"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def render_simple(lines, name, type, series)
|
|
74
|
+
lines << "# TYPE #{name} #{type}"
|
|
75
|
+
series.each do |key, value|
|
|
76
|
+
lines << "#{name}#{labels_for(key)} #{format_number(value)}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def render_summary(lines, name, series)
|
|
81
|
+
lines << "# TYPE #{name} summary"
|
|
82
|
+
series.each do |key, bucket|
|
|
83
|
+
labels = labels_for(key)
|
|
84
|
+
lines << "#{name}_sum#{labels} #{format_number(bucket[:sum])}"
|
|
85
|
+
lines << "#{name}_count#{labels} #{bucket[:count]}"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Stable, order-independent key for a tag set. Also memoizes the original
|
|
90
|
+
# (stringified) tag hash so render can reproduce the label set.
|
|
91
|
+
def tag_key(tags)
|
|
92
|
+
normalized = tags.each_with_object({}) do |(k, v), acc|
|
|
93
|
+
acc[k.to_s] = v.to_s
|
|
94
|
+
end
|
|
95
|
+
key = normalized.sort.freeze
|
|
96
|
+
@tag_labels[key] ||= normalized
|
|
97
|
+
key
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def labels_for(key)
|
|
101
|
+
labels = @tag_labels[key]
|
|
102
|
+
return "" if labels.nil? || labels.empty?
|
|
103
|
+
|
|
104
|
+
inner = key.map { |k, _| %(#{k}="#{escape_label(labels[k])}") }.join(",")
|
|
105
|
+
"{#{inner}}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Prometheus label-value escaping: backslash, double-quote, newline.
|
|
109
|
+
def escape_label(value)
|
|
110
|
+
value.gsub("\\", "\\\\\\\\").gsub('"', '\\"').gsub("\n", '\\n')
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def format_number(value)
|
|
114
|
+
if value.is_a?(Float) && value.finite? && value == value.to_i
|
|
115
|
+
value.to_i.to_s
|
|
116
|
+
else
|
|
117
|
+
value.to_s
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
|
|
5
|
+
module Pgbus
|
|
6
|
+
module Metrics
|
|
7
|
+
module Backends
|
|
8
|
+
# StatsD backend emitting UDP datagrams in DogStatsD dialect.
|
|
9
|
+
#
|
|
10
|
+
# increment -> "name:value|c"
|
|
11
|
+
# gauge -> "name:value|g"
|
|
12
|
+
# histogram -> "name:value|ms"
|
|
13
|
+
#
|
|
14
|
+
# Tags are appended as DogStatsD `|#k:v,k:v`. No gem dependency — a plain
|
|
15
|
+
# UDPSocket. UDP send is fire-and-forget; any socket error is logged and
|
|
16
|
+
# swallowed so a metrics hiccup never propagates into the producer thread.
|
|
17
|
+
class Statsd < Backend
|
|
18
|
+
def initialize(host: "127.0.0.1", port: 8125, socket: nil)
|
|
19
|
+
super()
|
|
20
|
+
@host = host
|
|
21
|
+
@port = port
|
|
22
|
+
@socket = socket
|
|
23
|
+
@mutex = Mutex.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def increment(name, value = 1, tags = {})
|
|
27
|
+
emit(name, value, "c", tags)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def gauge(name, value, tags = {})
|
|
31
|
+
emit(name, value, "g", tags)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def histogram(name, value, tags = {})
|
|
35
|
+
emit(name, value, "ms", tags)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def emit(name, value, type, tags)
|
|
41
|
+
datagram = "#{name}:#{value}|#{type}#{format_tags(tags)}"
|
|
42
|
+
socket.send(datagram, 0, @host, @port)
|
|
43
|
+
nil
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
Pgbus.logger.warn do
|
|
46
|
+
"[Pgbus::Metrics::Statsd] send failed: #{e.class}: #{e.message}"
|
|
47
|
+
end
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def format_tags(tags)
|
|
52
|
+
pairs = tags.compact.map { |k, v| "#{k}:#{v}" }
|
|
53
|
+
pairs.empty? ? "" : "|##{pairs.join(",")}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# UDPSocket is not guaranteed thread-safe for concurrent sends; guard
|
|
57
|
+
# lazy creation so the first burst of instrumentation can't race.
|
|
58
|
+
def socket
|
|
59
|
+
@socket || @mutex.synchronize { @socket ||= UDPSocket.new }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module Metrics
|
|
5
|
+
# Rack app that renders the in-process Prometheus registry as text exposition
|
|
6
|
+
# format. Mount it in the host Rails app (or any Rack server) so Prometheus
|
|
7
|
+
# can scrape it — see the README. Mirrors the "mountable Rack app" shape used
|
|
8
|
+
# by Pgbus::Web::StreamApp.
|
|
9
|
+
#
|
|
10
|
+
# # config/routes.rb
|
|
11
|
+
# mount Pgbus::Metrics::PrometheusExporter.new => "/metrics"
|
|
12
|
+
#
|
|
13
|
+
# With no injected backend it reads Pgbus.configuration.metrics_backend, so a
|
|
14
|
+
# single `config.metrics_backend = :prometheus` wires both the subscriber and
|
|
15
|
+
# the exporter to the same registry.
|
|
16
|
+
class PrometheusExporter
|
|
17
|
+
CONTENT_TYPE = "text/plain; version=0.0.4"
|
|
18
|
+
|
|
19
|
+
def initialize(backend: nil)
|
|
20
|
+
@backend = backend
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(_env)
|
|
24
|
+
backend = @backend || Pgbus.configuration.metrics_backend
|
|
25
|
+
unless backend.is_a?(Backends::Prometheus)
|
|
26
|
+
return [503, { "content-type" => "text/plain" },
|
|
27
|
+
["metrics_backend is not a Prometheus backend\n"]]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
[200, { "content-type" => CONTENT_TYPE }, [backend.render]]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module Metrics
|
|
5
|
+
# Consumes Pgbus::Instrumentation events and forwards them to the configured
|
|
6
|
+
# metrics backend. The event→metric mapping (names, tags, counter vs
|
|
7
|
+
# histogram) intentionally mirrors the AppSignal subscriber so a team can
|
|
8
|
+
# switch backends and keep the same dashboards.
|
|
9
|
+
#
|
|
10
|
+
# Every handler runs inside `safely`: a raising backend (Prometheus registry
|
|
11
|
+
# bug, StatsD socket down, custom backend) is logged and swallowed — a metrics
|
|
12
|
+
# failure must never propagate into the producer thread that emitted the
|
|
13
|
+
# event.
|
|
14
|
+
module Subscriber
|
|
15
|
+
METRIC_PREFIX = "pgbus_"
|
|
16
|
+
private_constant :METRIC_PREFIX
|
|
17
|
+
|
|
18
|
+
@subscriptions = []
|
|
19
|
+
@backend = nil
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def install!(backend:)
|
|
23
|
+
return false if @installed
|
|
24
|
+
|
|
25
|
+
@backend = backend
|
|
26
|
+
@subscriptions = [
|
|
27
|
+
subscribe("pgbus.executor.execute") { |event| on_executor_execute(event) },
|
|
28
|
+
subscribe("pgbus.job_completed") { |event| on_job_completed(event) },
|
|
29
|
+
subscribe("pgbus.job_failed") { |event| on_job_failed(event) },
|
|
30
|
+
subscribe("pgbus.job_dead_lettered") { |event| on_job_dead_lettered(event) },
|
|
31
|
+
subscribe("pgbus.event_processed") { |event| on_event_processed(event) },
|
|
32
|
+
subscribe("pgbus.event_failed") { |event| on_event_failed(event) },
|
|
33
|
+
subscribe("pgbus.client.send_message") { |event| on_send_message(event) },
|
|
34
|
+
subscribe("pgbus.client.send_batch") { |event| on_send_batch(event) },
|
|
35
|
+
subscribe("pgbus.client.read_batch") { |event| on_read_batch(event) },
|
|
36
|
+
subscribe("pgbus.stream.broadcast") { |event| on_stream_broadcast(event) },
|
|
37
|
+
subscribe("pgbus.outbox.publish") { |event| on_outbox_publish(event) },
|
|
38
|
+
subscribe("pgbus.recurring.enqueue") { |event| on_recurring_enqueue(event) },
|
|
39
|
+
subscribe("pgbus.worker.recycle") { |event| on_worker_recycle(event) }
|
|
40
|
+
]
|
|
41
|
+
@installed = true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def installed?
|
|
45
|
+
@installed == true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def reset!
|
|
49
|
+
@subscriptions&.each { |s| ActiveSupport::Notifications.unsubscribe(s) }
|
|
50
|
+
@subscriptions = []
|
|
51
|
+
@backend = nil
|
|
52
|
+
@installed = false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
attr_reader :backend
|
|
58
|
+
|
|
59
|
+
def subscribe(name, &block)
|
|
60
|
+
ActiveSupport::Notifications.subscribe(name) do |*args|
|
|
61
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
62
|
+
safely { block.call(event) }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Errors in the subscriber must never affect the producer thread.
|
|
67
|
+
def safely
|
|
68
|
+
yield
|
|
69
|
+
rescue StandardError => e
|
|
70
|
+
Pgbus.logger.warn do
|
|
71
|
+
"[Pgbus::Metrics] subscriber error: #{e.class}: #{e.message}"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# ── Job execution ─────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
def on_executor_execute(event)
|
|
78
|
+
payload = event.payload
|
|
79
|
+
backend.histogram(
|
|
80
|
+
"#{METRIC_PREFIX}job_duration_ms",
|
|
81
|
+
event.duration,
|
|
82
|
+
compact(queue: payload[:queue], job_class: payload[:job_class])
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def on_job_completed(event)
|
|
87
|
+
payload = event.payload
|
|
88
|
+
backend.increment(
|
|
89
|
+
"#{METRIC_PREFIX}queue_job_count", 1,
|
|
90
|
+
compact(queue: payload[:queue], job_class: payload[:job_class], status: "processed")
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def on_job_failed(event)
|
|
95
|
+
payload = event.payload
|
|
96
|
+
backend.increment(
|
|
97
|
+
"#{METRIC_PREFIX}queue_job_count", 1,
|
|
98
|
+
compact(queue: payload[:queue], job_class: payload[:job_class], status: "failed")
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def on_job_dead_lettered(event)
|
|
103
|
+
payload = event.payload
|
|
104
|
+
backend.increment(
|
|
105
|
+
"#{METRIC_PREFIX}queue_job_count", 1,
|
|
106
|
+
compact(queue: payload[:queue], job_class: payload[:job_class], status: "dead_lettered")
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# ── Event handler ─────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
def on_event_processed(event)
|
|
113
|
+
payload = event.payload
|
|
114
|
+
tags = compact(handler: payload[:handler], routing_key: payload[:routing_key])
|
|
115
|
+
backend.histogram("#{METRIC_PREFIX}event_duration_ms", event.duration, tags)
|
|
116
|
+
backend.increment("#{METRIC_PREFIX}event_count", 1, tags.merge(status: "processed"))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def on_event_failed(event)
|
|
120
|
+
payload = event.payload
|
|
121
|
+
backend.increment(
|
|
122
|
+
"#{METRIC_PREFIX}event_count", 1,
|
|
123
|
+
compact(handler: payload[:handler], routing_key: payload[:routing_key], status: "failed")
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# ── Client (PGMQ wrapper) ─────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
def on_send_message(event)
|
|
130
|
+
backend.increment("#{METRIC_PREFIX}messages_sent", 1, compact(queue: event.payload[:queue]))
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def on_send_batch(event)
|
|
134
|
+
payload = event.payload
|
|
135
|
+
count = payload[:count] || payload[:batch_size] || 1
|
|
136
|
+
backend.increment("#{METRIC_PREFIX}messages_sent", count, compact(queue: payload[:queue]))
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def on_read_batch(event)
|
|
140
|
+
payload = event.payload
|
|
141
|
+
count = payload[:count] || payload[:fetched] || 0
|
|
142
|
+
backend.increment("#{METRIC_PREFIX}messages_read", count, compact(queue: payload[:queue]))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# ── Streams ───────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
def on_stream_broadcast(event)
|
|
148
|
+
payload = event.payload
|
|
149
|
+
backend.increment(
|
|
150
|
+
"#{METRIC_PREFIX}stream_broadcast_count", 1,
|
|
151
|
+
compact(stream: payload[:stream], deferred: payload[:deferred] ? "true" : "false")
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# ── Outbox ────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
def on_outbox_publish(event)
|
|
158
|
+
backend.increment(
|
|
159
|
+
"#{METRIC_PREFIX}outbox_published", 1,
|
|
160
|
+
compact(kind: event.payload[:kind] || "job")
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# ── Recurring scheduler ───────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
def on_recurring_enqueue(event)
|
|
167
|
+
payload = event.payload
|
|
168
|
+
backend.increment(
|
|
169
|
+
"#{METRIC_PREFIX}recurring_enqueued", 1,
|
|
170
|
+
compact(task: payload[:task], class_name: payload[:class_name])
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# ── Worker lifecycle ──────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
def on_worker_recycle(event)
|
|
177
|
+
backend.increment(
|
|
178
|
+
"#{METRIC_PREFIX}worker_recycled", 1,
|
|
179
|
+
compact(reason: event.payload[:reason])
|
|
180
|
+
)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Drop nil-valued tags so backends don't emit empty labels.
|
|
184
|
+
def compact(tags)
|
|
185
|
+
tags.compact
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
# Backend-agnostic metrics adapter.
|
|
5
|
+
#
|
|
6
|
+
# Pgbus emits ~16 `pgbus.*` ActiveSupport::Notifications events (see
|
|
7
|
+
# Pgbus::Instrumentation). The AppSignal integration is one consumer of those
|
|
8
|
+
# events; this namespace is a second, vendor-neutral consumer that forwards the
|
|
9
|
+
# same event→metric mapping to a configurable backend so teams on Prometheus,
|
|
10
|
+
# Datadog, or plain StatsD get metrics without hand-writing subscribers.
|
|
11
|
+
#
|
|
12
|
+
# Opt in via `config.metrics_backend = :prometheus | :statsd | <Backend>`. The
|
|
13
|
+
# default is nil, which installs no subscription (zero overhead). Installation
|
|
14
|
+
# happens from the engine, mirroring the AppSignal initializer.
|
|
15
|
+
module Metrics
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# Coerce a configuration value into a concrete Backend instance.
|
|
19
|
+
#
|
|
20
|
+
# nil -> Backend::Null (no-op)
|
|
21
|
+
# :prometheus -> Backends::Prometheus
|
|
22
|
+
# :statsd -> Backends::Statsd (host/port from configuration)
|
|
23
|
+
# Backend inst -> returned unchanged
|
|
24
|
+
def resolve_backend(value)
|
|
25
|
+
case value
|
|
26
|
+
when nil
|
|
27
|
+
Backend::Null.new
|
|
28
|
+
when :prometheus
|
|
29
|
+
Backends::Prometheus.new
|
|
30
|
+
when :statsd
|
|
31
|
+
config = Pgbus.configuration
|
|
32
|
+
Backends::Statsd.new(host: config.statsd_host, port: config.statsd_port)
|
|
33
|
+
when Backend
|
|
34
|
+
value
|
|
35
|
+
else
|
|
36
|
+
raise ArgumentError,
|
|
37
|
+
"metrics_backend must be nil, :prometheus, :statsd, or a " \
|
|
38
|
+
"Pgbus::Metrics::Backend instance (got #{value.inspect})"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|