scout 5.1.5 → 5.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,9 @@
1
+ == 5.2.1
2
+
3
+ * Added private-key based code signing
4
+ * Added local plugin overrides
5
+ * Added local ad-hoc plugins
6
+
1
7
  == 5.1.5
2
8
 
3
9
  * Added sleep interval directive. Agent will only sleep when used in non-interactive mode.
@@ -0,0 +1,9 @@
1
+ -----BEGIN PUBLIC KEY-----
2
+ MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKCAQEA7y0b7SAkaqkKvXSQiN6o
3
+ u1/T56dpx8h74p61Gn4i1dwtogqsa/qSf+K7waFMu7XUeMx3z5ZabLu5ZZLONUFA
4
+ SbV4dN3dtz1ECSMuOleXsM+0HzyV8FqLZ1h6XZ65gUhbmTKRUcIfgse0lLWoVKxX
5
+ gtcaLdv/5nrZwZH9g5asGuzqyVx4JwopR/aedFsXSBZyo+FyLQoINrNL2lVllefR
6
+ 3EsVDLyZ0etL+K1MtVu1geS41dw4KBCAZQL6eO/88+A9wlJxCcWjXiBK6Gz55ytP
7
+ 1w1QrJ25MGVTrE89KGX0wQ9RJ5SLhuFcvRDhNO2T+4H4VZdEjIsIH68YUDNe5FnR
8
+ cwIBIw==
9
+ -----END PUBLIC KEY-----
data/lib/scout.rb CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env ruby -wKU
2
2
 
3
3
  module Scout
4
- VERSION = "5.1.5".freeze
4
+ VERSION = "5.2.1".freeze
5
5
  end
6
6
 
7
7
  require "scout/command"
8
8
  require "scout/plugin"
9
9
  require "scout/plugin_options"
10
+ require "scout/scout_logger"
10
11
  require "scout/server"
data/lib/scout/command.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env ruby -wKU
2
2
 
3
3
  require "optparse"
4
- require "logger"
5
4
  require "fileutils"
6
5
 
7
6
  module Scout
@@ -37,6 +36,9 @@ module Scout
37
36
  opts.separator " #{program_name}"
38
37
  opts.separator " ... OR ..."
39
38
  opts.separator " #{program_name} [OPTIONS] install"
39
+ opts.separator " Troubleshoot:"
40
+ opts.separator " #{program_name} troubleshoot"
41
+ opts.separator " ... print Scout environment info. Recommend directing output to a file."
40
42
  opts.separator " Local plugin testing:"
41
43
  opts.separator " #{program_name} [OPTIONS] test " +
42
44
  "PATH_TO_PLUGIN [PLUGIN_OPTIONS]"
@@ -136,17 +138,18 @@ module Scout
136
138
  @force = options[:force] || false
137
139
 
138
140
  @args = args
139
- end
140
-
141
- attr_reader :server, :history
142
141
 
143
- def config_dir
144
- return @config_dir if defined? @config_dir
142
+ # create config dir if necessary
145
143
  @config_dir = File.dirname(history)
146
144
  FileUtils.mkdir_p(@config_dir) # ensure dir exists
147
- @config_dir
145
+
146
+ @log_path = File.join(@config_dir, "latest_run.log")
147
+
148
148
  end
149
149
 
150
+ attr_reader :server, :history, :config_dir, :log_path
151
+
152
+
150
153
  def verbose?
151
154
  @verbose
152
155
  end
@@ -154,12 +157,15 @@ module Scout
154
157
  def log
155
158
  return @log if defined? @log
156
159
  @log = if verbose?
157
- log = Logger.new($stdout)
160
+ log = ScoutLogger.new($stdout)
158
161
  log.datetime_format = "%Y-%m-%d %H:%M:%S "
159
162
  log.level = level
160
163
  log
161
164
  else
162
- nil
165
+ log = ScoutLogger.new(nil)
166
+ log.datetime_format = "%Y-%m-%d %H:%M:%S "
167
+ log.level = Logger::DEBUG
168
+ log
163
169
  end
164
170
  end
165
171
 
@@ -30,8 +30,25 @@ module Scout
30
30
  create_pid_file_or_exit
