scout_agent 3.0.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/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
|