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,214 @@
|
|
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
|
+
|
33
|
+
context.application_root = $stdin.gets&.chomp
|
34
|
+
|
35
|
+
# Load in the dynamic and state based config settings.
|
36
|
+
context.config = Config.with_file(context, determine_scout_config_filepath)
|
37
|
+
|
38
|
+
daemonize_process!
|
39
|
+
end
|
40
|
+
|
41
|
+
def setup!
|
42
|
+
context.config.logger.info('Monitor daemon process started')
|
43
|
+
|
44
|
+
add_exit_handler!
|
45
|
+
|
46
|
+
unless has_logs_to_monitor?
|
47
|
+
context.config.logger.warn('No logs are set to be monitored. Please set the `logs_monitored` config setting. Exiting.')
|
48
|
+
return
|
49
|
+
end
|
50
|
+
|
51
|
+
initiate_collector_setup! unless has_previous_collector_setup?
|
52
|
+
|
53
|
+
@latest_state_sha = get_state_file_sha
|
54
|
+
|
55
|
+
run!
|
56
|
+
end
|
57
|
+
|
58
|
+
def run!
|
59
|
+
# Prevent the monitor from checking the collector health before it's fully started.
|
60
|
+
# Having this be configurable is useful for testing.
|
61
|
+
sleep context.config.value('monitor_interval_delay')
|
62
|
+
|
63
|
+
loop do
|
64
|
+
sleep context.config.value('monitor_interval')
|
65
|
+
|
66
|
+
check_collector_health
|
67
|
+
|
68
|
+
check_state_change
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Only useful for testing.
|
73
|
+
def config=(config)
|
74
|
+
context.config = config
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def daemonize_process!
|
80
|
+
# Similar to that of Process.daemon, but we want to keep the dir, STDOUT and STDERR.
|
81
|
+
exit if fork
|
82
|
+
Process.setsid
|
83
|
+
exit if fork
|
84
|
+
$stdin.reopen '/dev/null'
|
85
|
+
|
86
|
+
File.write(context.config.value('monitor_pid_file'), Process.pid)
|
87
|
+
end
|
88
|
+
|
89
|
+
def has_logs_to_monitor?
|
90
|
+
context.config.value('logs_monitored').any?
|
91
|
+
end
|
92
|
+
|
93
|
+
def has_previous_collector_setup?
|
94
|
+
return false unless context.config.value('health_check_port') != 0
|
95
|
+
|
96
|
+
healthy_response = request_health_check_port("http://localhost:#{context.config.value('health_check_port')}/")
|
97
|
+
|
98
|
+
if healthy_response
|
99
|
+
context.logger.info("Collector already setup on port #{context.config.value('health_check_port')}")
|
100
|
+
else
|
101
|
+
context.logger.info('Setting up new collector')
|
102
|
+
end
|
103
|
+
|
104
|
+
healthy_response
|
105
|
+
end
|
106
|
+
|
107
|
+
def initiate_collector_setup!
|
108
|
+
set_health_check_port!
|
109
|
+
|
110
|
+
Collector::Manager.new(context).setup!
|
111
|
+
end
|
112
|
+
|
113
|
+
def is_port_available?(port)
|
114
|
+
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
|
115
|
+
remote_address = Socket.sockaddr_in(port, '127.0.0.1')
|
116
|
+
|
117
|
+
begin
|
118
|
+
socket.connect_nonblock(remote_address)
|
119
|
+
rescue Errno::EINPROGRESS
|
120
|
+
IO.select(nil, [socket])
|
121
|
+
retry
|
122
|
+
rescue Errno::EISCONN, Errno::ECONNRESET
|
123
|
+
false
|
124
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
|
125
|
+
true
|
126
|
+
ensure
|
127
|
+
socket.close if socket && !socket.closed?
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def set_health_check_port!
|
132
|
+
health_check_port = 13_133
|
133
|
+
until is_port_available?(health_check_port)
|
134
|
+
sleep 0.1
|
135
|
+
health_check_port += 1
|
136
|
+
end
|
137
|
+
|
138
|
+
Config::ConfigDynamic.set_value('health_check_port', health_check_port)
|
139
|
+
context.config.state.flush_state!
|
140
|
+
end
|
141
|
+
|
142
|
+
def request_health_check_port(endpoint)
|
143
|
+
uri = URI(endpoint)
|
144
|
+
|
145
|
+
begin
|
146
|
+
response = Net::HTTP.get_response(uri)
|
147
|
+
|
148
|
+
unless response.is_a?(Net::HTTPSuccess)
|
149
|
+
context.logger.error("Error occurred while checking collector health: #{response.message}")
|
150
|
+
return false
|
151
|
+
end
|
152
|
+
rescue StandardError => e
|
153
|
+
context.logger.error("Error occurred while checking collector health: #{e.message}")
|
154
|
+
return false
|
155
|
+
end
|
156
|
+
|
157
|
+
true
|
158
|
+
end
|
159
|
+
|
160
|
+
def check_collector_health
|
161
|
+
context.logger.debug('Checking collector health')
|
162
|
+
collector_health_endpoint = "http://localhost:#{context.config.value('health_check_port')}/"
|
163
|
+
|
164
|
+
healthy_response = request_health_check_port(collector_health_endpoint)
|
165
|
+
|
166
|
+
initiate_collector_setup! unless healthy_response
|
167
|
+
end
|
168
|
+
|
169
|
+
def remove_collector_process # rubocop:disable Metrics/AbcSize
|
170
|
+
return unless File.exist? context.config.value('collector_pid_file')
|
171
|
+
|
172
|
+
process_id = File.read(context.config.value('collector_pid_file'))
|
173
|
+
return if process_id.empty?
|
174
|
+
|
175
|
+
begin
|
176
|
+
Process.kill('TERM', process_id.to_i)
|
177
|
+
rescue Errno::ENOENT, Errno::ESRCH => e
|
178
|
+
context.logger.error("Error occurred while removing collector process from monitor: #{e.message}")
|
179
|
+
ensure
|
180
|
+
File.delete(context.config.value('collector_pid_file'))
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def check_state_change
|
185
|
+
current_sha = get_state_file_sha
|
186
|
+
|
187
|
+
return if current_sha == latest_state_sha
|
188
|
+
|
189
|
+
remove_collector_process
|
190
|
+
initiate_collector_setup!
|
191
|
+
|
192
|
+
# File SHA can change due to port mappings on collector setup.
|
193
|
+
@latest_state_sha = get_state_file_sha
|
194
|
+
end
|
195
|
+
|
196
|
+
def add_exit_handler!
|
197
|
+
at_exit do
|
198
|
+
# There may not be a file to delete, as the monitor manager ensures cleaning it up when monitoring is disabled.
|
199
|
+
File.delete(context.config.value('monitor_pid_file')) if File.exist?(context.config.value('monitor_pid_file'))
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def get_state_file_sha
|
204
|
+
return nil unless File.exist?(context.config.value('monitor_state_file'))
|
205
|
+
|
206
|
+
`sha256sum #{context.config.value('monitor_state_file')}`.split(' ').first
|
207
|
+
end
|
208
|
+
|
209
|
+
def determine_scout_config_filepath
|
210
|
+
"#{context.application_root}/config/scout_apm.yml"
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
@@ -0,0 +1,150 @@
|
|
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
|
+
|
28
|
+
# Continue to hold the lock until we have written the PID file.
|
29
|
+
ensure_monitor_pid_file_exists
|
30
|
+
end
|
31
|
+
|
32
|
+
def determine_configuration_state
|
33
|
+
monitoring_enabled = context.config.value('logs_monitor')
|
34
|
+
|
35
|
+
if monitoring_enabled
|
36
|
+
context.logger.info('Log monitoring enabled')
|
37
|
+
create_process
|
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
|
+
at_exit do
|
51
|
+
# Only remove/restart the monitor and collector if we are exiting from an app_server process.
|
52
|
+
# We need to wait on this check, as the process command line changes at some point.
|
53
|
+
remove_processes if Utils.current_process_is_app_server?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def create_process
|
58
|
+
return if process_exists?
|
59
|
+
|
60
|
+
Utils.ensure_directory_exists(context.config.value('monitor_pid_file'))
|
61
|
+
|
62
|
+
reader, writer = IO.pipe
|
63
|
+
|
64
|
+
gem_directory = File.expand_path('../../../..', __dir__)
|
65
|
+
|
66
|
+
# As we daemonize the process, we will write to the pid file within the process.
|
67
|
+
Process.spawn("ruby #{gem_directory}/bin/scout_apm_logging_monitor", in: reader)
|
68
|
+
|
69
|
+
reader.close
|
70
|
+
# TODO: Add support for Sinatra.
|
71
|
+
writer.puts Rails.root if defined?(Rails)
|
72
|
+
writer.close
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def ensure_monitor_pid_file_exists
|
78
|
+
start_time = Time.now
|
79
|
+
# We don't want to hold up the initial Rails boot time for very long.
|
80
|
+
timeout_seconds = 0.1
|
81
|
+
|
82
|
+
# Naive benchmarks show this taking ~0.01 seconds.
|
83
|
+
loop do
|
84
|
+
break if File.exist?(context.config.value('monitor_pid_file'))
|
85
|
+
|
86
|
+
if Time.now - start_time > timeout_seconds
|
87
|
+
context.logger.warn('Unable to verify monitor PID file write. Releasing lock.')
|
88
|
+
break
|
89
|
+
end
|
90
|
+
|
91
|
+
sleep 0.01
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def process_exists?
|
96
|
+
return false unless File.exist? context.config.value('monitor_pid_file')
|
97
|
+
|
98
|
+
process_id = File.read(context.config.value('monitor_pid_file'))
|
99
|
+
return false if process_id.empty?
|
100
|
+
|
101
|
+
process_exists = Utils.check_process_liveliness(process_id.to_i, 'scout_apm_logging_monitor')
|
102
|
+
File.delete(context.config.value('monitor_pid_file')) unless process_exists
|
103
|
+
|
104
|
+
process_exists
|
105
|
+
end
|
106
|
+
|
107
|
+
def remove_monitor_process # rubocop:disable Metrics/AbcSize
|
108
|
+
return unless File.exist? context.config.value('monitor_pid_file')
|
109
|
+
|
110
|
+
process_id = File.read(context.config.value('monitor_pid_file'))
|
111
|
+
return if process_id.empty?
|
112
|
+
|
113
|
+
begin
|
114
|
+
Process.kill('TERM', process_id.to_i)
|
115
|
+
rescue Errno::ENOENT, Errno::ESRCH => e
|
116
|
+
context.logger.error("Error occurred while removing monitor process: #{e.message}")
|
117
|
+
File.delete(context.config.value('monitor_pid_file'))
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def remove_collector_process # rubocop:disable Metrics/AbcSize
|
122
|
+
return unless File.exist? context.config.value('collector_pid_file')
|
123
|
+
|
124
|
+
process_id = File.read(context.config.value('collector_pid_file'))
|
125
|
+
return if process_id.empty?
|
126
|
+
|
127
|
+
begin
|
128
|
+
Process.kill('TERM', process_id.to_i)
|
129
|
+
rescue Errno::ENOENT, Errno::ESRCH => e
|
130
|
+
context.logger.error("Error occurred while removing collector process from manager: #{e.message}")
|
131
|
+
ensure
|
132
|
+
File.delete(context.config.value('collector_pid_file'))
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def remove_data_file
|
137
|
+
return unless File.exist? context.config.value('monitor_state_file')
|
138
|
+
|
139
|
+
File.delete(context.config.value('monitor_state_file'))
|
140
|
+
end
|
141
|
+
|
142
|
+
# Remove both the monitor and collector processes that we have spawned.
|
143
|
+
def remove_processes
|
144
|
+
remove_monitor_process
|
145
|
+
remove_collector_process
|
146
|
+
remove_data_file
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,70 @@
|
|
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
|
+
unless updated_log_locations.empty?
|
32
|
+
contents = file.read
|
33
|
+
|
34
|
+
olds_log_files = if contents.empty?
|
35
|
+
[]
|
36
|
+
else
|
37
|
+
current_data = JSON.parse(contents)
|
38
|
+
current_data['logs_monitored']
|
39
|
+
end
|
40
|
+
|
41
|
+
data['logs_monitored'] = merge_and_dedup_log_locations(updated_log_locations, olds_log_files)
|
42
|
+
end
|
43
|
+
|
44
|
+
file.rewind # Move cursor to beginning of the file
|
45
|
+
file.truncate(0) # Truncate existing content
|
46
|
+
file.write(JSON.pretty_generate(data))
|
47
|
+
rescue StandardError => e
|
48
|
+
context.logger.error("Error occurred while flushing state to file: #{e.message}. Unlocking.")
|
49
|
+
ensure
|
50
|
+
file.flock(File::LOCK_UN)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
# Should we add better detection for similar basenames but different paths?
|
57
|
+
# May be a bit tricky with tools like capistrano and releases paths differentiated by time.
|
58
|
+
def merge_and_dedup_log_locations(new_logs, old_logs)
|
59
|
+
# Take the new logs if duplication, as we could be in a newer release.
|
60
|
+
merged = (new_logs + old_logs).each_with_object({}) do |log_path, hash|
|
61
|
+
base_name = File.basename(log_path)
|
62
|
+
hash[base_name] ||= log_path
|
63
|
+
end
|
64
|
+
|
65
|
+
merged.values
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module ScoutApm
|
6
|
+
module Logging
|
7
|
+
# Miscellaneous utilities for the logging module.
|
8
|
+
module Utils
|
9
|
+
# Takes a complete file path, and ensures that the directory structure exists.
|
10
|
+
def self.ensure_directory_exists(file_path)
|
11
|
+
file_path = File.dirname(file_path) unless file_path[-1] == '/'
|
12
|
+
|
13
|
+
FileUtils.mkdir_p(file_path) unless File.directory?(file_path)
|
14
|
+
end
|
15
|
+
|
16
|
+
# TODO: Add support for other platforms
|
17
|
+
def self.get_architecture
|
18
|
+
if /arm/ =~ RbConfig::CONFIG['arch']
|
19
|
+
'arm64'
|
20
|
+
else
|
21
|
+
'amd64'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.get_host_os
|
26
|
+
if /darwin|mac os/ =~ RbConfig::CONFIG['host_os']
|
27
|
+
'darwin'
|
28
|
+
else
|
29
|
+
'linux'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.check_process_liveliness(pid, name)
|
34
|
+
# Pipe to cat to prevent truncation of the output
|
35
|
+
process_information = `ps -p #{pid} -o pid=,stat=,command= | cat`
|
36
|
+
return false if process_information.empty?
|
37
|
+
|
38
|
+
process_information_parts = process_information.split(' ')
|
39
|
+
process_information_status = process_information_parts[1]
|
40
|
+
|
41
|
+
return false if process_information_status == 'Z'
|
42
|
+
return false unless process_information.include?(name)
|
43
|
+
|
44
|
+
true
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.current_process_is_app_server?
|
48
|
+
# TODO: Add more app servers.
|
49
|
+
process_command = `ps -p #{Process.pid} -o command= | cat`.downcase
|
50
|
+
[
|
51
|
+
process_command.include?('puma'),
|
52
|
+
process_command.include?('unicorn'),
|
53
|
+
process_command.include?('passenger')
|
54
|
+
].any?
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.skip_setup?
|
58
|
+
[
|
59
|
+
ARGV.include?('assets:precompile'),
|
60
|
+
ARGV.include?('assets:clean'),
|
61
|
+
(defined?(::Rails::Console) && $stdout.isatty && $stdin.isatty)
|
62
|
+
].any?
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.attempt_exclusive_lock(context)
|
66
|
+
lock_file = context.config.value('manager_lock_file')
|
67
|
+
ensure_directory_exists(lock_file)
|
68
|
+
|
69
|
+
begin
|
70
|
+
file = File.open(lock_file, File::RDWR | File::CREAT | File::EXCL)
|
71
|
+
rescue Errno::EEXIST
|
72
|
+
context.logger.info('Manager lock file held, continuing.')
|
73
|
+
return
|
74
|
+
end
|
75
|
+
|
76
|
+
# Ensure the lock file is deleted when the block completes
|
77
|
+
begin
|
78
|
+
yield
|
79
|
+
ensure
|
80
|
+
file.close
|
81
|
+
File.delete(lock_file) if File.exist?(lock_file)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'scout_apm'
|
4
|
+
|
5
|
+
require 'scout_apm/logging/config'
|
6
|
+
require 'scout_apm/logging/logger'
|
7
|
+
require 'scout_apm/logging/context'
|
8
|
+
require 'scout_apm/logging/utils'
|
9
|
+
require 'scout_apm/logging/state'
|
10
|
+
|
11
|
+
require 'scout_apm/logging/loggers/capture'
|
12
|
+
|
13
|
+
require 'scout_apm/logging/monitor_manager/manager'
|
14
|
+
|
15
|
+
module ScoutApm
|
16
|
+
## This module is responsible for setting up monitoring of the application's logs.
|
17
|
+
module Logging
|
18
|
+
if defined?(Rails) && defined?(Rails::Railtie)
|
19
|
+
# If we are in a Rails environment, setup the monitor daemon manager.
|
20
|
+
class RailTie < ::Rails::Railtie
|
21
|
+
initializer 'scout_apm_logging.monitor' do
|
22
|
+
context = ScoutApm::Logging::MonitorManager.instance.context
|
23
|
+
|
24
|
+
Loggers::Capture.new(context).capture_log_locations!
|
25
|
+
|
26
|
+
unless Utils.skip_setup?
|
27
|
+
Utils.attempt_exclusive_lock(context) do
|
28
|
+
ScoutApm::Logging::MonitorManager.instance.setup!
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
$LOAD_PATH.push File.expand_path('lib', __dir__)
|
4
|
+
require 'scout_apm/logging/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'scout_apm_logging'
|
8
|
+
s.version = ScoutApm::Logging::VERSION
|
9
|
+
s.authors = 'Scout APM'
|
10
|
+
s.email = ['support@scoutapp.com']
|
11
|
+
s.homepage = 'https://github.com/scoutapp/scout_apm_ruby_logging'
|
12
|
+
s.summary = 'Ruby Logging Support'
|
13
|
+
s.description = 'Sets up log monitoring for Scout APM Ruby clients.'
|
14
|
+
s.license = 'MIT'
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.require_paths = ['lib']
|
19
|
+
|
20
|
+
s.required_ruby_version = '>= 2.6'
|
21
|
+
|
22
|
+
s.add_dependency 'scout_apm'
|
23
|
+
|
24
|
+
s.add_development_dependency 'rspec'
|
25
|
+
s.add_development_dependency 'rubocop', '1.50.2'
|
26
|
+
s.add_development_dependency 'rubocop-ast', '1.30.0'
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
common: &defaults
|
2
|
+
name: Scout APM
|
3
|
+
key: 000011110000
|
4
|
+
log_level: debug
|
5
|
+
monitor: true
|
6
|
+
|
7
|
+
###
|
8
|
+
# Logging
|
9
|
+
###
|
10
|
+
logs_monitor: true
|
11
|
+
logs_ingest_key: "00001000010000abc"
|
12
|
+
logs_monitored: ["/tmp/fake_log_file.log"]
|
13
|
+
|
14
|
+
production:
|
15
|
+
<<: *defaults
|
16
|
+
name: APM Test Conf (Production)
|
17
|
+
|
18
|
+
development:
|
19
|
+
<<: *defaults
|
20
|
+
name: APM Test Conf (Development)
|
21
|
+
host: http://localhost:3000
|
22
|
+
monitor: true
|
23
|
+
|
24
|
+
test:
|
25
|
+
<<: *defaults
|
26
|
+
name: APM Test Conf (Test)
|
27
|
+
monitor: false
|
File without changes
|
@@ -0,0 +1,29 @@
|
|
1
|
+
common: &defaults
|
2
|
+
name: Scout APM
|
3
|
+
key: 000011110000
|
4
|
+
log_level: debug
|
5
|
+
monitor: true
|
6
|
+
|
7
|
+
###
|
8
|
+
# Logging
|
9
|
+
###
|
10
|
+
logs_monitor: true
|
11
|
+
logs_ingest_key: "00001000010000abc"
|
12
|
+
logs_monitored: ["/tmp/fake_log_file.log"]
|
13
|
+
# Need to give a high enough number for the original health check to pass
|
14
|
+
monitor_interval: 10
|
15
|
+
|
16
|
+
production:
|
17
|
+
<<: *defaults
|
18
|
+
name: APM Test Conf (Production)
|
19
|
+
|
20
|
+
development:
|
21
|
+
<<: *defaults
|
22
|
+
name: APM Test Conf (Development)
|
23
|
+
host: http://localhost:3000
|
24
|
+
monitor: true
|
25
|
+
|
26
|
+
test:
|
27
|
+
<<: *defaults
|
28
|
+
name: APM Test Conf (Test)
|
29
|
+
monitor: false
|