31
31
  @scout.run_plugins_by_plan
32
32
  @scout.save_history
33
+
34
+ begin
35
+ # Since this is a new checkin, overwrite the existing log
36
+ File.open(log_path, "w") do|log_file|
37
+ log_file.puts log.messages # log.messages is an array of every message logged during this run
38
+ end
39
+ rescue
40
+ log.info "Could not write to #{log_path}."
41
+ end
33
42
  else
34
43
  log.info "Not time to checkin yet. Next checkin in #{@scout.next_checkin}. Override by passing --force to the scout command" if log
44
+ begin
45
+ # Since this a ping, append to the existing log
46
+ File.open(log_path, "a") do|log_file|
47
+ log_file.puts log.messages
48
+ end
49
+ rescue
50
+ log.info "Could not write to #{log_path}."
51
+ end
35
52
  end
36
53
  end
37
54
  end
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby -wKU
2
+
3
+ require "pp"
4
+
5
+ module Scout
6
+ class Command
7
+ class Troubleshoot < Command
8
+ def initialize(options, args)
9
+ @contents=[]
10
+ super
11
+ end
12
+
13
+ def run
14
+ puts "Gathering troubleshooting information about your Scout install ... "
15
+
16
+ heading "Scout Info"
17
+ bullet "History file", history
18
+ bullet "Version", Scout::VERSION
19
+
20
+ heading "Latest Log"
21
+ contents=File.read(log_path) rescue "Log not found at #{log_path}"
22
+ text contents
23
+
24
+ heading "Rubygems Environment"
25
+ text `gem env`
26
+
27
+ heading "Ruby info"
28
+ bullet "Path to executable", `which ruby`
29
+ bullet "Version", `ruby -v`
30
+ bullet "Ruby's internal path", $:.join(', ')
31
+
32
+ heading "Installed Gems"
33
+ text `gem list --local`
34
+
35
+ heading "History file Contents"
36
+ contents=File.read(history) rescue "History not found at #{log_path}"
37
+ text contents
38
+
39
+ heading "Agent directory Contents"
40
+ text `ls -la #{config_dir}`
41
+
42
+ heading ""
43
+
44
+ puts "Done"
45
+
46
+ puts @contents.join("\n")
47
+
48
+ end
49
+ end
50
+
51
+ private
52
+ def heading(s)
53
+ @contents += ["",s,"**************************************************************************************************",""]
54
+ end
55
+
56
+ def bullet(label,s)
57
+ @contents << " - #{label} : #{s.chomp}"
58
+ end
59
+
60
+ def text(s)
61
+ @contents << s
62
+ end
63
+
64
+ end
65
+ end
data/lib/scout/plugin.rb CHANGED
@@ -59,8 +59,7 @@ module Scout
59
59
  end
60
60
 
61
61
  def option(name)
62
- @options[name] ||
63
- @options[name.is_a?(String) ? name.to_sym : String(name)]
62
+ @options[name] || @options[name.is_a?(String) ? name.to_sym : String(name)]
64
63
  end
65
64
 
66
65
  # Builds the data to send to the server.
@@ -0,0 +1,19 @@
1
+ # We use this subclass of Logger in the Scout Agent so we can retrieve all the logged messages at the end of the run.
2
+ # This works well only because the Agent is not a long-running process.
3
+
4
+ require 'logger'
5
+
6
+ class ScoutLogger < Logger
7
+ attr_reader :messages
8
+
9
+ def initialize(*args)
10
+ @messages=[]
11
+ super
12
+ end
13
+
14
+ # this is the method that ultimately gets called whenever you invoke info, debug, etc.
15
+ def add(severity, message=nil, progname = nil, &block)
16
+ @messages << "[#{Time.now.strftime('%Y-%m-%d %H:%M:%S ')} ##{$$}] : #{progname}"
17
+ super
18
+ end
19
+ end
data/lib/scout/server.rb CHANGED
@@ -7,6 +7,7 @@ require "timeout"
7
7
  require "stringio"
8
8
  require "zlib"
9
9
  require "socket"
10
+ require "base64"
10
11
 
11
12
  $LOAD_PATH << File.join(File.dirname(__FILE__), *%w[.. .. vendor json_pure lib])
12
13
  require "json"
@@ -37,6 +38,7 @@ module Scout
37
38
 
38
39
  attr_reader :new_plan
39
40
  attr_reader :directives
41
+ attr_reader :plugin_config
40
42
 
41
43
  # Creates a new Scout Server connection.
42
44
  def initialize(server, client_key, history_file, logger = nil)
@@ -48,6 +50,9 @@ module Scout
48
50
  @plugin_plan = []
49
51
  @directives = {} # take_snapshots, interval, sleep_interval
50
52
  @new_plan = false
53
+ @local_plugin_path = File.dirname(history_file) # just put overrides and ad-hoc plugins in same directory as history file.
54
+ @plugin_config_path = File.join(@local_plugin_path, "plugins.properties")
55
+ @plugin_config = load_plugin_configs(@plugin_config_path)
51
56
 
52
57
  # the block is only passed for install and test, since we split plan retrieval outside the lockfile for run
53
58
  if block_given?
@@ -62,12 +67,13 @@ module Scout
62
67
  # to execute, along with all options.
63
68
  #
64
69
  # This method has a couple of side effects:
65
- # 1) it sets the @plugin plan with either A) whatever is in history, B) the results of the /plan retrieval
70
+ # 1) it sets the @plugin_plan with either A) whatever is in history, B) the results of the /plan retrieval
66
71
  # 2) it sets @checkin_to = true IF so directed by the scout server
67
72
  def fetch_plan
68
73
  url = urlify(:plan)
69
74
  info "Pinging server at #{url}..."
70
75
  headers = {"x-scout-tty" => ($stdin.tty? ? 'true' : 'false')}
76
+
71
77
  if @history["plan_last_modified"] and @history["old_plugins"]
72
78
  headers["If-Modified-Since"] = @history["plan_last_modified"]
73
79
  end
@@ -75,7 +81,10 @@ module Scout
75
81
  if res.is_a? Net::HTTPNotModified
76
82
  info "Plan not modified. Will reuse saved plan."
77
83
  @plugin_plan = Array(@history["old_plugins"])
84
+ # Add local plugins to the plan. Note that local plugins are NOT saved to history file
85
+ @plugin_plan += get_local_plugins
78
86
  @directives = @history["directives"] || Hash.new
87
+
79
88
  else
80
89
  info "plan has been modified. Will run the new plan now."
81
90
  begin
@@ -85,26 +94,80 @@ module Scout
85
94
  end
86
95
 
87
96
  body_as_hash = JSON.parse(body)
88
- @plugin_plan = Array(body_as_hash["plugins"])
89
- @directives = body_as_hash["directives"].is_a?(Hash) ? body_as_hash["directives"] : Hash.new
90
97
 
91
- @history["plan_last_modified"] = res["last-modified"]
92
- @history["old_plugins"] = @plugin_plan
93
- @history["directives"] = @directives
98
+ # Ensure all the plugins in the new plan are properly signed. Load the public key for this.
99
+ public_key_text = File.read(File.join( File.dirname(__FILE__), *%w[.. .. data code_id_rsa.pub] ))
100
+ debug "Loaded public key used for verifying code signatures (#{public_key_text.size} bytes)"
101
+ code_public_key = OpenSSL::PKey::RSA.new(public_key_text)
94
102
 
95
- info "Plan loaded. (#{@plugin_plan.size} plugins: " +
96
- "#{@plugin_plan.map { |p| p['name'] }.join(', ')})" +
97
- ". Directives: #{@directives.to_a.map{|a| "#{a.first}:#{a.last}"}.join(", ")}"
103
+ temp_plugins=Array(body_as_hash["plugins"])
104
+ plugin_signature_error = false
105
+ temp_plugins.each do |plugin|
106
+ signature=plugin['signature']
107
+ id_and_name = "#{plugin['id']}-#{plugin['name']}".sub(/\A-/, "")
108
+ if signature
109
+ code=plugin['code'].gsub(/ +$/,'') # we strip trailing whitespace before calculating signatures. Same here.
110
+ decoded_signature=Base64.decode64(signature)
111
+ if !code_public_key.verify(OpenSSL::Digest::SHA1.new, decoded_signature, code)
112
+ info "#{id_and_name} signature doesn't match!"
113
+ plugin_signature_error=true
114
+ end
115
+ else
116
+ info "#{id_and_name} has no signature!"
117
+ plugin_signature_error=true
118
+ end
119
+ end
98
120
 
