hq-log-monitor-server 0.0.0 → 0.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/bin/hq-log-monitor-server +2 -2
- data/features/support/env.rb +1 -1
- data/features/support/steps.rb +1 -1
- data/lib/hq/log-monitor-server/script.rb +687 -0
- metadata +3 -4
- data/lib/hq/systools/monitoring/log-monitor-client-script.rb +0 -396
- data/lib/hq/systools/monitoring/log-monitor-server-script.rb +0 -299
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hq-log-monitor-server
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-04-02 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bson_ext
|
@@ -211,8 +211,7 @@ executables:
|
|
211
211
|
extensions: []
|
212
212
|
extra_rdoc_files: []
|
213
213
|
files:
|
214
|
-
- lib/hq/
|
215
|
-
- lib/hq/systools/monitoring/log-monitor-server-script.rb
|
214
|
+
- lib/hq/log-monitor-server/script.rb
|
216
215
|
- features/submit-event.feature
|
217
216
|
- features/overview.feature
|
218
217
|
- features/support/steps.rb
|
@@ -1,396 +0,0 @@
|
|
1
|
-
require "hq/tools/getopt"
|
2
|
-
require "net/http"
|
3
|
-
require "multi_json"
|
4
|
-
require "xml"
|
5
|
-
|
6
|
-
module HQ
|
7
|
-
module SysTools
|
8
|
-
module Monitoring
|
9
|
-
class LogMonitorClientScript
|
10
|
-
|
11
|
-
attr_accessor :args
|
12
|
-
attr_accessor :status
|
13
|
-
|
14
|
-
attr_accessor :stdout
|
15
|
-
attr_accessor :stderr
|
16
|
-
|
17
|
-
def main
|
18
|
-
process_args
|
19
|
-
read_config
|
20
|
-
read_cache
|
21
|
-
perform_checks
|
22
|
-
write_cache
|
23
|
-
end
|
24
|
-
|
25
|
-
def process_args
|
26
|
-
|
27
|
-
@opts, @args =
|
28
|
-
Tools::Getopt.process @args, [
|
29
|
-
|
30
|
-
{ :name => :config,
|
31
|
-
:required => true },
|
32
|
-
|
33
|
-
]
|
34
|
-
|
35
|
-
@args.empty? \
|
36
|
-
or raise "Extra args on command line"
|
37
|
-
|
38
|
-
end
|
39
|
-
|
40
|
-
def read_config
|
41
|
-
|
42
|
-
config_doc =
|
43
|
-
XML::Document.file @opts[:config]
|
44
|
-
|
45
|
-
@config_elem =
|
46
|
-
config_doc.root
|
47
|
-
|
48
|
-
@cache_elem =
|
49
|
-
@config_elem.find_first("cache")
|
50
|
-
|
51
|
-
@client_elem =
|
52
|
-
@config_elem.find_first("client")
|
53
|
-
|
54
|
-
@server_elem =
|
55
|
-
@config_elem.find_first("server")
|
56
|
-
|
57
|
-
@service_elems =
|
58
|
-
@config_elem.find("service").to_a
|
59
|
-
|
60
|
-
end
|
61
|
-
|
62
|
-
def read_cache
|
63
|
-
|
64
|
-
cache_path = @cache_elem["path"]
|
65
|
-
|
66
|
-
if File.exist? cache_path
|
67
|
-
|
68
|
-
@cache =
|
69
|
-
YAML.load File.read cache_path
|
70
|
-
|
71
|
-
else
|
72
|
-
|
73
|
-
@cache = {
|
74
|
-
files: {},
|
75
|
-
}
|
76
|
-
|
77
|
-
end
|
78
|
-
|
79
|
-
end
|
80
|
-
|
81
|
-
def write_cache
|
82
|
-
|
83
|
-
cache_path = @cache_elem["path"]
|
84
|
-
cache_temp_path = "#{cache_path}.new"
|
85
|
-
|
86
|
-
File.open cache_temp_path, "w" do
|
87
|
-
|cache_temp_io|
|
88
|
-
|
89
|
-
cache_temp_io.write YAML.dump @cache
|
90
|
-
cache_temp_io.fsync
|
91
|
-
|
92
|
-
end
|
93
|
-
|
94
|
-
File.rename cache_temp_path, cache_path
|
95
|
-
|
96
|
-
end
|
97
|
-
|
98
|
-
def perform_checks
|
99
|
-
|
100
|
-
@service_elems.each do
|
101
|
-
|service_elem|
|
102
|
-
|
103
|
-
fileset_elems = service_elem.find("fileset").to_a
|
104
|
-
|
105
|
-
fileset_elems.each do
|
106
|
-
|fileset_elem|
|
107
|
-
|
108
|
-
scan_elems = fileset_elem.find("scan").to_a
|
109
|
-
match_elems = fileset_elem.find("match").to_a
|
110
|
-
|
111
|
-
max_before =
|
112
|
-
match_elems.map {
|
113
|
-
|match_elem|
|
114
|
-
(match_elem["before"] || 0).to_i
|
115
|
-
}.max
|
116
|
-
|
117
|
-
max_after =
|
118
|
-
match_elems.map {
|
119
|
-
|match_elem|
|
120
|
-
(match_elem["after"] || 0).to_i
|
121
|
-
}.max
|
122
|
-
|
123
|
-
# find files
|
124
|
-
|
125
|
-
file_names =
|
126
|
-
scan_elems.map {
|
127
|
-
|scan_elem|
|
128
|
-
Dir[scan_elem["glob"]]
|
129
|
-
}.flatten
|
130
|
-
|
131
|
-
# scan files
|
132
|
-
|
133
|
-
file_names.each do
|
134
|
-
|file_name|
|
135
|
-
|
136
|
-
file_mtime = File.mtime file_name
|
137
|
-
file_size = File.size file_name
|
138
|
-
|
139
|
-
# fast check for modified files
|
140
|
-
|
141
|
-
cache_file = @cache[:files][file_name]
|
142
|
-
|
143
|
-
if cache_file &&
|
144
|
-
file_mtime == cache_file[:mtime] &&
|
145
|
-
file_size == cache_file[:size]
|
146
|
-
next
|
147
|
-
end
|
148
|
-
|
149
|
-
# scan the file for matching lines
|
150
|
-
|
151
|
-
mode = cache_file ? :scan : :report
|
152
|
-
|
153
|
-
File.open file_name, "r" do
|
154
|
-
|file_io|
|
155
|
-
|
156
|
-
file_reader =
|
157
|
-
ContextReader.new \
|
158
|
-
file_io,
|
159
|
-
max_before + max_after + 1
|
160
|
-
|
161
|
-
file_hash = 0
|
162
|
-
|
163
|
-
# check if the file has changed
|
164
|
-
|
165
|
-
if cache_file
|
166
|
-
|
167
|
-
if file_size < cache_file[:size]
|
168
|
-
|
169
|
-
changed = true
|
170
|
-
|
171
|
-
else
|
172
|
-
|
173
|
-
changed = false
|
174
|
-
|
175
|
-
cache_file[:lines].times do
|
176
|
-
|
177
|
-
line = file_reader.gets
|
178
|
-
|
179
|
-
unless line
|
180
|
-
changed = true
|
181
|
-
break
|
182
|
-
end
|
183
|
-
|
184
|
-
file_hash = [ file_hash, line.hash ].hash
|
185
|
-
|
186
|
-
end
|
187
|
-
|
188
|
-
if file_hash != cache_file[:hash]
|
189
|
-
changed = true
|
190
|
-
end
|
191
|
-
|
192
|
-
end
|
193
|
-
|
194
|
-
end
|
195
|
-
|
196
|
-
# go back to start if it changed
|
197
|
-
|
198
|
-
if changed
|
199
|
-
file_io.seek 0
|
200
|
-
file_reader.reset
|
201
|
-
file_hash = 0
|
202
|
-
end
|
203
|
-
|
204
|
-
# scan the new part of the file
|
205
|
-
|
206
|
-
while line = file_reader.gets
|
207
|
-
|
208
|
-
file_hash = [ file_hash, line.hash ].hash
|
209
|
-
|
210
|
-
# check for a match
|
211
|
-
|
212
|
-
match_elem =
|
213
|
-
match_elems.find {
|
214
|
-
|match_elem|
|
215
|
-
line =~ /#{match_elem["regex"]}/
|
216
|
-
}
|
217
|
-
|
218
|
-
# report the match
|
219
|
-
|
220
|
-
if match_elem
|
221
|
-
|
222
|
-
# get context
|
223
|
-
|
224
|
-
lines_before =
|
225
|
-
file_reader.lines_before \
|
226
|
-
(match_elem["before"] || 0).to_i + 1
|
227
|
-
|
228
|
-
lines_before.pop
|
229
|
-
|
230
|
-
lines_after =
|
231
|
-
file_reader.lines_after \
|
232
|
-
(match_elem["after"] || 0).to_i
|
233
|
-
|
234
|
-
# send event
|
235
|
-
|
236
|
-
submit_event({
|
237
|
-
type: match_elem["type"],
|
238
|
-
source: {
|
239
|
-
class: @client_elem["class"],
|
240
|
-
host: @client_elem["host"],
|
241
|
-
service: service_elem["name"],
|
242
|
-
},
|
243
|
-
location: {
|
244
|
-
file: file_name,
|
245
|
-
line: file_reader.last_line_number,
|
246
|
-
},
|
247
|
-
lines: {
|
248
|
-
before: lines_before,
|
249
|
-
matching: line,
|
250
|
-
after: lines_after,
|
251
|
-
},
|
252
|
-
})
|
253
|
-
|
254
|
-
end
|
255
|
-
|
256
|
-
end
|
257
|
-
|
258
|
-
# save the file's current info in the cache
|
259
|
-
|
260
|
-
@cache[:files][file_name] = {
|
261
|
-
mtime: file_mtime,
|
262
|
-
size: file_size,
|
263
|
-
lines: file_reader.next_line_number,
|
264
|
-
hash: file_hash,
|
265
|
-
}
|
266
|
-
|
267
|
-
end
|
268
|
-
|
269
|
-
end
|
270
|
-
|
271
|
-
end
|
272
|
-
|
273
|
-
end
|
274
|
-
|
275
|
-
end
|
276
|
-
|
277
|
-
def submit_event event
|
278
|
-
|
279
|
-
url =
|
280
|
-
URI.parse @server_elem["url"]
|
281
|
-
|
282
|
-
http =
|
283
|
-
Net::HTTP.new url.host, url.port
|
284
|
-
|
285
|
-
request =
|
286
|
-
Net::HTTP::Post.new url.path
|
287
|
-
|
288
|
-
request.body =
|
289
|
-
MultiJson.dump event
|
290
|
-
|
291
|
-
response =
|
292
|
-
http.request request
|
293
|
-
|
294
|
-
end
|
295
|
-
|
296
|
-
class ContextReader
|
297
|
-
|
298
|
-
def initialize source, buffer_size
|
299
|
-
|
300
|
-
@source = source
|
301
|
-
@buffer_size = buffer_size
|
302
|
-
|
303
|
-
@buffer = Array.new @buffer_size
|
304
|
-
|
305
|
-
reset
|
306
|
-
|
307
|
-
end
|
308
|
-
|
309
|
-
def lines_before_count
|
310
|
-
return @buffer_cursor - @buffer_start
|
311
|
-
end
|
312
|
-
|
313
|
-
def lines_after_count
|
314
|
-
return @buffer_end - @buffer_cursor
|
315
|
-
end
|
316
|
-
|
317
|
-
def lines_before count
|
318
|
-
count = [ count, lines_before_count ].min
|
319
|
-
return (0...count).map {
|
320
|
-
|i| @buffer[(@buffer_cursor - count + i) % @buffer_size]
|
321
|
-
}
|
322
|
-
end
|
323
|
-
|
324
|
-
def lines_after count
|
325
|
-
count = [ count, @buffer_size ].min
|
326
|
-
while lines_after_count < count
|
327
|
-
read_next_line or break
|
328
|
-
end
|
329
|
-
count = [ count, @buffer_end - @buffer_cursor].min
|
330
|
-
return (0...count).map {
|
331
|
-
|i| @buffer[(@buffer_cursor + i) % @buffer_size]
|
332
|
-
}
|
333
|
-
end
|
334
|
-
|
335
|
-
def read_next_line
|
336
|
-
|
337
|
-
# read a line
|
338
|
-
|
339
|
-
line = @source.gets
|
340
|
-
return false unless line
|
341
|
-
|
342
|
-
line.strip!
|
343
|
-
line.freeze
|
344
|
-
|
345
|
-
# shrink buffer if full
|
346
|
-
|
347
|
-
if @buffer_end - @buffer_start == @buffer_size
|
348
|
-
@buffer_start += 1
|
349
|
-
end
|
350
|
-
|
351
|
-
# add line to buffer
|
352
|
-
|
353
|
-
@buffer[@buffer_end % @buffer_size] = line
|
354
|
-
@buffer_end += 1
|
355
|
-
|
356
|
-
return true
|
357
|
-
|
358
|
-
end
|
359
|
-
|
360
|
-
def gets
|
361
|
-
|
362
|
-
# make sure the next line is in the buffer
|
363
|
-
|
364
|
-
if lines_after_count == 0
|
365
|
-
read_next_line or return nil
|
366
|
-
end
|
367
|
-
|
368
|
-
# return the line, advancing the cursor
|
369
|
-
|
370
|
-
ret = @buffer[@buffer_cursor % @buffer_size]
|
371
|
-
@buffer_cursor += 1
|
372
|
-
return ret
|
373
|
-
|
374
|
-
end
|
375
|
-
|
376
|
-
def last_line_number
|
377
|
-
raise "No last line" unless @buffer_cursor > 0
|
378
|
-
@buffer_cursor - 1
|
379
|
-
end
|
380
|
-
|
381
|
-
def next_line_number
|
382
|
-
@buffer_cursor
|
383
|
-
end
|
384
|
-
|
385
|
-
def reset
|
386
|
-
@buffer_start = 0
|
387
|
-
@buffer_cursor = 0
|
388
|
-
@buffer_end = 0
|
389
|
-
end
|
390
|
-
|
391
|
-
end
|
392
|
-
|
393
|
-
end
|
394
|
-
end
|
395
|
-
end
|
396
|
-
end
|
@@ -1,299 +0,0 @@
|
|
1
|
-
require "mongo"
|
2
|
-
require "multi_json"
|
3
|
-
require "rack"
|
4
|
-
require "webrick"
|
5
|
-
require "xml"
|
6
|
-
|
7
|
-
require "hq/tools/escape"
|
8
|
-
require "hq/tools/getopt"
|
9
|
-
|
10
|
-
module HQ
|
11
|
-
module SysTools
|
12
|
-
module Monitoring
|
13
|
-
class LogMonitorServerScript
|
14
|
-
|
15
|
-
include Tools::Escape
|
16
|
-
|
17
|
-
attr_accessor :args
|
18
|
-
attr_accessor :status
|
19
|
-
|
20
|
-
def initialize
|
21
|
-
@status = 0
|
22
|
-
end
|
23
|
-
|
24
|
-
def main
|
25
|
-
setup
|
26
|
-
trap "INT" do
|
27
|
-
@web_server.shutdown
|
28
|
-
end
|
29
|
-
run
|
30
|
-
end
|
31
|
-
|
32
|
-
def start
|
33
|
-
setup
|
34
|
-
Thread.new { run }
|
35
|
-
end
|
36
|
-
|
37
|
-
def stop
|
38
|
-
@web_server.shutdown
|
39
|
-
end
|
40
|
-
|
41
|
-
def setup
|
42
|
-
process_args
|
43
|
-
read_config
|
44
|
-
connect_db
|
45
|
-
init_server
|
46
|
-
end
|
47
|
-
|
48
|
-
def run
|
49
|
-
@web_server.start
|
50
|
-
end
|
51
|
-
|
52
|
-
def process_args
|
53
|
-
|
54
|
-
@opts, @args =
|
55
|
-
Tools::Getopt.process @args, [
|
56
|
-
|
57
|
-
{ :name => :config,
|
58
|
-
:required => true },
|
59
|
-
|
60
|
-
{ :name => :quiet,
|
61
|
-
:boolean => true },
|
62
|
-
|
63
|
-
]
|
64
|
-
|
65
|
-
@args.empty? \
|
66
|
-
or raise "Extra args on command line"
|
67
|
-
|
68
|
-
end
|
69
|
-
|
70
|
-
def read_config
|
71
|
-
|
72
|
-
config_doc =
|
73
|
-
XML::Document.file @opts[:config]
|
74
|
-
|
75
|
-
@config_elem =
|
76
|
-
config_doc.root
|
77
|
-
|
78
|
-
@server_elem =
|
79
|
-
@config_elem.find_first("server")
|
80
|
-
|
81
|
-
@db_elem =
|
82
|
-
@config_elem.find_first("db")
|
83
|
-
|
84
|
-
end
|
85
|
-
|
86
|
-
def connect_db
|
87
|
-
|
88
|
-
@mongo =
|
89
|
-
Mongo::MongoClient.new \
|
90
|
-
@db_elem["host"],
|
91
|
-
@db_elem["port"].to_i
|
92
|
-
|
93
|
-
@db =
|
94
|
-
@mongo[@db_elem["name"]]
|
95
|
-
|
96
|
-
end
|
97
|
-
|
98
|
-
def init_server
|
99
|
-
|
100
|
-
@web_config = {
|
101
|
-
:Port => @server_elem["port"].to_i,
|
102
|
-
:AccessLog => [],
|
103
|
-
}
|
104
|
-
|
105
|
-
if @opts[:quiet]
|
106
|
-
@web_config.merge!({
|
107
|
-
:Logger => WEBrick::Log::new("/dev/null", 7),
|
108
|
-
:DoNotReverseLookup => true,
|
109
|
-
})
|
110
|
-
end
|
111
|
-
|
112
|
-
@web_server =
|
113
|
-
WEBrick::HTTPServer.new \
|
114
|
-
@web_config
|
115
|
-
|
116
|
-
@web_server.mount "/", Rack::Handler::WEBrick, self
|
117
|
-
|
118
|
-
end
|
119
|
-
|
120
|
-
def call env
|
121
|
-
|
122
|
-
case env["PATH_INFO"]
|
123
|
-
|
124
|
-
when "/submit-log-event"
|
125
|
-
submit_log_event env
|
126
|
-
|
127
|
-
when "/"
|
128
|
-
overview env
|
129
|
-
|
130
|
-
when "/favicon.ico"
|
131
|
-
[ 404, {}, [] ]
|
132
|
-
|
133
|
-
else
|
134
|
-
raise "Not found: #{env["PATH_INFO"]}"
|
135
|
-
|
136
|
-
end
|
137
|
-
|
138
|
-
end
|
139
|
-
|
140
|
-
def submit_log_event env
|
141
|
-
|
142
|
-
# decode it
|
143
|
-
|
144
|
-
event = MultiJson.load env["rack.input"].read
|
145
|
-
|
146
|
-
# add a timestamp
|
147
|
-
|
148
|
-
event["timestamp"] = Time.now
|
149
|
-
|
150
|
-
# insert it
|
151
|
-
|
152
|
-
@db["events"].insert event
|
153
|
-
|
154
|
-
# update summary
|
155
|
-
|
156
|
-
summary =
|
157
|
-
@db["summaries"].find({
|
158
|
-
"_id" => event["source"],
|
159
|
-
}).first
|
160
|
-
|
161
|
-
summary ||= {
|
162
|
-
"_id" => event["source"],
|
163
|
-
"combined" => { "new" => 0, "total" => 0 },
|
164
|
-
"types" => {},
|
165
|
-
}
|
166
|
-
|
167
|
-
summary["types"][event["type"]] ||=
|
168
|
-
{ "new" => 0, "total" => 0 }
|
169
|
-
|
170
|
-
summary["types"][event["type"]]["new"] += 1
|
171
|
-
summary["types"][event["type"]]["total"] += 1
|
172
|
-
|
173
|
-
summary["combined"]["new"] += 1
|
174
|
-
summary["combined"]["total"] += 1
|
175
|
-
|
176
|
-
@db["summaries"].save summary
|
177
|
-
|
178
|
-
# respond successfully
|
179
|
-
|
180
|
-
return 202, {}, []
|
181
|
-
|
182
|
-
end
|
183
|
-
|
184
|
-
def overview env
|
185
|
-
|
186
|
-
summaries = @db["summaries"].find.to_a
|
187
|
-
|
188
|
-
headers = {}
|
189
|
-
html = []
|
190
|
-
|
191
|
-
headers["content-type"] = "text/html"
|
192
|
-
|
193
|
-
html << "<! DOCTYPE html>\n"
|
194
|
-
html << "<html>\n"
|
195
|
-
html << "<head>\n"
|
196
|
-
|
197
|
-
html << "<title>Overview - Log monitor</title>\n"
|
198
|
-
|
199
|
-
html << "</head>\n"
|
200
|
-
html << "<body>\n"
|
201
|
-
|
202
|
-
html << "<h1>Overview - Log monitor</h1>\n"
|
203
|
-
|
204
|
-
if summaries.empty?
|
205
|
-
html << "<p>No events have been logged</p>\n"
|
206
|
-
else
|
207
|
-
|
208
|
-
html << "<table id=\"summaries\">\n"
|
209
|
-
html << "<thead>\n"
|
210
|
-
|
211
|
-
html << "<tr>\n"
|
212
|
-
html << "<th>Service</th>\n"
|
213
|
-
html << "<th>Alerts</th>\n"
|
214
|
-
html << "<th>Details</th>\n"
|
215
|
-
html << "<th>More</th>\n"
|
216
|
-
html << "</tr>\n"
|
217
|
-
|
218
|
-
html << "</thead>\n"
|
219
|
-
html << "<tbody>\n"
|
220
|
-
|
221
|
-
summaries.each do
|
222
|
-
|summary|
|
223
|
-
|
224
|
-
html << "<tr class=\"summary\">\n"
|
225
|
-
|
226
|
-
html << "<td class=\"service\">%s</td>\n" % [
|
227
|
-
esc_ht(summary["_id"]["service"]),
|
228
|
-
]
|
229
|
-
|
230
|
-
html << "<td class=\"alerts\">%s</td>\n" % [
|
231
|
-
esc_ht(summary["combined"]["new"].to_s),
|
232
|
-
]
|
233
|
-
|
234
|
-
html << "<td class=\"detail\">%s</td>\n" % [
|
235
|
-
esc_ht(
|
236
|
-
summary["types"].map {
|
237
|
-
|type, counts|
|
238
|
-
"%s %s" % [ counts["new"], type ]
|
239
|
-
}.join ", "
|
240
|
-
),
|
241
|
-
]
|
242
|
-
|
243
|
-
html << "<td class=\"more\">%s</td>\n" % [
|
244
|
-
"<a href=\"%s\">more...</a>" % [
|
245
|
-
"/service/%s" % [
|
246
|
-
esc_ue(summary["_id"]["service"]),
|
247
|
-
],
|
248
|
-
],
|
249
|
-
]
|
250
|
-
|
251
|
-
html << "</tr>\n"
|
252
|
-
|
253
|
-
end
|
254
|
-
|
255
|
-
html << "</tbody>\n"
|
256
|
-
html << "</table>\n"
|
257
|
-
|
258
|
-
end
|
259
|
-
|
260
|
-
html << "</body>\n"
|
261
|
-
html << "</html>\n"
|
262
|
-
|
263
|
-
return 200, headers, html
|
264
|
-
|
265
|
-
end
|
266
|
-
|
267
|
-
def sf format, *args
|
268
|
-
|
269
|
-
ret = []
|
270
|
-
|
271
|
-
format.scan(/%.|%%|[^%]+|%/).each do
|
272
|
-
|match|
|
273
|
-
|
274
|
-
case match
|
275
|
-
|
276
|
-
when ?%
|
277
|
-
raise "Error"
|
278
|
-
|
279
|
-
when "%%"
|
280
|
-
ret << ?%
|
281
|
-
|
282
|
-
when /^%(.)$/
|
283
|
-
ret << send("format_#{$1}", args.shift)
|
284
|
-
|
285
|
-
else
|
286
|
-
ret << match
|
287
|
-
|
288
|
-
end
|
289
|
-
|
290
|
-
end
|
291
|
-
|
292
|
-
return ret.join
|
293
|
-
|
294
|
-
end
|
295
|
-
|
296
|
-
end
|
297
|
-
end
|
298
|
-
end
|
299
|
-
end
|