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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +39 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +237 -0
  5. data/Rakefile +10 -0
  6. data/docs/redis_keys.md +18 -0
  7. data/docs/webhook_event.schema.json +41 -0
  8. data/docs/webhook_schema.md +64 -0
  9. data/exe/vigil +6 -0
  10. data/lib/generators/sidekiq_vigil/install_generator.rb +32 -0
  11. data/lib/generators/sidekiq_vigil/templates/sidekiq_vigil.rb +49 -0
  12. data/lib/sidekiq/vigil/version.rb +7 -0
  13. data/lib/sidekiq/vigil.rb +7 -0
  14. data/lib/sidekiq_vigil/alert/cron.rb +106 -0
  15. data/lib/sidekiq_vigil/alert/event.rb +110 -0
  16. data/lib/sidekiq_vigil/alert/grouping.rb +15 -0
  17. data/lib/sidekiq_vigil/alert/manager.rb +227 -0
  18. data/lib/sidekiq_vigil/alert/mute.rb +69 -0
  19. data/lib/sidekiq_vigil/alert/state.rb +51 -0
  20. data/lib/sidekiq_vigil/check/base.rb +52 -0
  21. data/lib/sidekiq_vigil/check/failure_rate.rb +45 -0
  22. data/lib/sidekiq_vigil/check/memory.rb +41 -0
  23. data/lib/sidekiq_vigil/check/process_alive.rb +64 -0
  24. data/lib/sidekiq_vigil/check/queue_latency.rb +21 -0
  25. data/lib/sidekiq_vigil/check/queue_size.rb +20 -0
  26. data/lib/sidekiq_vigil/check/redis_health.rb +55 -0
  27. data/lib/sidekiq_vigil/check/registry.rb +23 -0
  28. data/lib/sidekiq_vigil/check/scheduled_backlog.rb +21 -0
  29. data/lib/sidekiq_vigil/check/set_size.rb +76 -0
  30. data/lib/sidekiq_vigil/check/stuck_jobs.rb +44 -0
  31. data/lib/sidekiq_vigil/check/throughput_anomaly.rb +69 -0
  32. data/lib/sidekiq_vigil/check/utilization.rb +61 -0
  33. data/lib/sidekiq_vigil/checker.rb +149 -0
  34. data/lib/sidekiq_vigil/cli.rb +205 -0
  35. data/lib/sidekiq_vigil/collector.rb +74 -0
  36. data/lib/sidekiq_vigil/config.rb +267 -0
  37. data/lib/sidekiq_vigil/health_app.rb +64 -0
  38. data/lib/sidekiq_vigil/leader_election.rb +54 -0
  39. data/lib/sidekiq_vigil/middleware/server.rb +37 -0
  40. data/lib/sidekiq_vigil/notifier/base.rb +22 -0
  41. data/lib/sidekiq_vigil/notifier/http_transport.rb +32 -0
  42. data/lib/sidekiq_vigil/notifier/log.rb +14 -0
  43. data/lib/sidekiq_vigil/notifier/manager.rb +45 -0
  44. data/lib/sidekiq_vigil/notifier/slack.rb +192 -0
  45. data/lib/sidekiq_vigil/notifier/webhook.rb +25 -0
  46. data/lib/sidekiq_vigil/reporter.rb +103 -0
  47. data/lib/sidekiq_vigil/result.rb +43 -0
  48. data/lib/sidekiq_vigil/runtime.rb +44 -0
  49. data/lib/sidekiq_vigil/sidekiq_api.rb +40 -0
  50. data/lib/sidekiq_vigil/storage.rb +167 -0
  51. data/lib/sidekiq_vigil/timezone.rb +39 -0
  52. data/lib/sidekiq_vigil/version.rb +5 -0
  53. data/lib/sidekiq_vigil.rb +128 -0
  54. metadata +111 -0
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module SidekiqVigil
6
+ class Storage
7
+ class MissingTTL < ArgumentError; end
8
+ class UnmanagedPersistentKey < ArgumentError; end
9
+
10
+ KeyDefinition = Data.define(:pattern, :type, :ttl, :owner)
11
+ KEY_CATALOG = [
12
+ KeyDefinition.new(pattern: "stats:{yyyymmddHHMM}", type: "hash", ttl: "8 days", owner: "Reporter"),
13
+ KeyDefinition.new(pattern: "exec:{queue}", type: "hash", ttl: "1 hour", owner: "Reporter"),
14
+ KeyDefinition.new(pattern: "alerts", type: "hash", ttl: "managed", owner: "Alert::Manager"),
15
+ KeyDefinition.new(pattern: "history:{alert_id}", type: "list", ttl: "24 hours", owner: "Alert::Manager"),
16
+ KeyDefinition.new(pattern: "snapshot", type: "string", ttl: "interval × 4", owner: "Checker"),
17
+ KeyDefinition.new(pattern: "leader", type: "string", ttl: "interval × 3", owner: "LeaderElection"),
18
+ KeyDefinition.new(pattern: "mem:{process_id}", type: "string", ttl: "flush interval × 3", owner: "Reporter"),
19
+ KeyDefinition.new(pattern: "config_digest", type: "hash", ttl: "1 hour", owner: "Reporter"),
20
+ KeyDefinition.new(pattern: "mute", type: "string", ttl: "requested duration", owner: "Alert::Mute"),
21
+ KeyDefinition.new(
22
+ pattern: "check_state:{check-specific-suffix}",
23
+ type: "string",
24
+ ttl: "check-specific",
25
+ owner: "Checks"
26
+ )
27
+ ].freeze
28
+
29
+ attr_reader :prefix
30
+
31
+ def initialize(redis: nil, key_prefix: "default")
32
+ @redis = redis
33
+ @prefix = "vigil:#{key_prefix}:"
34
+ end
35
+
36
+ def key(suffix)
37
+ "#{prefix}#{suffix}"
38
+ end
39
+
40
+ def set(suffix, value, ttl:)
41
+ require_ttl!(ttl)
42
+ with_redis { |redis| redis.call("SET", key(suffix), value, "PX", ttl_ms(ttl)) }
43
+ end
44
+
45
+ def get(suffix)
46
+ with_redis { |redis| redis.call("GET", key(suffix)) }
47
+ end
48
+
49
+ def hash_write(suffix, values, ttl:)
50
+ require_ttl!(ttl)
51
+ return if values.empty?
52
+
53
+ with_redis do |redis|
54
+ redis.pipelined do |pipeline|
55
+ pipeline.call("HSET", key(suffix), *values.flatten)
56
+ pipeline.call("PEXPIRE", key(suffix), ttl_ms(ttl))
57
+ end
58
+ end
59
+ end
60
+
61
+ def hash_increment(suffix, values, ttl:)
62
+ require_ttl!(ttl)
63
+ return if values.empty?
64
+
65
+ with_redis do |redis|
66
+ redis.pipelined do |pipeline|
67
+ values.each do |field, amount|
68
+ command = amount.is_a?(Integer) ? "HINCRBY" : "HINCRBYFLOAT"
69
+ pipeline.call(command, key(suffix), field, amount)
70
+ end
71
+ pipeline.call("PEXPIRE", key(suffix), ttl_ms(ttl))
72
+ end
73
+ end
74
+ end
75
+
76
+ def managed_hash_write(suffix, field, value)
77
+ raise UnmanagedPersistentKey, "only alerts may be written without a ttl" unless suffix == "alerts"
78
+
79
+ with_redis { |redis| redis.call("HSET", key(suffix), field, value) }
80
+ end
81
+
82
+ def hash_get_all(suffix)
83
+ with_redis { |redis| redis.call("HGETALL", key(suffix)) }
84
+ end
85
+
86
+ def hash_delete(suffix, *fields)
87
+ return if fields.empty?
88
+
89
+ with_redis { |redis| redis.call("HDEL", key(suffix), *fields) }
90
+ end
91
+
92
+ def list_push(suffix, value, ttl:, limit: 30)
93
+ require_ttl!(ttl)
94
+ with_redis do |redis|
95
+ redis.pipelined do |pipeline|
96
+ pipeline.call("RPUSH", key(suffix), value)
97
+ pipeline.call("LTRIM", key(suffix), -limit, -1)
98
+ pipeline.call("PEXPIRE", key(suffix), ttl_ms(ttl))
99
+ end
100
+ end
101
+ end
102
+
103
+ def list_range(suffix)
104
+ with_redis { |redis| redis.call("LRANGE", key(suffix), 0, -1) }
105
+ end
106
+
107
+ def delete(suffix)
108
+ with_redis { |redis| redis.call("DEL", key(suffix)) }
109
+ end
110
+
111
+ def scan(pattern = "*")
112
+ with_redis do |redis|
113
+ cursor = "0"
114
+ keys = []
115
+ loop do
116
+ cursor, batch = redis.call("SCAN", cursor, "MATCH", key(pattern), "COUNT", 100)
117
+ keys.concat(batch)
118
+ break if cursor == "0"
119
+ end
120
+ keys
121
+ end
122
+ end
123
+
124
+ def acquire_lock(suffix, token, ttl_ms:)
125
+ raise MissingTTL, "a positive ttl is required" unless ttl_ms.is_a?(Numeric) && ttl_ms.positive?
126
+
127
+ with_redis { |redis| redis.call("SET", key(suffix), token, "NX", "PX", ttl_ms) == "OK" }
128
+ end
129
+
130
+ def eval(script, keys:, argv:)
131
+ namespaced_keys = keys.map { |item| key(item) }
132
+ with_redis { |redis| redis.call("EVAL", script, namespaced_keys.length, *namespaced_keys, *argv) }
133
+ end
134
+
135
+ def ping
136
+ with_redis { |redis| redis.call("PING") }
137
+ end
138
+
139
+ def info(section = nil)
140
+ with_redis { |redis| parse_info(redis.call("INFO", *Array(section))) }
141
+ end
142
+
143
+ private
144
+
145
+ def require_ttl!(ttl)
146
+ raise MissingTTL, "a positive ttl is required" unless ttl.is_a?(Numeric) && ttl.positive?
147
+ end
148
+
149
+ def ttl_ms(ttl)
150
+ (ttl * 1_000).ceil
151
+ end
152
+
153
+ def with_redis(&)
154
+ return yield @redis if @redis
155
+
156
+ Sidekiq.redis(&)
157
+ end
158
+
159
+ def parse_info(raw)
160
+ raw.each_line.filter_map do |line|
161
+ next if line.start_with?("#") || !line.include?(":")
162
+
163
+ line.strip.split(":", 2)
164
+ end.to_h
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module SidekiqVigil
6
+ module Timezone
7
+ MUTEX = Mutex.new
8
+
9
+ module_function
10
+
11
+ def local_time(time, zone)
12
+ return time.getutc if zone == "UTC"
13
+ return time.getlocal(zone) if zone.match?(/\A[+-]\d{2}:\d{2}\z/)
14
+
15
+ MUTEX.synchronize do
16
+ previous = ENV.fetch("TZ", nil)
17
+ ENV["TZ"] = zone
18
+ Time.at(time.to_f).localtime
19
+ ensure
20
+ ENV["TZ"] = previous
21
+ end
22
+ end
23
+
24
+ def same_local_days_ago(time, days, zone)
25
+ local = local_time(time, zone)
26
+ date = local.to_date - days
27
+ return Time.utc(date.year, date.month, date.day, local.hour, local.min, local.sec) if zone == "UTC"
28
+ return Time.new(date.year, date.month, date.day, local.hour, local.min, local.sec, zone) if zone.match?(/\A[+-]\d{2}:\d{2}\z/)
29
+
30
+ MUTEX.synchronize do
31
+ previous = ENV.fetch("TZ", nil)
32
+ ENV["TZ"] = zone
33
+ Time.local(date.year, date.month, date.day, local.hour, local.min, local.sec)
34
+ ensure
35
+ ENV["TZ"] = previous
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqVigil
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "sidekiq"
5
+ require "sidekiq/api"
6
+
7
+ require_relative "sidekiq_vigil/version"
8
+
9
+ module SidekiqVigil
10
+ class Error < StandardError; end
11
+
12
+ class << self
13
+ attr_writer :logger
14
+
15
+ def configure
16
+ yield config
17
+ config.validate!
18
+ config
19
+ end
20
+
21
+ def config
22
+ @config ||= Config.new
23
+ end
24
+
25
+ def collector
26
+ @collector ||= Collector.new
27
+ end
28
+
29
+ def logger
30
+ @logger ||= defined?(Sidekiq.logger) ? Sidekiq.logger : Logger.new($stdout)
31
+ end
32
+
33
+ def build_storage(configuration = config)
34
+ redis = if configuration.redis
35
+ options = configuration.redis.transform_keys(&:to_sym).compact
36
+ pool_size = options.delete(:pool_size) || 5
37
+ pool_timeout = options.delete(:pool_timeout) || 1
38
+ RedisClient.config(**options).new_pool(size: pool_size, timeout: pool_timeout)
39
+ end
40
+ Storage.new(redis:, key_prefix: configuration.key_prefix)
41
+ end
42
+
43
+ def install!
44
+ return if @installed
45
+
46
+ Sidekiq.configure_server do |server|
47
+ server.server_middleware { |chain| chain.add(Middleware::Server) }
48
+ server.on(:startup) { start_runtime }
49
+ server.on(:quiet) { @runtime&.quiet }
50
+ server.on(:shutdown) { stop_runtime }
51
+ end
52
+ @installed = true
53
+ end
54
+
55
+ def start_runtime
56
+ @runtime ||= Runtime.new(config:, storage: build_storage)
57
+ @runtime.start
58
+ end
59
+
60
+ def stop_runtime
61
+ @runtime&.stop
62
+ @runtime = nil
63
+ end
64
+
65
+ def reset!
66
+ @config = Config.new
67
+ @collector = Collector.new
68
+ @logger = nil
69
+ @runtime = nil
70
+ end
71
+ end
72
+ end
73
+
74
+ require_relative "sidekiq_vigil/result"
75
+ require_relative "sidekiq_vigil/config"
76
+ require_relative "sidekiq_vigil/storage"
77
+ require_relative "sidekiq_vigil/sidekiq_api"
78
+ require_relative "sidekiq_vigil/timezone"
79
+ require_relative "sidekiq_vigil/check/base"
80
+ require_relative "sidekiq_vigil/check/registry"
81
+ require_relative "sidekiq_vigil/check/queue_latency"
82
+ require_relative "sidekiq_vigil/check/queue_size"
83
+ require_relative "sidekiq_vigil/check/set_size"
84
+ require_relative "sidekiq_vigil/check/process_alive"
85
+ require_relative "sidekiq_vigil/check/utilization"
86
+ require_relative "sidekiq_vigil/check/failure_rate"
87
+ require_relative "sidekiq_vigil/check/stuck_jobs"
88
+ require_relative "sidekiq_vigil/check/memory"
89
+ require_relative "sidekiq_vigil/check/redis_health"
90
+ require_relative "sidekiq_vigil/check/scheduled_backlog"
91
+ require_relative "sidekiq_vigil/check/throughput_anomaly"
92
+ require_relative "sidekiq_vigil/alert/state"
93
+ require_relative "sidekiq_vigil/alert/event"
94
+ require_relative "sidekiq_vigil/alert/cron"
95
+ require_relative "sidekiq_vigil/alert/mute"
96
+ require_relative "sidekiq_vigil/alert/grouping"
97
+ require_relative "sidekiq_vigil/alert/manager"
98
+ require_relative "sidekiq_vigil/notifier/base"
99
+ require_relative "sidekiq_vigil/notifier/log"
100
+ require_relative "sidekiq_vigil/notifier/http_transport"
101
+ require_relative "sidekiq_vigil/notifier/webhook"
102
+ require_relative "sidekiq_vigil/notifier/slack"
103
+ require_relative "sidekiq_vigil/notifier/manager"
104
+ require_relative "sidekiq_vigil/leader_election"
105
+ require_relative "sidekiq_vigil/collector"
106
+ require_relative "sidekiq_vigil/middleware/server"
107
+ require_relative "sidekiq_vigil/reporter"
108
+ require_relative "sidekiq_vigil/checker"
109
+ require_relative "sidekiq_vigil/runtime"
110
+ require_relative "sidekiq_vigil/health_app"
111
+ require_relative "sidekiq_vigil/cli"
112
+
113
+ SidekiqVigil::BUILT_IN_CHECKS = {
114
+ queue_latency: SidekiqVigil::Check::QueueLatency,
115
+ queue_size: SidekiqVigil::Check::QueueSize,
116
+ retry_set: SidekiqVigil::Check::RetrySet,
117
+ dead_set: SidekiqVigil::Check::DeadSet,
118
+ process_alive: SidekiqVigil::Check::ProcessAlive,
119
+ utilization: SidekiqVigil::Check::Utilization,
120
+ failure_rate: SidekiqVigil::Check::FailureRate,
121
+ stuck_jobs: SidekiqVigil::Check::StuckJobs,
122
+ memory: SidekiqVigil::Check::Memory,
123
+ redis_health: SidekiqVigil::Check::RedisHealth,
124
+ scheduled_backlog: SidekiqVigil::Check::ScheduledBacklog,
125
+ throughput_anomaly: SidekiqVigil::Check::ThroughputAnomaly
126
+ }.freeze
127
+
128
+ SidekiqVigil.install!
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sidekiq-vigil
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yudai Takada
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: sidekiq
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.0'
26
+ description: Monitors Sidekiq health with Redis-backed checks, alert lifecycles, and
27
+ pluggable notifications.
28
+ email:
29
+ - t.yudai92@gmail.com
30
+ executables:
31
+ - vigil
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - ".rubocop.yml"
36
+ - LICENSE.txt
37
+ - README.md
38
+ - Rakefile
39
+ - docs/redis_keys.md
40
+ - docs/webhook_event.schema.json
41
+ - docs/webhook_schema.md
42
+ - exe/vigil
43
+ - lib/generators/sidekiq_vigil/install_generator.rb
44
+ - lib/generators/sidekiq_vigil/templates/sidekiq_vigil.rb
45
+ - lib/sidekiq/vigil.rb
46
+ - lib/sidekiq/vigil/version.rb
47
+ - lib/sidekiq_vigil.rb
48
+ - lib/sidekiq_vigil/alert/cron.rb
49
+ - lib/sidekiq_vigil/alert/event.rb
50
+ - lib/sidekiq_vigil/alert/grouping.rb
51
+ - lib/sidekiq_vigil/alert/manager.rb
52
+ - lib/sidekiq_vigil/alert/mute.rb
53
+ - lib/sidekiq_vigil/alert/state.rb
54
+ - lib/sidekiq_vigil/check/base.rb
55
+ - lib/sidekiq_vigil/check/failure_rate.rb
56
+ - lib/sidekiq_vigil/check/memory.rb
57
+ - lib/sidekiq_vigil/check/process_alive.rb
58
+ - lib/sidekiq_vigil/check/queue_latency.rb
59
+ - lib/sidekiq_vigil/check/queue_size.rb
60
+ - lib/sidekiq_vigil/check/redis_health.rb
61
+ - lib/sidekiq_vigil/check/registry.rb
62
+ - lib/sidekiq_vigil/check/scheduled_backlog.rb
63
+ - lib/sidekiq_vigil/check/set_size.rb
64
+ - lib/sidekiq_vigil/check/stuck_jobs.rb
65
+ - lib/sidekiq_vigil/check/throughput_anomaly.rb
66
+ - lib/sidekiq_vigil/check/utilization.rb
67
+ - lib/sidekiq_vigil/checker.rb
68
+ - lib/sidekiq_vigil/cli.rb
69
+ - lib/sidekiq_vigil/collector.rb
70
+ - lib/sidekiq_vigil/config.rb
71
+ - lib/sidekiq_vigil/health_app.rb
72
+ - lib/sidekiq_vigil/leader_election.rb
73
+ - lib/sidekiq_vigil/middleware/server.rb
74
+ - lib/sidekiq_vigil/notifier/base.rb
75
+ - lib/sidekiq_vigil/notifier/http_transport.rb
76
+ - lib/sidekiq_vigil/notifier/log.rb
77
+ - lib/sidekiq_vigil/notifier/manager.rb
78
+ - lib/sidekiq_vigil/notifier/slack.rb
79
+ - lib/sidekiq_vigil/notifier/webhook.rb
80
+ - lib/sidekiq_vigil/reporter.rb
81
+ - lib/sidekiq_vigil/result.rb
82
+ - lib/sidekiq_vigil/runtime.rb
83
+ - lib/sidekiq_vigil/sidekiq_api.rb
84
+ - lib/sidekiq_vigil/storage.rb
85
+ - lib/sidekiq_vigil/timezone.rb
86
+ - lib/sidekiq_vigil/version.rb
87
+ homepage: https://github.com/ydah/sidekiq-vigil
88
+ licenses:
89
+ - MIT
90
+ metadata:
91
+ homepage_uri: https://github.com/ydah/sidekiq-vigil
92
+ source_code_uri: https://github.com/ydah/sidekiq-vigil/tree/main
93
+ rubygems_mfa_required: 'true'
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: 3.2.0
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubygems_version: 4.0.6
109
+ specification_version: 4
110
+ summary: Battery-included monitoring and alerting for Sidekiq
111
+ test_files: []