fluent-plugin-tailpath 0.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ebda3e465349195927fef2fab4ca4b0926b960ae
4
+ data.tar.gz: 7bce2f30a26b4c2d5085f38ccdfba842f134d006
5
+ SHA512:
6
+ metadata.gz: c9c77945298c37e6f2da4ee3de05c9f39da1b14c048a2d651e41d3bf011bd4098a691e714b8345a081bab52f889006685c9ba1e31bc045ac31e3619d02ed4cb1
7
+ data.tar.gz: ee91142c0cb273a69a5d84da297df1b73b31992cc122ee58cf4c8f2575a160ea07ac3b0069560c1a55e555c67a6545464bdac2fcce0ae0031b537581323c965a
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /*.gem
2
+ ~*
3
+ #*
4
+ *~
5
+ .bundle
6
+ Gemfile.lock
7
+ .rbenv-version
8
+ vendor
9
+ doc/*
10
+ tmp/*
11
+ coverage
12
+ .yardoc
13
+ pkg/
14
+ .ruby-version
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,38 @@
1
+ Copyright (c) 2014 Jacob Wirth
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ Apache 2.0 License
25
+
26
+ Copyright (C) 2011 FURUHASHI Sadayuki
27
+
28
+ Licensed under the Apache License, Version 2.0 (the "License");
29
+ you may not use this file except in compliance with the License.
30
+ You may obtain a copy of the License at
31
+
32
+ http://www.apache.org/licenses/LICENSE-2.0
33
+
34
+ Unless required by applicable law or agreed to in writing, software
35
+ distributed under the License is distributed on an "AS IS" BASIS,
36
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
37
+ See the License for the specific language governing permissions and
38
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # fluent-plugin-tailpath
2
+
3
+ Fluentd plugin to tail files and add the file path to the message
4
+
5
+ ## Installation
6
+
7
+ Use RubyGems:
8
+
9
+ gem install fluent-plugin-tailpath
10
+
11
+ ## Configuration
12
+
13
+ For more info, the configuration is identical to [im_file](http://docs.fluentd.org/articles/in_tail)
14
+
15
+ Example:
16
+
17
+ <source>
18
+ type tailpath
19
+ path /var/log/httpd-access.log
20
+ pos_file /var/log/td-agent/httpd-access.log.pos
21
+ tag apache.access
22
+ format apache2
23
+ </source>
24
+
25
+ ## Copyright
26
+
27
+ Copyright (c) 2014 Jacob Wirth. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = "fluent-plugin-tailpath"
6
+ gem.version = "0.0.1"
7
+ gem.authors = ["Jacob Wirth"]
8
+ gem.email = "xthexder@live.com"
9
+ gem.homepage = "https://github.com/xthexder/fluent-plugin-tailpath"
10
+ gem.description = "Fluentd plugin to tail files and add the file path to the message"
11
+ gem.summary = gem.description
12
+ gem.licenses = ["MIT"]
13
+ gem.has_rdoc = false
14
+
15
+ gem.files = `git ls-files`.split("\n")
16
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ gem.require_paths = ['lib']
19
+
20
+ gem.add_dependency "fluentd", "~> 0.10.17"
21
+ gem.add_development_dependency "rake"
22
+ gem.add_development_dependency "rspec"
23
+ gem.add_development_dependency "pry"
24
+ gem.add_development_dependency "pry-nav"
25
+ end
@@ -0,0 +1,497 @@
1
+ #
2
+ # Fluent
3
+ #
4
+ # Copyright (C) 2011 FURUHASHI Sadayuki
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ module Fluent
19
+ class TailPathInput < Input
20
+ Plugin.register_input('tailpath', self)
21
+
22
+ def initialize
23
+ super
24
+ @paths = []
25
+ end
26
+
27
+ config_param :path, :string
28
+ config_param :tag, :string
29
+ config_param :rotate_wait, :time, :default => 5
30
+ config_param :pos_file, :string, :default => nil
31
+
32
+ attr_reader :paths
33
+
34
+ def configure(conf)
35
+ super
36
+
37
+ @paths = @path.split(',').map {|path| path.strip }
38
+ if @paths.empty?
39
+ raise ConfigError, "tailpath: 'path' parameter is required on tail input"
40
+ end
41
+
42
+ unless @pos_file
43
+ $log.warn "'pos_file PATH' parameter is not set to a 'tail' source."
44
+ $log.warn "this parameter is highly recommended to save the position to resume tailing."
45
+ end
46
+
47
+ configure_parser(conf)
48
+ end
49
+
50
+ def configure_parser(conf)
51
+ @parser = TextParser.new
52
+ @parser.configure(conf)
53
+ end
54
+
55
+ def start
56
+ if @pos_file
57
+ @pf_file = File.open(@pos_file, File::RDWR|File::CREAT, DEFAULT_FILE_PERMISSION)
58
+ @pf_file.sync = true
59
+ @pf = PositionFile.parse(@pf_file)
60
+ end
61
+
62
+ @loop = Coolio::Loop.new
63
+ @tails = @paths.map {|path|
64
+ pe = @pf ? @pf[path] : MemoryPositionEntry.new
65
+ TailWatcher.new(path, @rotate_wait, pe, &method(:receive_lines))
66
+ }
67
+ @tails.each {|tail|
68
+ tail.attach(@loop)
69
+ }
70
+ @thread = Thread.new(&method(:run))
71
+ end
72
+
73
+ def shutdown
74
+ @tails.each {|tail|
75
+ tail.close
76
+ }
77
+ @loop.stop
78
+ @thread.join
79
+ @pf_file.close if @pf_file
80
+ end
81
+
82
+ def run
83
+ @loop.run
84
+ rescue
85
+ $log.error "unexpected error", :error=>$!.to_s
86
+ $log.error_backtrace
87
+ end
88
+
89
+ def receive_lines(path, lines)
90
+ es = MultiEventStream.new
91
+ lines.each {|line|
92
+ begin
93
+ line.chomp! # remove \n
94
+ time, record = parse_line(line)
95
+ if time && record
96
+ record['path'] = path
97
+ es.add(time, record)
98
+ end
99
+ rescue
100
+ $log.warn line.dump, :error=>$!.to_s
101
+ $log.debug_backtrace
102
+ end
103
+ }
104
+
105
+ unless es.empty?
106
+ begin
107
+ Engine.emit_stream(@tag, es)
108
+ rescue
109
+ # ignore errors. Engine shows logs and backtraces.
110
+ end
111
+ end
112
+ end
113
+
114
+ def parse_line(line)
115
+ return @parser.parse(line)
116
+ end
117
+
118
+ class TailWatcher
119
+ def initialize(path, rotate_wait, pe, &receive_lines)
120
+ @path = path
121
+ @rotate_wait = rotate_wait
122
+ @pe = pe || MemoryPositionEntry.new
123
+ @receive_lines = receive_lines
124
+
125
+ @rotate_queue = []
126
+
127
+ @timer_trigger = TimerWatcher.new(1, true, &method(:on_notify))
128
+ @stat_trigger = StatWatcher.new(path, &method(:on_notify))
129
+
130
+ @rotate_handler = RotateHandler.new(path, &method(:on_rotate))
131
+ @io_handler = nil
132
+ end
133
+
134
+ def attach(loop)
135
+ @timer_trigger.attach(loop)
136
+ @stat_trigger.attach(loop)
137
+ on_notify
138
+ end
139
+
140
+ def detach
141
+ @timer_trigger.detach if @timer_trigger.attached?
142
+ @stat_trigger.detach if @stat_trigger.attached?
143
+ end
144
+
145
+ def close
146
+ @rotate_queue.reject! {|req|
147
+ req.io.close
148
+ true
149
+ }
150
+ detach
151
+ end
152
+
153
+ def on_notify
154
+ @rotate_handler.on_notify
155
+ return unless @io_handler
156
+ @io_handler.on_notify
157
+
158
+ # proceeds rotate queue
159
+ return if @rotate_queue.empty?
160
+ @rotate_queue.first.tick
161
+
162
+ while @rotate_queue.first.ready?
163
+ if io = @rotate_queue.first.io
164
+ stat = io.stat
165
+ inode = stat.ino
166
+ if inode == @pe.read_inode
167
+ # rotated file has the same inode number with the last file.
168
+ # assuming following situation:
169
+ # a) file was once renamed and backed, or
170
+ # b) symlink or hardlink to the same file is recreated
171
+ # in either case, seek to the saved position
172
+ pos = @pe.read_pos
173
+ else
174
+ pos = io.pos
175
+ end
176
+ @pe.update(inode, pos)
177
+ io_handler = IOHandler.new(io, @pe, &@receive_lines)
178
+ else
179
+ io_handler = NullIOHandler.new
180
+ end
181
+ @io_handler.close
182
+ @io_handler = io_handler
183
+ @rotate_queue.shift
184
+ break if @rotate_queue.empty?
185
+ end
186
+ end
187
+
188
+ def on_rotate(io)
189
+ if @io_handler == nil
190
+ if io
191
+ # first time
192
+ stat = io.stat
193
+ fsize = stat.size
194
+ inode = stat.ino
195
+
196
+ last_inode = @pe.read_inode
197
+ if inode == last_inode
198
+ # seek to the saved position
199
+ pos = @pe.read_pos
200
+ elsif last_inode != 0
201
+ # this is FilePositionEntry and fluentd once started.
202
+ # read data from the head of the rotated file.
203
+ # logs never duplicate because this file is a rotated new file.
204
+ pos = 0
205
+ @pe.update(inode, pos)
206
+ else
207
+ # this is MemoryPositionEntry or this is the first time fluentd started.
208
+ # seek to the end of the any files.
209
+ # logs may duplicate without this seek because it's not sure the file is
210
+ # existent file or rotated new file.
211
+ pos = fsize
212
+ @pe.update(inode, pos)
213
+ end
214
+ io.seek(pos)
215
+
216
+ @io_handler = IOHandler.new(io, @pe, &@receive_lines)
217
+ else
218
+ @io_handler = NullIOHandler.new
219
+ end
220
+
221
+ else
222
+ if io && @rotate_queue.find {|req| req.io == io }
223
+ return
224
+ end
225
+ last_io = @rotate_queue.empty? ? @io_handler.io : @rotate_queue.last.io
226
+ if last_io == nil
227
+ $log.info "detected rotation of #{@path}"
228
+ # rotate imeediately if previous file is nil
229
+ wait = 0
230
+ else
231
+ $log.info "detected rotation of #{@path}; waiting #{@rotate_wait} seconds"
232
+ wait = @rotate_wait
233
+ wait -= @rotate_queue.first.wait unless @rotate_queue.empty?
234
+ end
235
+ @rotate_queue << RotationRequest.new(io, wait)
236
+ end
237
+ end
238
+
239
+ class TimerWatcher < Coolio::TimerWatcher
240
+ def initialize(interval, repeat, &callback)
241
+ @callback = callback
242
+ super(interval, repeat)
243
+ end
244
+
245
+ def on_timer
246
+ @callback.call
247
+ rescue
248
+ # TODO log?
249
+ $log.error $!.to_s
250
+ $log.error_backtrace
251
+ end
252
+ end
253
+
254
+ class StatWatcher < Coolio::StatWatcher
255
+ def initialize(path, &callback)
256
+ @callback = callback
257
+ super(path)
258
+ end
259
+
260
+ def on_change(prev, cur)
261
+ @callback.call
262
+ rescue
263
+ # TODO log?
264
+ $log.error $!.to_s
265
+ $log.error_backtrace
266
+ end
267
+ end
268
+
269
+ class RotationRequest
270
+ def initialize(io, wait)
271
+ @io = io
272
+ @wait = wait
273
+ end
274
+
275
+ attr_reader :io, :wait
276
+
277
+ def tick
278
+ @wait -= 1
279
+ end
280
+
281
+ def ready?
282
+ @wait <= 0
283
+ end
284
+ end
285
+
286
+ MAX_LINES_AT_ONCE = 1000
287
+
288
+ class IOHandler
289
+ def initialize(io, pe, &receive_lines)
290
+ $log.info "following tail of #{io.path}"
291
+ @io = io
292
+ @pe = pe
293
+ @receive_lines = receive_lines
294
+ @buffer = ''.force_encoding('ASCII-8BIT')
295
+ @iobuf = ''.force_encoding('ASCII-8BIT')
296
+ end
297
+
298
+ attr_reader :io
299
+
300
+ def on_notify
301
+ begin
302
+ lines = []
303
+ read_more = false
304
+
305
+ begin
306
+ while true
307
+ if @buffer.empty?
308
+ @io.read_nonblock(2048, @buffer)
309
+ else
310
+ @buffer << @io.read_nonblock(2048, @iobuf)
311
+ end
312
+ while line = @buffer.slice!(/.*?\n/m)
313
+ lines << line
314
+ end
315
+ if lines.size >= MAX_LINES_AT_ONCE
316
+ # not to use too much memory in case the file is very large
317
+ read_more = true
318
+ break
319
+ end
320
+ end
321
+ rescue EOFError
322
+ end
323
+
324
+ unless lines.empty?
325
+ @receive_lines.call(@io.path, lines)
326
+ @pe.update_pos(@io.pos - @buffer.bytesize)
327
+ end
328
+
329
+ end while read_more
330
+
331
+ rescue
332
+ $log.error $!.to_s
333
+ $log.error_backtrace
334
+ close
335
+ end
336
+
337
+ def close
338
+ @io.close unless @io.closed?
339
+ end
340
+ end
341
+
342
+ class NullIOHandler
343
+ def initialize
344
+ end
345
+
346
+ def io
347
+ end
348
+
349
+ def on_notify
350
+ end
351
+
352
+ def close
353
+ end
354
+ end
355
+
356
+ class RotateHandler
357
+ def initialize(path, &on_rotate)
358
+ @path = path
359
+ @inode = nil
360
+ @fsize = -1 # first
361
+ @on_rotate = on_rotate
362
+ end
363
+
364
+ def on_notify
365
+ begin
366
+ io = File.open(@path)
367
+ stat = io.stat
368
+ inode = stat.ino
369
+ fsize = stat.size
370
+ rescue Errno::ENOENT
371
+ # moved or deleted
372
+ inode = nil
373
+ fsize = 0
374
+ end
375
+
376
+ begin
377
+ if @inode != inode || fsize < @fsize
378
+ # rotated or truncated
379
+ @on_rotate.call(io)
380
+ io = nil
381
+ end
382
+
383
+ @inode = inode
384
+ @fsize = fsize
385
+ ensure
386
+ io.close if io
387
+ end
388
+
389
+ rescue
390
+ $log.error $!.to_s
391
+ $log.error_backtrace
392
+ end
393
+ end
394
+ end
395
+
396
+
397
+ class PositionFile
398
+ def initialize(file, map, last_pos)
399
+ @file = file
400
+ @map = map
401
+ @last_pos = last_pos
402
+ end
403
+
404
+ def [](path)
405
+ if m = @map[path]
406
+ return m
407
+ end
408
+
409
+ @file.pos = @last_pos
410
+ @file.write path
411
+ @file.write "\t"
412
+ seek = @file.pos
413
+ @file.write "0000000000000000\t00000000\n"
414
+ @last_pos = @file.pos
415
+
416
+ @map[path] = FilePositionEntry.new(@file, seek)
417
+ end
418
+
419
+ def self.parse(file)
420
+ map = {}
421
+ file.pos = 0
422
+ file.each_line {|line|
423
+ m = /^([^\t]+)\t([0-9a-fA-F]+)\t([0-9a-fA-F]+)/.match(line)
424
+ next unless m
425
+ path = m[1]
426
+ pos = m[2].to_i(16)
427
+ ino = m[3].to_i(16)
428
+ seek = file.pos - line.bytesize + path.bytesize + 1
429
+ map[path] = FilePositionEntry.new(file, seek)
430
+ }
431
+ new(file, map, file.pos)
432
+ end
433
+ end
434
+
435
+ # pos inode
436
+ # ffffffffffffffff\tffffffff\n
437
+ class FilePositionEntry
438
+ POS_SIZE = 16
439
+ INO_OFFSET = 17
440
+ INO_SIZE = 8
441
+ LN_OFFSET = 25
442
+ SIZE = 26
443
+
444
+ def initialize(file, seek)
445
+ @file = file
446
+ @seek = seek
447
+ end
448
+
449
+ def update(ino, pos)
450
+ @file.pos = @seek
451
+ @file.write "%016x\t%08x" % [pos, ino]
452
+ @inode = ino
453
+ end
454
+
455
+ def update_pos(pos)
456
+ @file.pos = @seek
457
+ @file.write "%016x" % pos
458
+ end
459
+
460
+ def read_inode
461
+ @file.pos = @seek + INO_OFFSET
462
+ raw = @file.read(8)
463
+ raw ? raw.to_i(16) : 0
464
+ end
465
+
466
+ def read_pos
467
+ @file.pos = @seek
468
+ raw = @file.read(16)
469
+ raw ? raw.to_i(16) : 0
470
+ end
471
+ end
472
+
473
+ class MemoryPositionEntry
474
+ def initialize
475
+ @pos = 0
476
+ @inode = 0
477
+ end
478
+
479
+ def update(ino, pos)
480
+ @inode = ino
481
+ @pos = pos
482
+ end
483
+
484
+ def update_pos(pos)
485
+ @pos = pos
486
+ end
487
+
488
+ def read_pos
489
+ @pos
490
+ end
491
+
492
+ def read_inode
493
+ @inode
494
+ end
495
+ end
496
+ end
497
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-tailpath
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Jacob Wirth
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-01-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: fluentd
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 0.10.17
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 0.10.17
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry-nav
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Fluentd plugin to tail files and add the file path to the message
84
+ email: xthexder@live.com
85
+ executables: []
86
+ extensions: []
87
+ extra_rdoc_files: []
88
+ files:
89
+ - .gitignore
90
+ - Gemfile
91
+ - LICENSE
92
+ - README.md
93
+ - fluent-plugin-tailpath.gemspec
94
+ - lib/fluent/plugin/in_tailpath.rb
95
+ homepage: https://github.com/xthexder/fluent-plugin-tailpath
96
+ licenses:
97
+ - MIT
98
+ metadata: {}
99
+ post_install_message:
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - '>='
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - '>='
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubyforge_project:
115
+ rubygems_version: 2.0.14
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: Fluentd plugin to tail files and add the file path to the message
119
+ test_files: []