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

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



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/scout_apm/agent.rb', line 40

def initialize(options = {})
  @started = false
  @process_start_time = Time.now
  @options ||= options

  # Start up without attempting to load a configuration file. We need to be
  # able to lookup configuration options like "application_root" which would
  # then in turn influence where the configuration file came from.
  #
  # Later in initialization, we reset @config to include the file.
  @config = ScoutApm::Config.without_file

  @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(config, environment)
  @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

#ignored_urisObject (readonly)

Returns the value of attribute ignored_uris.



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

def ignored_uris
  @ignored_uris
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

#process_start_timeObject (readonly)

used when creating slow transactions to report how far from startup the transaction was recorded.



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

def process_start_time
  @process_start_time
end

#request_histogramsObject (readonly)

Histogram of the cumulative requests since the start of the process



26
27
28
# File 'lib/scout_apm/agent.rb', line 26

def request_histograms
  @request_histograms
end

#request_histograms_by_timeObject (readonly)

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



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

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.



33
34
35
# File 'lib/scout_apm/agent.rb', line 33

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

Instance Method Details

#apm_enabled?Boolean

Returns:

  • (Boolean)


69
70
71
# File 'lib/scout_apm/agent.rb', line 69

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)



162
163
164
# File 'lib/scout_apm/agent.rb', line 162

def app_server_load_hook
  AppServerLoad.new.run
end

#app_server_missing?(options = {}) ⇒ Boolean

Returns:

  • (Boolean)


331
332
333
# File 'lib/scout_apm/agent.rb', line 331

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

#background_job_missing?(options = {}) ⇒ Boolean

Returns:

  • (Boolean)


335
336
337
# File 'lib/scout_apm/agent.rb', line 335

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

#background_worker_running?Boolean

Returns:

  • (Boolean)


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

def background_worker_running?
  !! @background_worker_thread
end

#clean_old_percentilesObject



263
264
265
266
267
268
# File 'lib/scout_apm/agent.rb', line 263

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



327
328
329
# File 'lib/scout_apm/agent.rb', line 327

def deploy_integration
  environment.deploy_integration
end

#environmentObject



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

def environment
  ScoutApm::Environment.instance
end

#exit_handler_supported?Boolean

Returns:

  • (Boolean)


166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/scout_apm/agent.rb', line 166

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)


74
75
76
# File 'lib/scout_apm/agent.rb', line 74

def force?
  @options[:force]
end

#install_exit_handlerObject

at_exit, calls Agent#shutdown to wrapup metric reporting.



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

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



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/scout_apm/agent.rb', line 311

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.



279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/scout_apm/agent.rb', line 279

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

#preconditions_met?(options = {}) ⇒ Boolean

Returns:

  • (Boolean)


78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/scout_apm/agent.rb', line 78

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?
    if force?
      logger.warn "Agent starting (forced)"
    else
      logger.warn "Deferring agent start. Standing by for first request"
    end
    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)


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

def should_load_instruments?(options={})
  return true if options[:skip_app_server_check]
  return true if config.value('dev_trace')
  return false if !apm_enabled?
  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.



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

def shutdown
  logger.info "Shutting down ScoutApm"

  return if !started?

  return if @shutdown
  @shutdown = true

  if @background_worker
    logger.info("Stopping background worker")
    @background_worker.stop
    store.write_to_layaway(layaway, :force)
    if @background_worker_thread.alive?
      @background_worker_thread.wakeup
      @background_worker_thread.join
    end
  end
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).



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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/scout_apm/agent.rb', line 113

def start(options = {})
  @options.merge!(options)

  @config = ScoutApm::Config.with_file(@config.value("config_file"))
  layaway.config = config

  init_logger
  logger.info "Attempting to start Scout Agent [#{ScoutApm::VERSION}] on [#{environment.hostname}]"

  @ignored_uris = ScoutApm::IgnoredUris.new(config.value('ignore'))

  if environment.deploy_integration
    logger.info "Starting monitoring for [#{environment.deploy_integration.name}]]."
    return environment.deploy_integration.install
  end

  load_instruments if should_load_instruments?(options)

  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}]."


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



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/scout_apm/agent.rb', line 244

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)


231
232
233
234
235
236
# File 'lib/scout_apm/agent.rb', line 231

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)


223
224
225
# File 'lib/scout_apm/agent.rb', line 223

def started?
  @started
end