fluent-plugin-tailpath 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []