Class: ScoutApm::Agent

Inherits:
Object
  • Object
show all
Includes:
Logging, Reporting
Defined in:
lib/scout_apm/agent.rb,
lib/scout_apm/agent/logging.rb,
lib/scout_apm/agent/reporting.rb

Overview

The agent gathers performance data from a Ruby application. One Agent instance is created per-Ruby process.

Each Agent object creates a worker thread (unless monitoring is disabled or we’re forking). The worker thread wakes up every Agent#period, merges in-memory metrics w/those saved to disk, saves tshe merged data to disk, and sends it to the Scout server.

Defined Under Namespace

Modules: Logging, Reporting

Constant Summary collapse

@@instance =

see self.instance

nil

Constants included from Reporting

Reporting::MAX_AGE_TO_REPORT

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Reporting

#add_metric_ids, #deliver_period, #headers, #log_deliver, #process_metrics, #report_to_server, #reporter

Methods included from Logging

#apply_log_format, #default_log_path, #determine_log_destination, #init_logger, #log_file_path, #log_level, #wants_stderr?, #wants_stdout?

Constructor Details

#initialize(options = {}) ⇒ Agent

Note - this doesn’t start instruments or the worker thread. This is handled via #start as we don’t want to start the worker thread or install instrumentation if (1) disabled for this environment (2) a worker thread shouldn’t be started (when forking).



30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/scout_apm/agent.rb', line 30

def initialize(options = {})
  @started = false
  @options ||= options
  @config = ScoutApm::Config.new(options[:config_path])

  @store          = ScoutApm::Store.new
  @layaway        = ScoutApm::Layaway.new
  @metric_lookup  = Hash.new

  @slow_request_policy = ScoutApm::SlowRequestPolicy.new

  @capacity       = ScoutApm::Capacity.new
  @installed_instruments = []
end

Instance Attribute Details

#capacityObject

Returns the value of attribute capacity.



15
16
17
# File 'lib/scout_apm/agent.rb', line 15

def capacity
  @capacity
end

#configObject

Returns the value of attribute config.



14
15
16
# File 'lib/scout_apm/agent.rb', line 14

def config
  @config
end

#layawayObject

Returns the value of attribute layaway.



13
14
15
# File 'lib/scout_apm/agent.rb', line 13

def layaway
  @layaway
end

#log_fileObject

path to the log file



17
18
19
# File 'lib/scout_apm/agent.rb', line 17

def log_file
  @log_file
end

#loggerObject

Returns the value of attribute logger.



16
17
18
# File 'lib/scout_apm/agent.rb', line 16

def logger
  @logger
end

#metric_lookupObject

Hash used to lookup metric ids based on their name and scope



19
20
21
# File 'lib/scout_apm/agent.rb', line 19

def metric_lookup
  @metric_lookup
end

#optionsObject

options passed to the agent when #start is called.



18
19
20
# File 'lib/scout_apm/agent.rb', line 18

def options
  @options
end

#slow_request_policyObject (readonly)

Returns the value of attribute slow_request_policy.



20
21
22
# File 'lib/scout_apm/agent.rb', line 20

def slow_request_policy
  @slow_request_policy
end

#storeObject

Accessors below are for associated classes



12
13
14
# File 'lib/scout_apm/agent.rb', line 12

def store
  @store
end

Class Method Details

.instance(options = {}) ⇒ Object

All access to the agent is thru this class method to ensure multiple Agent instances are not initialized per-Ruby process.



23
24
25
# File 'lib/scout_apm/agent.rb', line 23

def self.instance(options = {})
  @@instance ||= self.new(options)
end

Instance Method Details

#apm_enabled?Boolean

Returns:

  • (Boolean)


49
50
51
# File 'lib/scout_apm/agent.rb', line 49

def apm_enabled?
  config.value('monitor')
end

#app_server_load_hookObject

Sends a ping to APM right away, smoothes out onboarding Collects up any relevant info (framework, app server, system time, ruby version, etc)



128
129
130
# File 'lib/scout_apm/agent.rb', line 128

def app_server_load_hook
  AppServerLoad.new.run
end

#app_server_missing?(options = {}) ⇒ Boolean

Returns:

  • (Boolean)


291
292
293
# File 'lib/scout_apm/agent.rb', line 291

def app_server_missing?(options = {})
  !environment.app_server_integration(true).found? && !options[:skip_app_server_check]
end

#background_job_missing?(options = {}) ⇒ Boolean

Returns:

  • (Boolean)


295
296
297
# File 'lib/scout_apm/agent.rb', line 295

def background_job_missing?(options = {})
  environment.background_job_integration.nil? && !options[:skip_background_job_check]
end

#background_worker_running?Boolean

