scout_apm_logging 0.0.0.1

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 (51) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +37 -0
  3. data/.gitignore +13 -0
  4. data/.rubocop.yml +65 -0
  5. data/Dockerfile +18 -0
  6. data/Gemfile +5 -0
  7. data/README.md +58 -0
  8. data/Rakefile +35 -0
  9. data/bin/scout_apm_logging_monitor +5 -0
  10. data/gems/rails.gemfile +3 -0
  11. data/lib/scout_apm/logging/config.rb +265 -0
  12. data/lib/scout_apm/logging/context.rb +58 -0
  13. data/lib/scout_apm/logging/logger.rb +26 -0
  14. data/lib/scout_apm/logging/loggers/capture.rb +46 -0
  15. data/lib/scout_apm/logging/loggers/formatter.rb +86 -0
  16. data/lib/scout_apm/logging/loggers/logger.rb +82 -0
  17. data/lib/scout_apm/logging/loggers/proxy.rb +39 -0
  18. data/lib/scout_apm/logging/loggers/swap.rb +82 -0
  19. data/lib/scout_apm/logging/monitor/collector/checksum.rb +51 -0
  20. data/lib/scout_apm/logging/monitor/collector/configuration.rb +148 -0
  21. data/lib/scout_apm/logging/monitor/collector/downloader.rb +78 -0
  22. data/lib/scout_apm/logging/monitor/collector/extractor.rb +37 -0
  23. data/lib/scout_apm/logging/monitor/collector/manager.rb +57 -0
  24. data/lib/scout_apm/logging/monitor/monitor.rb +214 -0
  25. data/lib/scout_apm/logging/monitor_manager/manager.rb +150 -0
  26. data/lib/scout_apm/logging/state.rb +70 -0
  27. data/lib/scout_apm/logging/utils.rb +86 -0
  28. data/lib/scout_apm/logging/version.rb +7 -0
  29. data/lib/scout_apm_logging.rb +35 -0
  30. data/scout_apm_logging.gemspec +27 -0
  31. data/spec/data/config_test_1.yml +27 -0
  32. data/spec/data/empty_logs_config.yml +0 -0
  33. data/spec/data/logs_config.yml +3 -0
  34. data/spec/data/mock_config.yml +29 -0
  35. data/spec/data/state_file.json +3 -0
  36. data/spec/integration/loggers/capture_spec.rb +78 -0
  37. data/spec/integration/monitor/collector/downloader/will_verify_checksum.rb +47 -0
  38. data/spec/integration/monitor/collector_healthcheck_spec.rb +27 -0
  39. data/spec/integration/monitor/continuous_state_collector_spec.rb +29 -0
  40. data/spec/integration/monitor/previous_collector_setup_spec.rb +42 -0
  41. data/spec/integration/monitor_manager/disable_agent_spec.rb +28 -0
  42. data/spec/integration/monitor_manager/monitor_pid_file_spec.rb +36 -0
  43. data/spec/integration/monitor_manager/single_monitor_spec.rb +53 -0
  44. data/spec/integration/rails/lifecycle_spec.rb +29 -0
  45. data/spec/spec_helper.rb +65 -0
  46. data/spec/unit/config_spec.rb +25 -0
  47. data/spec/unit/loggers/capture_spec.rb +64 -0
  48. data/spec/unit/monitor/collector/configuration_spec.rb +64 -0
  49. data/spec/unit/state_spec.rb +20 -0
  50. data/tooling/checksums.rb +106 -0
  51. 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
@@ -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