scout_apm_logging 0.0.12 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -4
  3. data/.rubocop.yml +1 -1
  4. data/CHANGELOG.md +8 -0
  5. data/NOTICE +4 -0
  6. data/lib/scout_apm/logging/config.rb +5 -131
  7. data/lib/scout_apm/logging/loggers/capture.rb +8 -6
  8. data/lib/scout_apm/logging/loggers/formatter.rb +21 -2
  9. data/lib/scout_apm/logging/loggers/opentelemetry/LICENSE +201 -0
  10. data/lib/scout_apm/logging/loggers/opentelemetry/NOTICE +9 -0
  11. data/lib/scout_apm/logging/loggers/opentelemetry/api/logs/log_record.rb +18 -0
  12. data/lib/scout_apm/logging/loggers/opentelemetry/api/logs/logger.rb +64 -0
  13. data/lib/scout_apm/logging/loggers/opentelemetry/api/logs/logger_provider.rb +31 -0
  14. data/lib/scout_apm/logging/loggers/opentelemetry/api/logs/severity_number.rb +43 -0
  15. data/lib/scout_apm/logging/loggers/opentelemetry/api/logs/version.rb +18 -0
  16. data/lib/scout_apm/logging/loggers/opentelemetry/api/logs.rb +28 -0
  17. data/lib/scout_apm/logging/loggers/opentelemetry/exporter/exporter/otlp/logs_exporter.rb +389 -0
  18. data/lib/scout_apm/logging/loggers/opentelemetry/exporter/exporter/otlp/version.rb +20 -0
  19. data/lib/scout_apm/logging/loggers/opentelemetry/exporter/proto/collector/logs/v1/logs_service_pb.rb +43 -0
  20. data/lib/scout_apm/logging/loggers/opentelemetry/exporter/proto/common/v1/common_pb.rb +58 -0
  21. data/lib/scout_apm/logging/loggers/opentelemetry/exporter/proto/logs/v1/logs_pb.rb +91 -0
  22. data/lib/scout_apm/logging/loggers/opentelemetry/exporter/proto/resource/v1/resource_pb.rb +33 -0
  23. data/lib/scout_apm/logging/loggers/opentelemetry/opentelemetry.rb +62 -0
  24. data/lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/export/batch_log_record_processor.rb +225 -0
  25. data/lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/export/log_record_exporter.rb +64 -0
  26. data/lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/export.rb +34 -0
  27. data/lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/log_record.rb +115 -0
  28. data/lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/log_record_data.rb +31 -0
  29. data/lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/log_record_processor.rb +53 -0
  30. data/lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/logger.rb +94 -0
  31. data/lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/logger_provider.rb +158 -0
  32. data/lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/version.rb +20 -0
  33. data/lib/scout_apm/logging/loggers/opentelemetry/sdk/logs.rb +28 -0
  34. data/lib/scout_apm/logging/loggers/patches/rails_logger.rb +17 -0
  35. data/lib/scout_apm/logging/loggers/proxy.rb +22 -5
  36. data/lib/scout_apm/logging/loggers/swaps/rails.rb +4 -12
  37. data/lib/scout_apm/logging/loggers/swaps/scout.rb +2 -10
  38. data/lib/scout_apm/logging/loggers/swaps/sidekiq.rb +2 -6
  39. data/lib/scout_apm/logging/utils.rb +0 -69
  40. data/lib/scout_apm/logging/version.rb +1 -1
  41. data/lib/scout_apm_logging.rb +3 -12
  42. data/scout_apm_logging.gemspec +7 -0
  43. data/spec/data/config_test_1.yml +0 -1
  44. data/spec/data/mock_config.yml +0 -3
  45. data/spec/integration/rails/lifecycle_spec.rb +57 -23
  46. data/spec/spec_helper.rb +0 -12
  47. data/spec/unit/config_spec.rb +0 -12
  48. data/spec/unit/loggers/capture_spec.rb +0 -6
  49. metadata +127 -39
  50. data/bin/scout_apm_logging_monitor +0 -6
  51. data/lib/scout_apm/logging/monitor/_rails.rb +0 -22
  52. data/lib/scout_apm/logging/monitor/collector/checksum.rb +0 -51
  53. data/lib/scout_apm/logging/monitor/collector/configuration.rb +0 -150
  54. data/lib/scout_apm/logging/monitor/collector/downloader.rb +0 -78
  55. data/lib/scout_apm/logging/monitor/collector/extractor.rb +0 -37
  56. data/lib/scout_apm/logging/monitor/collector/manager.rb +0 -57
  57. data/lib/scout_apm/logging/monitor/monitor.rb +0 -216
  58. data/lib/scout_apm/logging/monitor_manager/manager.rb +0 -162
  59. data/lib/scout_apm/logging/state.rb +0 -69
  60. data/spec/data/empty_logs_config.yml +0 -0
  61. data/spec/data/logs_config.yml +0 -3
  62. data/spec/data/state_file.json +0 -3
  63. data/spec/integration/loggers/capture_spec.rb +0 -68
  64. data/spec/integration/monitor/collector/downloader/will_verify_checksum.rb +0 -49
  65. data/spec/integration/monitor/collector_healthcheck_spec.rb +0 -29
  66. data/spec/integration/monitor/continuous_state_collector_spec.rb +0 -31
  67. data/spec/integration/monitor/previous_collector_setup_spec.rb +0 -45
  68. data/spec/integration/monitor_manager/disable_agent_spec.rb +0 -30
  69. data/spec/integration/monitor_manager/monitor_pid_file_spec.rb +0 -38
  70. data/spec/integration/monitor_manager/single_monitor_spec.rb +0 -53
  71. data/spec/unit/monitor/collector/configuration_spec.rb +0 -64
  72. data/spec/unit/state_spec.rb +0 -20
  73. data/tooling/checksums.rb +0 -106