99
- @new_plan = true # used in determination if we should checkin this time or not
100
- rescue Exception
101
- fatal "Plan from server was malformed."
121
+
122
+ if(!plugin_signature_error)
123
+ @plugin_plan = temp_plugins
124
+ @directives = body_as_hash["directives"].is_a?(Hash) ? body_as_hash["directives"] : Hash.new
125
+ @history["plan_last_modified"] = res["last-modified"]
126
+ @history["old_plugins"] = @plugin_plan.clone # important that the plan is cloned -- we're going to add local plugins, and they shouldn't go into history
127
+ @history["directives"] = @directives
128
+
129
+ info "Plan loaded. (#{@plugin_plan.size} plugins: " +
130
+ "#{@plugin_plan.map { |p| p['name'] }.join(', ')})" +
131
+ ". Directives: #{@directives.to_a.map{|a| "#{a.first}:#{a.last}"}.join(", ")}"
132
+
133
+ @new_plan = true # used in determination if we should checkin this time or not
134
+ else
135
+ info "There was a problem with plugin signatures. Reusing old plan."
136
+ @plugin_plan = Array(@history["old_plugins"])
137
+ @directives = @history["directives"] || Hash.new
138
+ end
139
+
140
+ # Add local plugins to the plan. Note that local plugins are NOT saved to history file
141
+ @plugin_plan += get_local_plugins
142
+ rescue Exception =>e
143
+ fatal "Plan from server was malformed: #{e.message} - #{e.backtrace}"
102
144
  exit
103
145
  end
104
146
  end
105
147
  end
106
148
  end
107
-
149
+
150
+ # returns an array of hashes representing local plugins found on the filesystem
151
+ # The glob pattern requires that filenames begin with a letter,
152
+ # which excludes plugin overrides (like 12345.rb)
153
+ def get_local_plugins
154
+ local_plugin_paths=Dir.glob(File.join(@local_plugin_path,"[a-zA-Z]*.rb"))
155
+ local_plugin_paths.map do |plugin_path|
156
+ begin
157
+ {
158
+ 'name' => File.basename(plugin_path),
159
+ 'local_filename' => File.basename(plugin_path),
160
+ 'origin' => 'LOCAL',
161
+ 'code' => File.read(plugin_path),
162
+ 'interval' => 0
163
+ }
164
+ rescue => e
165
+ info "Error trying to read local plugin: #{plugin_path} -- #{e.backtrace.join('\n')}"
166
+ nil
167
+ end
168
+ end.compact
169
+ end
170
+
108
171
  # To distribute pings across a longer timeframe, the agent will sleep for a given
109
172
  # amount of time. When using the --force option the sleep_interval is ignored.
110
173
  def sleep_interval
@@ -153,11 +216,12 @@ module Scout
153
216
  process_plugin(plugin)
154
217
  rescue Exception
155
218
  @checkin[:errors] << build_report(
156
- plugin['id'],
219
+ plugin,
157
220
  :subject => "Exception: #{$!.message}.",
158
221
  :body => $!.backtrace
159
222
  )
160
223
  error("Encountered an error: #{$!.message}")
224
+ puts $!.backtrace.join('\n')
161
225
  end
162
226
  end
163
227
  take_snapshot if @directives['take_snapshots']
@@ -172,10 +236,12 @@ module Scout
172
236
  # It then loads the plugin and runs it with a PLUGIN_TIMEOUT time limit.
173
237
  # The plugin generates data, alerts, and errors. In addition, it will
174
238
  # set memory and last_run information in the history file.
175
- #
239
+ #
240
+ # The plugin argument is a hash with keys: id, name, code, timeout, options, signature.
176
241
  def process_plugin(plugin)
177
242
  info "Processing the '#{plugin['name']}' plugin:"
178
243
  id_and_name = "#{plugin['id']}-#{plugin['name']}".sub(/\A-/, "")