Returns:

  • (Boolean)


198
199
200
# File 'lib/scout_apm/agent.rb', line 198

def background_worker_running?
  !! @background_worker_thread
end

#deploy_integrationObject



273
274
275
# File 'lib/scout_apm/agent.rb', line 273

def deploy_integration
  environment.deploy_integration
end

#environmentObject



45
46
47
# File 'lib/scout_apm/agent.rb', line 45

def environment
  ScoutApm::Environment.instance
end

#exit_handler_supported?Boolean

Returns:

  • (Boolean)


132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/scout_apm/agent.rb', line 132

def exit_handler_supported?
  if environment.sinatra?
    logger.debug "Exit handler not supported for Sinatra"
    false
  elsif environment.jruby?
    logger.debug "Exit handler not supported for JRuby"
    false
  elsif environment.rubinius?
    logger.debug "Exit handler not supported for Rubinius"
    false
  else
    true
  end
end

#force?Boolean

If true, the agent will start regardless of safety checks. Currently just used for testing.

Returns:

  • (Boolean)


54
55
56
# File 'lib/scout_apm/agent.rb', line 54

def force?
  @options[:force]
end

#install_exit_handlerObject

at_exit, calls Agent#shutdown to wrapup metric reporting.



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/scout_apm/agent.rb', line 148

def install_exit_handler
  logger.debug "Shutdown handler not supported" and return unless exit_handler_supported?
  logger.debug "Installing Shutdown Handler"

  at_exit do
    logger.info "Shutting down Scout Agent"
    # MRI 1.9 bug drops exit codes.
    # http://bugs.ruby-lang.org/issues/5218
    if environment.ruby_19?
      status = $!.status if $!.is_a?(SystemExit)
      shutdown
      exit status if status
    else
      shutdown
    end
  end
end

#install_instrument(instrument_klass) ⇒ Object



265
266
267
268
269
270
271
# File 'lib/scout_apm/agent.rb', line 265

def install_instrument(instrument_klass)
  # Don't attempt to install the same instrument twice
  return if @installed_instruments.any? { |already_installed_instrument| instrument_klass === already_installed_instrument }
  instance = instrument_klass.new
  @installed_instruments << instance
  instance.install
end

#load_instrumentsObject

Loads the instrumention logic.



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/scout_apm/agent.rb', line 234

def load_instruments
  if !background_job_missing?
    case environment.background_job_name
    when :delayed_job
      install_instrument(ScoutApm::Instruments::DelayedJob)
    end
  else
    case environment.framework
    when :rails       then install_instrument(ScoutApm::Instruments::ActionControllerRails2)
    when :rails3_or_4 then
      install_instrument(ScoutApm::Instruments::ActionControllerRails3Rails4)
      install_instrument(ScoutApm::Instruments::MiddlewareSummary)
      install_instrument(ScoutApm::Instruments::RailsRouter)
    when :sinatra     then install_instrument(ScoutApm::Instruments::Sinatra)
    end
  end

  install_instrument(ScoutApm::Instruments::ActiveRecord)
  install_instrument(ScoutApm::Instruments::Moped)
  install_instrument(ScoutApm::Instruments::Mongoid)
  install_instrument(ScoutApm::Instruments::NetHttp)

  if StackProf.respond_to?(:fake?) && StackProf.fake?
    logger.info 'StackProf not found - add `gem "stackprof"` to your Gemfile to enable advanced code profiling (only for Ruby 2.1+)'
  end
rescue
  logger.warn "Exception loading instruments:"
  logger.warn $!.message
  logger.warn $!.backtrace
end

#preconditions_met?(options = {}) ⇒ Boolean

Returns:

  • (Boolean)


58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/scout_apm/agent.rb', line 58

def preconditions_met?(options={})
  if !apm_enabled?
    logger.warn "Monitoring isn't enabled for the [#{environment.env}] environment. #{force? ? 'Forcing agent to start' : 'Not starting agent'}"
    return false unless force?
  end

  if !environment.application_name
    logger.warn "An application name could not be determined. Specify the :name value in scout_apm.yml. #{force? ? 'Forcing agent to start' : 'Not starting agent'}."
    return false unless force?
  end

  if app_server_missing?(options) && background_job_missing?
    logger.warn "Couldn't find a supported app server or background job framework. #{force? ? 'Forcing agent to start' : 'Not starting agent'}."
    return false unless force?
  end

  if started?
    logger.warn "Already started agent."
    return false
  end

  if defined?(::ScoutRails)
    logger.warn "ScoutAPM is incompatible with the old Scout Rails plugin. Please remove scout_rails from your Gemfile"
    return false
  end

  true
end

#run_samplersObject

