neeto-monitor-ruby 1.0.42

Sign up to get free protection for your applications and to get access to all the features.
@@ -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