scout 1.1.0

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/Rakefile ADDED
@@ -0,0 +1,120 @@
1
+ require "rake/rdoctask"
2
+ require "rake/testtask"
3
+ require "rake/gempackagetask"
4
+ require "rake/contrib/rubyforgepublisher"
5
+ require "net/ssh"
6
+
7
+ require "rubygems"
8
+ require "rubyforge"
9
+
10
+ dir = File.dirname(__FILE__)
11
+ lib = File.join(dir, "lib", "scout.rb")
12
+ version = File.read(lib)[/^\s*VERSION\s*=\s*(['"])(\d\.\d\.\d)\1/, 2]
13
+ history = File.read("CHANGELOG").split(/^(===.*)/)
14
+ changes ||= history[0..2].join.strip
15
+
16
+ need_tar = true
17
+ need_zip = true
18
+
19
+ task :default => [:test]
20
+
21
+ Rake::TestTask.new do |test|
22
+ test.libs << "test"
23
+ test.test_files = [ "test/scout_test.rb" ]
24
+ test.verbose = true
25
+ end
26
+
27
+ Rake::RDocTask.new do |rdoc|
28
+ rdoc.main = "README"
29
+ rdoc.rdoc_dir = "doc/html"
30
+ rdoc.title = "Scout Client Documentation"
31
+ rdoc.rdoc_files.include( "README", "INSTALL",
32
+ "TODO", "CHANGELOG",
33
+ "AUTHORS", "COPYING",
34
+ "LICENSE", "lib/" )
35
+ end
36
+
37
+ desc "Upload current documentation to Scout Gem Server"
38
+ task :upload_docs => [:rdoc] do
39
+ sh "scp -r doc/html/* " +
40
+ "deploy@gems.scoutapp.com:/var/www/gems/docs"
41
+ end
42
+
43
+ spec = Gem::Specification.new do |spec|
44
+ spec.name = "scout"
45
+ spec.version = version
46
+
47
+ spec.platform = Gem::Platform::RUBY
48
+ spec.summary = "Scout makes monitoring and reporting on your web applications as flexible and simple as possible."
49
+
50
+ # TODO: test suite
51
+ # spec.test_suite_file = "test/ts_all.rb"
52
+ spec.files = Dir.glob("{lib,test,examples}/**/*.rb").
53
+ reject { |item| item.include?(".svn") } +
54
+ Dir.glob("{test,examples}/**/*.csv").
55
+ reject { |item| item.include?(".svn") } +
56
+ ["Rakefile", "setup.rb"]
57
+ spec.executables = ["scout"]
58
+
59
+ spec.has_rdoc = true
60
+ spec.extra_rdoc_files = %w[ AUTHORS COPYING README INSTALL TODO CHANGELOG
61
+ LICENSE ]
62
+ spec.rdoc_options << "--title" << "Scout Client Documentation" <<
63
+ "--main" << "README"
64
+
65
+ spec.require_path = "lib"
66
+
67
+ spec.add_dependency "elif"
68
+ # spec.add_dependency "hpricot", "=0.6"
69
+
70
+ spec.author = "Highgroove Studios"
71
+ spec.email = "scout@highgroove.com"
72
+ spec.rubyforge_project = "scout"
73
+ spec.homepage = "http://scoutapp.com"
74
+ spec.description = <<END_DESC
75
+ Scout makes monitoring and reporting on your web applications as flexible and simple as possible.
76
+
77
+ Scout is a product of Highgroove Studios.
78
+ END_DESC
79
+ end
80
+
81
+ Rake::GemPackageTask.new(spec) do |pkg|
82
+ pkg.need_zip = need_tar
83
+ pkg.need_tar = need_zip
84
+ end
85
+
86
+ desc "Publish Gem to Scout Gem Server"
87
+ task :publish => [:package] do
88
+ pkg = "pkg/#{spec.name}-#{spec.version}"
89
+
90
+ if $DEBUG then
91
+ puts "release_id = rf.add_release #{spec.rubyforge_project.inspect}, #{spec.name.inspect}, #{spec.version.inspect}, \"#{pkg}.tgz\""
92
+ puts "rf.add_file #{spec.rubyforge_project.inspect}, #{spec.name.inspect}, release_id, \"#{pkg}.gem\""
93
+ end
94
+
95
+ puts "Publishing on RubyForge"
96
+ rf = RubyForge.new
97
+ puts "Logging in"
98
+ rf.login
99
+
100
+ c = rf.userconfig
101
+ puts rf.inspect
102
+ c["release_notes"] = spec.description if spec.description
103
+ c["release_changes"] = changes if changes
104
+ c["preformatted"] = true
105
+
106
+ files = [(need_tar ? "#{pkg}.tgz" : nil),
107
+ (need_zip ? "#{pkg}.zip" : nil),
108
+ "#{pkg}.gem"].compact
109
+
110
+ puts "Releasing #{spec.name} v. #{spec.version}"
111
+ rf.add_release spec.rubyforge_project, spec.name, spec.version, *files
112
+
113
+ puts "Publishing on Scout Server"
114
+ sh "scp -r pkg/*.gem " +
115
+ "deploy@gems.scoutapp.com:/var/www/gems/gems"
116
+ ssh = Net::SSH.start('gems.scoutapp.com','deploy')
117
+ ssh_shell = ssh.shell.sync
118
+ ssh_out = ssh_shell.send_command "/usr/bin/index_gem_repository.rb -d /var/www/gems"
119
+ puts "Published, and updated gem server." if ssh_out.stdout.empty? && !ssh_out.stderr
120
+ end
data/TODO ADDED
@@ -0,0 +1,6 @@
1
+ = To Do List
2
+
3
+ The following is a list of planned expansions for the Scout Client:
4
+
5
+ * optionally find the "nice" command if available and use that in the
6
+ crontab output
data/bin/scout ADDED
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $VERBOSE = true # -w
4
+ $KCODE = "u" # -Ku
5
+
6
+ $LOAD_PATH << File.join(File.dirname(__FILE__), *%w[.. lib])
7
+ require "scout"
8
+ require "optparse"
9
+ require "logger"
10
+ require "fileutils"
11
+ require "pp"
12
+
13
+ CONFIG_DIR = File.join((File.expand_path("~") rescue "/"), ".scout")
14
+ USER = ENV["USER"] || ENV["USERNAME"] || "root"
15
+
16
+ options = { :server => "https://scoutapp.com/",
17
+ :history => File.join(CONFIG_DIR , "client_history.yaml"),
18
+ :verbose => false,
19
+ :level => "info" }
20
+
21
+ ARGV.options do |opts|
22
+ opts.banner = "Usage: #{File.basename($PROGRAM_NAME)} [OPTIONS] CLIENT_KEY"
23
+
24
+ opts.separator ""
25
+ opts.separator "CLIENT_KEY is the indentification key assigned to this " +
26
+ "client by the server."
27
+ opts.separator ""
28
+ opts.separator "Note: This client is meant to be installed and invoked " +
29
+ "through cron or any other scheduler."
30
+ opts.separator ""
31
+ opts.separator "Specific Options:"
32
+
33
+ opts.on( "-s", "--server SERVER", String,
34
+ "The URL for the server this client reports to." ) do |url|
35
+ options[:server] = url
36
+ end
37
+ opts.on( "-p", "--plugin PLUGIN", String,
38
+ "The path to a plugin to run locally. " +
39
+ "Useful for Testing." ) do |plugin|
40
+ options[:plugin] = plugin
41
+ end
42
+
43
+ opts.separator ""
44
+
45
+ opts.on( "-d", "--data DATA", String,
46
+ "The data file used to track the history of executions." ) do |file|
47
+ options[:history] = file
48
+ end
49
+ opts.on( "-l", "--level LEVEL", Logger::SEV_LABEL.map { |l| l.downcase },
50
+ "The level of logging to report." ) do |level|
51
+ options[:level] = level
52
+ end
53
+ opts.on( "-o", "--plugin-options PLUGIN_OPTIONS", String,
54
+ "The options YAML file to pass to the plugin." ) do |plugin_options|
55
+ options[:plugin_options] = plugin_options
56
+ end
57
+
58
+ opts.separator "Common Options:"
59
+
60
+ opts.on( "-h", "--help",
61
+ "Show this message." ) do
62
+ puts opts
63
+ exit
64
+ end
65
+ opts.on( "-v", "--[no-]verbose",
66
+ "Turn on logging to STDOUT" ) do |bool|
67
+ options[:verbose] = bool
68
+ end
69
+
70
+ begin
71
+ opts.parse!
72
+ options[:client_key] = ARGV.shift if ARGV.size == 1
73
+ rescue
74
+ puts opts
75
+ exit
76
+ end
77
+ end
78
+
79
+ log = Logger.new($stdout)
80
+ log.datetime_format = "%Y-%m-%d %H:%M "
81
+ log.level = Logger.const_get(options[:level].upcase) \
82
+ rescue Logger::INFO
83
+
84
+ real_config_dir = File.dirname(options[:history])
85
+ FileUtils.mkdir_p(real_config_dir) # ensure dir exists
86
+
87
+ # make sure only one copy is ever running at a time
88
+ pid_file = File.join(real_config_dir, "scout_client_pid.txt")
89
+ begin
90
+ File.open(pid_file, File::CREAT|File::EXCL|File::WRONLY) { |pid| pid.puts $$ }
91
+ at_exit do
92
+ begin
93
+ File.unlink(pid_file)
94
+ rescue
95
+ log.error "Unable to unlink pid file: #{$!.message}"
96
+ end
97
+ end
98
+ rescue
99
+ pid = File.read(pid_file).strip.to_i rescue "unknown"
100
+ running = true
101
+ begin
102
+ Process.kill(0, pid)
103
+ rescue Errno::ESRCH
104
+ running = false
105
+ rescue
106
+ # do nothing, we didn't have permission to the running process
107
+ end
108
+ if running
109
+ log.warn "Process #{pid} was already running"
110
+ exit
111
+ else
112
+ log.info "Stale PID file found. Clearing it and reloading..."
113
+ File.unlink(pid_file)
114
+ retry
115
+ end
116
+ end
117
+
118
+ if [:client_key, :plugin].all? { |o| options[o].nil? } and
119
+ $stdin.tty? # install wizard
120
+ puts <<-END_INTRO.gsub(/^ {2}/, "")
121
+ == Scout Installation Wizard ==
122
+
123
+ You need the Client Key displayed in the Client Settings
124
+ tab. It looks like:
125
+
126
+ 6ecad322-0d17-4cb8-9b2c-a12c4541853f
127
+
128
+ Enter the Client Key:
129
+ END_INTRO
130
+ options[:client_key] = gets.to_s.chomp!
131
+
132
+ # puts "Attempting to contact the server..."
133
+ begin
134
+ Scout::Server.new( options[:server],
135
+ options[:client_key],
136
+ options[:history],
137
+ options[:verbose] ? log : nil ) { |server| server.test }
138
+
139
+ puts <<-END_SUCCESS.gsub(/^ {4}/, "")
140
+
141
+ Success!
142
+
143
+ ******* NOW, INSTALL IN CRONTAB *******
144
+
145
+ */10 * * * * #{USER} #{File.expand_path($PROGRAM_NAME)} #{options[:client_key]}
146
+
147
+ ******* END CRONTAB SAMPLE *******
148
+
149
+ For help setting up Scout with crontab, please visit:
150
+
151
+ http://scoutapp.com/help#cron
152
+
153
+ END_SUCCESS
154
+ rescue SystemExit
155
+ puts <<-END_ERROR.gsub(/^ {4}/, "")
156
+
157
+ Could not contact server. The client key may be incorrect. For more help,
158
+ please visit:
159
+
160
+ http://scoutapp.com/help
161
+
162
+ END_ERROR
163
+ end
164
+ elsif options[:plugin] # local plugin
165
+ # read the plugin_code from the file specified
166
+ plugin_code = File.read(options[:plugin])
167
+
168
+ plugin_options = if options[:plugin_options].to_s[0..0] == "{"
169
+ eval(options[:plugin_options]) # options from command-line
170
+ elsif options[:plugin_options]
171
+ #
172
+ # read the plugin_options from the YAML file specified,
173
+ # parse each option and use the default value specified
174
+ # in the options as the value to be passed to the test plugin
175
+ #
176
+ Hash[ *File.open(options[:plugin_options]) { |f| YAML.load(f) }["options"].
177
+ map { |name, details| [name, details["default"]] }.flatten ]
178
+ else
179
+ Hash.new
180
+ end
181
+
182
+ Scout::Server.new( nil,
183
+ options[:client_key],
184
+ options[:history],
185
+ options[:verbose] ? log : nil ) do |server|
186
+ pp server.process_plugin( { :interval => 0,
187
+ :plugin_id => 1,
188
+ :name => "Local Plugin",
189
+ :code => plugin_code,
190
+ :options => plugin_options,
191
+ :path => options[:plugin] } )
192
+ end
193
+ else # normal run
194
+ Scout::Server.new( options[:server],
195
+ options[:client_key],
196
+ options[:history],
197
+ options[:verbose] ? log : nil ) do |server|
198
+ server.run_plugins_by_plan
199
+ end
200
+ end
data/lib/scout.rb ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby -wKU
2
+
3
+ require "scout/plugin"
4
+ require "scout/server"
5
+
6
+ module Scout
7
+ VERSION = "1.1.0".freeze
8
+ end
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby -wKU
2
+
3
+ module Scout
4
+ class Plugin
5
+ class << self
6
+ attr_reader :last_defined
7
+
8
+ def inherited(new_plugin)
9
+ @last_defined = new_plugin
10
+ end
11
+
12
+ def load(last_run, memory, options)
13
+ new(last_run, memory, options)
14
+ end
15
+ end
16
+
17
+ # Creates a new Scout Plugin to run.
18
+ #
19
+ def initialize(last_run, memory, options)
20
+ @last_run = last_run
21
+ @memory = memory
22
+ @options = options
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env ruby -wKU
2
+
3
+ require "net/https"
4
+ require "uri"
5
+ require "yaml"
6
+ require "timeout"
7
+
8
+ module Scout
9
+ class Server
10
+ # A new class for plugin Timeout errors.
11
+ class PluginTimeoutError < RuntimeError; end
12
+
13
+ # The default URLS are used to communicate with the Scout Server.
14
+ URLS = { :plan => "/clients/CLIENT_KEY/plugins.scout?version=CLIENT_VERSION",
15
+ :report => "/clients/CLIENT_KEY/plugins/PLUGIN_ID/reports.scout?version=CLIENT_VERSION",
16
+ :error => "/clients/CLIENT_KEY/plugins/PLUGIN_ID/errors.scout?version=CLIENT_VERSION",
17
+ :alert => "/clients/CLIENT_KEY/plugins/PLUGIN_ID/alerts.scout?version=CLIENT_VERSION" }
18
+
19
+ #
20
+ # A plugin cannot take more than PLUGIN_TIMEOUT seconds to execute,
21
+ # otherwise, a timeout error is generated.
22
+ #
23
+ PLUGIN_TIMEOUT = 60
24
+
25
+ # Creates a new Scout Server connection.
26
+ def initialize(server, client_key, history_file, logger = nil)
27
+ @server = server
28
+ @client_key = client_key
29
+ @history_file = history_file
30
+ @history = Hash.new
31
+ @logger = logger
32
+
33
+ if block_given?
34
+ load_history
35
+ yield self
36
+ save_history
37
+ end
38
+ end
39
+
40
+ #
41
+ # Loads the history file from disk. If the file does not exist,
42
+ # it creates one.
43
+ #
44
+ def load_history
45
+ unless File.exist? @history_file
46
+ debug "Creating empty history file..."
47
+ File.open(@history_file, "w") do |file|
48
+ YAML.dump({"last_runs" => Hash.new, "memory" => Hash.new}, file)
49
+ end
50
+ info "History file created."
51
+ end
52
+ debug "Loading history file..."
53
+ @history = File.open(@history_file) { |file| YAML.load(file) }
54
+ info "History file loaded."
55
+ end
56
+
57
+ # Saves the history file to disk.
58
+ def save_history
59
+ debug "Saving history file..."
60
+ File.open(@history_file, "w") { |file| YAML.dump(@history, file) }
61
+ info "History file saved."
62
+ end
63
+
64
+ # Runs all plugins from a given plan. Calls process_plugin on each plugin.
65
+ def run_plugins_by_plan
66
+ plan do |plugin|
67
+ process_plugin(plugin)
68
+ end
69
+ end
70
+
71
+ #
72
+ # This is the heart of Scout.
73
+ #
74
+ # First, it determines if a plugin is past interval and needs to be run.
75
+ # If it is, it simply evals the code, compiling it.
76
+ # It then loads the plugin and runs it with a PLUGIN_TIMEOUT time limit.
77
+ # The plugin generates data, alerts, and errors. In addition, it will
78
+ # set memory and last_run information in the history file.
79
+ #
80
+ def process_plugin(plugin)
81
+ info "Processing the #{plugin[:name]} plugin:"
82
+ last_run = @history["last_runs"][plugin[:name]]
83
+ memory = @history["memory"][plugin[:name]]
84
+ run_time = Time.now
85
+ if last_run.nil? or run_time > last_run + plugin[:interval]
86
+ debug "Plugin is past interval and needs to be run. " +
87
+ "(last run: #{last_run || 'nil'})"
88
+ debug "Compiling plugin..."
89
+ begin
90
+ eval(plugin[:code], TOPLEVEL_BINDING, plugin[:path] || plugin[:name])
91
+ info "Plugin compiled."
92
+ rescue Exception
93
+ error "Plugin would not compile: #{$!.message}"
94
+ return
95
+ end
96
+ debug "Loading plugin..."
97
+ if job = Plugin.last_defined.load( last_run, (memory || Hash.new),
98
+ plugin[:options] || Hash.new )
99
+ info "Plugin loaded."
100
+ debug "Running plugin..."
101
+ begin
102
+ data = {}
103
+ Timeout.timeout(PLUGIN_TIMEOUT, PluginTimeoutError) do
104
+ data = job.run
105
+ end
106
+ rescue Timeout::Error
107
+ error "Plugin took too long to run."
108
+ return
109
+ rescue Exception
110
+ error "Plugin failed to run: #{$!.backtrace}"
111
+ end
112
+ info "Plugin completed its run."
113
+
114
+ # handle single report or array of reports
115
+ send_report(data[:report], plugin[:plugin_id]) if data[:report]
116
+ if data[:reports] and not data[:reports].empty?
117
+ data[:reports].each { |r| send_report(r, plugin[:plugin_id]) }
118
+ end
119
+ # handle single alert or array of alerts
120
+ send_alert(data[:alert], plugin[:plugin_id]) if data[:alert]
121
+ if data[:alerts] and not data[:alerts].empty?
122
+ data[:alerts].each { |a| send_alert(a, plugin[:plugin_id]) }
123
+ end
124
+ # handle single error or array of errors
125
+ send_error(data[:error], plugin[:plugin_id]) if data[:error]
126
+ if data[:errors] and not data[:errors].empty?
127
+ data[:errors].each { |e| send_error(e, plugin[:plugin_id]) }
128
+ end
129
+
130
+ @history["last_runs"][plugin[:name]] = run_time
131
+ @history["memory"][plugin[:name]] = data[:memory]
132
+ else
133
+ scout_error({:subject => "Plugin would not load."}, plugin[:plugin_id])
134
+ end
135
+ else
136
+ debug "Plugin does not need to be run at this time. " +
137
+ "(last run: #{last_run || 'nil'})"
138
+ end
139
+ info "Plugin #{plugin[:name]} processing complete."
140
+ data
141
+ end
142
+
143
+ #
144
+ # Retrieves the Plugin Plan from the server. This is the list of plugins
145
+ # to execute, along with all options.
146
+ #
147
+ def plan
148
+ url = urlify(:plan)
149
+ info "Loading plan from #{url}..."
150
+ get(url, "Could not retrieve plan from server.") do |res|
151
+ begin
152
+ plugin_execution_plan = Marshal.load(res.body)
153
+ info "Plan loaded. (#{plugin_execution_plan.size} plugins: " +
154
+ "#{plugin_execution_plan.map { |p| p[:name] }.join(', ')})"
155
+ rescue TypeError
156
+ fatal "Plan from server was malformed."
157
+ exit
158
+ end
159
+ plugin_execution_plan.each do |plugin|
160
+ begin
161
+ yield plugin if block_given?
162
+ rescue RuntimeError
163
+ scout_error( { :subject => "Exception: #{$!.message}.",
164
+ :body => $!.backtrace },
165
+ plugin[:plugin_id] )
166
+ end
167
+ end
168
+ end
169
+ end
170
+ alias_method :test, :plan
171
+
172
+ # Sends report data to the Scout Server.
173
+ def send_report(data, plugin_id)
174
+ url = urlify(:report, :plugin_id => plugin_id)
175
+ report_hash = {:data => data, :plugin_id => plugin_id}
176
+
177
+ # add in any special fields
178
+ if time = ( data.delete(:scout_time) || data.delete("scout_time") )
179
+ report_hash[:time] = time
180
+ end
181
+
182
+ debug "Sending report to #{url} (#{data.inspect})..."
183
+ post url,
184
+ "Unable to send report to server.",
185
+ :report => report_hash
186
+ info "Report sent."
187
+ end
188
+
189
+ # Sends an alert to the Scout Server.
190
+ def send_alert(data, plugin_id)
191
+ url = urlify(:alert, :plugin_id => plugin_id)
192
+ debug "Sending alert to #{url} (subject: #{data[:subject]})..."
193
+ post url,
194
+ "Unable to send alert to server.",
195
+ :alert => data.merge(:plugin_id => plugin_id)
196
+ info "Alert sent."
197
+ end
198
+
199
+ # Sends an error to the Scout Server.
200
+ def send_error(data, plugin_id)
201
+ url = urlify(:error, :plugin_id => plugin_id)
202
+ debug "Sending error to #{url} (subject: #{data[:subject]})..."
203
+ post url,
204
+ "Unable to log error on server.",
205
+ :error => data.merge(:plugin_id => plugin_id)
206
+ info "Error sent."
207
+ end
208
+
209
+ private
210
+
211
+ def urlify(url_name, options = Hash.new)
212
+ return unless @server
213
+ options.merge!(:client_version => Scout::VERSION)
214
+ URI.join( @server,
215
+ URLS[url_name].
216
+ gsub(/\bCLIENT_KEY\b/, @client_key).
217
+ gsub(/\b[A-Z_]+\b/) { |k| options[k.downcase.to_sym] || k } )
218
+ end
219
+
220
+ def paramify(params, prefix = nil)
221
+ params.inject(Hash.new) do |all, (key, value)|
222
+ parent = prefix ? "#{prefix}[#{key}]" : String(key)
223
+ if value.is_a? Hash
224
+ all.merge(paramify(value, parent))
225
+ else
226
+ all.merge(parent => String(value))
227
+ end
228
+ end
229
+ end
230
+
231
+ def post(url, error, params = {}, &response_handler)
232
+ return unless url
233
+ request(url, response_handler, error) do |connection|
234
+ post = Net::HTTP::Post.new(url.path + (url.query ? ('?' + url.query) : ''))
235
+ post.set_form_data(paramify(params))
236
+ connection.request(post)
237
+ end
238
+ end
239
+
240
+ def get(url, error, params = {}, &response_handler)
241
+ return unless url
242
+ request(url, response_handler, error) do |connection|
243
+ connection.get(url.path + (url.query ? ('?' + url.query) : ''))
244
+ end
245
+ end
246
+
247
+ def request(url, response_handler, error, &connector)
248
+ http = Net::HTTP.new(url.host, url.port)
249
+ if url.is_a? URI::HTTPS
250
+ http.use_ssl = true
251
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
252
+ end
253
+ case response = no_warnings { http.start(&connector) }
254
+ when Net::HTTPSuccess
255
+ response_handler[response] unless response_handler.nil?
256
+ else
257
+ fatal error
258
+ exit
259
+ end
260
+ rescue Timeout::Error
261
+ fatal "Request timed out."
262
+ exit
263
+ rescue Exception
264
+ fatal "An HTTP error occurred: #{$!.message}"
265
+ exit
266
+ end
267
+
268
+ def no_warnings
269
+ old_verbose = $VERBOSE
270
+ $VERBOSE = false
271
+ yield
272
+ ensure
273
+ $VERBOSE = old_verbose
274
+ end
275
+
276
+ # Forward Logger methods to an active instance, when there is one.
277
+ def method_missing(meth, *args, &block)
278
+ if (Logger::SEV_LABEL - %w[ANY]).include? meth.to_s.upcase
279
+ @logger.send(meth, *args, &block) unless @logger.nil?
280
+ else
281
+ super
282
+ end
283
+ end
284
+ end
285
+ end