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.
- 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
|