fluentd 0.10.44 → 0.10.45
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of fluentd might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/ChangeLog +9 -0
- data/fluent.conf +9 -3
- data/fluentd.gemspec +1 -0
- data/lib/fluent/engine.rb +14 -5
- data/lib/fluent/load.rb +5 -1
- data/lib/fluent/log.rb +4 -4
- data/lib/fluent/parser.rb +80 -0
- data/lib/fluent/plugin/exec_util.rb +38 -2
- data/lib/fluent/plugin/in_forward.rb +5 -3
- data/lib/fluent/plugin/in_tail.rb +661 -1
- data/lib/fluent/plugin/out_exec.rb +29 -15
- data/lib/fluent/plugin/out_exec_filter.rb +8 -44
- data/lib/fluent/version.rb +1 -1
- data/test/parser.rb +81 -0
- data/test/plugin/data/2010/01/20100102-030405.log +0 -0
- data/test/plugin/data/2010/01/20100102-030406.log +0 -0
- data/test/plugin/data/2010/01/20100102.log +0 -0
- data/test/plugin/data/log/bar +0 -0
- data/test/plugin/data/log/foo/bar.log +0 -0
- data/test/plugin/data/log/test.log +0 -0
- data/test/plugin/in_tail.rb +236 -3
- data/test/plugin/out_exec.rb +44 -9
- metadata +70 -69
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: df6457fef8673bd624e1baa875292fd83ee71f56
|
4
|
+
data.tar.gz: 829ece938ca14738720a69d9642c3f24859a595a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ffe86b09fb5aef434fc97fda76ba8697d48aadd8954cea28285cdb419447800e46b1df868faa549185ba7d058dae7058bec0338b87a2330eebbb8bd5e5c76539
|
7
|
+
data.tar.gz: 3949343e2b31f9eef4e6edc045e5e84f58adc4db9546ea30966116ef9457897dd3f291bca75260407197a6952682f69cef8da8862f49841c7582695725419737
|
data/ChangeLog
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
Release 0.10.45 - 2014/03/28
|
2
|
+
|
3
|
+
* in_tail: Merge in_tail_ex and in_tail_multiline features
|
4
|
+
* in_forward: Add linger_timeout option
|
5
|
+
* out_exec: Support json and msgpack format
|
6
|
+
* Engine: Fix signal handling when receives SIGTERM before loop starts
|
7
|
+
* Use correct out_forward option in fluent.conf with --setup option
|
8
|
+
* Ignore sigdump exception on signal unsupported environment
|
9
|
+
|
1
10
|
Release 0.10.44 - 2014/02/26
|
2
11
|
|
3
12
|
* in_forward: Print remote address and port in trace log
|
data/fluent.conf
CHANGED
@@ -56,9 +56,13 @@
|
|
56
56
|
# match tag=system.** and forward to another fluent server
|
57
57
|
<match system.**>
|
58
58
|
type forward
|
59
|
-
|
59
|
+
<server>
|
60
|
+
host 192.168.0.11
|
61
|
+
</server>
|
60
62
|
<secondary>
|
61
|
-
|
63
|
+
<server>
|
64
|
+
host 192.168.0.12
|
65
|
+
</server>
|
62
66
|
</secondary>
|
63
67
|
</match>
|
64
68
|
|
@@ -67,11 +71,13 @@
|
|
67
71
|
# type copy
|
68
72
|
# <store>
|
69
73
|
# type forward
|
70
|
-
# host 192.168.0.13
|
71
74
|
# buffer_type file
|
72
75
|
# buffer_path /var/log/fluent/myapp-forward
|
73
76
|
# retry_limit 50
|
74
77
|
# flush_interval 10s
|
78
|
+
# <server>
|
79
|
+
# host 192.168.0.13
|
80
|
+
# </server>
|
75
81
|
# </store>
|
76
82
|
# <store>
|
77
83
|
# type file
|
data/fluentd.gemspec
CHANGED
@@ -26,6 +26,7 @@ Gem::Specification.new do |gem|
|
|
26
26
|
gem.add_runtime_dependency("sigdump", ["~> 0.2.2"])
|
27
27
|
|
28
28
|
gem.add_development_dependency("rake", [">= 0.9.2"])
|
29
|
+
gem.add_development_dependency("flexmock")
|
29
30
|
gem.add_development_dependency("parallel_tests", [">= 0.15.3"])
|
30
31
|
gem.add_development_dependency("rr", [">= 1.0.0"])
|
31
32
|
gem.add_development_dependency("timecop", [">= 0.3.0"])
|
data/lib/fluent/engine.rb
CHANGED
@@ -24,6 +24,7 @@ module Fluent
|
|
24
24
|
@match_cache_keys = []
|
25
25
|
@started = []
|
26
26
|
@default_loop = nil
|
27
|
+
@engine_stopped = false
|
27
28
|
|
28
29
|
@log_emit_thread = nil
|
29
30
|
@log_event_loop_stop = false
|
@@ -208,11 +209,18 @@ module Fluent
|
|
208
209
|
@log_emit_thread = Thread.new(&method(:log_event_loop))
|
209
210
|
end
|
210
211
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
212
|
+
unless @engine_stopped
|
213
|
+
# for empty loop
|
214
|
+
@default_loop = Coolio::Loop.default
|
215
|
+
@default_loop.attach Coolio::TimerWatcher.new(1, true)
|
216
|
+
# TODO attach async watch for thread pool
|
217
|
+
@default_loop.run
|
218
|
+
end
|
219
|
+
|
220
|
+
if @engine_stopped and @default_loop
|
221
|
+
@default_loop.stop
|
222
|
+
@default_loop = nil
|
223
|
+
end
|
216
224
|
|
217
225
|
rescue => e
|
218
226
|
$log.error "unexpected error", :error_class=>e.class, :error=>e
|
@@ -228,6 +236,7 @@ module Fluent
|
|
228
236
|
end
|
229
237
|
|
230
238
|
def stop
|
239
|
+
@engine_stopped = true
|
231
240
|
if @default_loop
|
232
241
|
@default_loop.stop
|
233
242
|
@default_loop = nil
|
data/lib/fluent/load.rb
CHANGED
@@ -9,7 +9,11 @@ require 'json'
|
|
9
9
|
require 'yajl'
|
10
10
|
require 'uri'
|
11
11
|
require 'msgpack'
|
12
|
-
|
12
|
+
begin
|
13
|
+
require 'sigdump/setup'
|
14
|
+
rescue
|
15
|
+
# ignore setup error on Win or similar platform which doesn't support signal
|
16
|
+
end
|
13
17
|
# I hate global variable but we suffer pain now for the sake of future.
|
14
18
|
# We will switch to MessagePack 0.5 and deprecate 0.4.
|
15
19
|
$use_msgpack_5 = defined?(MessagePack::Packer) ? true : false
|
data/lib/fluent/log.rb
CHANGED
@@ -265,6 +265,10 @@ module Fluent
|
|
265
265
|
end
|
266
266
|
}
|
267
267
|
|
268
|
+
map.each_pair {|k,v|
|
269
|
+
message << " #{k}=#{v.inspect}"
|
270
|
+
}
|
271
|
+
|
268
272
|
unless @threads_exclude_events.include?(Thread.current)
|
269
273
|
record = map.dup
|
270
274
|
record.keys.each {|key|
|
@@ -274,10 +278,6 @@ module Fluent
|
|
274
278
|
Engine.push_log_event("#{@tag}.#{level}", time.to_i, record)
|
275
279
|
end
|
276
280
|
|
277
|
-
map.each_pair {|k,v|
|
278
|
-
message << " #{k}=#{v.inspect}"
|
279
|
-
}
|
280
|
-
|
281
281
|
return time, message
|
282
282
|
end
|
283
283
|
|
data/lib/fluent/parser.rb
CHANGED
@@ -367,6 +367,83 @@ module Fluent
|
|
367
367
|
end
|
368
368
|
end
|
369
369
|
|
370
|
+
class MultilineParser
|
371
|
+
include Configurable
|
372
|
+
|
373
|
+
config_param :format_firstline, :string, :default => nil
|
374
|
+
|
375
|
+
FORMAT_MAX_NUM = 20
|
376
|
+
|
377
|
+
def configure(conf)
|
378
|
+
super
|
379
|
+
|
380
|
+
formats = parse_formats(conf).compact.map { |f| f[1..-2] }.join
|
381
|
+
begin
|
382
|
+
@regex = Regexp.new(formats, Regexp::MULTILINE)
|
383
|
+
if @regex.named_captures.empty?
|
384
|
+
raise "No named captures"
|
385
|
+
end
|
386
|
+
@parser = RegexpParser.new(@regex, conf)
|
387
|
+
rescue => e
|
388
|
+
raise ConfigError, "Invalid regexp '#{formats}': #{e}"
|
389
|
+
end
|
390
|
+
|
391
|
+
if @format_firstline
|
392
|
+
check_format_regexp(@format_firstline, 'format_firstline')
|
393
|
+
@regex = Regexp.new(@format_firstline[1..-2])
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
def call(text)
|
398
|
+
@parser.call(text)
|
399
|
+
end
|
400
|
+
|
401
|
+
def firstline?(text)
|
402
|
+
@regex.match(text)
|
403
|
+
end
|
404
|
+
|
405
|
+
private
|
406
|
+
|
407
|
+
def parse_formats(conf)
|
408
|
+
check_format_range(conf)
|
409
|
+
|
410
|
+
prev_format = nil
|
411
|
+
(1..FORMAT_MAX_NUM).map { |i|
|
412
|
+
format = conf["format#{i}"]
|
413
|
+
if (i > 1) && prev_format.nil? && !format.nil?
|
414
|
+
raise ConfigError, "Jump of format index found. format#{i - 1} is missing."
|
415
|
+
end
|
416
|
+
prev_format = format
|
417
|
+
next if format.nil?
|
418
|
+
|
419
|
+
check_format_regexp(format, "format#{i}")
|
420
|
+
format
|
421
|
+
}
|
422
|
+
end
|
423
|
+
|
424
|
+
def check_format_range(conf)
|
425
|
+
invalid_formats = conf.keys.select { |k|
|
426
|
+
m = k.match(/^format(\d+)$/)
|
427
|
+
m ? !((1..FORMAT_MAX_NUM).include?(m[1].to_i)) : false
|
428
|
+
}
|
429
|
+
unless invalid_formats.empty?
|
430
|
+
raise ConfigError, "Invalid formatN found. N should be 1 - #{FORMAT_MAX_NUM}: " + invalid_formats.join(",")
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
def check_format_regexp(format, key)
|
435
|
+
if format[0] == '/' && format[-1] == '/'
|
436
|
+
begin
|
437
|
+
Regexp.new(format[1..-2], Regexp::MULTILINE)
|
438
|
+
rescue => e
|
439
|
+
raise ConfigError, "Invalid regexp in #{key}: #{e}"
|
440
|
+
end
|
441
|
+
else
|
442
|
+
raise ConfigError, "format should be Regexp, need //, in #{key}: '#{format}'"
|
443
|
+
end
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
370
447
|
TEMPLATE_FACTORIES = {
|
371
448
|
'apache' => Proc.new { RegexpParser.new(/^(?<host>[^ ]*) [^ ]* (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^ ]*) +\S*)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$/, {'time_format'=>"%d/%b/%Y:%H:%M:%S %z"}) },
|
372
449
|
'apache2' => Proc.new { ApacheParser.new },
|
@@ -377,6 +454,7 @@ module Fluent
|
|
377
454
|
'csv' => Proc.new { CSVParser.new },
|
378
455
|
'nginx' => Proc.new { RegexpParser.new(/^(?<remote>[^ ]*) (?<host>[^ ]*) (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^\"]*) +\S*)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$/, {'time_format'=>"%d/%b/%Y:%H:%M:%S %z"}) },
|
379
456
|
'none' => Proc.new { NoneParser.new },
|
457
|
+
'multiline' => Proc.new { MultilineParser.new },
|
380
458
|
}
|
381
459
|
|
382
460
|
def self.register_template(name, regexp_or_proc, time_format=nil)
|
@@ -394,6 +472,8 @@ module Fluent
|
|
394
472
|
@parser = nil
|
395
473
|
end
|
396
474
|
|
475
|
+
attr_reader :parser
|
476
|
+
|
397
477
|
def configure(conf, required=true)
|
398
478
|
format = conf['format']
|
399
479
|
|
@@ -1,5 +1,11 @@
|
|
1
1
|
module Fluent
|
2
2
|
module ExecUtil
|
3
|
+
SUPPORTED_FORMAT = {
|
4
|
+
'tsv' => :tsv,
|
5
|
+
'json' => :json,
|
6
|
+
'msgpack' => :msgpack,
|
7
|
+
}
|
8
|
+
|
3
9
|
class Parser
|
4
10
|
def initialize(on_message)
|
5
11
|
@on_message = on_message
|
@@ -43,7 +49,37 @@ module Fluent
|
|
43
49
|
end
|
44
50
|
end
|
45
51
|
end
|
46
|
-
end
|
47
|
-
end
|
48
52
|
|
53
|
+
class Formatter
|
54
|
+
end
|
49
55
|
|
56
|
+
class TSVFormatter < Formatter
|
57
|
+
def initialize(in_keys)
|
58
|
+
@in_keys = in_keys
|
59
|
+
super()
|
60
|
+
end
|
61
|
+
|
62
|
+
def call(record, out)
|
63
|
+
last = @in_keys.length-1
|
64
|
+
for i in 0..last
|
65
|
+
key = @in_keys[i]
|
66
|
+
out << record[key].to_s
|
67
|
+
out << "\t" if i != last
|
68
|
+
end
|
69
|
+
out << "\n"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class JSONFormatter < Formatter
|
74
|
+
def call(record, out)
|
75
|
+
out << Yajl.dump(record) << "\n"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class MessagePackFormatter < Formatter
|
80
|
+
def call(record, out)
|
81
|
+
record.to_msgpack(out)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -29,6 +29,8 @@ module Fluent
|
|
29
29
|
config_param :port, :integer, :default => DEFAULT_LISTEN_PORT
|
30
30
|
config_param :bind, :string, :default => '0.0.0.0'
|
31
31
|
config_param :backlog, :integer, :default => nil
|
32
|
+
# SO_LINGER 0 to send RST rather than FIN to avoid lots of connections sitting in TIME_WAIT at src
|
33
|
+
config_param :linger_timeout, :integer, :default => 0
|
32
34
|
|
33
35
|
def configure(conf)
|
34
36
|
super
|
@@ -64,7 +66,7 @@ module Fluent
|
|
64
66
|
|
65
67
|
def listen
|
66
68
|
log.info "listening fluent socket on #{@bind}:#{@port}"
|
67
|
-
s = Coolio::TCPServer.new(@bind, @port, Handler, log, method(:on_message))
|
69
|
+
s = Coolio::TCPServer.new(@bind, @port, Handler, @linger_timeout, log, method(:on_message))
|
68
70
|
s.listen(@backlog) unless @backlog.nil?
|
69
71
|
s
|
70
72
|
end
|
@@ -145,10 +147,10 @@ module Fluent
|
|
145
147
|
end
|
146
148
|
|
147
149
|
class Handler < Coolio::Socket
|
148
|
-
def initialize(io, log, on_message)
|
150
|
+
def initialize(io, linger_timeout, log, on_message)
|
149
151
|
super(io)
|
150
152
|
if io.is_a?(TCPSocket)
|
151
|
-
opt = [1,
|
153
|
+
opt = [1, linger_timeout].pack('I!I!') # { int l_onoff; int l_linger; }
|
152
154
|
io.setsockopt(Socket::SOL_SOCKET, Socket::SO_LINGER, opt)
|
153
155
|
end
|
154
156
|
@on_message = on_message
|
@@ -16,9 +16,669 @@
|
|
16
16
|
# limitations under the License.
|
17
17
|
#
|
18
18
|
module Fluent
|
19
|
-
class
|
19
|
+
class NewTailInput < Input
|
20
20
|
Plugin.register_input('tail', self)
|
21
21
|
|
22
|
+
def initialize
|
23
|
+
super
|
24
|
+
@paths = []
|
25
|
+
@tails = {}
|
26
|
+
end
|
27
|
+
|
28
|
+
config_param :path, :string
|
29
|
+
config_param :tag, :string
|
30
|
+
config_param :rotate_wait, :time, :default => 5
|
31
|
+
config_param :pos_file, :string, :default => nil
|
32
|
+
config_param :read_from_head, :bool, :default => false
|
33
|
+
config_param :refresh_interval, :time, :default => 60
|
34
|
+
|
35
|
+
attr_reader :paths
|
36
|
+
|
37
|
+
def configure(conf)
|
38
|
+
super
|
39
|
+
|
40
|
+
@paths = @path.split(',').map {|path| path.strip }
|
41
|
+
if @paths.empty?
|
42
|
+
raise ConfigError, "tail: 'path' parameter is required on tail input"
|
43
|
+
end
|
44
|
+
|
45
|
+
unless @pos_file
|
46
|
+
$log.warn "'pos_file PATH' parameter is not set to a 'tail' source."
|
47
|
+
$log.warn "this parameter is highly recommended to save the position to resume tailing."
|
48
|
+
end
|
49
|
+
|
50
|
+
configure_parser(conf)
|
51
|
+
configure_tag
|
52
|
+
|
53
|
+
@multiline_mode = conf['format'] == 'multiline'
|
54
|
+
@receive_handler = if @multiline_mode
|
55
|
+
method(:parse_multilines)
|
56
|
+
else
|
57
|
+
method(:parse_singleline)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def configure_parser(conf)
|
62
|
+
@parser = TextParser.new
|
63
|
+
@parser.configure(conf)
|
64
|
+
end
|
65
|
+
|
66
|
+
def configure_tag
|
67
|
+
if @tag.index('*')
|
68
|
+
@tag_prefix, @tag_suffix = @tag.split('*')
|
69
|
+
@tag_suffix ||= ''
|
70
|
+
else
|
71
|
+
@tag_prefix = nil
|
72
|
+
@tag_suffix = nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def start
|
77
|
+
if @pos_file
|
78
|
+
@pf_file = File.open(@pos_file, File::RDWR|File::CREAT, DEFAULT_FILE_PERMISSION)
|
79
|
+
@pf_file.sync = true
|
80
|
+
@pf = PositionFile.parse(@pf_file)
|
81
|
+
end
|
82
|
+
|
83
|
+
@loop = Coolio::Loop.new
|
84
|
+
refresh_watchers
|
85
|
+
|
86
|
+
@refresh_trigger = TailWatcher::TimerWatcher.new(@refresh_interval, true, log, &method(:refresh_watchers))
|
87
|
+
@refresh_trigger.attach(@loop)
|
88
|
+
@thread = Thread.new(&method(:run))
|
89
|
+
end
|
90
|
+
|
91
|
+
def shutdown
|
92
|
+
@refresh_trigger.detach if @refresh_trigger && @refresh_trigger.attached?
|
93
|
+
|
94
|
+
stop_watchers(@tails.keys, true)
|
95
|
+
@loop.stop rescue nil # when all watchers are detached, `stop` raises RuntimeError. We can ignore this exception.
|
96
|
+
@thread.join
|
97
|
+
@pf_file.close if @pf_file
|
98
|
+
end
|
99
|
+
|
100
|
+
def expand_paths
|
101
|
+
date = Time.now
|
102
|
+
paths = []
|
103
|
+
@paths.each { |path|
|
104
|
+
path = date.strftime(path)
|
105
|
+
if path.include?('*')
|
106
|
+
paths += Dir.glob(path)
|
107
|
+
else
|
108
|
+
# When file is not created yet, Dir.glob returns an empty array. So just add when path is static.
|
109
|
+
paths << path
|
110
|
+
end
|
111
|
+
}
|
112
|
+
paths
|
113
|
+
end
|
114
|
+
|
115
|
+
# in_tail with '*' path doesn't check rotation file equality at refresh phase.
|
116
|
+
# So you should not use '*' path when your logs will be rotated by another tool.
|
117
|
+
# It will cause log duplication after updated watch files.
|
118
|
+
# In such case, you should separate log directory and specify two paths in path parameter.
|
119
|
+
# e.g. path /path/to/dir/*,/path/to/rotated_logs/target_file
|
120
|
+
def refresh_watchers
|
121
|
+
target_paths = expand_paths
|
122
|
+
existence_paths = @tails.keys
|
123
|
+
|
124
|
+
unwatched = existence_paths - target_paths
|
125
|
+
added = target_paths - existence_paths
|
126
|
+
|
127
|
+
stop_watchers(unwatched, false, true) unless unwatched.empty?
|
128
|
+
start_watchers(added) unless added.empty?
|
129
|
+
end
|
130
|
+
|
131
|
+
def setup_watcher(path, pe)
|
132
|
+
tw = TailWatcher.new(path, @rotate_wait, pe, log, method(:update_watcher), &method(:receive_lines))
|
133
|
+
tw.attach(@loop)
|
134
|
+
tw
|
135
|
+
end
|
136
|
+
|
137
|
+
def start_watchers(paths)
|
138
|
+
paths.each { |path|
|
139
|
+
pe = nil
|
140
|
+
if @pf
|
141
|
+
pe = @pf[path]
|
142
|
+
if @read_from_head && pe.read_inode.zero?
|
143
|
+
pe.update(File::Stat.new(path).ino, 0)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
@tails[path] = setup_watcher(path, pe)
|
148
|
+
}
|
149
|
+
end
|
150
|
+
|
151
|
+
def stop_watchers(paths, immediate = false, unwatched = false)
|
152
|
+
paths.each { |path|
|
153
|
+
tw = @tails.delete(path)
|
154
|
+
if tw
|
155
|
+
tw.unwatched = unwatched
|
156
|
+
if immediate
|
157
|
+
close_watcher(tw)
|
158
|
+
else
|
159
|
+
close_watcher_after_rotate_wait(tw)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
}
|
163
|
+
end
|
164
|
+
|
165
|
+
# refresh_watchers calls @tails.keys so we don't use stop_watcher -> start_watcher sequence for safety.
|
166
|
+
def update_watcher(path, pe)
|
167
|
+
rotated_tw = @tails[path]
|
168
|
+
@tails[path] = setup_watcher(path, pe)
|
169
|
+
close_watcher_after_rotate_wait(rotated_tw) if rotated_tw
|
170
|
+
end
|
171
|
+
|
172
|
+
def close_watcher(tw)
|
173
|
+
tw.close
|
174
|
+
flush_buffer(tw)
|
175
|
+
if tw.unwatched && @pf
|
176
|
+
@pf[tw.path].update_pos(PositionFile::UNWATCHED_POSITION)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def close_watcher_after_rotate_wait(tw)
|
181
|
+
closer = TailWatcher::Closer.new(@rotate_wait, tw, log, &method(:close_watcher))
|
182
|
+
closer.attach(@loop)
|
183
|
+
end
|
184
|
+
|
185
|
+
def flush_buffer(tw)
|
186
|
+
if lb = tw.line_buffer
|
187
|
+
lb.chomp!
|
188
|
+
time, record = parse_line(lb)
|
189
|
+
if time && record
|
190
|
+
tag = if @tag_prefix || @tag_suffix
|
191
|
+
@tag_prefix + tail_watcher.tag + @tag_suffix
|
192
|
+
else
|
193
|
+
@tag
|
194
|
+
end
|
195
|
+
Engine.emit(tag, time, record)
|
196
|
+
else
|
197
|
+
log.warn "got incomplete line at shutdown from #{tw.path}: #{lb.inspect}"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def run
|
203
|
+
@loop.run
|
204
|
+
rescue
|
205
|
+
log.error "unexpected error", :error=>$!.to_s
|
206
|
+
log.error_backtrace
|
207
|
+
end
|
208
|
+
|
209
|
+
def receive_lines(lines, tail_watcher)
|
210
|
+
es = @receive_handler.call(lines, tail_watcher)
|
211
|
+
unless es.empty?
|
212
|
+
tag = if @tag_prefix || @tag_suffix
|
213
|
+
@tag_prefix + tail_watcher.tag + @tag_suffix
|
214
|
+
else
|
215
|
+
@tag
|
216
|
+
end
|
217
|
+
begin
|
218
|
+
Engine.emit_stream(tag, es)
|
219
|
+
rescue
|
220
|
+
# ignore errors. Engine shows logs and backtraces.
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def parse_line(line)
|
226
|
+
return @parser.parse(line)
|
227
|
+
end
|
228
|
+
|
229
|
+
def convert_line_to_event(line, es)
|
230
|
+
begin
|
231
|
+
line.chomp! # remove \n
|
232
|
+
time, record = parse_line(line)
|
233
|
+
if time && record
|
234
|
+
es.add(time, record)
|
235
|
+
else
|
236
|
+
log.warn "pattern not match: #{line.inspect}"
|
237
|
+
end
|
238
|
+
rescue => e
|
239
|
+
log.warn line.dump, :error => e.to_s
|
240
|
+
log.debug_backtrace(e)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def parse_singleline(lines, tail_watcher)
|
245
|
+
es = MultiEventStream.new
|
246
|
+
lines.each { |line|
|
247
|
+
convert_line_to_event(line, es)
|
248
|
+
}
|
249
|
+
es
|
250
|
+
end
|
251
|
+
|
252
|
+
def parse_multilines(lines, tail_watcher)
|
253
|
+
lb = tail_watcher.line_buffer
|
254
|
+
es = MultiEventStream.new
|
255
|
+
lines.each { |line|
|
256
|
+
if @parser.parser.firstline?(line)
|
257
|
+
if lb
|
258
|
+
convert_line_to_event(lb, es)
|
259
|
+
end
|
260
|
+
lb = line
|
261
|
+
else
|
262
|
+
if lb.nil?
|
263
|
+
log.warn "got incomplete line before first line from #{tail_watcher.path}: #{lb.inspect}"
|
264
|
+
else
|
265
|
+
lb << line
|
266
|
+
end
|
267
|
+
end
|
268
|
+
}
|
269
|
+
tail_watcher.line_buffer = lb
|
270
|
+
es
|
271
|
+
end
|
272
|
+
|
273
|
+
class TailWatcher
|
274
|
+
def initialize(path, rotate_wait, pe, log, update_watcher, &receive_lines)
|
275
|
+
@path = path
|
276
|
+
@rotate_wait = rotate_wait
|
277
|
+
@pe = pe || MemoryPositionEntry.new
|
278
|
+
@receive_lines = receive_lines
|
279
|
+
@update_watcher = update_watcher
|
280
|
+
|
281
|
+
@timer_trigger = TimerWatcher.new(1, true, log, &method(:on_notify))
|
282
|
+
@stat_trigger = StatWatcher.new(path, log, &method(:on_notify))
|
283
|
+
|
284
|
+
@rotate_handler = RotateHandler.new(path, log, &method(:on_rotate))
|
285
|
+
@io_handler = nil
|
286
|
+
@log = log
|
287
|
+
end
|
288
|
+
|
289
|
+
attr_reader :path
|
290
|
+
attr_accessor :line_buffer
|
291
|
+
attr_accessor :unwatched # This is used for removing position entry from PositionFile
|
292
|
+
|
293
|
+
def tag
|
294
|
+
@parsed_tag ||= @path.tr('/', '.').gsub(/\.+/, '.').gsub(/^\./, '')
|
295
|
+
end
|
296
|
+
|
297
|
+
def wrap_receive_lines(lines)
|
298
|
+
@receive_lines.call(lines, self)
|
299
|
+
end
|
300
|
+
|
301
|
+
def attach(loop)
|
302
|
+
@timer_trigger.attach(loop)
|
303
|
+
@stat_trigger.attach(loop)
|
304
|
+
on_notify
|
305
|
+
end
|
306
|
+
|
307
|
+
def detach
|
308
|
+
@timer_trigger.detach if @timer_trigger.attached?
|
309
|
+
@stat_trigger.detach if @stat_trigger.attached?
|
310
|
+
end
|
311
|
+
|
312
|
+
def close
|
313
|
+
if @io_handler
|
314
|
+
@io_handler.on_notify
|
315
|
+
@io_handler.close
|
316
|
+
end
|
317
|
+
detach
|
318
|
+
end
|
319
|
+
|
320
|
+
def on_notify
|
321
|
+
@rotate_handler.on_notify
|
322
|
+
return unless @io_handler
|
323
|
+
@io_handler.on_notify
|
324
|
+
end
|
325
|
+
|
326
|
+
def on_rotate(io)
|
327
|
+
if @io_handler == nil
|
328
|
+
if io
|
329
|
+
# first time
|
330
|
+
stat = io.stat
|
331
|
+
fsize = stat.size
|
332
|
+
inode = stat.ino
|
333
|
+
|
334
|
+
last_inode = @pe.read_inode
|
335
|
+
if inode == last_inode
|
336
|
+
# rotated file has the same inode number with the last file.
|
337
|
+
# assuming following situation:
|
338
|
+
# a) file was once renamed and backed, or
|
339
|
+
# b) symlink or hardlink to the same file is recreated
|
340
|
+
# in either case, seek to the saved position
|
341
|
+
pos = @pe.read_pos
|
342
|
+
elsif last_inode != 0
|
343
|
+
# this is FilePositionEntry and fluentd once started.
|
344
|
+
# read data from the head of the rotated file.
|
345
|
+
# logs never duplicate because this file is a rotated new file.
|
346
|
+
pos = 0
|
347
|
+
@pe.update(inode, pos)
|
348
|
+
else
|
349
|
+
# this is MemoryPositionEntry or this is the first time fluentd started.
|
350
|
+
# seek to the end of the any files.
|
351
|
+
# logs may duplicate without this seek because it's not sure the file is
|
352
|
+
# existent file or rotated new file.
|
353
|
+
pos = fsize
|
354
|
+
@pe.update(inode, pos)
|
355
|
+
end
|
356
|
+
io.seek(pos)
|
357
|
+
|
358
|
+
@io_handler = IOHandler.new(io, @pe, @log, &method(:wrap_receive_lines))
|
359
|
+
else
|
360
|
+
@io_handler = NullIOHandler.new
|
361
|
+
end
|
362
|
+
else
|
363
|
+
log_msg = "detected rotation of #{@path}"
|
364
|
+
log_msg << "; waiting #{@rotate_wait} seconds" if @io_handler.io # wait rotate_time if previous file is exist
|
365
|
+
@log.info log_msg
|
366
|
+
|
367
|
+
if io
|
368
|
+
stat = io.stat
|
369
|
+
inode = stat.ino
|
370
|
+
if inode == @pe.read_inode # truncated
|
371
|
+
@pe.update_pos(stat.size)
|
372
|
+
io_handler = IOHandler.new(io, @pe, @log, &method(:wrap_receive_lines))
|
373
|
+
@io_handler.close
|
374
|
+
@io_handler = io_handler
|
375
|
+
elsif @io_handler.io.nil? # There is no previous file. Reuse TailWatcher
|
376
|
+
@pe.update(inode, io.pos)
|
377
|
+
io_handler = IOHandler.new(io, @pe, @log, &method(:wrap_receive_lines))
|
378
|
+
@io_handler = io_handler
|
379
|
+
else
|
380
|
+
@update_watcher.call(@path, swap_state(@pe))
|
381
|
+
end
|
382
|
+
else
|
383
|
+
@io_handler.close
|
384
|
+
@io_handler = NullIOHandler.new
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
def swap_state(pe)
|
389
|
+
# Use MemoryPositionEntry for rotated file temporary
|
390
|
+
mpe = MemoryPositionEntry.new
|
391
|
+
mpe.update(pe.read_inode, pe.read_pos)
|
392
|
+
@pe = mpe
|
393
|
+
@io_handler.pe = mpe # Don't re-create IOHandler because IOHandler has an internal buffer.
|
394
|
+
|
395
|
+
pe # This pe will be updated in on_rotate after TailWatcher is initialized
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
class TimerWatcher < Coolio::TimerWatcher
|
400
|
+
def initialize(interval, repeat, log, &callback)
|
401
|
+
@callback = callback
|
402
|
+
@log = log
|
403
|
+
super(interval, repeat)
|
404
|
+
end
|
405
|
+
|
406
|
+
def on_timer
|
407
|
+
@callback.call
|
408
|
+
rescue
|
409
|
+
# TODO log?
|
410
|
+
@log.error $!.to_s
|
411
|
+
@log.error_backtrace
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
class StatWatcher < Coolio::StatWatcher
|
416
|
+
def initialize(path, log, &callback)
|
417
|
+
@callback = callback
|
418
|
+
@log = log
|
419
|
+
super(path)
|
420
|
+
end
|
421
|
+
|
422
|
+
def on_change(prev, cur)
|
423
|
+
@callback.call
|
424
|
+
rescue
|
425
|
+
# TODO log?
|
426
|
+
@log.error $!.to_s
|
427
|
+
@log.error_backtrace
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
class Closer < Coolio::TimerWatcher
|
432
|
+
def initialize(interval, tw, log, &callback)
|
433
|
+
@callback = callback
|
434
|
+
@tw = tw
|
435
|
+
@log = log
|
436
|
+
super(interval, false)
|
437
|
+
end
|
438
|
+
|
439
|
+
def on_timer
|
440
|
+
@callback.call(@tw)
|
441
|
+
rescue => e
|
442
|
+
@log.error e.to_s
|
443
|
+
@log.error_backtrace(e.backtrace)
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
MAX_LINES_AT_ONCE = 1000
|
448
|
+
|
449
|
+
class IOHandler
|
450
|
+
def initialize(io, pe, log, first = true, &receive_lines)
|
451
|
+
@log = log
|
452
|
+
@log.info "following tail of #{io.path}" if first
|
453
|
+
@io = io
|
454
|
+
@pe = pe
|
455
|
+
@receive_lines = receive_lines
|
456
|
+
@buffer = ''.force_encoding('ASCII-8BIT')
|
457
|
+
@iobuf = ''.force_encoding('ASCII-8BIT')
|
458
|
+
end
|
459
|
+
|
460
|
+
attr_reader :io
|
461
|
+
attr_accessor :pe
|
462
|
+
|
463
|
+
def on_notify
|
464
|
+
begin
|
465
|
+
lines = []
|
466
|
+
read_more = false
|
467
|
+
|
468
|
+
begin
|
469
|
+
while true
|
470
|
+
if @buffer.empty?
|
471
|
+
@io.read_nonblock(2048, @buffer)
|
472
|
+
else
|
473
|
+
@buffer << @io.read_nonblock(2048, @iobuf)
|
474
|
+
end
|
475
|
+
while line = @buffer.slice!(/.*?\n/m)
|
476
|
+
lines << line
|
477
|
+
end
|
478
|
+
if lines.size >= MAX_LINES_AT_ONCE
|
479
|
+
# not to use too much memory in case the file is very large
|
480
|
+
read_more = true
|
481
|
+
break
|
482
|
+
end
|
483
|
+
end
|
484
|
+
rescue EOFError
|
485
|
+
end
|
486
|
+
|
487
|
+
unless lines.empty?
|
488
|
+
@receive_lines.call(lines)
|
489
|
+
@pe.update_pos(@io.pos - @buffer.bytesize)
|
490
|
+
end
|
491
|
+
end while read_more
|
492
|
+
|
493
|
+
rescue
|
494
|
+
@log.error $!.to_s
|
495
|
+
@log.error_backtrace
|
496
|
+
close
|
497
|
+
end
|
498
|
+
|
499
|
+
def close
|
500
|
+
@io.close unless @io.closed?
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
class NullIOHandler
|
505
|
+
def initialize
|
506
|
+
end
|
507
|
+
|
508
|
+
def io
|
509
|
+
end
|
510
|
+
|
511
|
+
def on_notify
|
512
|
+
end
|
513
|
+
|
514
|
+
def close
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
class RotateHandler
|
519
|
+
def initialize(path, log, &on_rotate)
|
520
|
+
@path = path
|
521
|
+
@inode = nil
|
522
|
+
@fsize = -1 # first
|
523
|
+
@on_rotate = on_rotate
|
524
|
+
@log = log
|
525
|
+
end
|
526
|
+
|
527
|
+
def on_notify
|
528
|
+
begin
|
529
|
+
io = File.open(@path)
|
530
|
+
stat = io.stat
|
531
|
+
inode = stat.ino
|
532
|
+
fsize = stat.size
|
533
|
+
rescue Errno::ENOENT
|
534
|
+
# moved or deleted
|
535
|
+
inode = nil
|
536
|
+
fsize = 0
|
537
|
+
end
|
538
|
+
|
539
|
+
begin
|
540
|
+
if @inode != inode || fsize < @fsize
|
541
|
+
# rotated or truncated
|
542
|
+
@on_rotate.call(io)
|
543
|
+
io = nil
|
544
|
+
end
|
545
|
+
|
546
|
+
@inode = inode
|
547
|
+
@fsize = fsize
|
548
|
+
ensure
|
549
|
+
io.close if io
|
550
|
+
end
|
551
|
+
|
552
|
+
rescue
|
553
|
+
@log.error $!.to_s
|
554
|
+
@log.error_backtrace
|
555
|
+
end
|
556
|
+
end
|
557
|
+
end
|
558
|
+
|
559
|
+
|
560
|
+
class PositionFile
|
561
|
+
UNWATCHED_POSITION = 0xffffffffffffffff
|
562
|
+
|
563
|
+
def initialize(file, map, last_pos)
|
564
|
+
@file = file
|
565
|
+
@map = map
|
566
|
+
@last_pos = last_pos
|
567
|
+
end
|
568
|
+
|
569
|
+
def [](path)
|
570
|
+
if m = @map[path]
|
571
|
+
return m
|
572
|
+
end
|
573
|
+
|
574
|
+
@file.pos = @last_pos
|
575
|
+
@file.write path
|
576
|
+
@file.write "\t"
|
577
|
+
seek = @file.pos
|
578
|
+
@file.write "0000000000000000\t00000000\n"
|
579
|
+
@last_pos = @file.pos
|
580
|
+
|
581
|
+
@map[path] = FilePositionEntry.new(@file, seek)
|
582
|
+
end
|
583
|
+
|
584
|
+
def self.parse(file)
|
585
|
+
compact(file)
|
586
|
+
|
587
|
+
map = {}
|
588
|
+
file.pos = 0
|
589
|
+
file.each_line {|line|
|
590
|
+
m = /^([^\t]+)\t([0-9a-fA-F]+)\t([0-9a-fA-F]+)/.match(line)
|
591
|
+
next unless m
|
592
|
+
path = m[1]
|
593
|
+
pos = m[2].to_i(16)
|
594
|
+
ino = m[3].to_i(16)
|
595
|
+
seek = file.pos - line.bytesize + path.bytesize + 1
|
596
|
+
map[path] = FilePositionEntry.new(file, seek)
|
597
|
+
}
|
598
|
+
new(file, map, file.pos)
|
599
|
+
end
|
600
|
+
|
601
|
+
# Clean up unwatched file entries
|
602
|
+
def self.compact(file)
|
603
|
+
file.pos = 0
|
604
|
+
existent_entries = file.each_line.select { |line|
|
605
|
+
m = /^([^\t]+)\t([0-9a-fA-F]+)\t([0-9a-fA-F]+)/.match(line)
|
606
|
+
next unless m
|
607
|
+
pos = m[2].to_i(16)
|
608
|
+
pos == UNWATCHED_POSITION ? nil : line
|
609
|
+
}
|
610
|
+
|
611
|
+
file.pos = 0
|
612
|
+
file.truncate(0)
|
613
|
+
file.write(existent_entries.join)
|
614
|
+
end
|
615
|
+
end
|
616
|
+
|
617
|
+
# pos inode
|
618
|
+
# ffffffffffffffff\tffffffff\n
|
619
|
+
class FilePositionEntry
|
620
|
+
POS_SIZE = 16
|
621
|
+
INO_OFFSET = 17
|
622
|
+
INO_SIZE = 8
|
623
|
+
LN_OFFSET = 25
|
624
|
+
SIZE = 26
|
625
|
+
|
626
|
+
def initialize(file, seek)
|
627
|
+
@file = file
|
628
|
+
@seek = seek
|
629
|
+
end
|
630
|
+
|
631
|
+
def update(ino, pos)
|
632
|
+
@file.pos = @seek
|
633
|
+
@file.write "%016x\t%08x" % [pos, ino]
|
634
|
+
end
|
635
|
+
|
636
|
+
def update_pos(pos)
|
637
|
+
@file.pos = @seek
|
638
|
+
@file.write "%016x" % pos
|
639
|
+
end
|
640
|
+
|
641
|
+
def read_inode
|
642
|
+
@file.pos = @seek + INO_OFFSET
|
643
|
+
raw = @file.read(8)
|
644
|
+
raw ? raw.to_i(16) : 0
|
645
|
+
end
|
646
|
+
|
647
|
+
def read_pos
|
648
|
+
@file.pos = @seek
|
649
|
+
raw = @file.read(16)
|
650
|
+
raw ? raw.to_i(16) : 0
|
651
|
+
end
|
652
|
+
end
|
653
|
+
|
654
|
+
class MemoryPositionEntry
|
655
|
+
def initialize
|
656
|
+
@pos = 0
|
657
|
+
@inode = 0
|
658
|
+
end
|
659
|
+
|
660
|
+
def update(ino, pos)
|
661
|
+
@inode = ino
|
662
|
+
@pos = pos
|
663
|
+
end
|
664
|
+
|
665
|
+
def update_pos(pos)
|
666
|
+
@pos = pos
|
667
|
+
end
|
668
|
+
|
669
|
+
def read_pos
|
670
|
+
@pos
|
671
|
+
end
|
672
|
+
|
673
|
+
def read_inode
|
674
|
+
@inode
|
675
|
+
end
|
676
|
+
end
|
677
|
+
end
|
678
|
+
|
679
|
+
# This TailInput is for existence plugins which extends old in_tail
|
680
|
+
# This class will be removed after release v1.
|
681
|
+
class TailInput < Input
|
22
682
|
def initialize
|
23
683
|
super
|
24
684
|
@paths = []
|