TODO: Extract a proper class / registery for these. They don’t really belong here



278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/scout_apm/agent.rb', line 278

def run_samplers
  @samplers.each do |sampler|
    begin
      result = sampler.run
      store.track_one!(sampler.metric_type, sampler.metric_name, result) if result
    rescue => e
      logger.info "Error reading #{sampler.human_name}"
      logger.debug e.message
      logger.debug e.backtrace.join("\n")
    end
  end
end

#should_load_instruments?(options = {}) ⇒ Boolean

If we want to skip the app_server_check, then we must load it.

Returns:

  • (Boolean)


228
229
230
231
# File 'lib/scout_apm/agent.rb', line 228

def should_load_instruments?(options={})
  return true if options[:skip_app_server_check]
  environment.app_server_integration.found? || !background_job_missing?
end

#shutdownObject

Called via an at_exit handler, it: (1) Stops the background worker (2) Stores metrics locally (forcing current-minute metrics to be written) It does not attempt to actually report metrics.



170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/scout_apm/agent.rb', line 170

def shutdown
  logger.info "Shutting down ScoutApm"
  return if !started?
  if @background_worker
    @background_worker.stop
    store.write_to_layaway(layaway, :force)
  end

  # Make sure we don't exit the process while the background worker is running its task.
  logger.debug "Joining background worker thread"
  @background_worker_thread.join if @background_worker_thread
end

#start(options = {}) ⇒ Object

This is called via ScoutApm::Agent.instance.start when ScoutApm is required in a Ruby application. It initializes the agent and starts the worker thread (if appropiate).



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/scout_apm/agent.rb', line 89

def start(options = {})
  @options.merge!(options)
  init_logger
  logger.info "Attempting to start Scout Agent [#{ScoutApm::VERSION}] on [#{environment.hostname}]"

  if environment.deploy_integration
    logger.info "Starting monitoring for [#{environment.deploy_integration.name}]]."
    return environment.deploy_integration.install
  end
  return false unless preconditions_met?(options)
  @started = true
  logger.info "Starting monitoring for [#{environment.application_name}]. Framework [#{environment.framework}] App Server [#{environment.app_server}] Background Job Framework [#{environment.background_job_name}]."

  load_instruments if should_load_instruments?(options)

  @samplers = [
    ScoutApm::Instruments::Process::ProcessCpu.new(environment.processors, logger),
    ScoutApm::Instruments::Process::ProcessMemory.new(logger)
  ]

  app_server_load_hook

  # start_background_worker? is true on non-forking servers, and directly
  # starts the background worker.  On forking servers, a server-specific
  # hook is inserted to start the background worker after forking.
  if start_background_worker?
    start_background_worker
    logger.info "Scout Agent [#{ScoutApm::VERSION}] Initialized"
  elsif environment.background_job_integration
    environment.background_job_integration.install
    logger.info "Scout Agent [#{ScoutApm::VERSION}] loaded in [#{environment.background_job_name}] master process. Monitoring will start after background job framework forks its workers."
  else
    environment.app_server_integration.install
    logger.info "Scout Agent [#{ScoutApm::VERSION}] loaded in [#{environment.app_server}] master process. Monitoring will start after server forks its workers."
  end
end

#start_background_workerObject

Creates the worker thread. The worker thread is a loop that runs continuously. It sleeps for Agent#period and when it wakes, processes data, either saving it to disk or reporting to Scout.



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/scout_apm/agent.rb', line 204

def start_background_worker
  if !apm_enabled?
    logger.debug "Not starting background worker as monitoring isn't enabled."
    return false
  end
  logger.info "Not starting background worker, already started" and return if background_worker_running?
  logger.info "Initializing worker thread."

  install_exit_handler

  @background_worker = ScoutApm::BackgroundWorker.new
  @background_worker_thread = Thread.new do
    @background_worker.start {
      # First, run periodic samplers. These should run once a minute,
      # rather than per-request. "CPU Load" and similar.
      run_samplers
      capacity.process

      ScoutApm::Agent.instance.process_metrics
    }
  end
end

#start_background_worker?Boolean

The worker thread will automatically start UNLESS:

  • A supported application server isn’t detected (example: running via Rails console)

  • A supported application server is detected, but it forks. In this case, the agent is started in the forked process.

Returns:

  • (Boolean)


191
192
193
194
195
196
# File 'lib/scout_apm/agent.rb', line 191

def start_background_worker?
  return true if environment.app_server == :thin
  return true if environment.app_server == :webrick
  return true if force?
  return !environment.forking?
end

#started?Boolean

Returns:

  • (Boolean)


183
184
185
# File 'lib/scout_apm/agent.rb', line 183

def started?
  @started
end