scout_rails_proxy_proxy 1.0.5
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.
- data/.DS_Store +0 -0
- data/.gitignore +5 -0
- data/CHANGELOG.markdown +50 -0
- data/Gemfile +4 -0
- data/README.markdown +44 -0
- data/Rakefile +1 -0
- data/lib/scout_rails_proxy.rb +32 -0
- data/lib/scout_rails_proxy/agent.rb +319 -0
- data/lib/scout_rails_proxy/config.rb +34 -0
- data/lib/scout_rails_proxy/environment.rb +122 -0
- data/lib/scout_rails_proxy/instruments/active_record_instruments.rb +83 -0
- data/lib/scout_rails_proxy/instruments/net_http.rb +14 -0
- data/lib/scout_rails_proxy/instruments/process/process_cpu.rb +27 -0
- data/lib/scout_rails_proxy/instruments/process/process_memory.rb +40 -0
- data/lib/scout_rails_proxy/instruments/rails/action_controller_instruments.rb +46 -0
- data/lib/scout_rails_proxy/instruments/rails3/action_controller_instruments.rb +38 -0
- data/lib/scout_rails_proxy/instruments/sinatra_instruments.rb +33 -0
- data/lib/scout_rails_proxy/layaway.rb +76 -0
- data/lib/scout_rails_proxy/layaway_file.rb +70 -0
- data/lib/scout_rails_proxy/metric_meta.rb +34 -0
- data/lib/scout_rails_proxy/metric_stats.rb +49 -0
- data/lib/scout_rails_proxy/stack_item.rb +18 -0
- data/lib/scout_rails_proxy/store.rb +159 -0
- data/lib/scout_rails_proxy/tracer.rb +105 -0
- data/lib/scout_rails_proxy/transaction_sample.rb +10 -0
- data/lib/scout_rails_proxy/version.rb +3 -0
- data/scout_rails_proxy.gemspec +24 -0
- metadata +75 -0
data/.DS_Store
ADDED
Binary file
|
data/CHANGELOG.markdown
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# 1.0.3
|
2
|
+
|
3
|
+
* MetricMeta equality - downcase
|
4
|
+
* Suppressing "cat: /proc/cpuinfo: No such file or directory" error on distros that don't support it.
|
5
|
+
|
6
|
+
# 1.0.2
|
7
|
+
|
8
|
+
* Net::HTTP instrumentation
|
9
|
+
* ActionController::Metal instrumentation
|
10
|
+
* Determining number of processors for CPU % calculation
|
11
|
+
|
12
|
+
# 1.0.1
|
13
|
+
|
14
|
+
* Unicorn support (requires "preload_app true" in unicorn config file)
|
15
|
+
* Fix for Thin detection - ensure it's actually running
|
16
|
+
* Fixing name conflict btw Tracer#store and ActiveRecord::Store
|
17
|
+
|
18
|
+
# 1.0.0
|
19
|
+
|
20
|
+
* Release!
|
21
|
+
|
22
|
+
# 0.0.6.pre
|
23
|
+
|
24
|
+
* Rails 2 - Not collecting traces when an exception occurs
|
25
|
+
* Increased Transaction Sample Storage to 2 seconds from 1 second to decrease noise in UI
|
26
|
+
|
27
|
+
# 0.0.5
|
28
|
+
|
29
|
+
* Support for custom categories
|
30
|
+
* Not raising an exception w/an unbalanced stack
|
31
|
+
* Only allows controllers as the entry point for a transaction
|
32
|
+
|
33
|
+
# 0.0.4
|
34
|
+
|
35
|
+
* Transaction Sampling
|
36
|
+
|
37
|
+
# 0.0.3.pre
|
38
|
+
|
39
|
+
* Removed dynamic ActiveRecord caller instrumentation
|
40
|
+
* Fixed issue that prevents the app from loading if ActiveRecord isn't used.
|
41
|
+
* Using a metric hash for each request, then merging when complete. Ensures data associated w/requests that overlap a
|
42
|
+
minute boundary are correctly associated.
|
43
|
+
|
44
|
+
# 0.0.2
|
45
|
+
|
46
|
+
* Doesn't prevent app from loading if no configuration exists for the current environment.
|
47
|
+
|
48
|
+
# 0.0.1
|
49
|
+
|
50
|
+
* Boom! Initial Release.
|
data/Gemfile
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# ScoutRailsProxy
|
2
|
+
|
3
|
+
A Ruby gem for detailed Rails application performance analysis. Metrics are reported to [Scout](https://scoutapp.com), a hosted server and application monitoring service. For general server monitoring, see our [server monitoring agent](https://github.com/scoutapp/scout-client).
|
4
|
+
|
5
|
+

|
6
|
+
|
7
|
+
## Getting Started
|
8
|
+
|
9
|
+
Install the gem:
|
10
|
+
|
11
|
+
gem install scout_rails_proxy
|
12
|
+
|
13
|
+
Signup for a [Scout](https://scoutapp.com) account and copy the config file to `RAILS_ROOT/config/scout_rails_proxy.yml`.
|
14
|
+
|
15
|
+
Your config file should look like:
|
16
|
+
|
17
|
+
common: &defaults
|
18
|
+
name: YOUR_APPLICATION_NAME
|
19
|
+
key: YOUR_APPLICATION_KEY
|
20
|
+
monitor: true
|
21
|
+
|
22
|
+
production:
|
23
|
+
<<: *defaults
|
24
|
+
|
25
|
+
## Supported Frameworks
|
26
|
+
|
27
|
+
* Rails 2.2 and greater
|
28
|
+
|
29
|
+
## Supported Rubies
|
30
|
+
|
31
|
+
* Ruby 1.8.7
|
32
|
+
* Ruby 1.9.2
|
33
|
+
* Ruby 1.9.3
|
34
|
+
|
35
|
+
## Supported Application Servers
|
36
|
+
|
37
|
+
* Phusion Passenger
|
38
|
+
* Thin
|
39
|
+
* WEBrick
|
40
|
+
* Unicorn (make sure to add `preload_app true` to `config/unicorn.rb`)
|
41
|
+
|
42
|
+
## Help
|
43
|
+
|
44
|
+
See our [troubleshooting tips](https://scoutapp.com/info/support_app_monitoring) and/or email support@scoutapp.com if you need a hand.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module ScoutRailsProxy
|
2
|
+
end
|
3
|
+
require 'socket'
|
4
|
+
require 'set'
|
5
|
+
require 'net/http'
|
6
|
+
require File.expand_path('../scout_rails_proxy/version.rb', __FILE__)
|
7
|
+
require File.expand_path('../scout_rails_proxy/agent.rb', __FILE__)
|
8
|
+
require File.expand_path('../scout_rails_proxy/layaway.rb', __FILE__)
|
9
|
+
require File.expand_path('../scout_rails_proxy/layaway_file.rb', __FILE__)
|
10
|
+
require File.expand_path('../scout_rails_proxy/config.rb', __FILE__)
|
11
|
+
require File.expand_path('../scout_rails_proxy/environment.rb', __FILE__)
|
12
|
+
require File.expand_path('../scout_rails_proxy/metric_meta.rb', __FILE__)
|
13
|
+
require File.expand_path('../scout_rails_proxy/metric_stats.rb', __FILE__)
|
14
|
+
require File.expand_path('../scout_rails_proxy/stack_item.rb', __FILE__)
|
15
|
+
require File.expand_path('../scout_rails_proxy/store.rb', __FILE__)
|
16
|
+
require File.expand_path('../scout_rails_proxy/tracer.rb', __FILE__)
|
17
|
+
require File.expand_path('../scout_rails_proxy/transaction_sample.rb', __FILE__)
|
18
|
+
require File.expand_path('../scout_rails_proxy/instruments/process/process_cpu.rb', __FILE__)
|
19
|
+
require File.expand_path('../scout_rails_proxy/instruments/process/process_memory.rb', __FILE__)
|
20
|
+
|
21
|
+
if defined?(Rails) and Rails.respond_to?(:version) and Rails.version =~ /^3/
|
22
|
+
module ScoutRailsProxy
|
23
|
+
class Railtie < Rails::Railtie
|
24
|
+
initializer "scout_rails_proxy.start" do |app|
|
25
|
+
ScoutRailsProxy::Agent.instance.start
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
else
|
30
|
+
ScoutRailsProxy::Agent.instance.start
|
31
|
+
end
|
32
|
+
|
@@ -0,0 +1,319 @@
|
|
1
|
+
module ScoutRailsProxy
|
2
|
+
# The agent gathers performance data from a Ruby application. One Agent instance is created per-Ruby process.
|
3
|
+
#
|
4
|
+
# Each Agent object creates a worker thread (unless monitoring is disabled or we're forking).
|
5
|
+
# The worker thread wakes up every +Agent#period+, merges in-memory metrics w/those saved to disk,
|
6
|
+
# saves the merged data to disk, and sends it to the Scout server.
|
7
|
+
class Agent
|
8
|
+
# Headers passed up with all API requests.
|
9
|
+
HTTP_HEADERS = { "Agent-Hostname" => Socket.gethostname }
|
10
|
+
DEFAULT_HOST = 'scoutapp.com'
|
11
|
+
# see self.instance
|
12
|
+
@@instance = nil
|
13
|
+
|
14
|
+
# Accessors below are for associated classes
|
15
|
+
attr_accessor :store
|
16
|
+
attr_accessor :layaway
|
17
|
+
attr_accessor :config
|
18
|
+
attr_accessor :environment
|
19
|
+
|
20
|
+
attr_accessor :logger
|
21
|
+
attr_accessor :log_file # path to the log file
|
22
|
+
attr_accessor :options # options passed to the agent when +#start+ is called.
|
23
|
+
attr_accessor :metric_lookup # Hash used to lookup metric ids based on their name and scope
|
24
|
+
|
25
|
+
# All access to the agent is thru this class method to ensure multiple Agent instances are not initialized per-Ruby process.
|
26
|
+
def self.instance(options = {})
|
27
|
+
@@instance ||= self.new(options)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Note - this doesn't start instruments or the worker thread. This is handled via +#start+ as we don't
|
31
|
+
# want to start the worker thread or install instrumentation if (1) disabled for this environment (2) a worker thread shouldn't
|
32
|
+
# be started (when forking).
|
33
|
+
def initialize(options = {})
|
34
|
+
@started = false
|
35
|
+
@options ||= options
|
36
|
+
@store = ScoutRailsProxy::Store.new
|
37
|
+
@layaway = ScoutRailsProxy::Layaway.new
|
38
|
+
@config = ScoutRailsProxy::Config.new(options[:config_path])
|
39
|
+
@metric_lookup = Hash.new
|
40
|
+
@process_cpu=ScoutRailsProxy::Instruments::Process::ProcessCpu.new(environment.processors)
|
41
|
+
@process_memory=ScoutRailsProxy::Instruments::Process::ProcessMemory.new
|
42
|
+
end
|
43
|
+
|
44
|
+
def environment
|
45
|
+
@environment ||= ScoutRailsProxy::Environment.new
|
46
|
+
end
|
47
|
+
|
48
|
+
# This is called via +ScoutRailsProxy::Agent.instance.start+ when ScoutRailsProxy is required in a Ruby application.
|
49
|
+
# It initializes the agent and starts the worker thread (if appropiate).
|
50
|
+
def start(options = {})
|
51
|
+
@options.merge!(options)
|
52
|
+
init_logger
|
53
|
+
logger.info "Attempting to start Scout Agent [#{ScoutRailsProxy::VERSION}] on [#{Socket.gethostname}]"
|
54
|
+
if !config.settings['monitor'] and !@options[:force]
|
55
|
+
logger.warn "Monitoring isn't enabled for the [#{environment.env}] environment."
|
56
|
+
return false
|
57
|
+
elsif !environment.app_server
|
58
|
+
logger.warn "Couldn't find a supported app server. Not starting agent."
|
59
|
+
return false
|
60
|
+
elsif started?
|
61
|
+
logger.warn "Already started agent."
|
62
|
+
return false
|
63
|
+
end
|
64
|
+
@started = true
|
65
|
+
logger.info "Starting monitoring. Framework [#{environment.framework}] App Server [#{environment.app_server}]."
|
66
|
+
start_instruments
|
67
|
+
if !start_worker_thread?
|
68
|
+
logger.debug "Not starting worker thread"
|
69
|
+
install_passenger_worker_process_event if environment.app_server == :passenger
|
70
|
+
install_unicorn_worker_loop if environment.app_server == :unicorn
|
71
|
+
return
|
72
|
+
end
|
73
|
+
start_worker_thread
|
74
|
+
handle_exit
|
75
|
+
logger.info "Scout Agent [#{ScoutRailsProxy::VERSION}] Initialized"
|
76
|
+
end
|
77
|
+
|
78
|
+
# Placeholder: store metrics locally on exit so those in memory aren't lost. Need to decide
|
79
|
+
# whether we'll report these immediately or just store locally and risk having stale data.
|
80
|
+
def handle_exit
|
81
|
+
if environment.sinatra? || environment.jruby? || environment.rubinius?
|
82
|
+
logger.debug "Exit handler not supported"
|
83
|
+
else
|
84
|
+
at_exit { at_exit { logger.debug "Shutdown!" } }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def started?
|
89
|
+
@started
|
90
|
+
end
|
91
|
+
|
92
|
+
def gem_root
|
93
|
+
File.expand_path(File.join("..","..",".."), __FILE__)
|
94
|
+
end
|
95
|
+
|
96
|
+
def init_logger
|
97
|
+
@log_file = "#{log_path}/scout_rails_proxy.log"
|
98
|
+
@logger = Logger.new(@log_file)
|
99
|
+
@logger.level = Logger::DEBUG
|
100
|
+
def logger.format_message(severity, timestamp, progname, msg)
|
101
|
+
prefix = "[#{timestamp.strftime("%m/%d/%y %H:%M:%S %z")} #{Socket.gethostname} (#{$$})] #{severity} : #{msg}\n"
|
102
|
+
end
|
103
|
+
@logger
|
104
|
+
end
|
105
|
+
|
106
|
+
# The worker thread will automatically start UNLESS:
|
107
|
+
# * A supported application server isn't detected (example: running via Rails console)
|
108
|
+
# * A supported application server is detected, but it forks (Passenger). In this case,
|
109
|
+
# the agent is started in the forked process.
|
110
|
+
def start_worker_thread?
|
111
|
+
!environment.forking? or environment.app_server == :thin
|
112
|
+
end
|
113
|
+
|
114
|
+
def install_passenger_worker_process_event
|
115
|
+
PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
116
|
+
logger.debug "Passenger is starting a worker process. Starting worker thread."
|
117
|
+
self.class.instance.start_worker_thread
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def install_unicorn_worker_loop
|
122
|
+
logger.debug "Installing Unicorn worker loop."
|
123
|
+
Unicorn::HttpServer.class_eval do
|
124
|
+
old = instance_method(:worker_loop)
|
125
|
+
define_method(:worker_loop) do |worker|
|
126
|
+
ScoutRailsProxy::Agent.instance.start_worker_thread
|
127
|
+
old.bind(self).call(worker)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def log_path
|
133
|
+
"#{environment.root}/log"
|
134
|
+
end
|
135
|
+
|
136
|
+
# in seconds, time between when the worker thread wakes up and runs.
|
137
|
+
def period
|
138
|
+
60
|
139
|
+
end
|
140
|
+
|
141
|
+
# Creates the worker thread. The worker thread is a loop that runs continuously. It sleeps for +Agent#period+ and when it wakes,
|
142
|
+
# processes data, either saving it to disk or reporting to Scout.
|
143
|
+
def start_worker_thread
|
144
|
+
logger.debug "Creating worker thread."
|
145
|
+
@worker_thread = Thread.new do
|
146
|
+
begin
|
147
|
+
logger.debug "Starting worker thread, running every #{period} seconds"
|
148
|
+
next_time = Time.now + period
|
149
|
+
while true do
|
150
|
+
now = Time.now
|
151
|
+
while now < next_time
|
152
|
+
sleep_time = next_time - now
|
153
|
+
sleep(sleep_time) if sleep_time > 0
|
154
|
+
now = Time.now
|
155
|
+
end
|
156
|
+
process_metrics
|
157
|
+
while next_time <= now
|
158
|
+
next_time += period
|
159
|
+
end
|
160
|
+
end
|
161
|
+
rescue
|
162
|
+
logger.debug "Worker Thread Exception!!!!!!!"
|
163
|
+
logger.debug $!.message
|
164
|
+
logger.debug $!.backtrace
|
165
|
+
end
|
166
|
+
end # thread new
|
167
|
+
logger.debug "Done creating worker thread."
|
168
|
+
end
|
169
|
+
|
170
|
+
# Writes each payload to a file for auditing.
|
171
|
+
def write_to_file(object)
|
172
|
+
logger.debug "Writing to file"
|
173
|
+
full_path = Pathname.new(RAILS_ROOT+'/log/audit/scout')
|
174
|
+
( full_path +
|
175
|
+
"#{Time.now.strftime('%Y-%m-%d_%H:%M:%S')}.json" ).open("w") do |f|
|
176
|
+
f.puts object.to_json
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Before reporting, lookup metric_id for each MetricMeta. This speeds up
|
181
|
+
# reporting on the server-side.
|
182
|
+
def add_metric_ids(metrics)
|
183
|
+
metrics.each do |meta,stats|
|
184
|
+
if metric_id = metric_lookup[meta]
|
185
|
+
meta.metric_id = metric_id
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Called from #process_metrics, which is run via the worker thread.
|
191
|
+
def run_samplers
|
192
|
+
begin
|
193
|
+
cpu_util=@process_cpu.run # returns a hash
|
194
|
+
logger.debug "Process CPU: #{cpu_util.inspect} [#{environment.processors} CPU(s)]"
|
195
|
+
store.track!("CPU/Utilization",cpu_util) if cpu_util
|
196
|
+
rescue => e
|
197
|
+
logger.info "Error reading ProcessCpu"
|
198
|
+
logger.debug e.message
|
199
|
+
logger.debug e.backtrace.join("\n")
|
200
|
+
end
|
201
|
+
|
202
|
+
begin
|
203
|
+
mem_usage=@process_memory.run # returns a single number, in MB
|
204
|
+
logger.debug "Process Memory: #{mem_usage}MB"
|
205
|
+
store.track!("Memory/Physical",mem_usage) if mem_usage
|
206
|
+
rescue => e
|
207
|
+
logger.info "Error reading ProcessMemory"
|
208
|
+
logger.debug e.message
|
209
|
+
logger.debug e.backtrace.join("\n")
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Called in the worker thread. Merges in-memory metrics w/those on disk and reports metrics
|
214
|
+
# to the server.
|
215
|
+
def process_metrics
|
216
|
+
logger.debug "Processing metrics"
|
217
|
+
run_samplers
|
218
|
+
metrics = layaway.deposit_and_deliver
|
219
|
+
if metrics.any?
|
220
|
+
add_metric_ids(metrics)
|
221
|
+
# for debugging, count the total number of requests
|
222
|
+
controller_count = 0
|
223
|
+
metrics.each do |meta,stats|
|
224
|
+
if meta.metric_name =~ /\AController/
|
225
|
+
controller_count += stats.call_count
|
226
|
+
end
|
227
|
+
end
|
228
|
+
logger.debug "#{config.settings['name']} Delivering metrics for #{controller_count} requests."
|
229
|
+
response = post( checkin_uri,
|
230
|
+
Marshal.dump(:metrics => metrics, :sample => store.sample),
|
231
|
+
"Content-Type" => "application/json" )
|
232
|
+
if response and response.is_a?(Net::HTTPSuccess)
|
233
|
+
directives = Marshal.load(response.body)
|
234
|
+
self.metric_lookup.merge!(directives[:metric_lookup])
|
235
|
+
store.transaction_sample_lock.synchronize do
|
236
|
+
store.sample = nil
|
237
|
+
end
|
238
|
+
logger.debug "Metric Cache Size: #{metric_lookup.size}"
|
239
|
+
end
|
240
|
+
end
|
241
|
+
rescue
|
242
|
+
logger.info "Error on checkin to #{checkin_uri.to_s}"
|
243
|
+
logger.info $!.message
|
244
|
+
logger.debug $!.backtrace
|
245
|
+
end
|
246
|
+
|
247
|
+
def checkin_uri
|
248
|
+
URI.parse("http://#{config.settings['host'] || DEFAULT_HOST}/app/#{config.settings['key']}/checkin.scout?name=#{CGI.escape(config.settings['name'])}")
|
249
|
+
end
|
250
|
+
|
251
|
+
def post(url, body, headers = Hash.new)
|
252
|
+
response = nil
|
253
|
+
request(url) do |connection|
|
254
|
+
post = Net::HTTP::Post.new( url.path +
|
255
|
+
(url.query ? ('?' + url.query) : ''),
|
256
|
+
HTTP_HEADERS.merge(headers) )
|
257
|
+
post.body = body
|
258
|
+
response=connection.request(post)
|
259
|
+
end
|
260
|
+
response
|
261
|
+
end
|
262
|
+
|
263
|
+
def request(url, &connector)
|
264
|
+
response = nil
|
265
|
+
|
266
|
+
http_class = if config.settings['proxy']
|
267
|
+
Net::HTTP::Proxy(
|
268
|
+
config.settings['proxy']['host'],
|
269
|
+
config.settings['proxy']['port'],
|
270
|
+
config.settings['proxy']['user'],
|
271
|
+
config.settings['proxy']['password']
|
272
|
+
)
|
273
|
+
else
|
274
|
+
Net::HTTP
|
275
|
+
end
|
276
|
+
|
277
|
+
http = http_class.new url.host, url.port
|
278
|
+
response = http.start(&connector)
|
279
|
+
logger.debug "got response: #{response.inspect}"
|
280
|
+
case response
|
281
|
+
when Net::HTTPSuccess, Net::HTTPNotModified
|
282
|
+
logger.debug "/checkin OK"
|
283
|
+
when Net::HTTPBadRequest
|
284
|
+
logger.warn "/checkin FAILED: The Account Key [#{config.settings['key']}] is invalid."
|
285
|
+
else
|
286
|
+
logger.debug "/checkin FAILED: #{response.inspect}"
|
287
|
+
end
|
288
|
+
rescue Exception
|
289
|
+
logger.debug "Exception sending request to server: #{$!.message}"
|
290
|
+
ensure
|
291
|
+
response
|
292
|
+
end
|
293
|
+
|
294
|
+
# Loads the instrumention logic.
|
295
|
+
def load_instruments
|
296
|
+
case environment.framework
|
297
|
+
when :rails
|
298
|
+
require File.expand_path(File.join(File.dirname(__FILE__),'instruments/rails/action_controller_instruments.rb'))
|
299
|
+
when :rails3
|
300
|
+
require File.expand_path(File.join(File.dirname(__FILE__),'instruments/rails3/action_controller_instruments.rb'))
|
301
|
+
when :sinatra
|
302
|
+
require File.expand_path(File.join(File.dirname(__FILE__),'instruments/sinatra_instruments.rb'))
|
303
|
+
end
|
304
|
+
require File.expand_path(File.join(File.dirname(__FILE__),'instruments/active_record_instruments.rb'))
|
305
|
+
require File.expand_path(File.join(File.dirname(__FILE__),'instruments/net_http.rb'))
|
306
|
+
rescue
|
307
|
+
logger.warn "Exception loading instruments:"
|
308
|
+
logger.warn $!.message
|
309
|
+
logger.warn $!.backtrace
|
310
|
+
end
|
311
|
+
|
312
|
+
# Injects instruments into the Ruby application.
|
313
|
+
def start_instruments
|
314
|
+
logger.debug "Installing instrumentation"
|
315
|
+
load_instruments
|
316
|
+
end
|
317
|
+
|
318
|
+
end # class Agent
|
319
|
+
end # module ScoutRailsProxy
|