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.
- checksums.yaml +4 -4
- data/.gitignore +0 -4
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +8 -0
- data/NOTICE +4 -0
- data/lib/scout_apm/logging/config.rb +5 -131
- data/lib/scout_apm/logging/loggers/capture.rb +8 -6
- data/lib/scout_apm/logging/loggers/formatter.rb +21 -2
- data/lib/scout_apm/logging/loggers/opentelemetry/LICENSE +201 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/NOTICE +9 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/api/logs/log_record.rb +18 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/api/logs/logger.rb +64 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/api/logs/logger_provider.rb +31 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/api/logs/severity_number.rb +43 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/api/logs/version.rb +18 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/api/logs.rb +28 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/exporter/exporter/otlp/logs_exporter.rb +389 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/exporter/exporter/otlp/version.rb +20 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/exporter/proto/collector/logs/v1/logs_service_pb.rb +43 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/exporter/proto/common/v1/common_pb.rb +58 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/exporter/proto/logs/v1/logs_pb.rb +91 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/exporter/proto/resource/v1/resource_pb.rb +33 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/opentelemetry.rb +62 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/export/batch_log_record_processor.rb +225 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/export/log_record_exporter.rb +64 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/export.rb +34 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/log_record.rb +115 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/log_record_data.rb +31 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/log_record_processor.rb +53 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/logger.rb +94 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/logger_provider.rb +158 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/version.rb +20 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/sdk/logs.rb +28 -0
- data/lib/scout_apm/logging/loggers/patches/rails_logger.rb +17 -0
- data/lib/scout_apm/logging/loggers/proxy.rb +22 -5
- data/lib/scout_apm/logging/loggers/swaps/rails.rb +4 -12
- data/lib/scout_apm/logging/loggers/swaps/scout.rb +2 -10
- data/lib/scout_apm/logging/loggers/swaps/sidekiq.rb +2 -6
- data/lib/scout_apm/logging/utils.rb +0 -69
- data/lib/scout_apm/logging/version.rb +1 -1
- data/lib/scout_apm_logging.rb +3 -12
- data/scout_apm_logging.gemspec +7 -0
- data/spec/data/config_test_1.yml +0 -1
- data/spec/data/mock_config.yml +0 -3
- data/spec/integration/rails/lifecycle_spec.rb +57 -23
- data/spec/spec_helper.rb +0 -12
- data/spec/unit/config_spec.rb +0 -12
- data/spec/unit/loggers/capture_spec.rb +0 -6
- metadata +127 -39
- data/bin/scout_apm_logging_monitor +0 -6
- data/lib/scout_apm/logging/monitor/_rails.rb +0 -22
- data/lib/scout_apm/logging/monitor/collector/checksum.rb +0 -51
- data/lib/scout_apm/logging/monitor/collector/configuration.rb +0 -150
- data/lib/scout_apm/logging/monitor/collector/downloader.rb +0 -78
- data/lib/scout_apm/logging/monitor/collector/extractor.rb +0 -37
- data/lib/scout_apm/logging/monitor/collector/manager.rb +0 -57
- data/lib/scout_apm/logging/monitor/monitor.rb +0 -216
- data/lib/scout_apm/logging/monitor_manager/manager.rb +0 -162
- data/lib/scout_apm/logging/state.rb +0 -69
- data/spec/data/empty_logs_config.yml +0 -0
- data/spec/data/logs_config.yml +0 -3
- data/spec/data/state_file.json +0 -3
- data/spec/integration/loggers/capture_spec.rb +0 -68
- data/spec/integration/monitor/collector/downloader/will_verify_checksum.rb +0 -49
- data/spec/integration/monitor/collector_healthcheck_spec.rb +0 -29
- data/spec/integration/monitor/continuous_state_collector_spec.rb +0 -31
- data/spec/integration/monitor/previous_collector_setup_spec.rb +0 -45
- data/spec/integration/monitor_manager/disable_agent_spec.rb +0 -30
- data/spec/integration/monitor_manager/monitor_pid_file_spec.rb +0 -38
- data/spec/integration/monitor_manager/single_monitor_spec.rb +0 -53
- data/spec/unit/monitor/collector/configuration_spec.rb +0 -64
- data/spec/unit/state_spec.rb +0 -20
- 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
|
data/spec/data/logs_config.yml
DELETED
data/spec/data/state_file.json
DELETED