fluent-plugin-tail-multiline-ex 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fluent-plugin-tail-multiline-ex.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 TODO: Write your name
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.
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # fluent-tail-multiline-ex
2
+
3
+ Now under construction.
4
+
5
+ TODO: Write a gem description
6
+
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'fluent-plugin-tail-multiline-ex'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install fluent-plugin-tail-multiline-ex
21
+
22
+ ## Usage
23
+
24
+ TODO: Write usage instructions here
25
+
26
+ ## Contributing
27
+
28
+ 1. Fork it
29
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
30
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
31
+ 4. Push to the branch (`git push origin my-new-feature`)
32
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "fluent-plugin-tail-multiline-ex"
7
+ spec.version = "0.0.1"
8
+ spec.authors = ["Yoshiharu Mori"]
9
+ spec.email = ["y-mori@sraoss.co.jp"]
10
+ spec.description = %q{merge tail_ex and tail_multiline input plugin}
11
+ spec.summary = %q{merge tail_ex and tail_multiline input plugin}
12
+ spec.homepage = "https://github.com/y-mori0110/fluent-plugin-multiline-ex"
13
+ spec.license = "Apache 2.0"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ requires = ['fluentd', 'fluent-mixin-config-placeholders']
21
+ requires.each {|name| spec.add_runtime_dependency name}
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+
25
+ requires += ['rake', 'flexmock']
26
+ requires.each {|name| spec.add_development_dependency name}
27
+ end
@@ -0,0 +1,351 @@
1
+ module Fluent
2
+ require 'fluent/plugin/in_tail'
3
+ require 'fluent/mixin/config_placeholders'
4
+
5
+ class TailMultilineInput_EX < TailInput
6
+
7
+ class MultilineTextParser_EX < TextParser
8
+ def configure(conf, required=true)
9
+ format = conf['format']
10
+ if format == nil
11
+ raise ConfigError, "'format' parameter is required"
12
+ elsif format[0] != ?/ || format[format.length-1] != ?/
13
+ raise ConfigError, "'format' should be RegEx. Template is not supported in multiline mode"
14
+ end
15
+
16
+ begin
17
+ @regex = Regexp.new(format[1..-2],Regexp::MULTILINE)
18
+ if @regex.named_captures.empty?
19
+ raise "No named captures"
20
+ end
21
+ rescue
22
+ raise ConfigError, "Invalid regexp in format '#{format[1..-2]}': #{$!}"
23
+ end
24
+
25
+ @parser = RegexpParser.new(@regex, conf)
26
+
27
+ format_firstline = conf['format_firstline']
28
+ if format_firstline
29
+ # Use custom matcher for 1st line
30
+ if format_firstline[0] == '/' && format_firstline[format_firstline.length-1] == '/'
31
+ @regex = Regexp.new(format_firstline[1..-2])
32
+ else
33
+ raise ConfigError, "Invalid regexp in format_firstline '#{format_firstline[1..-2]}': #{$!}"
34
+ end
35
+ end
36
+
37
+ return true
38
+ end
39
+
40
+ def match_firstline(text)
41
+ @regex.match(text)
42
+ end
43
+ end
44
+
45
+ Plugin.register_input('tail_multiline_ex', self)
46
+
47
+ FORMAT_MAX_NUMS = 20
48
+
49
+ config_param :format, :string
50
+ config_param :format_firstline, :string, :default => nil
51
+ config_param :rawdata_key, :string, :default => nil
52
+ config_param :auto_flush_sec, :integer, :default => 1
53
+ config_param :read_newfile_from_head, :bool, :default => false
54
+ (1..FORMAT_MAX_NUMS).each do |i|
55
+ config_param "format#{i}".to_sym, :string, :default => nil
56
+ end
57
+
58
+ config_param :expand_date, :bool, :default => true
59
+ config_param :read_all, :bool, :default => true
60
+ config_param :refresh_interval, :integer, :default => 3600
61
+
62
+ include Fluent::Mixin::ConfigPlaceholders
63
+
64
+ def initialize
65
+ super
66
+ @locker = Monitor.new
67
+ @logbuf = nil
68
+ @logbuf_flusher = CallLater_EX::new
69
+ @ready = false
70
+ end
71
+
72
+ def configure(conf)
73
+ if conf['format'].nil?
74
+ invalids = conf.keys.select{|k| k =~ /^format(\d+)$/ and not (1..FORMAT_MAX_NUMS).include?($1.to_i)}
75
+ if invalids.size > 0
76
+ raise ConfigError, "invalid number formats (valid format number:1-#{FORMAT_MAX_NUMS}):" + invalids.join(",")
77
+ end
78
+ format_index_list = conf.keys.select{|s| s =~ /^format\d+$/}.map{|v| (/^format(\d+)$/.match(v))[1].to_i}
79
+ if (1..format_index_list.max).map{|i| conf["format#{i}"]}.include?(nil)
80
+ raise Fluent::ConfigError, "jump of format index found"
81
+ end
82
+ formats = (1..FORMAT_MAX_NUMS).map {|i|
83
+ conf["format#{i}"]
84
+ }.delete_if {|format|
85
+ format.nil?
86
+ }.map {|format|
87
+ format[1..-2]
88
+ }.join
89
+ conf['format'] = '/' + formats + '/'
90
+ end
91
+ super
92
+ if @tag.index('*')
93
+ @tag_prefix, @tag_suffix = @tag.split('*')
94
+ @tag_suffix ||= ''
95
+ else
96
+ @tag_prefix = nil
97
+ @tag_suffix = nil
98
+ end
99
+ @watchers = {}
100
+ @refresh_trigger = TailWatcher::TimerWatcher.new(@refresh_interval, true, &method(:refresh_watchers))
101
+ if read_newfile_from_head and @pf
102
+ # Tread new file as rotated file
103
+ # Use temp file inode number as previos logfile
104
+ @paths.map {|path|
105
+ pe = @pf[path]
106
+ if pe.read_inode == 0
107
+ require 'tempfile'
108
+ tmpfile = Tempfile.new('gettempinode')
109
+ pe.update(File.stat(tmpfile).ino, 0)
110
+ tmpfile.unlink
111
+ end
112
+ }
113
+ end
114
+ end
115
+
116
+ def expand_paths
117
+ date = Time.now
118
+ paths = []
119
+ for path in @paths
120
+ if @expand_date
121
+ path = date.strftime(path)
122
+ end
123
+ paths += Dir.glob(path)
124
+ end
125
+ paths
126
+ end
127
+
128
+ def refresh_watchers
129
+ paths = expand_paths
130
+ missing = @watchers.keys - paths
131
+ added = paths - @watchers.keys
132
+
133
+ stop_watch(missing) unless missing.empty?
134
+ start_watch(added) unless added.empty?
135
+ end
136
+
137
+ def start_watch(paths)
138
+ paths.each do |path|
139
+ if @pf
140
+ pe = @pf[path]
141
+ if @read_all && pe.read_inode == 0
142
+ inode = File::Stat.new(path).ino
143
+ pe.update(inode, 0)
144
+ end
145
+ else
146
+ pe = nil
147
+ end
148
+
149
+ watcher = TailExWatcher_EX.new(path, @rotate_wait, pe, &method(:receive_lines))
150
+ watcher.attach(@loop)
151
+ @watchers[path] = watcher
152
+ end
153
+ end
154
+
155
+ def stop_watch(paths, immediate=false)
156
+ paths.each do |path|
157
+ watcher = @watchers.delete(path)
158
+ if watcher
159
+ watcher.close(immediate ? nil : @loop)
160
+ end
161
+ end
162
+ end
163
+
164
+ def start
165
+ paths, @paths = @paths, []
166
+ super
167
+ @thread.join
168
+ @paths = paths
169
+ refresh_watchers
170
+ @refresh_trigger.attach(@loop)
171
+ @ready = true
172
+ @thread = Thread.new(&method(:run))
173
+ end
174
+
175
+ def run
176
+ # don't run unless ready to avoid coolio error
177
+ if @ready
178
+ super
179
+ end
180
+ end
181
+
182
+ def configure_parser(conf)
183
+ @parser = MultilineTextParser_EX.new
184
+ @parser.configure(conf)
185
+ end
186
+
187
+ def receive_lines(lines,tag)
188
+ if @tag_prefix || @tag_suffix
189
+ @tag = @tag_prefix + tag + @tag_suffix
190
+ end
191
+ @logbuf_flusher.cancel()
192
+ es = MultiEventStream.new
193
+ @locker.synchronize do
194
+ lines.each {|line|
195
+ if @parser.match_firstline(line)
196
+ time, record = parse_logbuf(@logbuf)
197
+ if time && record
198
+ es.add(time, record)
199
+ end
200
+ @logbuf = line
201
+ else
202
+ @logbuf += line if(@logbuf)
203
+ end
204
+ }
205
+ end
206
+ unless es.empty?
207
+ begin
208
+ Engine.emit_stream(@tag, es)
209
+ rescue
210
+ # ignore errors. Engine shows logs and backtraces.
211
+ end
212
+ end
213
+ @logbuf_flusher.call_later(@auto_flush_sec) do
214
+ flush_logbuf()
215
+ end
216
+ end
217
+
218
+ def shutdown
219
+ @refresh_trigger.detach
220
+ stop_watch(@watchers.keys, true)
221
+ @loop.stop
222
+ @thread.join
223
+ @pf_file.close if @pf_file
224
+ flush_logbuf()
225
+ @logbuf_flusher.shutdown()
226
+ end
227
+
228
+ def flush_logbuf
229
+ time, record = nil,nil
230
+ @locker.synchronize do
231
+ time, record = parse_logbuf(@logbuf)
232
+ @logbuf = nil
233
+ end
234
+ if time && record
235
+ Engine.emit(@tag, time, record)
236
+ end
237
+ end
238
+
239
+ def parse_logbuf(buf)
240
+ return nil,nil unless buf
241
+ buf.chomp!
242
+ begin
243
+ time, record = @parser.parse(buf)
244
+ rescue
245
+ $log.warn buf.dump, :error=>$!.to_s
246
+ $log.debug_backtrace
247
+ end
248
+ return nil,nil unless time && record
249
+ record[@rawdata_key] = buf if @rawdata_key
250
+ return time, record
251
+ end
252
+
253
+ class TailExWatcher_EX < TailWatcher
254
+ def initialize(path, rotate_wait, pe, &receive_lines)
255
+ @parent_receive_lines = receive_lines
256
+ super(path, rotate_wait, pe, &method(:_receive_lines))
257
+ #super(path, rotate_wait, pe, &receive_lines)
258
+ @close_trigger = TimerWatcher.new(rotate_wait * 2, false, &method(:_close))
259
+ end
260
+
261
+ def _receive_lines(lines)
262
+ tag = @path.tr('/', '.').gsub(/\.+/, '.').gsub(/^\./, '')
263
+ @parent_receive_lines.call(lines, tag)
264
+ end
265
+
266
+ def close(loop=nil)
267
+ detach # detach first to avoid timer conflict
268
+ if loop
269
+ @close_trigger.attach(loop)
270
+ else
271
+ _close
272
+ end
273
+ end
274
+
275
+ def _close
276
+ @close_trigger.detach if @close_trigger.attached?
277
+ self.class.superclass.instance_method(:close).bind(self).call
278
+
279
+ @io_handler.on_notify
280
+ @io_handler.close
281
+ $log.info "stop following of #{@path}"
282
+ end
283
+ end
284
+ end
285
+
286
+ class CallLater_EX
287
+ def initialize
288
+ @locker = Monitor::new
289
+ initExecBlock()
290
+ @thread = Thread.new(&method(:run))
291
+ end
292
+
293
+ def call_later(delay,&block)
294
+ @locker.synchronize do
295
+ @exec_time = Engine.now + delay
296
+ @exec_block = block
297
+ end
298
+ @thread.run
299
+ end
300
+
301
+ def run
302
+ @running = true
303
+ while true
304
+ sleepSec = -1
305
+ @locker.synchronize do
306
+ now = Engine.now
307
+ if @exec_block && @exec_time <= now
308
+ @exec_block.call()
309
+ initExecBlock()
310
+ end
311
+ return unless @running
312
+ unless(@exec_time == -1)
313
+ sleepSec = @exec_time - now
314
+ end
315
+ end
316
+ if (sleepSec == -1)
317
+ sleep()
318
+ else
319
+ sleep(sleepSec)
320
+ end
321
+ end
322
+ rescue => e
323
+ puts e
324
+ end
325
+
326
+ def cancel()
327
+ initExecBlock()
328
+ end
329
+
330
+ def shutdown()
331
+ @locker.synchronize do
332
+ @running = false
333
+ end
334
+ if(@thread)
335
+ @thread.run
336
+ @thread.join
337
+ @thread = nil
338
+ end
339
+ end
340
+
341
+ private
342
+
343
+ def initExecBlock()
344
+ @locker.synchronize do
345
+ @exec_time = -1
346
+ @exec_block = nil
347
+ end
348
+ end
349
+
350
+ end
351
+ end
metadata ADDED
@@ -0,0 +1,165 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-tail-multiline-ex
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Yoshiharu Mori
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-04-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: fluentd
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: fluent-mixin-config-placeholders
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: bundler
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '1.3'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ - !ruby/object:Gem::Dependency
63
+ name: fluentd
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: fluent-mixin-config-placeholders
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: rake
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: flexmock
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description: merge tail_ex and tail_multiline input plugin
127
+ email:
128
+ - y-mori@sraoss.co.jp
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - .gitignore
134
+ - Gemfile
135
+ - LICENSE.txt
136
+ - README.md
137
+ - Rakefile
138
+ - fluent-plugin-tail-multiline-ex.gemspec
139
+ - lib/fluent/plugin/in_tail_multiline_ex.rb
140
+ homepage: https://github.com/y-mori0110/fluent-plugin-multiline-ex
141
+ licenses:
142
+ - Apache 2.0
143
+ post_install_message:
144
+ rdoc_options: []
145
+ require_paths:
146
+ - lib
147
+ required_ruby_version: !ruby/object:Gem::Requirement
148
+ none: false
149
+ requirements:
150
+ - - ! '>='
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ required_rubygems_version: !ruby/object:Gem::Requirement
154
+ none: false
155
+ requirements:
156
+ - - ! '>='
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ requirements: []
160
+ rubyforge_project:
161
+ rubygems_version: 1.8.23
162
+ signing_key:
163
+ specification_version: 3
164
+ summary: merge tail_ex and tail_multiline input plugin
165
+ test_files: []