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.

@@ -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
@@ -56,9 +56,13 @@
56
56
  # match tag=system.** and forward to another fluent server
57
57
  <match system.**>
58
58
  type forward
59
- host 192.168.0.11
59
+ <server>
60
+ host 192.168.0.11
61
+ </server>
60
62
  <secondary>
61
- host 192.168.0.12
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
@@ -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"])
@@ -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
- # for empty loop
212
- @default_loop = Coolio::Loop.default
213
- @default_loop.attach Coolio::TimerWatcher.new(1, true)
214
- # TODO attach async watch for thread pool
215
- @default_loop.run
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
@@ -9,7 +9,11 @@ require 'json'
9
9
  require 'yajl'
10
10
  require 'uri'
11
11
  require 'msgpack'
12
- require 'sigdump/setup'
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
@@ -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
 
@@ -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, @timeout.to_i].pack('I!I!') # { int l_onoff; int l_linger; }
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 TailInput < Input
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 = []