sidekiq-vigil 0.1.0
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 +7 -0
- data/.rubocop.yml +39 -0
- data/LICENSE.txt +21 -0
- data/README.md +237 -0
- data/Rakefile +10 -0
- data/docs/redis_keys.md +18 -0
- data/docs/webhook_event.schema.json +41 -0
- data/docs/webhook_schema.md +64 -0
- data/exe/vigil +6 -0
- data/lib/generators/sidekiq_vigil/install_generator.rb +32 -0
- data/lib/generators/sidekiq_vigil/templates/sidekiq_vigil.rb +49 -0
- data/lib/sidekiq/vigil/version.rb +7 -0
- data/lib/sidekiq/vigil.rb +7 -0
- data/lib/sidekiq_vigil/alert/cron.rb +106 -0
- data/lib/sidekiq_vigil/alert/event.rb +110 -0
- data/lib/sidekiq_vigil/alert/grouping.rb +15 -0
- data/lib/sidekiq_vigil/alert/manager.rb +227 -0
- data/lib/sidekiq_vigil/alert/mute.rb +69 -0
- data/lib/sidekiq_vigil/alert/state.rb +51 -0
- data/lib/sidekiq_vigil/check/base.rb +52 -0
- data/lib/sidekiq_vigil/check/failure_rate.rb +45 -0
- data/lib/sidekiq_vigil/check/memory.rb +41 -0
- data/lib/sidekiq_vigil/check/process_alive.rb +64 -0
- data/lib/sidekiq_vigil/check/queue_latency.rb +21 -0
- data/lib/sidekiq_vigil/check/queue_size.rb +20 -0
- data/lib/sidekiq_vigil/check/redis_health.rb +55 -0
- data/lib/sidekiq_vigil/check/registry.rb +23 -0
- data/lib/sidekiq_vigil/check/scheduled_backlog.rb +21 -0
- data/lib/sidekiq_vigil/check/set_size.rb +76 -0
- data/lib/sidekiq_vigil/check/stuck_jobs.rb +44 -0
- data/lib/sidekiq_vigil/check/throughput_anomaly.rb +69 -0
- data/lib/sidekiq_vigil/check/utilization.rb +61 -0
- data/lib/sidekiq_vigil/checker.rb +149 -0
- data/lib/sidekiq_vigil/cli.rb +205 -0
- data/lib/sidekiq_vigil/collector.rb +74 -0
- data/lib/sidekiq_vigil/config.rb +267 -0
- data/lib/sidekiq_vigil/health_app.rb +64 -0
- data/lib/sidekiq_vigil/leader_election.rb +54 -0
- data/lib/sidekiq_vigil/middleware/server.rb +37 -0
- data/lib/sidekiq_vigil/notifier/base.rb +22 -0
- data/lib/sidekiq_vigil/notifier/http_transport.rb +32 -0
- data/lib/sidekiq_vigil/notifier/log.rb +14 -0
- data/lib/sidekiq_vigil/notifier/manager.rb +45 -0
- data/lib/sidekiq_vigil/notifier/slack.rb +192 -0
- data/lib/sidekiq_vigil/notifier/webhook.rb +25 -0
- data/lib/sidekiq_vigil/reporter.rb +103 -0
- data/lib/sidekiq_vigil/result.rb +43 -0
- data/lib/sidekiq_vigil/runtime.rb +44 -0
- data/lib/sidekiq_vigil/sidekiq_api.rb +40 -0
- data/lib/sidekiq_vigil/storage.rb +167 -0
- data/lib/sidekiq_vigil/timezone.rb +39 -0
- data/lib/sidekiq_vigil/version.rb +5 -0
- data/lib/sidekiq_vigil.rb +128 -0
- metadata +111 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SidekiqVigil
|
|
4
|
+
module Notifier
|
|
5
|
+
class Base
|
|
6
|
+
attr_reader :options, :logger
|
|
7
|
+
|
|
8
|
+
def initialize(options: {}, logger: SidekiqVigil.logger, **_dependencies)
|
|
9
|
+
@options = options
|
|
10
|
+
@logger = logger
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def notify(_event)
|
|
14
|
+
raise NotImplementedError, "#{self.class} must implement #notify"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def inspect
|
|
18
|
+
"#<#{self.class} options=[FILTERED]>"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module SidekiqVigil
|
|
7
|
+
module Notifier
|
|
8
|
+
class HttpTransport
|
|
9
|
+
Response = Data.define(:code, :body) do
|
|
10
|
+
def success?
|
|
11
|
+
code.to_i.between?(200, 299)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def post(url, body, headers: {})
|
|
16
|
+
uri = URI.parse(url)
|
|
17
|
+
raise Error, "notification URL must use HTTP or HTTPS" unless %w[http https].include?(uri.scheme)
|
|
18
|
+
|
|
19
|
+
request = Net::HTTP::Post.new(uri.request_uri, { "Content-Type" => "application/json" }.merge(headers))
|
|
20
|
+
request.body = body
|
|
21
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
22
|
+
http.open_timeout = 5
|
|
23
|
+
http.read_timeout = 10
|
|
24
|
+
http.request(request)
|
|
25
|
+
end
|
|
26
|
+
Response.new(code: response.code, body: response.body)
|
|
27
|
+
rescue URI::InvalidURIError, SocketError, SystemCallError, Timeout::Error => e
|
|
28
|
+
raise Error, "notification request failed (#{e.class})"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SidekiqVigil
|
|
4
|
+
module Notifier
|
|
5
|
+
class Manager
|
|
6
|
+
def initialize(notifiers:, logger: SidekiqVigil.logger)
|
|
7
|
+
@notifiers = notifiers
|
|
8
|
+
@logger = logger
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def notify(events)
|
|
12
|
+
Array(events).each do |event|
|
|
13
|
+
notifiers.each { |notifier| notify_one(notifier, event) }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
attr_reader :notifiers, :logger
|
|
20
|
+
|
|
21
|
+
def notify_one(notifier, event)
|
|
22
|
+
notifier.notify(event)
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
logger.error("[sidekiq-vigil] #{notifier.class} failed: #{e.class}: #{e.message}")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module Factory
|
|
29
|
+
CLASSES = {
|
|
30
|
+
log: Log,
|
|
31
|
+
slack: Slack,
|
|
32
|
+
webhook: Webhook
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
def build(config, logger: SidekiqVigil.logger)
|
|
38
|
+
config.runnable_notifiers.map do |definition|
|
|
39
|
+
klass = definition.klass || CLASSES.fetch(definition.name)
|
|
40
|
+
klass.new(options: definition.options, logger:)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
module SidekiqVigil
|
|
7
|
+
module Notifier
|
|
8
|
+
class Slack < Base
|
|
9
|
+
SPARKS = "▁▂▃▄▅▆▇█"
|
|
10
|
+
|
|
11
|
+
def initialize(
|
|
12
|
+
options: {},
|
|
13
|
+
logger: SidekiqVigil.logger,
|
|
14
|
+
transport: HttpTransport.new,
|
|
15
|
+
sleeper: ->(seconds) { sleep(seconds) },
|
|
16
|
+
fallback: Log.new(logger:)
|
|
17
|
+
)
|
|
18
|
+
super(options:, logger:)
|
|
19
|
+
@transport = transport
|
|
20
|
+
@sleeper = sleeper
|
|
21
|
+
@fallback = fallback
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def notify(event)
|
|
25
|
+
attempts = options.fetch(:retries, 3) + 1
|
|
26
|
+
attempts.times do |attempt|
|
|
27
|
+
response = transport.post(webhook_url(event), JSON.generate(payload(event)))
|
|
28
|
+
return true if response.success?
|
|
29
|
+
|
|
30
|
+
raise Error, "Slack returned HTTP #{response.code}"
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
if attempt == attempts - 1
|
|
33
|
+
logger.error("[sidekiq-vigil] Slack notification failed after #{attempts} attempts: #{safe_error(e)}")
|
|
34
|
+
fallback.notify(event)
|
|
35
|
+
return false
|
|
36
|
+
end
|
|
37
|
+
sleeper.call(2**attempt)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def payload(event)
|
|
42
|
+
{
|
|
43
|
+
text: header(event),
|
|
44
|
+
blocks: event.transition == :digest ? digest_blocks(event) : alert_blocks(event)
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
attr_reader :transport, :sleeper, :fallback
|
|
51
|
+
|
|
52
|
+
def webhook_url(event)
|
|
53
|
+
routes = options.fetch(:routes, {})
|
|
54
|
+
routes[event.severity] || routes[event.severity.to_s] || options.fetch(:webhook_url)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def header(event)
|
|
58
|
+
return "📦 DIGEST — #{event.result.message}" if event.transition == :digest
|
|
59
|
+
return "✅ RESOLVED — #{event.result.check_name}" if event.transition == :resolved
|
|
60
|
+
|
|
61
|
+
icon = %i[critical error].include?(event.severity) ? "🔥" : "⚠️"
|
|
62
|
+
"#{icon} #{event.severity.to_s.upcase} — #{event.result.check_name}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def fields(event)
|
|
66
|
+
result = event.result
|
|
67
|
+
[
|
|
68
|
+
markdown_field("*Check*\n#{display(result.check_name)}"),
|
|
69
|
+
markdown_field("*Target*\n#{result.target}"),
|
|
70
|
+
markdown_field("*Value / threshold*\n#{display(result.value)} / #{display(result.threshold)}"),
|
|
71
|
+
markdown_field("*Duration*\n#{duration(event)}"),
|
|
72
|
+
markdown_field("*Environment*\n#{environment}"),
|
|
73
|
+
markdown_field("*Host*\n#{host}")
|
|
74
|
+
]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def context_block(event)
|
|
78
|
+
parts = ["Event #{event.transition}"]
|
|
79
|
+
spark = sparkline(event.history)
|
|
80
|
+
parts << "Trend #{spark}" unless spark.empty?
|
|
81
|
+
parts << "<#{options[:web_ui_url]}|Open Sidekiq>" if options[:web_ui_url]
|
|
82
|
+
mentions = options.fetch(:mention, {})
|
|
83
|
+
mention = mentions[event.severity] || mentions[event.severity.to_s]
|
|
84
|
+
parts << mention if mention
|
|
85
|
+
|
|
86
|
+
{ type: "context", elements: [{ type: "mrkdwn", text: parts.join(" • ") }] }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def alert_blocks(event)
|
|
90
|
+
[
|
|
91
|
+
header_block(event),
|
|
92
|
+
{ type: "section", fields: fields(event) },
|
|
93
|
+
message_block(event.result.message),
|
|
94
|
+
context_block(event)
|
|
95
|
+
].compact
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def digest_blocks(event)
|
|
99
|
+
metadata = event.result.metadata
|
|
100
|
+
[
|
|
101
|
+
header_block(event),
|
|
102
|
+
{ type: "section", text: { type: "mrkdwn", text: digest_counts(metadata.fetch(:counts)) } },
|
|
103
|
+
{ type: "section", text: { type: "mrkdwn", text: digest_top(metadata.fetch(:top)) } },
|
|
104
|
+
context_block(event)
|
|
105
|
+
]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def header_block(event)
|
|
109
|
+
{ type: "header", text: { type: "plain_text", text: header(event) } }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def message_block(message)
|
|
113
|
+
return unless message
|
|
114
|
+
|
|
115
|
+
{ type: "section", text: { type: "mrkdwn", text: "*Message*\n#{message}" } }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def digest_counts(counts)
|
|
119
|
+
labels = { error: "Error", critical: "Critical", warn: "Warn", ok: "OK" }
|
|
120
|
+
values = labels.filter_map do |severity, label|
|
|
121
|
+
count = counts[severity] || counts[severity.to_s]
|
|
122
|
+
"#{label}: #{count}" if count.to_i.positive?
|
|
123
|
+
end
|
|
124
|
+
"*Severity counts*\n#{values.join(' • ')}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def digest_top(alerts)
|
|
128
|
+
lines = alerts.map do |alert|
|
|
129
|
+
severity = fetch_value(alert, :severity).to_s.upcase
|
|
130
|
+
check = fetch_value(alert, :check_name)
|
|
131
|
+
target = fetch_value(alert, :target)
|
|
132
|
+
value = display(fetch_value(alert, :value))
|
|
133
|
+
threshold = display(fetch_value(alert, :threshold))
|
|
134
|
+
"• *#{severity}* #{check} / #{target} — #{value} / #{threshold}"
|
|
135
|
+
end
|
|
136
|
+
"*Top alerts*\n#{lines.join("\n")}"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def markdown_field(text)
|
|
140
|
+
{ type: "mrkdwn", text: }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def sparkline(history)
|
|
144
|
+
values = history.filter_map { |point| numeric(point["value"] || point[:value]) }
|
|
145
|
+
return "" if values.empty?
|
|
146
|
+
return SPARKS[0] * values.length if values.min == values.max
|
|
147
|
+
|
|
148
|
+
values.map do |value|
|
|
149
|
+
index = ((value - values.min) / (values.max - values.min) * (SPARKS.length - 1)).round
|
|
150
|
+
SPARKS[index]
|
|
151
|
+
end.join
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def numeric(value)
|
|
155
|
+
Float(value)
|
|
156
|
+
rescue TypeError, ArgumentError
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def display(value)
|
|
161
|
+
value.nil? ? "-" : value.to_s
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def duration(event)
|
|
165
|
+
started_at = event.state.first_seen_at
|
|
166
|
+
return "-" unless started_at
|
|
167
|
+
|
|
168
|
+
seconds = [event.timestamp.to_f - started_at.to_f, 0].max.round
|
|
169
|
+
return "#{seconds}s" if seconds < 60
|
|
170
|
+
return "#{seconds / 60}m #{seconds % 60}s" if seconds < 3_600
|
|
171
|
+
|
|
172
|
+
"#{seconds / 3_600}h #{(seconds % 3_600) / 60}m"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def environment
|
|
176
|
+
options.fetch(:environment) { ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "unknown" }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def host
|
|
180
|
+
options.fetch(:host) { Socket.gethostname }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def fetch_value(hash, key)
|
|
184
|
+
hash[key] || hash[key.to_s]
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def safe_error(error)
|
|
188
|
+
error.message.gsub(%r{https?://\S+}, "[FILTERED]")
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module SidekiqVigil
|
|
6
|
+
module Notifier
|
|
7
|
+
class Webhook < Base
|
|
8
|
+
def initialize(options: {}, logger: SidekiqVigil.logger, transport: HttpTransport.new)
|
|
9
|
+
super(options:, logger:)
|
|
10
|
+
@transport = transport
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def notify(event)
|
|
14
|
+
response = transport.post(options.fetch(:url), JSON.generate(event.to_h), headers: options.fetch(:headers, {}))
|
|
15
|
+
raise Error, "webhook returned HTTP #{response.code}" unless response.success?
|
|
16
|
+
|
|
17
|
+
true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
attr_reader :transport
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
module SidekiqVigil
|
|
7
|
+
class Reporter
|
|
8
|
+
STATS_TTL = 8 * 24 * 60 * 60
|
|
9
|
+
EXECUTION_TTL = 60 * 60
|
|
10
|
+
CONFIG_TTL = 60 * 60
|
|
11
|
+
|
|
12
|
+
attr_reader :thread
|
|
13
|
+
|
|
14
|
+
def initialize(storage:, collector:, config:, logger: SidekiqVigil.logger, sleeper: ->(seconds) { sleep(seconds) })
|
|
15
|
+
@storage = storage
|
|
16
|
+
@collector = collector
|
|
17
|
+
@config = config
|
|
18
|
+
@logger = logger
|
|
19
|
+
@sleeper = sleeper
|
|
20
|
+
@running = false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def start
|
|
24
|
+
return thread if thread&.alive?
|
|
25
|
+
|
|
26
|
+
@running = true
|
|
27
|
+
@thread = Thread.new { run }
|
|
28
|
+
@thread.name = "sidekiq-vigil-reporter" if @thread.respond_to?(:name=)
|
|
29
|
+
@thread
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def stop
|
|
33
|
+
@running = false
|
|
34
|
+
thread&.wakeup if thread&.status == "sleep"
|
|
35
|
+
thread&.join(config.flush_interval + 1)
|
|
36
|
+
@thread = nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def flush_once
|
|
40
|
+
snapshot = collector.drain
|
|
41
|
+
flush_snapshot(snapshot)
|
|
42
|
+
report_rss
|
|
43
|
+
storage.hash_write("config_digest", { process_id => config.digest }, ttl: CONFIG_TTL)
|
|
44
|
+
true
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
collector.merge(snapshot) if snapshot
|
|
47
|
+
logger.error("[sidekiq-vigil] reporter flush failed: #{e.class}: #{e.message}")
|
|
48
|
+
false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def rss_kb
|
|
52
|
+
return linux_rss if File.readable?("/proc/self/status")
|
|
53
|
+
return macos_rss if RUBY_PLATFORM.include?("darwin")
|
|
54
|
+
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
attr_reader :storage, :collector, :config, :logger, :sleeper
|
|
61
|
+
|
|
62
|
+
def run
|
|
63
|
+
while @running
|
|
64
|
+
flush_once
|
|
65
|
+
sleeper.call(config.flush_interval)
|
|
66
|
+
end
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
logger.error("[sidekiq-vigil] reporter thread failed: #{e.class}: #{e.message}")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def flush_snapshot(snapshot)
|
|
72
|
+
snapshot.stats.each do |minute, fields|
|
|
73
|
+
storage.hash_increment("stats:#{minute}", fields, ttl: STATS_TTL)
|
|
74
|
+
end
|
|
75
|
+
snapshot.executions.each do |queue, values|
|
|
76
|
+
storage.hash_increment("exec:#{queue}", values.transform_keys(&:to_s), ttl: EXECUTION_TTL)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def report_rss
|
|
81
|
+
rss = rss_kb
|
|
82
|
+
return unless rss
|
|
83
|
+
|
|
84
|
+
storage.set("mem:#{process_id}", rss, ttl: config.flush_interval * 3)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def process_id
|
|
88
|
+
@process_id ||= "#{Socket.gethostname}:#{Process.pid}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def linux_rss
|
|
92
|
+
match = File.read("/proc/self/status").match(/^VmRSS:\s+(\d+)\s+kB/)
|
|
93
|
+
match && Integer(match[1])
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def macos_rss
|
|
97
|
+
output = IO.popen(["ps", "-o", "rss=", "-p", Process.pid.to_s], &:read)
|
|
98
|
+
Integer(output.strip, exception: false)
|
|
99
|
+
rescue StandardError
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SidekiqVigil
|
|
4
|
+
class Result
|
|
5
|
+
SEVERITIES = %i[ok warn critical error].freeze
|
|
6
|
+
|
|
7
|
+
attr_reader :check_name, :target, :severity, :value, :threshold, :message, :metadata
|
|
8
|
+
|
|
9
|
+
def initialize(check_name:, severity:, target: "global", value: nil, threshold: nil, message: nil, metadata: {})
|
|
10
|
+
severity = severity.to_sym
|
|
11
|
+
raise ArgumentError, "unknown severity: #{severity}" unless SEVERITIES.include?(severity)
|
|
12
|
+
|
|
13
|
+
@check_name = check_name.to_s
|
|
14
|
+
@target = target.to_s
|
|
15
|
+
@severity = severity
|
|
16
|
+
@value = value
|
|
17
|
+
@threshold = threshold
|
|
18
|
+
@message = message
|
|
19
|
+
@metadata = metadata.freeze
|
|
20
|
+
freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def ok?
|
|
24
|
+
severity == :ok
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def alert_id
|
|
28
|
+
"#{check_name}:#{target}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_h
|
|
32
|
+
{
|
|
33
|
+
check_name: check_name,
|
|
34
|
+
target: target,
|
|
35
|
+
severity: severity,
|
|
36
|
+
value: value,
|
|
37
|
+
threshold: threshold,
|
|
38
|
+
message: message,
|
|
39
|
+
metadata: metadata
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SidekiqVigil
|
|
4
|
+
class Runtime
|
|
5
|
+
attr_reader :reporter, :checker
|
|
6
|
+
|
|
7
|
+
def initialize(
|
|
8
|
+
config:,
|
|
9
|
+
storage:,
|
|
10
|
+
collector: SidekiqVigil.collector,
|
|
11
|
+
logger: SidekiqVigil.logger,
|
|
12
|
+
reporter: nil,
|
|
13
|
+
checker: nil
|
|
14
|
+
)
|
|
15
|
+
@config = config
|
|
16
|
+
@reporter = reporter || Reporter.new(storage:, collector:, config:, logger:)
|
|
17
|
+
@checker = checker || Checker.new(storage:, config:, logger:)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def start
|
|
21
|
+
return false unless config.enabled?
|
|
22
|
+
|
|
23
|
+
reporter.start
|
|
24
|
+
checker.start
|
|
25
|
+
true
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
SidekiqVigil.logger.error("[sidekiq-vigil] startup failed: #{e.class}: #{e.message}")
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def quiet
|
|
32
|
+
checker.stop
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def stop
|
|
36
|
+
checker.stop
|
|
37
|
+
reporter.stop
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
attr_reader :config
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SidekiqVigil
|
|
4
|
+
class SidekiqApi
|
|
5
|
+
def queues(names = :all)
|
|
6
|
+
return Sidekiq::Queue.all if names == :all
|
|
7
|
+
|
|
8
|
+
Array(names).map { |name| Sidekiq::Queue.new(name.to_s) }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def retry_size
|
|
12
|
+
Sidekiq::RetrySet.new.size
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def dead_size
|
|
16
|
+
Sidekiq::DeadSet.new.size
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def processes
|
|
20
|
+
Sidekiq::ProcessSet.new.to_a
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def workers
|
|
24
|
+
Sidekiq::Workers.new.map do |process_id, thread_id, work|
|
|
25
|
+
{
|
|
26
|
+
process_id:,
|
|
27
|
+
thread_id:,
|
|
28
|
+
work: {
|
|
29
|
+
"run_at" => work.run_at.to_f,
|
|
30
|
+
"payload" => work.payload
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def scheduled
|
|
37
|
+
Sidekiq::ScheduledSet.new.to_a
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|