scout_agent 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS +4 -0
- data/CHANGELOG +3 -0
- data/COPYING +340 -0
- data/INSTALL +17 -0
- data/LICENSE +6 -0
- data/README +3 -0
- data/Rakefile +123 -0
- data/TODO +3 -0
- data/bin/scout_agent +11 -0
- data/lib/scout_agent.rb +73 -0
- data/lib/scout_agent/agent.rb +42 -0
- data/lib/scout_agent/agent/communication_agent.rb +85 -0
- data/lib/scout_agent/agent/master_agent.rb +301 -0
- data/lib/scout_agent/api.rb +241 -0
- data/lib/scout_agent/assignment.rb +105 -0
- data/lib/scout_agent/assignment/configuration.rb +30 -0
- data/lib/scout_agent/assignment/identify.rb +110 -0
- data/lib/scout_agent/assignment/queue.rb +95 -0
- data/lib/scout_agent/assignment/reset.rb +91 -0
- data/lib/scout_agent/assignment/snapshot.rb +92 -0
- data/lib/scout_agent/assignment/start.rb +149 -0
- data/lib/scout_agent/assignment/status.rb +44 -0
- data/lib/scout_agent/assignment/stop.rb +60 -0
- data/lib/scout_agent/assignment/upload_log.rb +61 -0
- data/lib/scout_agent/core_extensions.rb +260 -0
- data/lib/scout_agent/database.rb +386 -0
- data/lib/scout_agent/database/mission_log.rb +282 -0
- data/lib/scout_agent/database/queue.rb +126 -0
- data/lib/scout_agent/database/snapshots.rb +187 -0
- data/lib/scout_agent/database/statuses.rb +65 -0
- data/lib/scout_agent/dispatcher.rb +157 -0
- data/lib/scout_agent/id_card.rb +143 -0
- data/lib/scout_agent/lifeline.rb +243 -0
- data/lib/scout_agent/mission.rb +212 -0
- data/lib/scout_agent/order.rb +58 -0
- data/lib/scout_agent/order/check_in_order.rb +32 -0
- data/lib/scout_agent/order/snapshot_order.rb +33 -0
- data/lib/scout_agent/plan.rb +306 -0
- data/lib/scout_agent/server.rb +123 -0
- data/lib/scout_agent/tracked.rb +59 -0
- data/lib/scout_agent/wire_tap.rb +513 -0
- data/setup.rb +1360 -0
- data/test/tc_core_extensions.rb +89 -0
- data/test/tc_id_card.rb +115 -0
- data/test/tc_plan.rb +285 -0
- data/test/test_helper.rb +22 -0
- data/test/ts_all.rb +7 -0
- metadata +171 -0
@@ -0,0 +1,123 @@
|
|
1
|
+
#!/usr/bin/env ruby -wKU
|
2
|
+
|
3
|
+
module ScoutAgent
|
4
|
+
#
|
5
|
+
# This class is a thin wrapper over RestClient for Scout's check-in API.
|
6
|
+
# Public methods are provided for each action you can perform againt the API.
|
7
|
+
#
|
8
|
+
class Server
|
9
|
+
#
|
10
|
+
# Create a new API wrapper, optionally with a +log+ to write connection
|
11
|
+
# details to.
|
12
|
+
#
|
13
|
+
def initialize(log = WireTap.new(nil))
|
14
|
+
@log = log
|
15
|
+
@rest_client = RestClient::Resource.new(
|
16
|
+
Plan.agent_url,
|
17
|
+
:headers => { :client_version => ScoutAgent::VERSION,
|
18
|
+
:accept_encoding => "gzip" }
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
# The log connection notes will be written to.
|
23
|
+
attr_reader :log
|
24
|
+
|
25
|
+
#
|
26
|
+
# This method fetches the current plan for this agent. You can pass any
|
27
|
+
# +additional_headers+ for the request if needed (mainly useful for
|
28
|
+
# <tt>:if_modified_since</tt>).
|
29
|
+
#
|
30
|
+
# This method will return the raw plan when it is successfully fetched. An
|
31
|
+
# empty String is returned if the plan is malformed or unchanged. Finally,
|
32
|
+
# +nil+ is returned if the plan cannot be retrieved for some reason.
|
33
|
+
#
|
34
|
+
def get_plan(additional_headers = { })
|
35
|
+
no_warnings { # keep OpenSSL quiet
|
36
|
+
@rest_client["plan.scout"].get(additional_headers)
|
37
|
+
}
|
38
|
+
rescue Zlib::Error # could not decompress response
|
39
|
+
log.warn("Plan was malformed zipped data.")
|
40
|
+
"" # replace bad plan with empty plan
|
41
|
+
rescue RestClient::RequestFailed => error # RestClient bug workaround
|
42
|
+
# all success codes are OK, but other codes are not
|
43
|
+
if error.http_code.between? 200, 299
|
44
|
+
error.response.body # the returned plan
|
45
|
+
else
|
46
|
+
log.warn("Plan was returned as a failure code: #{error.http_code}.")
|
47
|
+
nil # failed to retrieve plan
|
48
|
+
end
|
49
|
+
rescue RestClient::NotModified
|
50
|
+
log.info("Plan was not modified.")
|
51
|
+
"" # empty plan
|
52
|
+
rescue Errno::ECONNREFUSED, RestClient::Exception => error # bad networking
|
53
|
+
log.warn("Plan could not be retrieved: #{error.class}.")
|
54
|
+
nil # failed to retrieve plan
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# This method can be used to send +data+ to the Scout API as a check-in.
|
59
|
+
# The data is zipped before it is sent to reduce the cost duplicated reports
|
60
|
+
# as much as possible.
|
61
|
+
#
|
62
|
+
# If the ckeck-in succeeds, +true+ is returned. However, +false+ doesn't
|
63
|
+
# ensure that we failed. RestClient may time out a long connection attempt,
|
64
|
+
# but the server may still complete it eventually.
|
65
|
+
#
|
66
|
+
def post_checkin(data)
|
67
|
+
io = StringIO.new
|
68
|
+
gzip = Zlib::GzipWriter.new(io)
|
69
|
+
gzip << data.to_json
|
70
|
+
gzip.close
|
71
|
+
no_warnings do # keep OpenSSL quiet
|
72
|
+
@rest_client["checkin.scout"].post(
|
73
|
+
io.string,
|
74
|
+
:content_type => "application/json",
|
75
|
+
:content_encoding => "gzip"
|
76
|
+
)
|
77
|
+
end
|
78
|
+
true
|
79
|
+
rescue Zlib::Error # could not compress data for sending
|
80
|
+
log.error("Check-in could not be zipped.")
|
81
|
+
false
|
82
|
+
rescue RestClient::RequestFailed => error # RestClient bug workaround
|
83
|
+
# all success codes are OK, but other codes are not
|
84
|
+
if error.http_code.between? 200, 299
|
85
|
+
true
|
86
|
+
else
|
87
|
+
log.warn( "Check-in was returned as a failure code: " +
|
88
|
+
"#{error.http_code}." )
|
89
|
+
false
|
90
|
+
end
|
91
|
+
rescue Errno::ECONNREFUSED, RestClient::Exception # networking problem
|
92
|
+
log.warn("Check-in could not be sent: #{error.class}.")
|
93
|
+
false # we failed to send and will retry later
|
94
|
+
end
|
95
|
+
|
96
|
+
#
|
97
|
+
# Uploads +log_file+ to the server for troubleshooting. Returns +true+ if
|
98
|
+
# the upload succeeded.
|
99
|
+
#
|
100
|
+
def post_log(log_file)
|
101
|
+
no_warnings do # keep OpenSSL quiet
|
102
|
+
@rest_client["log.scout"].post(
|
103
|
+
log_file.read,
|
104
|
+
:content_type => "text/plain",
|
105
|
+
:content_encoding => "gzip"
|
106
|
+
)
|
107
|
+
end
|
108
|
+
true
|
109
|
+
rescue RestClient::RequestFailed => error # RestClient bug workaround
|
110
|
+
# all success codes are OK, but other codes are not
|
111
|
+
if error.http_code.between? 200, 299
|
112
|
+
true
|
113
|
+
else
|
114
|
+
log.warn( "Log upload was returned as a failure code: " +
|
115
|
+
"#{error.http_code}." )
|
116
|
+
false
|
117
|
+
end
|
118
|
+
rescue Errno::ECONNREFUSED, RestClient::Exception # networking problem
|
119
|
+
log.warn("Log could not be sent: #{error.class}.")
|
120
|
+
false # could not send log
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
#!/usr/bin/env ruby -wKU
|
2
|
+
|
3
|
+
module ScoutAgent
|
4
|
+
#
|
5
|
+
# A mix-in for adding status tracking to various objects. Methods are
|
6
|
+
# provided to set your current status and clear your status.
|
7
|
+
#
|
8
|
+
module Tracked
|
9
|
+
#
|
10
|
+
# Returns the <tt>log()</tt> for this object, if supported and set, or a bit
|
11
|
+
# bucket log object.
|
12
|
+
#
|
13
|
+
def status_log
|
14
|
+
(respond_to?(:log) and log) or WireTap.new(nil)
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# Returns the status database this object this object can use to update
|
19
|
+
# its status.
|
20
|
+
#
|
21
|
+
# This value is cached after the first call to speed up future interactions
|
22
|
+
# with the database. You'll need to reset the cached value with a call to
|
23
|
+
# force_status_database_reload() after doing something that would invalidate
|
24
|
+
# a SQLite database handle like <tt>fork()</tt>.
|
25
|
+
#
|
26
|
+
def status_database(log = nil)
|
27
|
+
@status_database ||= Database.load(:statuses, status_log)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Call this to clear the cached database handle after a <tt>fork()</tt>.
|
31
|
+
def force_status_database_reload
|
32
|
+
@status_database = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# :call-seq:
|
37
|
+
# status(status, name = IDCard.me && IDCard.me.process_name)
|
38
|
+
#
|
39
|
+
# Sets the +status+ of this process. You can pass +name+ explicitly if it
|
40
|
+
# cannot be properly determined from <tt>IDCard.me()</tt>.
|
41
|
+
#
|
42
|
+
def status(*args)
|
43
|
+
status_database.update_status(*args) if status_database
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# :call-seq:
|
48
|
+
# clear_status(name = IDCard.me && IDCard.me.process_name)
|
49
|
+
#
|
50
|
+
# Clears any status currently set for this process. This should be called
|
51
|
+
# for any process setting status messages <tt>at_exit()</tt>. You can pass
|
52
|
+
# +name+ explicitly if it cannot be properly determined from
|
53
|
+
# <tt>IDCard.me()</tt>.
|
54
|
+
#
|
55
|
+
def clear_status(*args)
|
56
|
+
status_database.clear_status(*args) if status_database
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,513 @@
|
|
1
|
+
#!/usr/bin/env ruby -wKU
|
2
|
+
|
3
|
+
module ScoutAgent
|
4
|
+
#
|
5
|
+
# WireTap is a complete replacement for Ruby's standard Logger class, with a
|
6
|
+
# few changes:
|
7
|
+
#
|
8
|
+
# * The code is multi-Process safe
|
9
|
+
# * Log can be duplicated on a second device
|
10
|
+
# * The default formatter produces a trimmed down output
|
11
|
+
# * Severity levels can be dynamically altered (not used in the agent)
|
12
|
+
#
|
13
|
+
# The main change was to make the code work for multiple processes. This is
|
14
|
+
# done by forcing code to aquire the proper locks during all file
|
15
|
+
# interactions. Log rotation also had to be changed to avoid moving an active
|
16
|
+
# log file out from under another process. Instead, files are copied and
|
17
|
+
# truncated. This also required a seek() to the end of the file before each
|
18
|
+
# write to be added to defeat some pos() caching that seemed to be occurring.
|
19
|
+
# The end results of all of this is that WireTap is safer than Logger, but
|
20
|
+
# slower because of the extra precausions it must take.
|
21
|
+
#
|
22
|
+
# The tap=() method on instances of this class, allows you to set a second
|
23
|
+
# device input can be copied to.
|
24
|
+
#
|
25
|
+
class WireTap
|
26
|
+
# A base Class for all WireTap failure conditions.
|
27
|
+
class Error < RuntimeError; end
|
28
|
+
# The Error thrown when WireTap is unable to shift logs for whatever reason.
|
29
|
+
class ShiftingError < Error; end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Instances of this Class are used to format all outgoing log messages.
|
33
|
+
# WireTap will pass messages to call() which is expected to return a String
|
34
|
+
# ready for output.
|
35
|
+
#
|
36
|
+
# This class works very similar to Logger's default formatter (supporting
|
37
|
+
# things like logging Exception backtraces and Ruby objects with inspect())
|
38
|
+
# with the following changes:
|
39
|
+
#
|
40
|
+
# * The default message format is simplified
|
41
|
+
# * The default timestamp format is more readable
|
42
|
+
# * All lines after the first in a logged message are indented
|
43
|
+
# * Program names are shown with an attached PID
|
44
|
+
#
|
45
|
+
class Formatter
|
46
|
+
#
|
47
|
+
# The sprintf() style format used to format messages:
|
48
|
+
#
|
49
|
+
# SEVERITY [TIME PROCESS]: MESSAGE
|
50
|
+
#
|
51
|
+
FORMAT = "%s [%s %s]: %s\n"
|
52
|
+
|
53
|
+
# Creates a new formatter without a timestamp format.
|
54
|
+
def initialize
|
55
|
+
@datetime_format = nil
|
56
|
+
end
|
57
|
+
|
58
|
+
# Used to manually set the timestamp format, overriding the default.
|
59
|
+
attr_accessor :datetime_format
|
60
|
+
|
61
|
+
#
|
62
|
+
# The main interface required by Formatter instances. Turns +severity+,
|
63
|
+
# +time+, +progname+, and +message+ into a String for writing to a log
|
64
|
+
# file.
|
65
|
+
#
|
66
|
+
def call(severity, time, progname, message)
|
67
|
+
FORMAT % [ severity,
|
68
|
+
format_time(time),
|
69
|
+
format_progname(progname),
|
70
|
+
format_message(message) ]
|
71
|
+
end
|
72
|
+
|
73
|
+
#######
|
74
|
+
private
|
75
|
+
#######
|
76
|
+
|
77
|
+
# Formats +time+ using the set strftime() pattern, or a readable default.
|
78
|
+
def format_time(time)
|
79
|
+
if @datetime_format.nil?
|
80
|
+
time.strftime("%Y-%m-%d %H:%M:%S")
|
81
|
+
else
|
82
|
+
time.strftime(@datetime_format)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
#
|
87
|
+
# Formats +progname+ by adding on a PID, or replacing it with a PID when
|
88
|
+
# +nil+.
|
89
|
+
#
|
90
|
+
def format_progname(progname)
|
91
|
+
pid = Process.pid
|
92
|
+
if progname
|
93
|
+
"%s(%d)" % [progname, pid]
|
94
|
+
else
|
95
|
+
pid
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# Formats the +message+ object into a String. String objects are
|
101
|
+
# unchanged, Exception objects are converted to a message, type, and
|
102
|
+
# backtrace, and all other objects have inspect() called on them.
|
103
|
+
#
|
104
|
+
# If this process results in more than one line, all other lines are
|
105
|
+
# indented two spaces to make the resulting file trivial to parse.
|
106
|
+
#
|
107
|
+
def format_message(message)
|
108
|
+
output = case message
|
109
|
+
when String
|
110
|
+
message
|
111
|
+
when Exception
|
112
|
+
"%s (%s)\n%s" % [ message.message,
|
113
|
+
message.class,
|
114
|
+
message.backtrace.join("\n") ]
|
115
|
+
else
|
116
|
+
message.inspect
|
117
|
+
end
|
118
|
+
output.strip.gsub("\n", "\n ")
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
#
|
123
|
+
# This Class is used to wrap a device passed to WireTap and then manage
|
124
|
+
# interactions with that device. These interactions are handled in a
|
125
|
+
# multi-Thread and multi-Process safe way.
|
126
|
+
#
|
127
|
+
# WireTap's LogDevice requires a bigger underlying File-like interface than
|
128
|
+
# Logger's version do to the extra locking and rotation requirements. It
|
129
|
+
# skips these steps when the device does not support them though, to make it
|
130
|
+
# possible to log to something like <tt>$stdout</tt>.
|
131
|
+
#
|
132
|
+
class LogDevice
|
133
|
+
# Used in log rotation calculations.
|
134
|
+
SECONDS_IN_A_DAY = 60 * 60 * 24
|
135
|
+
|
136
|
+
#
|
137
|
+
# Wraps +log+ with +options+, which may include <tt>:shift_age</tt> and
|
138
|
+
# <tt>:shift_size</tt>. See WireTap::new() for details on these settings.
|
139
|
+
#
|
140
|
+
def initialize(log = nil, options = Hash.new)
|
141
|
+
if log.respond_to?(:write) and log.respond_to?(:close)
|
142
|
+
@dev = log
|
143
|
+
@filename = nil
|
144
|
+
else
|
145
|
+
@dev = open_log(log)
|
146
|
+
@filename = log
|
147
|
+
end
|
148
|
+
@shift_age = options[:shift_age] || 7
|
149
|
+
@shift_size = options[:shift_size] || 1048576
|
150
|
+
@lock = Mutex.new
|
151
|
+
end
|
152
|
+
|
153
|
+
# The underlying device being managed.
|
154
|
+
attr_reader :dev
|
155
|
+
# A base name for files created by this device.
|
156
|
+
attr_reader :filename
|
157
|
+
|
158
|
+
#
|
159
|
+
# Writes +message+ to +dev+ in a multi-Thread and multi-Process safe
|
160
|
+
# manner.
|
161
|
+
#
|
162
|
+
# Before any write occurs, a check is made to see if the content should be
|
163
|
+
# shifted. That process will be trigger, if needed.
|
164
|
+
#
|
165
|
+
def write(message)
|
166
|
+
@lock.synchronize do
|
167
|
+
not @dev.respond_to?(:flock) or @dev.flock(File::LOCK_EX)
|
168
|
+
begin
|
169
|
+
begin
|
170
|
+
shift_log if @filename and @dev.respond_to?(:stat)
|
171
|
+
rescue Exception => error # shifting failed for whatever reason
|
172
|
+
raise ShiftingError, "Shifting failed: #{error.message}"
|
173
|
+
end
|
174
|
+
begin
|
175
|
+
#
|
176
|
+
# make sure we are in the right place, even if the data has been
|
177
|
+
# truncated() by another process as part of rotation
|
178
|
+
#
|
179
|
+
@dev.seek(0, IO::SEEK_END)
|
180
|
+
rescue Errno::ESPIPE # illegal seek, probably $stdout or $stderr
|
181
|
+
# do nothing: such streams are always at the end
|
182
|
+
end
|
183
|
+
@dev.write(message)
|
184
|
+
ensure
|
185
|
+
not @dev.respond_to?(:flock) or @dev.flock(File::LOCK_UN)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Closes the underlying +dev+ in a multi-Thread safe manner.
|
191
|
+
def close
|
192
|
+
@lock.synchronize do
|
193
|
+
@dev.close
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
#######
|
198
|
+
private
|
199
|
+
#######
|
200
|
+
|
201
|
+
#
|
202
|
+
# Opens a new log file in a multi-Process safe manner, adding a header
|
203
|
+
# message, if this Process was the one to create the file.
|
204
|
+
#
|
205
|
+
def open_log(filename)
|
206
|
+
dev = open(filename, File::CREAT | File::EXCL | File::WRONLY)
|
207
|
+
dev.chmod(0777) # make sure this file is available to all processes
|
208
|
+
write_header(dev, :created)
|
209
|
+
dev
|
210
|
+
rescue Errno::EEXIST # file already exists
|
211
|
+
dev = open(filename, File::CREAT | File::APPEND | File::WRONLY)
|
212
|
+
ensure
|
213
|
+
dev.sync = true if dev
|
214
|
+
end
|
215
|
+
|
216
|
+
#
|
217
|
+
# Sends a simple header to +dev+ saying when it was created or rotated.
|
218
|
+
#
|
219
|
+
# Technically, this method is not perfectly multi-Process safe. It's
|
220
|
+
# possible for one Process to create the file, but have another Process
|
221
|
+
# log one or more messages before this header can be added. This only
|
222
|
+
# affects the position of this message though and thus is deemed and
|
223
|
+
# acceptable risk. The agent is careful never to trigger this scenario.
|
224
|
+
#
|
225
|
+
def write_header(dev, verb)
|
226
|
+
dev.write "# Log %s at %s by %s\n" %
|
227
|
+
[verb, Time.now.strftime("%Y-%m-%d %H:%M:%S"), Process.pid]
|
228
|
+
end
|
229
|
+
|
230
|
+
#
|
231
|
+
# Forwards to shift_log_by_size() or shift_log_by_period() as dictated by
|
232
|
+
# the settings for this LogDevice.
|
233
|
+
#
|
234
|
+
def shift_log
|
235
|
+
if @shift_age.is_a? Integer
|
236
|
+
shift_log_by_size
|
237
|
+
else
|
238
|
+
shift_log_by_period
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
# Shifts log files if the file size has been exceeded.
|
243
|
+
def shift_log_by_size
|
244
|
+
return false unless @shift_age > 0 and @dev.stat.size > @shift_size
|
245
|
+
(@shift_age - 3).downto(0) do |i|
|
246
|
+
if File.exist? shifted_filename(i)
|
247
|
+
FileUtils.mv(shifted_filename(i), shifted_filename(i + 1))
|
248
|
+
end
|
249
|
+
end
|
250
|
+
multiprocessing_safe_shift(shifted_filename(0))
|
251
|
+
end
|
252
|
+
|
253
|
+
# Shifts log files if the period length has been exceeded.
|
254
|
+
def shift_log_by_period
|
255
|
+
now = Time.now
|
256
|
+
period_end = previous_period_end(now)
|
257
|
+
return false unless @dev.stat.mtime <= period_end
|
258
|
+
shifted_file = shifted_filename(period_end.strftime("%Y%m%d"))
|
259
|
+
if File.exist? shifted_file
|
260
|
+
raise "'%s' already exists." % shifted_file
|
261
|
+
end
|
262
|
+
multiprocessing_safe_shift(shifted_file)
|
263
|
+
end
|
264
|
+
|
265
|
+
# Returns a modified name for the file including +shifted_extension+.
|
266
|
+
def shifted_filename(shifted_extension)
|
267
|
+
"#{@filename}.#{shifted_extension}"
|
268
|
+
end
|
269
|
+
|
270
|
+
# Shifts the log file with a copy, truncate(), and rewind() process.
|
271
|
+
def multiprocessing_safe_shift(to_file)
|
272
|
+
FileUtils.cp(@filename, to_file)
|
273
|
+
@dev.truncate(0)
|
274
|
+
@dev.rewind
|
275
|
+
write_header(@dev, :rotated)
|
276
|
+
true
|
277
|
+
end
|
278
|
+
|
279
|
+
# A helper used to determine the previous period end from +now+.
|
280
|
+
def previous_period_end(now)
|
281
|
+
case @shift_age.to_s.downcase
|
282
|
+
when "daily"
|
283
|
+
end_of_day(now - SECONDS_IN_A_DAY)
|
284
|
+
when "weekly"
|
285
|
+
end_of_day(now - (now.wday + 1) * SECONDS_IN_A_DAY)
|
286
|
+
when "monthly"
|
287
|
+
end_of_day(now - now.mday * SECONDS_IN_A_DAY)
|
288
|
+
else
|
289
|
+
now
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
#
|
294
|
+
# A helper that converts a +day+ Time object into a new Time at the end of
|
295
|
+
# that +day+.
|
296
|
+
#
|
297
|
+
def end_of_day(day)
|
298
|
+
Time.local(day.year, day.month, day.mday, 23, 59, 59)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
#
|
303
|
+
# This module includes the severity levels used by all WireTap instances.
|
304
|
+
# Changing the constants in this module will rework the interface methods
|
305
|
+
# in WireTap used to log messages. Any changes made here should be done
|
306
|
+
# before logs are created.
|
307
|
+
#
|
308
|
+
# WireTap instances will have the following methods for each constant in
|
309
|
+
# this module (examples are for the +WARN+ constant):
|
310
|
+
#
|
311
|
+
# # check if a level is currently being logged
|
312
|
+
# if wire_tap.warn?
|
313
|
+
# # ...
|
314
|
+
# end
|
315
|
+
#
|
316
|
+
# # log a message at the indicated level
|
317
|
+
# wire_tap.warn "Message goes here."
|
318
|
+
#
|
319
|
+
# # log a message, skipping creation if the level is not being logged
|
320
|
+
# wire_tap.warn { build_expensive_warning_message() }
|
321
|
+
#
|
322
|
+
# # log a message, overriding +progname+
|
323
|
+
# wire_tap.warn("my_process_name") { "Message goes here." }
|
324
|
+
#
|
325
|
+
module Severity
|
326
|
+
# Low-lever information for developers.
|
327
|
+
DEBUG = 0
|
328
|
+
# Useful information about normal system operations.
|
329
|
+
INFO = 1
|
330
|
+
# A warning about something that may not have worked as expected.
|
331
|
+
WARN = 2
|
332
|
+
# A handled error condition.
|
333
|
+
ERROR = 3
|
334
|
+
# An unhandleable error.
|
335
|
+
FATAL = 4
|
336
|
+
#
|
337
|
+
# A non-classified message. (Dynamic method dispatch is not done for this
|
338
|
+
# special case.)
|
339
|
+
#
|
340
|
+
UNKNOWN = 5
|
341
|
+
end
|
342
|
+
|
343
|
+
# Make the level constants available at the instance level.
|
344
|
+
include Severity
|
345
|
+
|
346
|
+
#
|
347
|
+
# Wraps +logdev+ for logging.
|
348
|
+
#
|
349
|
+
# You can set +shift_age+ and/or +shift_size+ to setup log rotation. You
|
350
|
+
# can pass <tt>:daily</tt>, <tt>:weekly</tt>, or <tt>:monthly</tt> as
|
351
|
+
# +shift_age+ to get the named rotation. Alternately, you can set
|
352
|
+
# +shift_age+ to the number of log files to keep and +shift_size+ to the
|
353
|
+
# byte size at which log files are rotated out.
|
354
|
+
#
|
355
|
+
def initialize(logdev, shift_age = 0, shift_size = 1048576)
|
356
|
+
@logdev = logdev &&
|
357
|
+
LogDevice.new( logdev, :shift_age => shift_age,
|
358
|
+
:shift_size => shift_size )
|
359
|
+
@formatter = Formatter.new
|
360
|
+
@level = min_level
|
361
|
+
@progname = nil
|
362
|
+
end
|
363
|
+
|
364
|
+
#
|
365
|
+
# The Formatter instance being used to format the messages logged by this
|
366
|
+
# WireTap instance.
|
367
|
+
#
|
368
|
+
attr_accessor :formatter
|
369
|
+
# The current level below which no messages will be logged.
|
370
|
+
attr_accessor :level
|
371
|
+
alias_method :sev_threshold, :level
|
372
|
+
alias_method :sev_threshold=, :level=
|
373
|
+
# The Process name used in messages logged by this instance.
|
374
|
+
attr_accessor :progname
|
375
|
+
|
376
|
+
#
|
377
|
+
# Sets a new timestamp format on the Formatter used to format messages for
|
378
|
+
# this instance. +datetime_format+ should be a strftime() pattern String.
|
379
|
+
#
|
380
|
+
def datetime_format=(datetime_format)
|
381
|
+
@formatter.datetime_format = datetime_format
|
382
|
+
end
|
383
|
+
|
384
|
+
#
|
385
|
+
# Returns the timestamp format currently in use (or +nil+ if the default is
|
386
|
+
# being used) to format messages for this instance.
|
387
|
+
#
|
388
|
+
def datetime_format
|
389
|
+
@formatter.datetime_format
|
390
|
+
end
|
391
|
+
|
392
|
+
# Sets a second +logdev+ all messages should be copied to.
|
393
|
+
def tap=(logdev)
|
394
|
+
@tap = logdev && LogDevice.new(logdev)
|
395
|
+
end
|
396
|
+
|
397
|
+
#
|
398
|
+
# Returns the secondary device messages are being logged to (or +nil+ if
|
399
|
+
# none is in use).
|
400
|
+
#
|
401
|
+
def tap
|
402
|
+
@tap ||= nil
|
403
|
+
end
|
404
|
+
|
405
|
+
# Returns +true+ if a +level+ message would currently be written.
|
406
|
+
def level?(level)
|
407
|
+
@level <= level_by_name(level)
|
408
|
+
end
|
409
|
+
|
410
|
+
#
|
411
|
+
# A low-level way to log a +message+ with +severity+. You can optionally
|
412
|
+
# pass a +progname+ override. You can also choose to pass the message in a
|
413
|
+
# +block+ that will be run to build the message only if it would be written.
|
414
|
+
#
|
415
|
+
def add(severity, message = nil, progname = nil, &block)
|
416
|
+
severity ||= max_level
|
417
|
+
return true if @logdev.nil? or severity < @level
|
418
|
+
progname ||= @progname
|
419
|
+
if message.nil?
|
420
|
+
if block.nil?
|
421
|
+
message, progname = progname, @progname
|
422
|
+
else
|
423
|
+
message = block.call
|
424
|
+
end
|
425
|
+
end
|
426
|
+
output = @formatter.call( level_names[severity] || "ANY",
|
427
|
+
Time.now,
|
428
|
+
progname,
|
429
|
+
message )
|
430
|
+
@logdev.write(output)
|
431
|
+
tap.write(output) if tap
|
432
|
+
true
|
433
|
+
end
|
434
|
+
alias_method :log, :add
|
435
|
+
|
436
|
+
#
|
437
|
+
# Write's a raw +message+ to the log without any formatting or
|
438
|
+
# preprocessing.
|
439
|
+
#
|
440
|
+
def <<(message)
|
441
|
+
@logdev.write(message) unless @logdev.nil?
|
442
|
+
end
|
443
|
+
|
444
|
+
# An override to show the dynamic level methods. See Severity for details.
|
445
|
+
def respond_to?(message, include_private = false)
|
446
|
+
if level_methods.include? message.to_s
|
447
|
+
true
|
448
|
+
else
|
449
|
+
super
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
# Dynamic level method dispatch. See Severity for details.
|
454
|
+
def method_missing(method, *args, &block)
|
455
|
+
case method.to_s
|
456
|
+
when /\A(#{level_re})\?\z/i
|
457
|
+
level?($1, *args, &block)
|
458
|
+
when /\A(#{level_re})\z/i
|
459
|
+
add(levels[$1.upcase], nil, *args, &block)
|
460
|
+
else
|
461
|
+
super
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
# Closes the underlying log.
|
466
|
+
def close
|
467
|
+
@logdev.close unless @logdev.nil?
|
468
|
+
end
|
469
|
+
|
470
|
+
#######
|
471
|
+
private
|
472
|
+
#######
|
473
|
+
|
474
|
+
# Fetches a level from Severity by +name+.
|
475
|
+
def level_by_name(name)
|
476
|
+
Severity.const_get(name.to_s.upcase)
|
477
|
+
end
|
478
|
+
|
479
|
+
# Builds a Hash of all levels used.
|
480
|
+
def levels
|
481
|
+
@levels ||= Hash[ *Severity.constants.
|
482
|
+
map { |name| [name, level_by_name(name)] }.
|
483
|
+
flatten ]
|
484
|
+
end
|
485
|
+
|
486
|
+
# Build the inverse Hash of levels by name.
|
487
|
+
def level_names
|
488
|
+
@level_names ||= levels.invert.merge(levels["UNKNOWN"] => "ANY")
|
489
|
+
end
|
490
|
+
|
491
|
+
# Builds an Array of level method names.
|
492
|
+
def level_methods
|
493
|
+
@level_methods ||= (levels.keys - ["UNKNOWN"]).map { |l| [l, "#{l}"] }.
|
494
|
+
flatten.
|
495
|
+
map { |l| l.downcase }
|
496
|
+
end
|
497
|
+
|
498
|
+
# Builds a Regexp that will match level method names.
|
499
|
+
def level_re
|
500
|
+
@level_re ||= levels.keys.map { |name| Regexp.escape(name) }.join("|")
|
501
|
+
end
|
502
|
+
|
503
|
+
# Returns the minimum level.
|
504
|
+
def min_level
|
505
|
+
@min_level ||= levels.values.min
|
506
|
+
end
|
507
|
+
|
508
|
+
# Returns the maximum level.
|
509
|
+
def max_level
|
510
|
+
@max_level ||= levels.values.min
|
511
|
+
end
|
512
|
+
end
|
513
|
+
end
|