244
+ plugin_id = plugin['id']
179
245
  last_run = @history["last_runs"][id_and_name] ||
180
246
  @history["last_runs"][plugin['name']]
181
247
  memory = @history["memory"][id_and_name] ||
@@ -186,21 +252,47 @@ module Scout
186
252
  if last_run.nil? or delta.between?(-RUN_DELTA, 0) or delta >= 0
187
253
  debug "Plugin is past interval and needs to be run. " +
188
254
  "(last run: #{last_run || 'nil'})"
255
+ code_to_run = plugin['code']
256
+ if plugin_id && plugin_id != ""
257
+ override_path=File.join(@local_plugin_path, "#{plugin_id}.rb")
258
+ # debug "Checking for local plugin override file at #{override_path}"
259
+ if File.exist?(override_path)
260
+ code_to_run = File.read(override_path)
261
+ debug "Override file found - Using #{code_to_run.size} chars of code in #{override_path} for plugin id=#{plugin_id}"
262
+ plugin['origin'] = "OVERRIDE"
263
+ else
264
+ plugin['origin'] = nil
265
+ end
266
+ end
189
267
  debug "Compiling plugin..."
190
268
  begin
191
- eval( plugin['code'],
269
+ eval( code_to_run,
192
270
  TOPLEVEL_BINDING,
193
271
  plugin['path'] || plugin['name'] )
194
272
  info "Plugin compiled."
195
273
  rescue Exception
196
274
  raise if $!.is_a? SystemExit
197
275
  error "Plugin would not compile: #{$!.message}"
198
- @checkin[:errors] << build_report(plugin['id'],:subject => "Plugin would not compile", :body=>"#{$!.message}\n\n#{$!.backtrace}")
276
+ @checkin[:errors] << build_report(plugin,:subject => "Plugin would not compile", :body=>"#{$!.message}\n\n#{$!.backtrace}")
199
277
  return
200
278
  end
279
+
280
+ # Lookup any local options in plugin_config.properies as needed
281
+ options=(plugin['options'] || Hash.new)
282
+ options.each_pair do |k,v|
283
+ if v=~/^lookup:(.+)$/
284
+ lookup_key = $1.strip
285
+ if plugin_config[lookup_key]
286
+ options[k]=plugin_config[lookup_key]
287
+ else
288
+ info "Plugin #{id_and_name}: option #{k} appears to be a lookup, but we can't find #{lookup_key} in #{@plugin_config_path}"
289
+ end
290
+ end
291
+ end
292
+
293
+
201
294
  debug "Loading plugin..."
202
- if job = Plugin.last_defined.load( last_run, (memory || Hash.new),
203
- plugin['options'] || Hash.new )
295
+ if job = Plugin.last_defined.load( last_run, (memory || Hash.new), options)
204
296
  info "Plugin loaded."
205
297
  debug "Running plugin..."
206
298
  begin
@@ -212,7 +304,7 @@ module Scout
212
304
  end
213
305
  rescue Timeout::Error, PluginTimeoutError
214
306
  error "Plugin took too long to run."
215
- @checkin[:errors] << build_report(plugin['id'],
307
+ @checkin[:errors] << build_report(plugin,
216
308
  :subject => "Plugin took too long to run",
217
309
  :body=>"Execution timed out.")
218
310
  return
@@ -220,7 +312,7 @@ module Scout
220
312
  raise if $!.is_a? SystemExit
221
313
  error "Plugin failed to run: #{$!.class}: #{$!.message}\n" +
222
314
  "#{$!.backtrace.join("\n")}"
223
- @checkin[:errors] << build_report(plugin['id'],
315
+ @checkin[:errors] << build_report(plugin,
224
316
  :subject => "Plugin failed to run",
225
317
  :body=>"#{$!.class}: #{$!.message}\n#{$!.backtrace.join("\n")}")
226
318
  end
@@ -234,7 +326,7 @@ module Scout
234
326
  reports << report
235
327
  end
236
328
  reports.each do |fields|
237
- @checkin[plural] << build_report(plugin['id'], fields)
329
+ @checkin[plural] << build_report(plugin, fields)
238
330
  end
239
331
  end
240
332
 
@@ -244,7 +336,7 @@ module Scout
244
336
  @history["memory"][id_and_name] = data[:memory]
245
337
  else
246
338
  @checkin[:errors] << build_report(
247
- plugin['id'],
339
+ plugin,
248
340
  :subject => "Plugin would not load."
249
341
  )
250
342
  end
@@ -285,7 +377,8 @@ module Scout
285
377
  :alerts => Array.new,
286
378
  :errors => Array.new,
287
379
  :summaries => Array.new,
288
- :snapshot => '' }
380
+ :snapshot => '',
381
+ :config_path => File.expand_path(File.dirname(@history_file))}
289
382
  end
290
383
 
291
384
  def show_checkin(printer = :p)
@@ -323,10 +416,13 @@ module Scout
323
416
 
324
417
  private
325
418
 
326
- def build_report(plugin_id, fields)
327
- { :plugin_id => plugin_id,
419
+ def build_report(plugin_hash, fields)
420
+ { :plugin_id => plugin_hash['id'],
328
421
  :created_at => Time.now.utc.strftime("%Y-%m-%d %H:%M:%S"),
329
- :fields => fields }
422
+ :fields => fields,
423
+ :local_filename => plugin_hash['local_filename'], # this will be nil unless it's an ad-hoc plugin
424
+ :origin => plugin_hash['origin'] # [LOCAL|OVERRIDE|nil]
425
+ }
330
426
  end
331
427
 
332
428
  def urlify(url_name, options = Hash.new)
@@ -422,5 +518,31 @@ module Scout
422
518
  super
423
519
  end
424
520
  end
521
+
522
+ private
523
+
524
+ # Called during initialization; loads the plugin_configs (local plugin configurations for passwords, etc)
525
+ # if the file is there. Returns a hash like {"db.username"=>"secr3t"}
526
+ def load_plugin_configs(path)
527
+ temp_configs={}
528
+ if File.exist?(path)
529
+ debug "Loading Plugin Configs at #{path}"
530
+ begin
531
+ File.open(path,"r").read.each_line do |line|
532
+ line.strip!
533
+ next if line[0] == '#'
534
+ next unless line.include? "="
535
+ k,v =line.split('=')
536
+ temp_configs[k]=v
537
+ end
538
+ debug("#{temp_configs.size} plugin config(s) loaded.")
539
+ rescue
540
+ info "Error loading Plugin Configs at #{path}: #{$!}"
541
+ end
542
+ else
543
+ debug "No Plugin Configs at #{path}"
544
+ end
545
+ return temp_configs
546
+ end
425
547
  end
426
548
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scout
3
3
  version: !ruby/object:Gem::Version
4
- hash: 57
5
- prerelease:
4
+ hash: 61
5
+ prerelease: false
6
6
  segments:
7
7
  - 5
8
+ - 2
8
9
  - 1
9
- - 5
10
- version: 5.1.5
10
+ version: 5.2.1
11
11
  platform: ruby
12
12
  authors:
13
13
  - Scout Monitoring
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-02-01 00:00:00 -08:00
18
+ date: 2011-02-28 00:00:00 -08:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -52,12 +52,15 @@ files:
52
52
  - lib/scout/command/install.rb
53
53
  - lib/scout/command/run.rb
54
54
  - lib/scout/command/test.rb
55
+ - lib/scout/command/troubleshoot.rb
55
56
  - lib/scout/command.rb
56
57
  - lib/scout/plugin.rb
57
58
  - lib/scout/plugin_options.rb
59
+ - lib/scout/scout_logger.rb
58
60
  - lib/scout/server.rb
59
61
  - lib/scout.rb
60
62
  - data/cacert.pem
63
+ - data/code_id_rsa.pub
61
64
  - data/gpl-2.0.txt
62
65
  - data/lgpl-2.1.txt
63
66
  - vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkComparison.log
@@ -218,7 +221,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
218
221
  requirements: []
219
222
 
220
223
  rubyforge_project: scout
221
- rubygems_version: 1.4.2
224
+ rubygems_version: 1.3.7
222
225
  signing_key:
223
226
  specification_version: 3
224
227
  summary: Scout makes monitoring and reporting on your web applications as flexible and simple as possible.