neeto-monitor-ruby 1.0.42

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.
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NeetoMonitorRuby
4
+ module MonitorUtils
5
+ MONITOR_TYPES = {
6
+ check: "check",
7
+ job: "job",
8
+ heartbeat: "heartbeat"
9
+ }.freeze
10
+
11
+ JOB_STATES = {
12
+ run: "run",
13
+ complete: "complete",
14
+ fail: "fail"
15
+ }.freeze
16
+
17
+ def self.included(klass)
18
+ klass.extend(ClassMethods)
19
+ end
20
+
21
+ module ClassMethods
22
+ def job(monitor_key, &block)
23
+ monitor = Monitor.new(monitor_key)
24
+ series = monitor.generate_stamp
25
+ monitor.job_ping(state: JOB_STATES[:run], series:)
26
+
27
+ block.call
28
+ monitor.job_ping(state: JOB_STATES[:complete], series:)
29
+ rescue StandardError => exception
30
+ monitor.job_ping(state: JOB_STATES[:fail], message: exception.message, series:)
31
+ raise exception
32
+ end
33
+ end
34
+
35
+ def generate_stamp
36
+ Time.now.utc.to_f
37
+ end
38
+
39
+ def generate_series
40
+ "#{generate_stamp}-#{random_string}"
41
+ end
42
+
43
+ def random_string
44
+ rand(2**256).to_s(36)[0..7]
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require_relative "configuration"
5
+ require_relative "plugin"
6
+
7
+ module NeetoMonitorRuby
8
+ extend Forwardable
9
+ extend self
10
+
11
+ attr_accessor :config, :logger
12
+
13
+ def_delegator "NeetoMonitorRuby::Monitor", :load_monitors!
14
+
15
+ def init!(options = {})
16
+ self.config = Configuration.configure!(options, true)
17
+ self.logger = self.config.logger
18
+ end
19
+
20
+ def load_plugins!
21
+ Dir[File.expand_path("../plugins/*.rb", __FILE__)].each do |plugin|
22
+ require plugin
23
+ end
24
+ Plugin.load!(self.config)
25
+ end
26
+ end
27
+
28
+ NeetoMonitor = NeetoMonitorRuby
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NeetoMonitorRuby
4
+ class Plugin
5
+ class << self
6
+ @@instances = {}
7
+
8
+ def instances
9
+ @@instances
10
+ end
11
+
12
+ def register(name = nil, &block)
13
+ raise(ArgumentError, "Plugin name is required, but was nil.") unless name
14
+
15
+ key = name.to_sym
16
+ raise("Already registered: #{name}") if instances[key]
17
+
18
+ instances[key] = new(name).tap { |d| d.instance_eval(&block) }
19
+ end
20
+
21
+ def load!(config)
22
+ instances.each_pair do |name, plugin|
23
+ if config.public_send("#{name}_enabled")
24
+ plugin.load!
25
+ else
26
+ NeetoMonitorRuby.logger.debug("skipped plugin #{name} load because it's disabled")
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ attr_reader :name, :requirements, :executions, :config
33
+
34
+ def initialize(name)
35
+ @name = name
36
+ @loaded = false
37
+ @requirements = []
38
+ @executions = []
39
+ end
40
+
41
+ def requirement(&block)
42
+ @requirements << block
43
+ end
44
+
45
+ def execution(&block)
46
+ @executions << block
47
+ end
48
+
49
+ def load!
50
+ if loaded?
51
+ NeetoMonitorRuby.logger.debug("skip plugin #{name} as already loaded")
52
+ return false
53
+ elsif fulfilled?
54
+ NeetoMonitorRuby.logger.debug("load plugin #{name}")
55
+ @executions.each { |block| instance_eval(&block) }
56
+ @loaded = true
57
+ else
58
+ NeetoMonitorRuby.logger.debug("skip plugin load #{name} requirement not fulfilled")
59
+ end
60
+
61
+ @loaded
62
+ rescue => exception
63
+ log_plugin_exception(exception)
64
+ @loaded = true
65
+ false
66
+ end
67
+
68
+ def fulfilled?
69
+ @requirements.all? { |block| instance_eval(&block) }
70
+ rescue => exception
71
+ log_plugin_exception(exception)
72
+ false
73
+ end
74
+
75
+ def loaded?
76
+ @loaded
77
+ end
78
+
79
+ private
80
+
81
+ def log_plugin_exception(exception)
82
+ NeetoMonitorRuby.logger.error("plugin error name=#{name} class=#{exception.class} #{exception.message}")
83
+ NeetoMonitorRuby.logger.error e.backtrace
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../monitor_utils"
4
+
5
+ module NeetoMonitorRuby
6
+ module Plugins
7
+ module Sidekiq
8
+ class ServerMiddleware
9
+ SKIPPED_WORKERS = %w(ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper).freeze
10
+
11
+ attr_reader :sidekiq_worker
12
+
13
+ def call(worker, _message, _queue)
14
+ @sidekiq_worker = worker
15
+
16
+ job_ping(state: MonitorUtils::JOB_STATES[:run])
17
+
18
+ worker_result = yield
19
+
20
+ job_ping(state: MonitorUtils::JOB_STATES[:complete])
21
+
22
+ worker_result
23
+ rescue => exception
24
+ job_ping(state: MonitorUtils::JOB_STATES[:fail], message: exception.message)
25
+
26
+ raise exception
27
+ end
28
+
29
+ private
30
+
31
+ def job_ping(state:, message: nil)
32
+ return unless can_monitor?
33
+
34
+ ::Sidekiq.logger.debug("neetoMonitor pinging key: #{monitor_key} state: #{state} message: #{message}")
35
+
36
+ neeto_monitor.job_ping(state:, message:, series:)
37
+
38
+ rescue => exception
39
+ ::Sidekiq.logger.error("Error: neetoMonitor ping #{monitor_key} - #{exception.message}")
40
+ ::Sidekiq.logger.error(exception.backtrace)
41
+ end
42
+
43
+ def series
44
+ @_series ||= neeto_monitor.generate_series
45
+ end
46
+
47
+ def neeto_monitor
48
+ @_neeto_monitor ||= NeetoMonitorRuby::Monitor.new(monitor_key)
49
+ end
50
+
51
+ def monitor_key
52
+ return @_monitor_key if @_monitor_key
53
+
54
+ @_monitor_key = fetch_sidekiq_option("neeto_monitor_key", sidekiq_worker.class.name)
55
+ return @_monitor_key unless NeetoMonitorRuby.config.key_prefix_enabled && NeetoMonitorRuby.config.key_prefix
56
+
57
+ @_monitor_key = [NeetoMonitorRuby.config.key_prefix, @_monitor_key].join("::")
58
+ end
59
+
60
+ def neeto_monitor_disabled?
61
+ fetch_sidekiq_option("neeto_monitor_disabled", false)
62
+ end
63
+
64
+ def fetch_sidekiq_option(key, default = nil)
65
+ sidekiq_worker.class.sidekiq_options.fetch(key, default)
66
+ end
67
+
68
+ def can_monitor?
69
+ @_can_monitor ||= !neeto_monitor.api_key.nil? && !neeto_monitor_disabled? && !skip?
70
+ end
71
+
72
+ def skip?
73
+ SKIPPED_WORKERS.include?(monitor_key) || SKIPPED_WORKERS.include?(monitor_key.split("::", 2).last)
74
+ end
75
+ end
76
+
77
+ Plugin.register "sidekiq" do
78
+ requirement { defined?(::Sidekiq) }
79
+
80
+ execution do
81
+ ::Sidekiq.configure_server do |sidekiq|
82
+ sidekiq.server_middleware do |chain|
83
+ chain.prepend ServerMiddleware
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NeetoMonitorRuby
4
+ VERSION = "1.0.42".freeze
5
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq"
4
+
5
+ module Dummy
6
+ class FailureWorker
7
+ include Sidekiq::Worker
8
+ sidekiq_options neeto_monitor_key: "failure_worker"
9
+
10
+ def perform
11
+ raise StandardError
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq"
4
+
5
+ module Dummy
6
+ class MonitorDisabledWorker
7
+ include Sidekiq::Worker
8
+ sidekiq_options neeto_monitor_disabled: true
9
+
10
+ def perform
11
+ true
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq"
4
+
5
+ module Dummy
6
+ class SuccessWorker
7
+ include Sidekiq::Worker
8
+
9
+ def perform
10
+ true
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../test_helper"
4
+
5
+ module NeetoMonitor
6
+ class ConfigurationTest < Minitest::Test
7
+ attr_reader :yaml_file_path
8
+
9
+ def setup
10
+ @yaml_file_path = "./test/config.yml"
11
+ NeetoMonitor::Configuration.reset!
12
+ end
13
+
14
+ def test_configure_with_options
15
+ config = NeetoMonitor::Configuration.configure!({ api_key: "api_key", environment: "test" }, true)
16
+
17
+ assert_equal "api_key", config.api_key
18
+ assert_equal "test", config.environment
19
+ end
20
+
21
+ def test_configure_with_default_values
22
+ config = NeetoMonitor::Configuration.configure!
23
+
24
+ assert_equal NeetoMonitor::Configuration::BASE_URL, config.base_url
25
+ assert_equal NeetoMonitor::Configuration::DEFAULT_PING_TIMEOUT, config.ping_timeout
26
+ assert_equal true, config.sidekiq_enabled
27
+ assert_nil config.config_path
28
+ end
29
+
30
+ def test_load_config_with_valid_file
31
+ config_file = File.new(yaml_file_path, "w")
32
+ config_file.write(YAML.dump(api_key: "dgs56sds7d", environment: "test"))
33
+ config_file.close
34
+
35
+ config = NeetoMonitor::Configuration.configure!({ config_path: config_file.path }, true)
36
+
37
+ assert_equal "dgs56sds7d", config.api_key
38
+ assert_equal "test", config.environment
39
+ assert_equal "https://neetomonitor.com", config.base_url
40
+
41
+ File.delete(yaml_file_path)
42
+ end
43
+
44
+ def test_load_config_with_invalid_file
45
+ config_file = File.new(yaml_file_path, "w")
46
+ config_file.write("api_key kbkj'api_key'\" \n}environment: 'test'")
47
+ config_file.close
48
+
49
+ assert_raises NeetoMonitor::ConfigurationError do
50
+ NeetoMonitor::Configuration.load_config(config_file.path)
51
+ end
52
+
53
+ File.delete(yaml_file_path)
54
+ end
55
+
56
+ def test_reset
57
+ config = NeetoMonitor::Configuration.configure!({ api_key: "api_key", config_path: "no-config.yml" }, true)
58
+
59
+ assert_equal "https://neetomonitor.com", config.base_url
60
+
61
+ NeetoMonitor::Configuration.reset!
62
+
63
+ assert_equal "3d6d07b40ac6", NeetoMonitor::Configuration.config.api_key
64
+ assert_nil NeetoMonitor::Configuration.config.config_path
65
+ end
66
+
67
+ def test_load_config_with_file_not_found
68
+ assert_raises NeetoMonitor::ConfigurationError do
69
+ NeetoMonitor::Configuration.load_config("./test/not_found.yml")
70
+ end
71
+ end
72
+
73
+ def test_fetch_or_set_value_with_environment_variable
74
+ original_value = ENV["NEETO_MONITOR_API_KEY"]
75
+ ENV["NEETO_MONITOR_API_KEY"] = "env_api_key"
76
+
77
+ config = NeetoMonitor::Configuration.new
78
+
79
+ assert_equal "env_api_key", config.api_key
80
+
81
+ ENV["NEETO_MONITOR_API_KEY"] = original_value
82
+ end
83
+
84
+ def test_fetch_or_set_value_with_options
85
+ config = NeetoMonitor::Configuration.new(api_key: "options_api_key")
86
+
87
+ assert_equal "options_api_key", config.api_key
88
+ end
89
+
90
+ def test_fetch_or_set_value_with_default_value
91
+ config = NeetoMonitor::Configuration.new
92
+
93
+ assert_equal 8, config.ping_timeout
94
+ assert_equal true, config.sidekiq_enabled
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class NeetoMonitor::HeartbeatRunnerJobTest < ActiveJob::TestCase
6
+ def test_job_pings_heartbeat_and_enqueues_another_job
7
+ heartbeat_name = "neeto-heartbeat"
8
+ schedule = "* * * * *"
9
+ stub_ping_request
10
+
11
+ NeetoMonitorRuby::HeartbeatRunnerJob.perform_now(heartbeat_name, schedule)
12
+
13
+ assert_enqueued_with(
14
+ job: NeetoMonitorRuby::HeartbeatRunnerJob,
15
+ args: [heartbeat_name, schedule],
16
+ at: ->(t) { t > Time.now }
17
+ ) do
18
+ NeetoMonitorRuby::HeartbeatRunnerJob.perform_now(heartbeat_name, schedule)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def stub_ping_request(monitor_key = "neeto-heartbeat", params = {}, status_code = 200)
25
+ stub_request(:get, "https://neetomonitor.com/tm/3d6d07b40ac6/#{monitor_key}")
26
+ .with(
27
+ query: hash_including({ "env" => "test" }.merge(params)),
28
+ headers: {
29
+ "Accept" => "application/json",
30
+ "Content-Type" => "application/json",
31
+ "User-Agent" => "neeto-monitor-ruby"
32
+ }
33
+ )
34
+ .to_return(status: status_code, body: "", headers: {})
35
+ end
36
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../test_helper"
4
+
5
+ class NeetoMonitor::MonitorTest < Minitest::Test
6
+ include ActiveJob::TestHelper
7
+
8
+ def test_monitor_sends_telemetry_event
9
+ stub_ping_request
10
+ monitor = NeetoMonitor::Monitor.new("website-check")
11
+
12
+ assert monitor.ping
13
+ end
14
+
15
+ def test_monitor_sends_telemetry_event_with_params
16
+ params = { state: "run", series: "job101" }
17
+ job_monitor_key = "night-job-check"
18
+
19
+ stub_ping_request(job_monitor_key, params)
20
+ monitor = NeetoMonitor::Monitor.new(job_monitor_key)
21
+
22
+ assert monitor.ping(params)
23
+ end
24
+
25
+ def test_monitor_job_sends_complete_event
26
+ job_monitor_key = "night-job-check"
27
+
28
+ stub_ping_request(job_monitor_key, { state: "run", kind: "job" })
29
+ stub_ping_request(job_monitor_key, { state: "complete", kind: "job" })
30
+
31
+ response = NeetoMonitor::Monitor.job job_monitor_key do
32
+ perform_addition_work(100, 200)
33
+ end
34
+
35
+ assert response
36
+ end
37
+
38
+ def test_monitor_job_sends_fail_event
39
+ job_monitor_key = "night-job-check"
40
+
41
+ stub_ping_request(job_monitor_key, { state: "run", kind: "job" })
42
+ stub_ping_request(job_monitor_key, { state: "fail", kind: "job" })
43
+
44
+ assert_raises(StandardError) do
45
+ NeetoMonitor::Monitor.job job_monitor_key { raise StandardError.new }
46
+ end
47
+ end
48
+
49
+ def test_load_monitors!
50
+ yaml_file_path = "./test/config.yml"
51
+ reset = true
52
+ stub_bulk_create_request
53
+ stub_ping_request("neeto-heartbeat")
54
+
55
+ assert_no_enqueued_jobs
56
+
57
+ NeetoMonitorRuby::HeartbeatRunnerJob.perform_later("neeto-heartbeat", "* * * * *")
58
+
59
+ assert_enqueued_jobs 1
60
+
61
+ config_file = File.new(yaml_file_path, "w")
62
+ config_file.write(YAML.dump(config_hash))
63
+ config_file.close
64
+
65
+ NeetoMonitor.init!({ config_path: config_file.path })
66
+ NeetoMonitor::Monitor.load_monitors!
67
+
68
+ assert_enqueued_jobs 1
69
+
70
+ NeetoMonitor.init!
71
+ File.delete(yaml_file_path)
72
+ end
73
+
74
+ private
75
+
76
+ def stub_ping_request(monitor_key = "website-check", params = {}, status_code = 200)
77
+ stub_request(:get, "https://neetomonitor.com/tm/3d6d07b40ac6/#{monitor_key}")
78
+ .with(
79
+ query: hash_including({ "env" => "test" }.merge(params)),
80
+ headers: {
81
+ "Accept" => "application/json",
82
+ "Content-Type" => "application/json",
83
+ "User-Agent" => "neeto-monitor-ruby"
84
+ }
85
+ )
86
+ .to_return(status: status_code, body: "", headers: {})
87
+ end
88
+
89
+ def perform_addition_work(param1, param2)
90
+ param1 + param2
91
+ end
92
+
93
+ def stub_bulk_create_request
94
+ stub_request(:post, "https://neetomonitor.com/api/v1/clients/bulk_checks")
95
+ .with(
96
+ body: bulk_create_params.to_json,
97
+ headers: {
98
+ "Accept" => "application/json",
99
+ "Content-Type" => "application/json",
100
+ "User-Agent" => "neeto-monitor-ruby"
101
+ }
102
+ )
103
+ .to_return(status: 200, body: "", headers: {})
104
+ end
105
+
106
+ def config_hash
107
+ {
108
+ api_key: "3d6d07b40ac6",
109
+ environment: "test",
110
+ monitors: {
111
+ jobs: [
112
+ {
113
+ name: "neeto-job",
114
+ schedule: "0 0 * * *"
115
+ }
116
+ ],
117
+ checks: [
118
+ {
119
+ name: "neeto-monitor",
120
+ request_attributes: {
121
+ endpoint: "http://app.neetomonitor.test/health_check/",
122
+ kind: "http",
123
+ verb: "get",
124
+ interval: 10,
125
+ timeout: 20
126
+ }
127
+ }
128
+ ],
129
+ heartbeats: [
130
+ {
131
+ name: "neeto-heartbeat",
132
+ schedule: "* * * * *"
133
+ }
134
+ ]
135
+ }
136
+ }
137
+ end
138
+
139
+ def bulk_create_params
140
+ {
141
+ checks: [
142
+ {
143
+ name: "neeto-job",
144
+ schedule: "0 0 * * *",
145
+ kind: "job",
146
+ environment: "test"
147
+ },
148
+ {
149
+ name: "neeto-monitor",
150
+ request_attributes: {
151
+ endpoint: "http://app.neetomonitor.test/health_check/",
152
+ kind: "http",
153
+ verb: "get",
154
+ interval: 10,
155
+ timeout: 20
156
+ },
157
+ kind: "check",
158
+ environment: "test"
159
+ }
160
+ ],
161
+ api_key: "3d6d07b40ac6"
162
+ }
163
+ end
164
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../test_helper"
4
+
5
+ class NeetoMonitorTest < Minitest::Test
6
+ def test_init!
7
+ assert_instance_of Logger, NeetoMonitor.logger
8
+ assert_instance_of NeetoMonitor::Configuration, NeetoMonitor.config
9
+ end
10
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../test_helper"
4
+
5
+ module NeetoMonitor
6
+ class PluginTest < Minitest::Test
7
+ attr_reader :config
8
+
9
+ def setup
10
+ NeetoMonitor::Plugin.instances.clear
11
+ @config = Configuration.new({ sidekiq_enabled: true })
12
+ end
13
+
14
+ def test_register
15
+ NeetoMonitor::Plugin.register("sidekiq", &requirement_block)
16
+ instances = NeetoMonitor::Plugin.instances
17
+
18
+ assert_equal 1, instances.size
19
+ assert_equal "sidekiq", instances[:sidekiq].name
20
+ assert_instance_of NeetoMonitor::Plugin, instances[:sidekiq]
21
+ end
22
+
23
+ def test_register_without_name
24
+ assert_raises(ArgumentError) { NeetoMonitor::Plugin.register }
25
+ end
26
+
27
+ def test_register_with_duplicate_name
28
+ NeetoMonitor::Plugin.register("sidekiq", &requirement_block)
29
+
30
+ assert_raises(RuntimeError) { NeetoMonitor::Plugin.register("sidekiq", &requirement_block) }
31
+ end
32
+
33
+ def test_load_enabled_plugins
34
+ @@executed = false
35
+
36
+ NeetoMonitor::Plugin.register("sidekiq") do
37
+ requirement { true }
38
+ execution { @@executed = true }
39
+ end
40
+
41
+ NeetoMonitor::Plugin.load!(config)
42
+
43
+ sidekiq_instance = NeetoMonitor::Plugin.instances[:sidekiq]
44
+
45
+ assert sidekiq_instance.loaded?
46
+ assert @@executed
47
+ end
48
+
49
+ def test_skip_disabled_plugins
50
+ NeetoMonitor::Plugin.register("sidekiq", &requirement_block)
51
+
52
+ new_config = Configuration.new({ sidekiq_enabled: false })
53
+ NeetoMonitor::Plugin.load!(new_config)
54
+
55
+ instances = NeetoMonitor::Plugin.instances
56
+ refute instances[:sidekiq].loaded?
57
+ end
58
+
59
+ def test_skip_plugins_with_unfulfilled_requirements
60
+ NeetoMonitor::Plugin.register("sidekiq", &requirement_unfulfilled_block)
61
+
62
+ NeetoMonitor::Plugin.load!(config)
63
+
64
+ instances = NeetoMonitor::Plugin.instances
65
+ refute instances[:sidekiq].loaded?
66
+ end
67
+
68
+ private
69
+
70
+ def requirement_block
71
+ proc { requirement { true } }
72
+ end
73
+
74
+ def requirement_unfulfilled_block
75
+ proc { requirement { false } }
76
+ end
77
+ end
78
+ end