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



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/scout_apm/agent.rb', line 38

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

  @slow_request_policy = ScoutApm::SlowRequestPolicy.new
  @slow_job_policy = ScoutApm::SlowJobPolicy.new
  @request_histograms = ScoutApm::RequestHistograms.new
  @request_histograms_by_time = Hash.new { |h, k| h[k] = ScoutApm::RequestHistograms.new }

  @store          = ScoutApm::Store.new
  @layaway        = ScoutApm::Layaway.new
  @metric_lookup  = Hash.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

#request_histogramsObject (readonly)

Histogram of the cumulative requests since the start of the process



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

def request_histograms
  @request_histograms
end

#request_histograms_by_timeObject (readonly)

Histogram of the requests, distinct by reporting period (minute) { StoreReportingPeriodTimestamp => RequestHistograms }



28
29
30
# File 'lib/scout_apm/agent.rb', line 28

def request_histograms_by_time
  @request_histograms_by_time
end

#slow_job_policyObject (readonly)

Returns the value of attribute slow_job_policy.



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

def slow_job_policy
  @slow_job_policy
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.



31
32
33
# File 'lib/scout_apm/agent.rb', line 31

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

Instance Method Details

#apm_enabled?Boolean

Returns:

  • (Boolean)


60
61
62
# File 'lib/scout_apm/agent.rb', line 60

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)



141
142
143
# File 'lib/scout_apm/agent.rb', line 141

def app_server_load_hook
  AppServerLoad.new.run
end

#app_server_missing?(options = {}) ⇒ Boolean

Returns:

  • (Boolean)


301
302
303
# File 'lib/scout_apm/agent.rb', line 301

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

#background_job_missing?(options = {}) ⇒ Boolean

Returns:

  • (Boolean)


305
306
307
# File 'lib/scout_apm/agent.rb', line 305

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

#background_worker_running?Boolean

Returns:

  • (Boolean)


211
212
213
# File 'lib/scout_apm/agent.rb', line 211

def background_worker_running?
  !! @background_worker_thread
end

#clean_old_percentilesObject



236
237
238
239
240
241
# File 'lib/scout_apm/agent.rb', line 236

def clean_old_percentiles
  request_histograms_by_time.
    keys.
    select {|timestamp| timestamp.age_in_seconds > 60 * 10 }.
    each {|old_timestamp| request_histograms_by_time.delete(old_timestamp) }
end

#deploy_integrationObject



297
298
299
# File 'lib/scout_apm/agent.rb', line 297

def deploy_integration
  environment.deploy_integration
end

#environmentObject



56
57
58
# File 'lib/scout_apm/agent.rb', line 56

def environment
  ScoutApm::Environment.instance
end

#exit_handler_supported?Boolean

Returns:

  • (Boolean)


145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/scout_apm/agent.rb', line 145

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)


65
66
67
# File 'lib/scout_apm/agent.rb', line 65

def force?
  @options[:force]
end

#install_exit_handlerObject

at_exit, calls Agent#shutdown to wrapup metric reporting.



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/scout_apm/agent.rb', line 161

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



281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/scout_apm/agent.rb', line 281

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 }

  # Allow users to skip individual instruments via the config file
  instrument_short_name = instrument_klass.name.split("::").last
  if (config.value("disabled_instruments") || []).include?(instrument_short_name)
    logger.info "Skipping Disabled Instrument: #{instrument_short_name} - To re-enable, change `disabled_instruments` key in scout_apm.yml"
    return
  end

  instance = instrument_klass.new
  @installed_instruments << instance
  instance.install
end

#load_instrumentsObject

Loads the instrumention logic.



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/scout_apm/agent.rb', line 250

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)
  install_instrument(ScoutApm::Instruments::HttpClient)
  install_instrument(ScoutApm::Instruments::Redis)
  install_instrument(ScoutApm::Instruments::InfluxDB)
  install_instrument(ScoutApm::Instruments::Elasticsearch)
rescue
  logger.warn "Exception loading instruments:"
  logger.warn $!.message
  logger.warn $!.backtrace
end

#preconditions_met?(options = {}) ⇒ Boolean

Returns:

  • (Boolean)


69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/scout_apm/agent.rb', line 69

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 unless force?
  end

  true
end

#should_load_instruments?(options = {}) ⇒ Boolean

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

Returns:

  • (Boolean)


244
245
246
247
# File 'lib/scout_apm/agent.rb', line 244

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.



183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/scout_apm/agent.rb', line 183

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



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
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/scout_apm/agent.rb', line 100

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)

  [ ScoutApm::Instruments::Process::ProcessCpu.new(environment.processors, logger),
    ScoutApm::Instruments::Process::ProcessMemory.new(logger),
    ScoutApm::Instruments::PercentileSampler.new(logger, 95),
  ].each { |s| store.add_sampler(s) }

  app_server_load_hook

  if environment.background_job_integration
    environment.background_job_integration.install
    logger.info "Installed Background Job Integration [#{environment.background_job_name}]"
  end

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



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/scout_apm/agent.rb', line 217

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 {
      ScoutApm::Agent.instance.process_metrics
      clean_old_percentiles
    }
  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)


204
205
206
207
208
209
# File 'lib/scout_apm/agent.rb', line 204

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)


196
197
198
# File 'lib/scout_apm/agent.rb', line 196

def started?
  @started
end