@@ -1,78 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ScoutApm
4
- module Logging
5
- module Collector
6
- # Downloads the collector-contrib binary from the OpenTelemetry project.
7
- class Downloader
8
- attr_accessor :failed_count
9
- attr_reader :context, :checksum
10
-
11
- def initialize(context)
12
- @context = context
13
- @checksum = Checksum.new(context)
14
- end
15
-
16
- def download!
17
- # Already downloaded the collector. Noop.
18
- return if checksum.verified_checksum?
19
-
20
- # Account for issues such as failed extractions or download corruptions.
21
- download_collector
22
- verify_checksum
23
- rescue StandardError => e
24
- # Bypass Rubcop useless asignment rule.
25
- failed_count ||= 0
26
-
27
- if failed_count < 3
28
- context.logger.error("Failed to download or extract otelcol-contrib: #{e}. Retrying...")
29
- failed_count += 1
30
- retry
31
- end
32
- end
33
-
34
- def download_collector(url = nil, redirect: false) # rubocop:disable Metrics/AbcSize
35
- # Prevent double logging.
36
- unless redirect
37
- context.logger.debug("Downloading otelcol-contrib for version #{context.config.value('collector_version')}")
38
- end
39
-
40
- url_to_download = url || collector_url
41
- uri = URI(url_to_download)
42
-
43
- Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
44
- request = Net::HTTP::Get.new(uri)
45
- http.request(request) do |response|
46
- return download_collector(response['location'], redirect: true) if response.code == '302'
47
-
48
- File.open(destination, 'wb') do |file|
49
- response.read_body do |chunk|
50
- file.write(chunk)
51
- end
52
- end
53
- end
54
- end
55
- end
56
-
57
- private
58
-
59
- def verify_checksum
60
- raise 'Invalid checksum on download.' unless checksum.verified_checksum?
61
- end
62
-
63
- def collector_url
64
- collector_version = context.config.value('collector_version')
65
- architecture = Utils.get_architecture
66
- host_os = Utils.get_host_os
67
-
68
- # https://opentelemetry.io/docs/collector/installation/#manual-linux-installation
69
- "https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v#{collector_version}/otelcol-contrib_#{collector_version}_#{host_os}_#{architecture}.tar.gz"
70
- end
71
-
72
- def destination
73
- "#{context.config.value('collector_download_dir')}/otelcol.tar.gz"
74
- end
75
- end
76
- end
77
- end
78
- end
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ScoutApm
4
- module Logging
5
- module Collector
6
- # Extracts the contents of the collector tar file.
7
- class Extractor
8
- attr_reader :context
9
-
10
- def initialize(context)
11
- @context = context
12
- end
13
-
14
- def extract!
15
- # Already extracted. Noop.
16
- return if has_been_extracted?
17
-
18
- system("tar -xzf #{tar_path} -C #{context.config.value('collector_download_dir')}")
19
- end
20
-
21
- def has_been_extracted?
22
- File.exist?(binary_path)
23
- end
24
-
25
- private
26
-
27
- def tar_path
28
- "#{context.config.value('collector_download_dir')}/otelcol.tar.gz"
29
- end
30
-
31
- def binary_path
32
- "#{context.config.value('collector_download_dir')}/otelcol-contrib"
33
- end
34
- end
35
- end
36
- end
37
- end
@@ -1,57 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative './checksum'
4
- require_relative './configuration'
5
- require_relative './downloader'
6
- require_relative './extractor'
7
-
8
- module ScoutApm
9
- module Logging
10
- module Collector
11
- # Manager class for the downloading, configuring, and starting of the collector.
12
- class Manager
13
- attr_reader :context
14
-
15
- def initialize(context)
16
- @context = context
17
-
18
- @checksum = Checksum.new(@context)
19
- @configuration = Configuration.new(@context)
20
- @downloader = Downloader.new(@context)
21
- @extractor = Extractor.new(@context)
22
- end
23
-
24
- def setup!
25
- @configuration.setup!
26
- @downloader.download!
27
- @extractor.extract!
28
-
29
- start_collector if verified_checksum_and_extracted?
30
- end
31
-
32
- def start_collector
33
- context.logger.info('Starting otelcol-contrib')
34
- collector_process = Process.spawn("#{extracted_collector_path}/otelcol-contrib --config #{config_file}")
35
- File.write(context.config.value('collector_pid_file'), collector_process)
36
- end
37
-
38
- private
39
-
40
- def verified_checksum_and_extracted?
41
- has_verfied_checksum = @checksum.verified_checksum?(should_log_failures: true)
42
- has_extracted_content = @extractor.has_been_extracted?
43
-
44
- has_verfied_checksum && has_extracted_content
45
- end
46
-
47
- def extracted_collector_path
48
- context.config.value('collector_download_dir')
49
- end
50
-
51
- def config_file
52
- context.config.value('collector_config_file')
53
- end
54
- end
55
- end
56
- end
57
- end
@@ -1,216 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- ##
4
- # Launched as a daemon process by the monitor manager at Rails startup.
5
- ##
6
- require 'json'
7
-
8
- require 'scout_apm'
9
-
10
- require_relative '../logger'
11
- require_relative '../context'
12
- require_relative '../config'
13
- require_relative '../utils'
14
- require_relative '../state'
15
- require_relative './collector/manager'
16
-
17
- module ScoutApm
18
- module Logging
19
- # Entry point for the monitor daemon process.
20
- class Monitor
21
- attr_reader :context
22
- attr_accessor :latest_state_sha
23
-
24
- @@instance = nil
25
-
26
- def self.instance
27
- @@instance ||= new
28
- end
29
-
30
- def initialize
31
- @context = Context.new
32
- context.logger.debug('Monitor instance created')
33
-
34
- context.application_root = $stdin.gets&.chomp
35
-
36
- # Load in the dynamic and state based config settings.
37
- context.config = Config.with_file(context, determine_scout_config_filepath)
38
-
39
- daemonize_process!
40
- end
41
-
42
- def setup!
43
- context.config.logger.info('Monitor daemon process started')
44
-
45
- add_exit_handler!
46
-
47
- unless has_logs_to_monitor?
48
- context.config.logger.warn('No logs are set to be monitored. Please set the `logs_monitored` config setting. Exiting.')
49
- return
50
- end
51
-
52
- initiate_collector_setup! unless has_previous_collector_setup?
53
-
54
- @latest_state_sha = get_state_file_sha
55
-
56
- run!
57
- end
58
-
59
- def run!
60
- # Prevent the monitor from checking the collector health before it's fully started.
61
- # Having this be configurable is useful for testing.
62
- sleep context.config.value('monitor_interval_delay')
63
-
64
- loop do
65
- sleep context.config.value('monitor_interval')
66
-
67
- check_collector_health
68
-
69
- check_state_change
70
- end
71
- end
72
-
73
- # Only useful for testing.
74
- def config=(config)
75
- context.config = config
76
- end
77
-
78
- private
79
-
80
- def daemonize_process!
81
- # Similar to that of Process.daemon, but we want to keep the dir, STDOUT and STDERR.
82
- exit if fork
83
- Process.setsid
84
- exit if fork
85
- $stdin.reopen '/dev/null'
86
-
87
- context.logger.debug("Monitor process daemonized, PID: #{Process.pid}")
88
- File.write(context.config.value('monitor_pid_file'), Process.pid)
89
- end
90
-
91
- def has_logs_to_monitor?
92
- context.config.value('logs_monitored').any?
93
- end
94
-
95
- def has_previous_collector_setup?
96
- return false unless context.config.value('health_check_port') != 0
97
-
98
- healthy_response = request_health_check_port("http://localhost:#{context.config.value('health_check_port')}/")
99
-
100
- if healthy_response
101
- context.logger.info("Collector already setup on port #{context.config.value('health_check_port')}")
102
- else
103
- context.logger.info('Setting up new collector')
104
- end
105
-
106
- healthy_response
107
- end
108
-
109
- def initiate_collector_setup!
110
- set_health_check_port!
111
-
112
- Collector::Manager.new(context).setup!
113
- end
114
-
115
- def is_port_available?(port)
116
- socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
117
- remote_address = Socket.sockaddr_in(port, '127.0.0.1')
118
-
119
- begin
120
- socket.connect_nonblock(remote_address)
121
- rescue Errno::EINPROGRESS
122
- IO.select(nil, [socket])
123
- retry
124
- rescue Errno::EISCONN, Errno::ECONNRESET
125
- false
126
- rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
127
- true
128
- ensure
129
- socket.close if socket && !socket.closed?
130
- end
131
- end
132
-
133
- def set_health_check_port!
134
- health_check_port = 13_133
135
- until is_port_available?(health_check_port)
136
- sleep 0.1
137
- health_check_port += 1
138
- end
139
-
140
- Config::ConfigDynamic.set_value('health_check_port', health_check_port)
141
- context.config.state.flush_state!
142
- end
143
-
144
- def request_health_check_port(endpoint)
145
- uri = URI(endpoint)
146
-
147
- begin
148
- response = Net::HTTP.get_response(uri)
149
-
150
- unless response.is_a?(Net::HTTPSuccess)
151
- context.logger.error("Error occurred while checking collector health: #{response.message}")
152
- return false
153
- end
154
- rescue StandardError => e
155
- context.logger.error("Error occurred while checking collector health: #{e.message}")
156
- return false
157
- end
158
-
159
- true
160
- end
161
-
162
- def check_collector_health
163
- context.logger.debug('Checking collector health')
164
- collector_health_endpoint = "http://localhost:#{context.config.value('health_check_port')}/"
165
-
166
- healthy_response = request_health_check_port(collector_health_endpoint)
167
-
168
- initiate_collector_setup! unless healthy_response
169
- end
170
-
171
- def remove_collector_process # rubocop:disable Metrics/AbcSize
172
- return unless File.exist? context.config.value('collector_pid_file')
173
-
174
- process_id = File.read(context.config.value('collector_pid_file'))
175
- return if process_id.empty?
176
-
177
- begin
178
- Process.kill('TERM', process_id.to_i)
179
- rescue Errno::ENOENT, Errno::ESRCH => e
180
- context.logger.error("Error occurred while removing collector process from monitor: #{e.message}")
181
- ensure
182
- File.delete(context.config.value('collector_pid_file'))
183
- end
184
- end
185
-
186
- def check_state_change
187
- current_sha = get_state_file_sha
188
-
189
- return if current_sha == latest_state_sha
190
-
191
- remove_collector_process
192
- initiate_collector_setup!
193
-
194
- # File SHA can change due to port mappings on collector setup.
195
- @latest_state_sha = get_state_file_sha
196
- end
197
-
198
- def add_exit_handler!
199
- at_exit do
200
- # There may not be a file to delete, as the monitor manager ensures cleaning it up when monitoring is disabled.
201
- File.delete(context.config.value('monitor_pid_file')) if File.exist?(context.config.value('monitor_pid_file'))
202
- end
203
- end
204
-
205
- def get_state_file_sha
206
- return nil unless File.exist?(context.config.value('monitor_state_file'))
207
-
208
- `sha256sum #{context.config.value('monitor_state_file')}`.split(' ').first
209
- end
210
-
211
- def determine_scout_config_filepath
212
- "#{context.application_root}/config/scout_apm.yml"
213
- end
214
- end
215
- end
216
- end
@@ -1,162 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ScoutApm
4
- module Logging
5
- # Manages the creation of the daemon monitor process.
6
- class MonitorManager
7
- attr_reader :context
8
-
9
- @@instance = nil
10
-
11
- def self.instance
12
- @@instance ||= new
13
- end
14
-
15
- def initialize
16
- @context = Context.new
17
- context.config = Config.with_file(context, context.config.value('config_file'))
18
- end
19
-
20
- def setup!
21
- context.config.log_settings(context.logger)
22
- context.logger.info('Setting up monitor daemon process')
23
-
24
- add_exit_handler!
25
-
26
- determine_configuration_state
27
- end
28
-
29
- def determine_configuration_state
30
- monitoring_enabled = context.config.value('logs_monitor')
31
-
32
- if monitoring_enabled
33
- context.logger.info('Log monitoring enabled')
34
- create_process
35
-
36
- # Continue to hold the lock until we have written the PID file.
37
- ensure_monitor_pid_file_exists
38
- else
39
- context.logger.info('Log monitoring disabled')
40
- remove_processes
41
- end
42
- end
43
-
44
- # With the use of fileoffsets in the collector, and the persistent queue of already collected logs,
45
- # we can safely restart the collector. Due to the way fingerprinting of the files works, if the
46
- # file path switches, but the beginning contents of the file remain the same, the file will be
47
- # treated as the same file as before.
48
- # If logs get rotated, the fingerprint changes, and the collector automatically detects this.
49
- def add_exit_handler!
50
- # With the use of unicorn and puma worker killer, we want to ensure we only restart (exit and
51
- # eventually start) the monitor and collector when the main process exits, and not the workers.
52
- initialized_process_id = Process.pid
53
- at_exit do
54
- # Only remove/restart the monitor and collector if we are exiting from an app_server process.
55
- # We need to wait on this check, as the process command line changes at some point.
56
- if Utils.current_process_is_app_server? && Process.pid == initialized_process_id
57
- context.logger.debug('Exiting from app server process. Removing monitor and collector processes.')
58
- remove_processes
59
- end
60
- end
61
- end
62
-
63
- def create_process
64
- return if process_exists?
65
-
66
- Utils.ensure_directory_exists(context.config.value('monitor_pid_file'))
67
-
68
- reader, writer = IO.pipe
69
-
70
- gem_directory = File.expand_path('../../../..', __dir__)
71
-
72
- # As we daemonize the process, we will write to the pid file within the process.
73
- pid = Process.spawn("ruby #{gem_directory}/bin/scout_apm_logging_monitor", in: reader)
74
-
75
- reader.close
76
- # TODO: Add support for Sinatra.
77
- writer.puts Rails.root if defined?(Rails)
78
- writer.close
79
- # Block until we have spawned the process and forked. This is to ensure
80
- # we keep the exclusive lock until the process has written the PID file.
81
- Process.wait(pid)
82
- end
83
-
84
- private
85
-
86
- def ensure_monitor_pid_file_exists
87
- start_time = Time.now
88
- # We don't want to hold up the initial Rails boot time for very long.
89
- timeout_seconds = 0.1
90
-
91
- # Naive benchmarks show this taking ~0.01 seconds.
92
- loop do
93
- if File.exist?(context.config.value('monitor_pid_file'))
94
- context.logger.debug('Monitor PID file exists. Releasing lock.')
95
- break
96
- end
97
-
98
- if Time.now - start_time > timeout_seconds
99
- context.logger.warn('Unable to verify monitor PID file write. Releasing lock.')
100
- break
101
- end
102
-
103
- sleep 0.01
104
- end
105
- end
106
-
107
- def process_exists?
108
- return false unless File.exist? context.config.value('monitor_pid_file')
109
-
110
- process_id = File.read(context.config.value('monitor_pid_file'))
111
- return false if process_id.empty?
112
-
113
- process_exists = Utils.check_process_liveliness(process_id.to_i, 'scout_apm_logging_monitor')
114
- File.delete(context.config.value('monitor_pid_file')) unless process_exists
115
-
116
- process_exists
117
- end
118
-
119
- def remove_monitor_process # rubocop:disable Metrics/AbcSize
120
- return unless File.exist? context.config.value('monitor_pid_file')
121
-
122
- process_id = File.read(context.config.value('monitor_pid_file'))
123
- return if process_id.empty?
124
-
125
- begin
126
- Process.kill('TERM', process_id.to_i)
127
- rescue Errno::ENOENT, Errno::ESRCH => e
128
- context.logger.error("Error occurred while removing monitor process: #{e.message}")
129
- File.delete(context.config.value('monitor_pid_file'))
130
- end
131
- end
132
-
133
- def remove_collector_process # rubocop:disable Metrics/AbcSize
134
- return unless File.exist? context.config.value('collector_pid_file')
135
-
136
- process_id = File.read(context.config.value('collector_pid_file'))
137
- return if process_id.empty?
138
-
139
- begin
140
- Process.kill('TERM', process_id.to_i)
141
- rescue Errno::ENOENT, Errno::ESRCH => e
142
- context.logger.error("Error occurred while removing collector process from manager: #{e.message}")
143
- ensure
144
- File.delete(context.config.value('collector_pid_file'))
145
- end
146
- end
147
-
148
- def remove_data_file
149
- return unless File.exist? context.config.value('monitor_state_file')
150
-
151
- File.delete(context.config.value('monitor_state_file'))
152
- end
153
-
154
- # Remove both the monitor and collector processes that we have spawned.
155
- def remove_processes
156
- remove_monitor_process
157
- remove_collector_process
158
- remove_data_file
159
- end
160
- end
161
- end
162
- end
@@ -1,69 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ScoutApm
4
- module Logging
5
- class Config
6
- # Responsibling for ensuring safe interprocess persitance around configuration state.
7
- class State
8
- attr_reader :context
9
-
10
- def initialize(context)
11
- @context = context
12
- end
13
-
14
- def load_state_from_file
15
- return unless File.exist?(context.config.value('monitor_state_file'))
16
-
17
- file_contents = File.read(context.config.value('monitor_state_file'))
18
- JSON.parse(file_contents)
19
- end
20
-
21
- def flush_to_file!(updated_log_locations = []) # rubocop:disable Metrics/AbcSize
22
- Utils.ensure_directory_exists(context.config.value('monitor_state_file'))
23
-
24
- File.open(context.config.value('monitor_state_file'), (File::RDWR | File::CREAT), 0o644) do |file|
25
- file.flock(File::LOCK_EX)
26
-
27
- data = Config::ConfigState.get_values_to_set.each_with_object({}) do |key, memo|
28
- memo[key] = context.config.value(key)
29
- end
30
-
31
- contents = file.read
32
- old_log_state_files = if contents.empty?
33
- []
34
- else
35
- current_data = JSON.parse(contents)
36
- current_data['logs_monitored']
37
- end
38
-
39
- data['logs_monitored'] =
40
- merge_and_dedup_log_locations(updated_log_locations, old_log_state_files, data['logs_monitored'])
41
-
42
- file.rewind # Move cursor to beginning of the file
43
- file.truncate(0) # Truncate existing content
44
- file.write(JSON.pretty_generate(data))
45
- rescue StandardError => e
46
- context.logger.error("Error occurred while flushing state to file: #{e.message}. Unlocking.")
47
- ensure
48
- file.flock(File::LOCK_UN)
49
- end
50
- end
51
-
52
- private
53
-
54
- # Should we add better detection for similar basenames but different paths?
55
- # May be a bit tricky with tools like capistrano and releases paths differentiated by time.
56
- def merge_and_dedup_log_locations(*log_locations)
57
- # Take the new logs if duplication (those first passed in the args) as we could be in a newer release.
58
- logs = log_locations.reduce([], :concat)
59
- merged = logs.each_with_object({}) do |log_path, hash|
60
- base_name = File.basename(log_path)
61
- hash[base_name] ||= log_path
62
- end
63
-
64
- merged.values
65
- end
66
- end
67
- end
68
- end
69
- end
File without changes
@@ -1,3 +0,0 @@
1
- extensions:
2
- file_storage/otc:
3
- directory: /dev/null
@@ -1,3 +0,0 @@
1
- {
2
- "health_check_port": 1234
3
- }