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
         
     |