scout_apm_logging 0.0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/test.yml +37 -0
- data/.gitignore +13 -0
- data/.rubocop.yml +65 -0
- data/Dockerfile +18 -0
- data/Gemfile +5 -0
- data/README.md +58 -0
- data/Rakefile +35 -0
- data/bin/scout_apm_logging_monitor +5 -0
- data/gems/rails.gemfile +3 -0
- data/lib/scout_apm/logging/config.rb +265 -0
- data/lib/scout_apm/logging/context.rb +58 -0
- data/lib/scout_apm/logging/logger.rb +26 -0
- data/lib/scout_apm/logging/loggers/capture.rb +46 -0
- data/lib/scout_apm/logging/loggers/formatter.rb +86 -0
- data/lib/scout_apm/logging/loggers/logger.rb +82 -0
- data/lib/scout_apm/logging/loggers/proxy.rb +39 -0
- data/lib/scout_apm/logging/loggers/swap.rb +82 -0
- data/lib/scout_apm/logging/monitor/collector/checksum.rb +51 -0
- data/lib/scout_apm/logging/monitor/collector/configuration.rb +148 -0
- data/lib/scout_apm/logging/monitor/collector/downloader.rb +78 -0
- data/lib/scout_apm/logging/monitor/collector/extractor.rb +37 -0
- data/lib/scout_apm/logging/monitor/collector/manager.rb +57 -0
- data/lib/scout_apm/logging/monitor/monitor.rb +214 -0
- data/lib/scout_apm/logging/monitor_manager/manager.rb +150 -0
- data/lib/scout_apm/logging/state.rb +70 -0
- data/lib/scout_apm/logging/utils.rb +86 -0
- data/lib/scout_apm/logging/version.rb +7 -0
- data/lib/scout_apm_logging.rb +35 -0
- data/scout_apm_logging.gemspec +27 -0
- data/spec/data/config_test_1.yml +27 -0
- data/spec/data/empty_logs_config.yml +0 -0
- data/spec/data/logs_config.yml +3 -0
- data/spec/data/mock_config.yml +29 -0
- data/spec/data/state_file.json +3 -0
- data/spec/integration/loggers/capture_spec.rb +78 -0
- data/spec/integration/monitor/collector/downloader/will_verify_checksum.rb +47 -0
- data/spec/integration/monitor/collector_healthcheck_spec.rb +27 -0
- data/spec/integration/monitor/continuous_state_collector_spec.rb +29 -0
- data/spec/integration/monitor/previous_collector_setup_spec.rb +42 -0
- data/spec/integration/monitor_manager/disable_agent_spec.rb +28 -0
- data/spec/integration/monitor_manager/monitor_pid_file_spec.rb +36 -0
- data/spec/integration/monitor_manager/single_monitor_spec.rb +53 -0
- data/spec/integration/rails/lifecycle_spec.rb +29 -0
- data/spec/spec_helper.rb +65 -0
- data/spec/unit/config_spec.rb +25 -0
- data/spec/unit/loggers/capture_spec.rb +64 -0
- data/spec/unit/monitor/collector/configuration_spec.rb +64 -0
- data/spec/unit/state_spec.rb +20 -0
- data/tooling/checksums.rb +106 -0
- metadata +167 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe ScoutApm::Logging::Loggers::Capture do
|
6
|
+
it 'should find the logger, capture the log destination, and rotate collector configs' do
|
7
|
+
ENV['SCOUT_MONITOR_INTERVAL'] = '10'
|
8
|
+
ENV['SCOUT_MONITOR_INTERVAL_DELAY'] = '10'
|
9
|
+
ENV['SCOUT_LOGS_MONITOR'] = 'true'
|
10
|
+
|
11
|
+
context = ScoutApm::Logging::MonitorManager.instance.context
|
12
|
+
|
13
|
+
state_file_location = context.config.value('monitor_state_file')
|
14
|
+
collector_pid_location = context.config.value('collector_pid_file')
|
15
|
+
ScoutApm::Logging::Utils.ensure_directory_exists(state_file_location)
|
16
|
+
|
17
|
+
first_logger = ScoutTestLogger.new('/tmp/first_file.log')
|
18
|
+
first_logger_basename = File.basename(first_logger.instance_variable_get(:@logdev).filename.to_s)
|
19
|
+
first_logger_updated_path = File.join(context.config.value('logs_proxy_log_dir'), first_logger_basename)
|
20
|
+
|
21
|
+
# While we only use the ObjectSpace for the test logger, we need to wait for it to be captured.
|
22
|
+
wait_for_logger
|
23
|
+
|
24
|
+
similuate_railtie
|
25
|
+
|
26
|
+
# Give the process time to initialize, download the collector, and start it
|
27
|
+
wait_for_process_with_timeout!('otelcol-contrib', 20)
|
28
|
+
|
29
|
+
expect(`pgrep otelcol-contrib --runstates D,R,S`).not_to be_empty
|
30
|
+
collector_pid = File.read(collector_pid_location)
|
31
|
+
|
32
|
+
content = File.read(state_file_location)
|
33
|
+
data = JSON.parse(content)
|
34
|
+
expect(data['logs_monitored']).to eq([first_logger_updated_path])
|
35
|
+
|
36
|
+
second_logger = ScoutTestLogger.new('/tmp/second_file.log')
|
37
|
+
second_logger_basename = File.basename(second_logger.instance_variable_get(:@logdev).filename.to_s)
|
38
|
+
second_logger_updated_path = File.join(context.config.value('logs_proxy_log_dir'), second_logger_basename)
|
39
|
+
|
40
|
+
similuate_railtie
|
41
|
+
|
42
|
+
content = File.read(state_file_location)
|
43
|
+
data = JSON.parse(content)
|
44
|
+
|
45
|
+
expect(data['logs_monitored'].sort).to eq([first_logger_updated_path, second_logger_updated_path])
|
46
|
+
|
47
|
+
# Need to wait for the delay first health check, next monitor interval to restart the collector, and then for
|
48
|
+
# the collector to restart
|
49
|
+
sleep 25
|
50
|
+
wait_for_process_with_timeout!('otelcol-contrib', 20)
|
51
|
+
|
52
|
+
expect(`pgrep otelcol-contrib --runstates D,R,S`).not_to be_empty
|
53
|
+
new_collector_pid = File.read(collector_pid_location)
|
54
|
+
|
55
|
+
# Should have restarted the collector based on the change
|
56
|
+
expect(new_collector_pid).not_to eq(collector_pid)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def similuate_railtie
|
62
|
+
context = ScoutApm::Logging::MonitorManager.instance.context
|
63
|
+
|
64
|
+
ScoutApm::Logging::Loggers::Capture.new(context).capture_log_locations!
|
65
|
+
ScoutApm::Logging::MonitorManager.new.setup!
|
66
|
+
end
|
67
|
+
|
68
|
+
def wait_for_logger
|
69
|
+
start_time = Time.now
|
70
|
+
loop do
|
71
|
+
break if ObjectSpace.each_object(ScoutTestLogger).count.positive?
|
72
|
+
|
73
|
+
raise 'Timed out while waiting for logger in ObjectSpace' if Time.now - start_time > 10
|
74
|
+
|
75
|
+
sleep 0.1
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require_relative '../../../../../lib/scout_apm/logging/monitor/collector/downloader'
|
4
|
+
|
5
|
+
describe ScoutApm::Logging::Collector::Downloader do
|
6
|
+
it 'should validate checksum, and correct download if neccessary' do
|
7
|
+
ENV['SCOUT_LOGS_MONITOR'] = 'true'
|
8
|
+
ENV['SCOUT_LOGS_MONITORED'] = '["/tmp/test.log"]'
|
9
|
+
|
10
|
+
otelcol_contrib_path = '/tmp/scout_apm/otelcol-contrib'
|
11
|
+
ScoutApm::Logging::Utils.ensure_directory_exists(otelcol_contrib_path)
|
12
|
+
|
13
|
+
File.write(otelcol_contrib_path, 'fake content')
|
14
|
+
|
15
|
+
ScoutApm::Logging::MonitorManager.instance.setup!
|
16
|
+
|
17
|
+
# Give the process time to initialize, download the collector, and start it
|
18
|
+
wait_for_process_with_timeout!('otelcol-contrib', 20)
|
19
|
+
|
20
|
+
download_time = File.mtime(otelcol_contrib_path)
|
21
|
+
|
22
|
+
expect(`pgrep otelcol-contrib --runstates D,R,S`).not_to be_empty
|
23
|
+
|
24
|
+
ENV['SCOUT_LOGS_MONITOR'] = 'false'
|
25
|
+
|
26
|
+
ScoutApm::Logging::MonitorManager.new.setup!
|
27
|
+
|
28
|
+
sleep 5 # Give the process time to exit
|
29
|
+
|
30
|
+
expect(File.exist?(ScoutApm::Logging::MonitorManager.instance.context.config.value('monitor_pid_file'))).to be_falsey
|
31
|
+
expect(`pgrep otelcol-contrib --runstates D,R,S`).to be_empty
|
32
|
+
expect(`pgrep scout_apm_log_monitor --runstates D,R,S`).to be_empty
|
33
|
+
|
34
|
+
ENV['SCOUT_LOGS_MONITOR'] = 'true'
|
35
|
+
|
36
|
+
ScoutApm::Logging::MonitorManager.new.setup!
|
37
|
+
|
38
|
+
# Give the process time to exit, and for the healthcheck to restart it
|
39
|
+
wait_for_process_with_timeout!('otelcol-contrib', 30)
|
40
|
+
|
41
|
+
expect(`pgrep otelcol-contrib --runstates D,R,S`).not_to be_empty
|
42
|
+
|
43
|
+
recheck_time = File.mtime(otelcol_contrib_path)
|
44
|
+
|
45
|
+
expect(download_time).to eq(recheck_time)
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require_relative '../../../lib/scout_apm/logging/monitor/monitor'
|
4
|
+
|
5
|
+
describe ScoutApm::Logging::Monitor do
|
6
|
+
it 'should recreate collector process on healthcheck if it has exited' do
|
7
|
+
ENV['SCOUT_MONITOR_INTERVAL'] = '10'
|
8
|
+
ENV['SCOUT_MONITOR_INTERVAL_DELAY'] = '10'
|
9
|
+
ENV['SCOUT_LOGS_MONITOR'] = 'true'
|
10
|
+
ENV['SCOUT_LOGS_MONITORED'] = '["/tmp/test.log"]'
|
11
|
+
|
12
|
+
ScoutApm::Logging::Utils.ensure_directory_exists('/tmp/scout_apm/scout_apm_log_monitor.pid')
|
13
|
+
|
14
|
+
ScoutApm::Logging::MonitorManager.instance.setup!
|
15
|
+
|
16
|
+
# Give the process time to initialize, download the collector, and start it
|
17
|
+
wait_for_process_with_timeout!('otelcol-contrib', 20)
|
18
|
+
|
19
|
+
expect(`pgrep otelcol-contrib --runstates D,R,S`).not_to be_empty
|
20
|
+
|
21
|
+
# Bypass gracefull shutdown
|
22
|
+
`pkill -9 otelcol-contrib`
|
23
|
+
|
24
|
+
# Give the process time to exit, and for the healthcheck to restart it
|
25
|
+
wait_for_process_with_timeout!('otelcol-contrib', 30)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require_relative '../../../lib/scout_apm/logging/monitor/monitor'
|
4
|
+
|
5
|
+
describe ScoutApm::Logging::Monitor do
|
6
|
+
it "Should not restart the collector if the state hasn't changed" do
|
7
|
+
ENV['SCOUT_MONITOR_INTERVAL'] = '10'
|
8
|
+
ENV['SCOUT_MONITOR_INTERVAL_DELAY'] = '10'
|
9
|
+
ENV['SCOUT_LOGS_MONITOR'] = 'true'
|
10
|
+
ENV['SCOUT_LOGS_MONITORED'] = '["/tmp/test.log"]'
|
11
|
+
|
12
|
+
context = ScoutApm::Logging::MonitorManager.instance.context
|
13
|
+
collector_pid_location = context.config.value('collector_pid_file')
|
14
|
+
ScoutApm::Logging::MonitorManager.instance.setup!
|
15
|
+
# Give the process time to initialize, download the collector, and start it
|
16
|
+
wait_for_process_with_timeout!('otelcol-contrib', 20)
|
17
|
+
|
18
|
+
expect(`pgrep otelcol-contrib --runstates D,R,S`).not_to be_empty
|
19
|
+
collector_pid = File.read(collector_pid_location)
|
20
|
+
|
21
|
+
# Give time for the monitor interval to run.
|
22
|
+
sleep 30
|
23
|
+
|
24
|
+
expect(`pgrep otelcol-contrib --runstates D,R,S`).not_to be_empty
|
25
|
+
second_read_pid = File.read(collector_pid_location)
|
26
|
+
|
27
|
+
expect(second_read_pid).to eq(collector_pid)
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require_relative '../../../lib/scout_apm/logging/monitor/monitor'
|
4
|
+
|
5
|
+
describe ScoutApm::Logging::Monitor do
|
6
|
+
it 'should use previous collector setup if monitor daemon exits' do
|
7
|
+
ENV['SCOUT_LOGS_MONITOR'] = 'true'
|
8
|
+
ENV['SCOUT_LOGS_MONITORED'] = '["/tmp/test.log"]'
|
9
|
+
|
10
|
+
monitor_pid_location = ScoutApm::Logging::MonitorManager.instance.context.config.value('monitor_pid_file')
|
11
|
+
collector_pid_location = ScoutApm::Logging::MonitorManager.instance.context.config.value('collector_pid_file')
|
12
|
+
ScoutApm::Logging::Utils.ensure_directory_exists(monitor_pid_location)
|
13
|
+
|
14
|
+
ScoutApm::Logging::MonitorManager.instance.setup!
|
15
|
+
# Give the process time to initialize, download the collector, and start it
|
16
|
+
wait_for_process_with_timeout!('otelcol-contrib', 20)
|
17
|
+
|
18
|
+
monitor_pid = File.read(monitor_pid_location)
|
19
|
+
|
20
|
+
otelcol_pid = `pgrep otelcol-contrib --runstates D,R,S`.strip!
|
21
|
+
stored_otelcol_pid = File.read(collector_pid_location)
|
22
|
+
expect(otelcol_pid).to eq(stored_otelcol_pid)
|
23
|
+
|
24
|
+
`kill -9 #{monitor_pid}`
|
25
|
+
|
26
|
+
# Create a separate monitor manager instance, or else we won't reload
|
27
|
+
# the configuraiton state.
|
28
|
+
ScoutApm::Logging::MonitorManager.new.setup!
|
29
|
+
|
30
|
+
sleep 5
|
31
|
+
|
32
|
+
expect(`pgrep -f /app/bin/scout_apm_logging_monitor --runstates D,R,S`).not_to be_empty
|
33
|
+
|
34
|
+
new_monitor_pid = File.read(monitor_pid_location)
|
35
|
+
expect(new_monitor_pid).not_to eq(monitor_pid)
|
36
|
+
|
37
|
+
should_be_same_otelcol_pid = `pgrep otelcol-contrib --runstates D,R,S`.strip!
|
38
|
+
should_be_same_stored_otelcol_pid = File.read(collector_pid_location)
|
39
|
+
expect(should_be_same_otelcol_pid).to eq(otelcol_pid)
|
40
|
+
expect(should_be_same_stored_otelcol_pid).to eq(stored_otelcol_pid)
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require_relative '../../../lib/scout_apm/logging/monitor/collector/manager'
|
4
|
+
|
5
|
+
describe ScoutApm::Logging::Collector::Manager do
|
6
|
+
it 'If monitor is false, it should remove the daemon and collector process if they are present' do
|
7
|
+
ENV['SCOUT_LOGS_MONITOR'] = 'true'
|
8
|
+
ENV['SCOUT_LOGS_MONITORED'] = '["/tmp/test.log"]'
|
9
|
+
|
10
|
+
expect(`pgrep otelcol-contrib --runstates D,R,S`).to be_empty
|
11
|
+
|
12
|
+
ScoutApm::Logging::MonitorManager.instance.setup!
|
13
|
+
|
14
|
+
wait_for_process_with_timeout!('otelcol-contrib', 20)
|
15
|
+
|
16
|
+
ENV['SCOUT_LOGS_MONITOR'] = 'false'
|
17
|
+
|
18
|
+
ScoutApm::Logging::MonitorManager.new.setup!
|
19
|
+
|
20
|
+
sleep 5 # Give the process time to exit
|
21
|
+
|
22
|
+
expect(File.exist?(ScoutApm::Logging::MonitorManager.instance.context.config.value('monitor_pid_file'))).to be_falsey
|
23
|
+
expect(`pgrep otelcol-contrib --runstates D,R,S`).to be_empty
|
24
|
+
expect(`pgrep scout_apm_log_monitor --runstates D,R,S`).to be_empty
|
25
|
+
|
26
|
+
ENV.delete('SCOUT_LOGS_MONITOR')
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require_relative '../../../lib/scout_apm/logging/monitor/collector/manager'
|
4
|
+
|
5
|
+
describe ScoutApm::Logging::Collector::Manager do
|
6
|
+
it 'should recreate the monitor process if monitor.pid file is errant' do
|
7
|
+
ENV['SCOUT_LOGS_MONITOR'] = 'true'
|
8
|
+
ENV['SCOUT_LOGS_MONITORED'] = '["/tmp/test.log"]'
|
9
|
+
|
10
|
+
ScoutApm::Logging::Utils.ensure_directory_exists('/tmp/scout_apm/scout_apm_log_monitor.pid')
|
11
|
+
|
12
|
+
# A high enough number to not be a PID of a running process
|
13
|
+
pid_file_path = '/tmp/scout_apm/scout_apm_log_monitor.pid'
|
14
|
+
File.open(pid_file_path, 'w') do |file|
|
15
|
+
file.write('123456')
|
16
|
+
end
|
17
|
+
|
18
|
+
ScoutApm::Logging::MonitorManager.instance.setup!
|
19
|
+
|
20
|
+
# Give the process time to initialize, download the collector, and start it
|
21
|
+
wait_for_process_with_timeout!('otelcol-contrib', 20)
|
22
|
+
|
23
|
+
new_pid = File.read(pid_file_path).to_i
|
24
|
+
|
25
|
+
expect(new_pid).not_to eq(12_345)
|
26
|
+
|
27
|
+
# Check if the process with the stored PID is running
|
28
|
+
expect(Process.kill(0, new_pid)).to be_truthy
|
29
|
+
ENV.delete('SCOUT_LOGS_MONITOR')
|
30
|
+
|
31
|
+
# Kill the process and ensure PID file clean up
|
32
|
+
Process.kill('TERM', new_pid)
|
33
|
+
Process.kill('TERM', `pgrep otelcol-contrib --runstates D,R,S`.to_i)
|
34
|
+
sleep 1 # Give the process time to exit
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# This is really meant to simulate multiple processes calling the MonitorManager setup.
|
2
|
+
# Trying to create **multiple fake** rails environments is a bit challening
|
3
|
+
# (such as a rails app and a couple rails runners).
|
4
|
+
|
5
|
+
# See: https://github.com/rails/rails/blob/6d126e03dbbf5d30fa97b580f7dee46343537b7b/railties/test/isolation/abstract_unit.rb#L339
|
6
|
+
|
7
|
+
# We simulate the ultimate outcome here -- ie Railtie invoking the setup.
|
8
|
+
require 'spec_helper'
|
9
|
+
|
10
|
+
describe ScoutApm::Logging do
|
11
|
+
it 'Should only create a single monitor daemon if manager is called multiple times' do
|
12
|
+
ENV['SCOUT_LOGS_MONITOR'] = 'true'
|
13
|
+
ENV['SCOUT_LOGS_MONITORED'] = '["/tmp/test.log"]'
|
14
|
+
|
15
|
+
pid_file = ScoutApm::Logging::MonitorManager.instance.context.config.value('monitor_pid_file')
|
16
|
+
expect(File.exist?(pid_file)).to be_falsey
|
17
|
+
|
18
|
+
context = ScoutApm::Logging::MonitorManager.instance.context
|
19
|
+
|
20
|
+
pids = 3.times.map do
|
21
|
+
fork do
|
22
|
+
puts 'fork attempting to gain lock'
|
23
|
+
ScoutApm::Logging::Utils.attempt_exclusive_lock(context) do
|
24
|
+
puts 'obtained lock'
|
25
|
+
ScoutApm::Logging::MonitorManager.new.setup!
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
pids.each { |pid| Process.wait(pid) }
|
31
|
+
|
32
|
+
wait_for_process_with_timeout!('otelcol-contrib', 20)
|
33
|
+
|
34
|
+
# The file lock really should be gone at this point.
|
35
|
+
expect(File.exist?(context.config.value('manager_lock_file'))).to be_falsey
|
36
|
+
expect(File.exist?(pid_file)).to be_truthy
|
37
|
+
|
38
|
+
original_pid = File.read(pid_file).to_i
|
39
|
+
expect(ScoutApm::Logging::Utils.check_process_liveliness(original_pid, 'scout_apm_logging_monitor')).to be_truthy
|
40
|
+
|
41
|
+
# Another process comes in and tries to start it again
|
42
|
+
ScoutApm::Logging::Utils.attempt_exclusive_lock(context) do
|
43
|
+
puts 'obtained lock later on'
|
44
|
+
ScoutApm::Logging::MonitorManager.new.setup!
|
45
|
+
end
|
46
|
+
|
47
|
+
expect(File.exist?(context.config.value('manager_lock_file'))).to be_falsey
|
48
|
+
expect(File.exist?(pid_file)).to be_truthy
|
49
|
+
updated_pid = File.read(pid_file).to_i
|
50
|
+
expect(updated_pid).to eq(original_pid)
|
51
|
+
expect(ScoutApm::Logging::Utils.check_process_liveliness(original_pid, 'scout_apm_logging_monitor')).to be_truthy
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ScoutApm::Logging do
|
4
|
+
it 'checks the Rails lifecycle for creating the daemon and collector processes' do
|
5
|
+
ENV['SCOUT_LOGS_MONITOR'] = 'true'
|
6
|
+
ENV['SCOUT_LOGS_MONITORED'] = '["/tmp/test.log"]'
|
7
|
+
|
8
|
+
pid_file = ScoutApm::Logging::MonitorManager.instance.context.config.value('monitor_pid_file')
|
9
|
+
expect(File.exist?(pid_file)).to be_falsey
|
10
|
+
|
11
|
+
make_basic_app
|
12
|
+
|
13
|
+
wait_for_process_with_timeout!('otelcol-contrib', 20)
|
14
|
+
|
15
|
+
# Check if the monitor PID file exists
|
16
|
+
expect(File.exist?(pid_file)).to be_truthy
|
17
|
+
|
18
|
+
# Read the PID from the PID file
|
19
|
+
pid = File.read(pid_file).to_i
|
20
|
+
|
21
|
+
# Check if the process with the stored PID is running
|
22
|
+
expect(ScoutApm::Logging::Utils.check_process_liveliness(pid, 'scout_apm_logging_monitor')).to be_truthy
|
23
|
+
|
24
|
+
# Kill the process and ensure PID file clean up
|
25
|
+
Process.kill('TERM', pid)
|
26
|
+
sleep 1 # Give the process time to exit
|
27
|
+
expect(File.exist?(pid_file)).to be_falsey
|
28
|
+
end
|
29
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'timeout'
|
4
|
+
|
5
|
+
require 'scout_apm'
|
6
|
+
|
7
|
+
require "rails"
|
8
|
+
require "action_controller/railtie"
|
9
|
+
require "action_view/railtie"
|
10
|
+
|
11
|
+
# Require after rails, to ensure the railtie is ran
|
12
|
+
require 'scout_apm_logging'
|
13
|
+
|
14
|
+
class TestLoggerWrapper
|
15
|
+
class << self
|
16
|
+
attr_accessor :logger
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class ScoutTestLogger < ::Logger
|
21
|
+
end
|
22
|
+
|
23
|
+
RSpec.configure do |config|
|
24
|
+
ENV["SCOUT_LOG_LEVEL"] = "debug"
|
25
|
+
ENV["SCOUT_COLLECTOR_LOG_LEVEL"] = "debug"
|
26
|
+
|
27
|
+
config.after(:each) do
|
28
|
+
RSpec::Mocks.space.reset_all
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Taken from:
|
33
|
+
# https://github.com/rails/rails/blob/v7.1.3.2/railties/test/isolation/abstract_unit.rb#L252
|
34
|
+
def make_basic_app
|
35
|
+
@app = Class.new(Rails::Application) do
|
36
|
+
def self.name; "RailtiesTestApp"; end
|
37
|
+
end
|
38
|
+
@app.config.hosts << proc { true }
|
39
|
+
@app.config.eager_load = false
|
40
|
+
@app.config.session_store :cookie_store, key: "_myapp_session"
|
41
|
+
@app.config.active_support.deprecation = :log
|
42
|
+
@app.config.log_level = :info
|
43
|
+
@app.config.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33"
|
44
|
+
|
45
|
+
yield @app if block_given?
|
46
|
+
@app.initialize!
|
47
|
+
|
48
|
+
@app.routes.draw do
|
49
|
+
get "/" => "omg#index"
|
50
|
+
end
|
51
|
+
|
52
|
+
require "rack/test"
|
53
|
+
extend ::Rack::Test::Methods
|
54
|
+
end
|
55
|
+
|
56
|
+
def wait_for_process_with_timeout!(name, timeout_time)
|
57
|
+
Timeout::timeout(timeout_time) do
|
58
|
+
loop do
|
59
|
+
break if `pgrep #{name} --runstates D,R,S`.strip != ""
|
60
|
+
sleep 0.1
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
sleep 1
|
65
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ScoutApm::Logging::Config do
|
4
|
+
it 'loads the config file' do
|
5
|
+
context = ScoutApm::Logging::Context.new
|
6
|
+
conf_file = File.expand_path('../data/config_test_1.yml', __dir__)
|
7
|
+
conf = ScoutApm::Logging::Config.with_file(context, conf_file)
|
8
|
+
|
9
|
+
expect(conf.value('log_level')).to eq('debug')
|
10
|
+
expect(conf.value('logs_monitor')).to eq(true)
|
11
|
+
expect(conf.value('monitor_pid_file')).to eq('/tmp/scout_apm/scout_apm_log_monitor.pid')
|
12
|
+
expect(conf.value('logs_ingest_key')).to eq('00001000010000abc')
|
13
|
+
expect(conf.value('logs_monitored')).to eq(['/tmp/fake_log_file.log'])
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'loads the state file into the config' do
|
17
|
+
ENV['SCOUT_MONITOR_STATE_FILE'] = File.expand_path('../data/state_file.json', __dir__)
|
18
|
+
|
19
|
+
context = ScoutApm::Logging::Context.new
|
20
|
+
conf_file = File.expand_path('../data/config_test_1.yml', __dir__)
|
21
|
+
conf = ScoutApm::Logging::Config.with_file(context, conf_file)
|
22
|
+
|
23
|
+
expect(conf.value('health_check_port')).to eq(1234)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
require 'spec_helper'
|
5
|
+
|
6
|
+
require_relative '../../../lib/scout_apm/logging/loggers/capture'
|
7
|
+
require_relative '../../../lib/scout_apm/logging/loggers/proxy'
|
8
|
+
|
9
|
+
def capture_stdout
|
10
|
+
old_stdout = $stdout
|
11
|
+
$stdout = StringIO.new
|
12
|
+
yield
|
13
|
+
$stdout.string
|
14
|
+
ensure
|
15
|
+
$stdout = old_stdout
|
16
|
+
end
|
17
|
+
|
18
|
+
describe ScoutApm::Logging::Loggers::Capture do
|
19
|
+
it 'should swap the STDOUT logger and create a proxy logger' do
|
20
|
+
ENV['SCOUT_MONITOR_INTERVAL'] = '10'
|
21
|
+
ENV['SCOUT_MONITOR_INTERVAL_DELAY'] = '10'
|
22
|
+
ENV['SCOUT_LOGS_MONITOR'] = 'true'
|
23
|
+
|
24
|
+
output_from_log = capture_stdout do
|
25
|
+
context = ScoutApm::Logging::Context.new
|
26
|
+
conf_file = File.expand_path('../data/config_test_1.yml', __dir__)
|
27
|
+
conf = ScoutApm::Logging::Config.with_file(context, conf_file)
|
28
|
+
context.config = conf
|
29
|
+
|
30
|
+
TestLoggerWrapper.logger = ScoutTestLogger.new($stdout)
|
31
|
+
|
32
|
+
# While we only use the ObjectSpace for the test logger, we need to wait for it to be captured.
|
33
|
+
wait_for_logger
|
34
|
+
|
35
|
+
capture = ScoutApm::Logging::Loggers::Capture.new(context)
|
36
|
+
capture.capture_log_locations!
|
37
|
+
|
38
|
+
expect(TestLoggerWrapper.logger.class).to eq(ScoutApm::Logging::Loggers::Proxy)
|
39
|
+
|
40
|
+
TestLoggerWrapper.logger.info('TEST')
|
41
|
+
|
42
|
+
log_path = File.join(context.config.value('logs_proxy_log_dir'), 'test.log')
|
43
|
+
content = File.read(log_path)
|
44
|
+
expect(content).to include('TEST')
|
45
|
+
|
46
|
+
state_file = File.read(context.config.value('monitor_state_file'))
|
47
|
+
state_data = JSON.parse(state_file)
|
48
|
+
expect(state_data['logs_monitored']).to eq([log_path])
|
49
|
+
end
|
50
|
+
|
51
|
+
expect(output_from_log).to include('TEST')
|
52
|
+
end
|
53
|
+
|
54
|
+
def wait_for_logger
|
55
|
+
start_time = Time.now
|
56
|
+
loop do
|
57
|
+
break if ObjectSpace.each_object(::ScoutTestLogger).count.positive?
|
58
|
+
|
59
|
+
raise 'Timed out while waiting for logger in ObjectSpace' if Time.now - start_time > 10
|
60
|
+
|
61
|
+
sleep 0.1
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
require_relative '../../../../lib/scout_apm/logging/monitor/collector/configuration'
|
6
|
+
|
7
|
+
describe ScoutApm::Logging::Collector::Configuration do
|
8
|
+
it 'creates the correct configuration for the otelcol' do
|
9
|
+
context = ScoutApm::Logging::Context.new
|
10
|
+
setup_collector_config!(context)
|
11
|
+
|
12
|
+
expect(File.exist?(context.config.value('collector_config_file'))).to be_truthy
|
13
|
+
config = YAML.load_file(context.config.value('collector_config_file'))
|
14
|
+
|
15
|
+
expect(config['exporters']['otlp']['endpoint']).to eq('https://otlp.telemetryhub.com:4317')
|
16
|
+
expect(config['exporters']['otlp']['headers']['x-telemetryhub-key']).to eq('00001000010000abc')
|
17
|
+
expect(config['receivers']['filelog']['include']).to eq(['/tmp/fake_log_file.log'])
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'merges in a logs config correctly' do
|
21
|
+
ENV['SCOUT_LOGS_CONFIG'] = File.expand_path('../../../data/logs_config.yml', __dir__)
|
22
|
+
|
23
|
+
context = ScoutApm::Logging::Context.new
|
24
|
+
setup_collector_config!(context)
|
25
|
+
|
26
|
+
expect(File.exist?(context.config.value('collector_config_file'))).to be_truthy
|
27
|
+
config = YAML.load_file(context.config.value('collector_config_file'))
|
28
|
+
|
29
|
+
expect(config['exporters']['otlp']['endpoint']).to eq('https://otlp.telemetryhub.com:4317')
|
30
|
+
expect(config['exporters']['otlp']['headers']['x-telemetryhub-key']).to eq('00001000010000abc')
|
31
|
+
expect(config['receivers']['filelog']['include']).to eq(['/tmp/fake_log_file.log'])
|
32
|
+
|
33
|
+
# Verify merge and consistent keys
|
34
|
+
expect(config['extensions']['file_storage/otc']['directory']).to eq('/dev/null')
|
35
|
+
expect(config['extensions']['file_storage/otc']['timeout']).to eq('10s')
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'handles an empty logs config file well' do
|
39
|
+
ENV['SCOUT_LOGS_CONFIG'] = File.expand_path('../../../data/empty_logs_config.yml', __dir__)
|
40
|
+
|
41
|
+
context = ScoutApm::Logging::Context.new
|
42
|
+
setup_collector_config!(context)
|
43
|
+
|
44
|
+
expect(File.exist?(context.config.value('collector_config_file'))).to be_truthy
|
45
|
+
config = YAML.load_file(context.config.value('collector_config_file'))
|
46
|
+
|
47
|
+
expect(config['exporters']['otlp']['endpoint']).to eq('https://otlp.telemetryhub.com:4317')
|
48
|
+
expect(config['exporters']['otlp']['headers']['x-telemetryhub-key']).to eq('00001000010000abc')
|
49
|
+
expect(config['receivers']['filelog']['include']).to eq(['/tmp/fake_log_file.log'])
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def setup_collector_config!(context)
|
55
|
+
ENV['SCOUT_MONITOR_STATE_FILE'] = File.expand_path('../../../data/state_file.json', __dir__)
|
56
|
+
conf_file = File.expand_path('../../../data/mock_config.yml', __dir__)
|
57
|
+
context.config = ScoutApm::Logging::Config.with_file(context, conf_file)
|
58
|
+
|
59
|
+
ScoutApm::Logging::Utils.ensure_directory_exists(context.config.value('collector_config_file'))
|
60
|
+
|
61
|
+
collector_config = ScoutApm::Logging::Collector::Configuration.new(context)
|
62
|
+
collector_config.setup!
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ScoutApm::Logging::Config do
|
4
|
+
it 'flushes and loads the state' do
|
5
|
+
context = ScoutApm::Logging::Context.new
|
6
|
+
conf_file = File.expand_path('../data/config_test_1.yml', __dir__)
|
7
|
+
conf = ScoutApm::Logging::Config.with_file(context, conf_file)
|
8
|
+
|
9
|
+
context.config = conf
|
10
|
+
|
11
|
+
ScoutApm::Logging::Config::ConfigDynamic.set_value('health_check_port', 1234)
|
12
|
+
|
13
|
+
context.config.state.flush_state!
|
14
|
+
|
15
|
+
data = ScoutApm::Logging::Config::State.new(context).load_state_from_file
|
16
|
+
|
17
|
+
expect(data['health_check_port']).to eq(context.config.value('health_check_port'))
|
18
|
+
expect(data['logs_monitored']).to eq(context.config.value('logs_monitored'))
|
19
|
+
end
|
20
|
+
end
|