fluent-plugin-tail-multiline 0.1.0

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: 30ae0c4b7c3d24c02a04d5fef674ca5e24f133ae
4
+ data.tar.gz: 10ac8354faf5e40d599d9dd9f8753c4ce46ecc4d
5
+ SHA512:
6
+ metadata.gz: 352fdfcb3089d9553db4930a7514cb0a843b47c73e8f0e3c7da5617b7faf090357340e807d2cc65fd4ef12acb6ad1b4c218f8627d33637ae10d124c1afcc9ec7
7
+ data.tar.gz: 663c365b32692660f80eb0c4b4c22985ed3733d73bf4020689d2b8c864fd37c9da1908265020e2de7c219b45f26aa0ace0152a93ac67a08aee8410675f1a98a6
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.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2013 - Tomohisa Ota
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # Fluent::Plugin::Tail-Multiline
2
+
3
+ Tail-Multiline plugin extends built-in tail plugin with following features
4
+ + Support log with multiple line output such as stacktrace
5
+ + RegEx parameter to detect first line
6
+ + Save raw log data
7
+
8
+ **built-in templates are not supported. It does not support multiple line log anyway**
9
+
10
+ ## Installation
11
+
12
+ Use ruby gem as :
13
+
14
+ gem 'fluent-plugin-tail-multiline'
15
+
16
+ Or, if you're using td-client, you can call td-client's gem
17
+
18
+ $ /usr/lib64/fluent/ruby/bin/gem install fluent-plugin-tail-multiline
19
+
20
+ ## Base Configuration
21
+ Tail-Multiline extends [tail plugin](http://docs.fluentd.org/categories/in_tail).
22
+
23
+ ## Configuration
24
+ ### Additional Parameters
25
+ name | type | description
26
+ ----------------------|---------------------------------|---------------------------
27
+ format_firstline | string(default = format) | RegEx to detect first line of multiple line log, no name capture required
28
+ rawdata_key | string(default = null) | Store raw data with given key
29
+
30
+ ## Examples
31
+ ### Java log with exception
32
+ #### Input
33
+ ```
34
+ 2013-3-03 14:27:33 [main] INFO Main - Start
35
+ 2013-3-03 14:27:33 [main] ERROR Main - Exception
36
+ javax.management.RuntimeErrorException: null
37
+ at Main.main(Main.java:16) ~[bin/:na]
38
+ 2013-3-03 14:27:33 [main] INFO Main - End
39
+ ```
40
+ #### Parameters
41
+ ```
42
+ tag test
43
+ format /^(?<time>\d{4}-\d{1,2}-\d{1,2} \d{1,2}:\d{1,2}:\d{1,2}) \[(?<thread>.*)\] (?<level>[^\s]+)(?<message>.*)/
44
+ ```
45
+ #### Output
46
+ ```
47
+ 2013-03-03 14:27:33 +0900 test: {"thread":"main","level":"INFO","message":" Main - Start"}
48
+ 2013-03-03 14:27:33 +0900 test: {"thread":"main","level":"ERROR","message":" Main - Exception\njavax.management.RuntimeErrorException: null\n\tat Main.main(Main.java:16) ~[bin/:na]"}
49
+ 2013-03-03 14:27:33 +0900 test: {"thread":"main","level":"INFO","message":" Main - End\n"}
50
+ ```
51
+
52
+ ### Case where first line does not have any name capture
53
+ #### Input
54
+ ```
55
+ ----
56
+ time=2013-3-03 14:27:33
57
+ message=test1
58
+ ----
59
+ time=2013-3-03 14:27:34
60
+ message=test2
61
+ ```
62
+
63
+ #### Parameters
64
+ ```
65
+ tag test
66
+ format /time=(?<time>\d{4}-\d{1,2}-\d{1,2} \d{1,2}:\d{1,2}:\d{1,2}).*message=(?<message>.*)/
67
+ format_firstline /----/
68
+ ```
69
+
70
+ #### Output
71
+ ```
72
+ 2013-03-03 14:27:33 +0900 test: {"message":"test1"}
73
+ 2013-03-03 14:27:34 +0900 test: {"message":"test2"}
74
+ ```
75
+
76
+ ## Contributing
77
+
78
+ 1. Fork it
79
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
80
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
81
+ 4. Push to the branch (`git push origin my-new-feature`)
82
+ 5. Create new Pull Request
83
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+ Rake::TestTask.new(:test) do |test|
5
+ test.libs << 'lib' << 'test'
6
+ test.pattern = 'test/**/test_*.rb'
7
+ test.verbose = true
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,21 @@
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 |gem|
6
+ gem.name = "fluent-plugin-tail-multiline"
7
+ gem.version = "0.1.0"
8
+ gem.authors = ["Tomohisa Ota"]
9
+ gem.email = ["tomohisa.ota+github@gmail.com"]
10
+ gem.description = ""
11
+ gem.summary = gem.description
12
+ gem.homepage = "http://github.com/tomohisaota/fluent-plugin-tail-multiline"
13
+
14
+ gem.files = `git ls-files`.split($/)
15
+ gem.files.reject! { |fn| fn.include? "doc/" }
16
+ gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_runtime_dependency "fluentd"
21
+ end
@@ -0,0 +1,192 @@
1
+ module Fluent
2
+ require 'fluent/plugin/in_tail'
3
+ class TailMultilineInput < TailInput
4
+
5
+ class MultilineTextParser < TextParser
6
+ def configure(conf, required=true)
7
+ format = conf['format']
8
+ if format == nil
9
+ raise ConfigError, "'format' parameter is required"
10
+ elsif format[0] != ?/ || format[format.length-1] != ?/
11
+ raise ConfigError, "'format' should be RegEx. Template is not supported in multiline mode"
12
+ end
13
+
14
+ begin
15
+ @regex = Regexp.new(format[1..-2],Regexp::MULTILINE)
16
+ if @regex.named_captures.empty?
17
+ raise "No named captures"
18
+ end
19
+ rescue
20
+ raise ConfigError, "Invalid regexp in format '#{format[1..-2]}': #{$!}"
21
+ end
22
+
23
+ @parser = RegexpParser.new(@regex)
24
+
25
+ if @parser.respond_to?(:configure)
26
+ @parser.configure(conf)
27
+ end
28
+
29
+ format_firstline = conf['format_firstline']
30
+ if format_firstline
31
+ # Use custom matcher for 1st line
32
+ if format_firstline[0] == '/' && format_firstline[format_firstline.length-1] == '/'
33
+ @regex = Regexp.new(format_firstline[1..-2])
34
+ else
35
+ raise ConfigError, "Invalid regexp in format_firstline '#{format_firstline[1..-2]}': #{$!}"
36
+ end
37
+ end
38
+
39
+ return true
40
+ end
41
+
42
+ def match_firstline(text)
43
+ @regex.match(text)
44
+ end
45
+ end
46
+
47
+ Plugin.register_input('tail_multiline', self)
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
+
54
+ def initialize
55
+ super
56
+ @locker = Monitor.new
57
+ @logbuf = nil
58
+ @logbuf_flusher = CallLater::new
59
+ end
60
+
61
+ def configure_parser(conf)
62
+ @parser = MultilineTextParser.new
63
+ @parser.configure(conf)
64
+ end
65
+
66
+ def receive_lines(lines)
67
+ @logbuf_flusher.cancel()
68
+ es = MultiEventStream.new
69
+ @locker.synchronize do
70
+ lines.each {|line|
71
+ if @parser.match_firstline(line)
72
+ time, record = parse_logbuf(@logbuf)
73
+ if time && record
74
+ es.add(time, record)
75
+ end
76
+ @logbuf = line
77
+ else
78
+ @logbuf += line if(@logbuf)
79
+ end
80
+ }
81
+ end
82
+ unless es.empty?
83
+ begin
84
+ Engine.emit_stream(@tag, es)
85
+ rescue
86
+ # ignore errors. Engine shows logs and backtraces.
87
+ end
88
+ end
89
+ @logbuf_flusher.call_later(@auto_flush_sec) do
90
+ flush_logbuf()
91
+ end
92
+ end
93
+
94
+ def shutdown
95
+ super
96
+ flush_logbuf()
97
+ @logbuf_flusher.shutdown()
98
+ end
99
+
100
+ def flush_logbuf
101
+ time, record = nil,nil
102
+ @locker.synchronize do
103
+ time, record = parse_logbuf(@logbuf)
104
+ @logbuf = nil
105
+ end
106
+ if time && record
107
+ Engine.emit(@tag, time, record)
108
+ end
109
+ end
110
+
111
+ def parse_logbuf(buf)
112
+ return nil,nil unless buf
113
+ buf.chomp!
114
+ begin
115
+ time, record = @parser.parse(buf)
116
+ rescue
117
+ $log.warn line.dump, :error=>$!.to_s
118
+ $log.debug_backtrace
119
+ end
120
+ return nil,nil unless time && record
121
+ record[@rawdata_key] = buf if @rawdata_key
122
+ return time, record
123
+ end
124
+
125
+ end
126
+
127
+ class CallLater
128
+ def initialize
129
+ @locker = Monitor::new
130
+ @thread = Thread.new(&method(:run))
131
+ initExecBlock()
132
+ end
133
+
134
+ def call_later(delay,&block)
135
+ @locker.synchronize do
136
+ @exec_time = Engine.now + delay
137
+ @exec_block = block
138
+ end
139
+ @thread.run
140
+ end
141
+
142
+ def run
143
+ @running = true
144
+ while true
145
+ sleepSec = -1
146
+ @locker.synchronize do
147
+ now = Engine.now
148
+ if @exec_block && @exec_time <= now
149
+ @exec_block.call()
150
+ initExecBlock()
151
+ end
152
+ return unless @running
153
+ unless(@exec_time == -1)
154
+ sleepSec = @exec_time - now
155
+ end
156
+ end
157
+ if (sleepSec == -1)
158
+ sleep()
159
+ else
160
+ sleep(sleepSec)
161
+ end
162
+ end
163
+ rescue => e
164
+ puts e
165
+ end
166
+
167
+ def cancel()
168
+ initExecBlock()
169
+ end
170
+
171
+ def shutdown()
172
+ @locker.synchronize do
173
+ @running = false
174
+ end
175
+ if(@thread)
176
+ @thread.run
177
+ @thread.join
178
+ @thread = nil
179
+ end
180
+ end
181
+
182
+ private
183
+
184
+ def initExecBlock()
185
+ @locker.synchronize do
186
+ @exec_time = -1
187
+ @exec_block = nil
188
+ end
189
+ end
190
+
191
+ end
192
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,28 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+
12
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
13
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
14
+ require 'fluent/test'
15
+ unless ENV.has_key?('VERBOSE')
16
+ nulllogger = Object.new
17
+ nulllogger.instance_eval {|obj|
18
+ def method_missing(method, *args)
19
+ # pass
20
+ end
21
+ }
22
+ $log = nulllogger
23
+ end
24
+
25
+ require 'fluent/plugin/in_tail_multiline'
26
+
27
+ class Test::Unit::TestCase
28
+ end
@@ -0,0 +1,203 @@
1
+ require 'helper'
2
+
3
+ require 'tempfile'
4
+
5
+ class TailMultilineInputTest < Test::Unit::TestCase
6
+ def setup
7
+ Fluent::Test.setup
8
+ end
9
+
10
+ CONFIG = %[
11
+ ]
12
+ # CONFIG = %[
13
+ # path #{TMP_DIR}/out_file_test
14
+ # compress gz
15
+ # utc
16
+ # ]
17
+
18
+ def create_driver(conf = CONFIG)
19
+ Fluent::Test::InputTestDriver.new(Fluent::TailMultilineInput).configure(conf)
20
+ end
21
+
22
+ def test_emit_no_additional_option
23
+ tmpFile = Tempfile.new("in_tail_multiline-")
24
+ begin
25
+ d = create_driver %[
26
+ path #{tmpFile.path}
27
+ tag test
28
+ format /^[s|f] (?<message>.*)/
29
+ ]
30
+ d.run do
31
+ File.open(tmpFile.path, "w") {|f|
32
+ f.puts "f test1"
33
+ f.puts "s test2"
34
+ f.puts "f test3"
35
+ f.puts "f test4"
36
+ f.puts "s test5"
37
+ f.puts "s test6"
38
+ f.puts "f test7"
39
+ f.puts "s test8"
40
+ }
41
+ sleep 1
42
+ end
43
+
44
+ emits = d.emits
45
+ assert_equal(true, emits.length > 0)
46
+ assert_equal({"message"=>"test1"}, emits[0][2])
47
+ assert_equal({"message"=>"test2"}, emits[1][2])
48
+ assert_equal({"message"=>"test3"}, emits[2][2])
49
+ assert_equal({"message"=>"test4"}, emits[3][2])
50
+ assert_equal({"message"=>"test5"}, emits[4][2])
51
+ assert_equal({"message"=>"test6"}, emits[5][2])
52
+ assert_equal({"message"=>"test7"}, emits[6][2])
53
+ assert_equal({"message"=>"test8"}, emits[7][2])
54
+ ensure
55
+ tmpFile.close(true)
56
+ end
57
+ end
58
+
59
+ def test_emit_with_rawdata
60
+ tmpFile = Tempfile.new("in_tail_multiline-")
61
+ begin
62
+ d = create_driver %[
63
+ path #{tmpFile.path}
64
+ tag test
65
+ format /^[s|f] (?<message>.*)/
66
+ rawdata_key rawdata
67
+ ]
68
+ d.run do
69
+ File.open(tmpFile.path, "w") {|f|
70
+ f.puts "f test1"
71
+ f.puts "s test2"
72
+ f.puts "f test3"
73
+ f.puts "f test4"
74
+ f.puts "s test5"
75
+ f.puts "s test6"
76
+ f.puts "f test7"
77
+ f.puts "s test8"
78
+ }
79
+ sleep 1
80
+ end
81
+
82
+ emits = d.emits
83
+ assert_equal(true, emits.length > 0)
84
+ assert_equal({"message"=>"test1","rawdata"=>"f test1"}, emits[0][2])
85
+ assert_equal({"message"=>"test2","rawdata"=>"s test2"}, emits[1][2])
86
+ assert_equal({"message"=>"test3","rawdata"=>"f test3"}, emits[2][2])
87
+ assert_equal({"message"=>"test4","rawdata"=>"f test4"}, emits[3][2])
88
+ assert_equal({"message"=>"test5","rawdata"=>"s test5"}, emits[4][2])
89
+ assert_equal({"message"=>"test6","rawdata"=>"s test6"}, emits[5][2])
90
+ assert_equal({"message"=>"test7","rawdata"=>"f test7"}, emits[6][2])
91
+ assert_equal({"message"=>"test8","rawdata"=>"s test8"}, emits[7][2])
92
+ ensure
93
+ tmpFile.close(true)
94
+ end
95
+ end
96
+ def test_emit_with_format_firstline
97
+ tmpFile = Tempfile.new("in_tail_multiline-")
98
+ begin
99
+ d = create_driver %[
100
+ path #{tmpFile.path}
101
+ tag test
102
+ format /^[s|f] (?<message>.*)/
103
+ format_firstline /^[s]/
104
+ ]
105
+ d.run do
106
+ File.open(tmpFile.path, "w") {|f|
107
+ f.puts "f test1"
108
+ f.puts "s test2"
109
+ f.puts "f test3"
110
+ f.puts "f test4"
111
+ f.puts "s test5"
112
+ f.puts "s test6"
113
+ f.puts "f test7"
114
+ f.puts "s test8"
115
+ }
116
+ sleep 1
117
+ end
118
+
119
+ emits = d.emits
120
+ assert_equal(true, emits.length > 0)
121
+ n = -1
122
+ assert_equal({"message"=>"test2\nf test3\nf test4"}, emits[0][2])
123
+ assert_equal({"message"=>"test5"}, emits[1][2])
124
+ assert_equal({"message"=>"test6\nf test7"}, emits[2][2])
125
+ assert_equal({"message"=>"test8"}, emits[3][2])
126
+ ensure
127
+ tmpFile.close(true)
128
+ end
129
+ end
130
+
131
+ def test_emit_with_format_firstline_with_rawdata
132
+ tmpFile = Tempfile.new("in_tail_multiline-")
133
+ begin
134
+ d = create_driver %[
135
+ path #{tmpFile.path}
136
+ tag test
137
+ format /^[s|f] (?<message>.*)/
138
+ format_firstline /^[s]/
139
+ rawdata_key rawdata
140
+ ]
141
+ d.run do
142
+ File.open(tmpFile.path, "w") {|f|
143
+ f.puts "f test1"
144
+ f.puts "s test2"
145
+ f.puts "f test3"
146
+ f.puts "f test4"
147
+ f.puts "s test5"
148
+ f.puts "s test6"
149
+ f.puts "f test7"
150
+ f.puts "s test8"
151
+ }
152
+ sleep 1
153
+ end
154
+
155
+ emits = d.emits
156
+ assert_equal(true, emits.length > 0)
157
+ n = -1
158
+ assert_equal({"message"=>"test2\nf test3\nf test4","rawdata"=>"s test2\nf test3\nf test4"}, emits[0][2])
159
+ assert_equal({"message"=>"test5","rawdata"=>"s test5"}, emits[1][2])
160
+ assert_equal({"message"=>"test6\nf test7","rawdata"=>"s test6\nf test7"}, emits[2][2])
161
+ assert_equal({"message"=>"test8","rawdata"=>"s test8"}, emits[3][2])
162
+ ensure
163
+ tmpFile.close(true)
164
+ end
165
+ end
166
+
167
+ def test_multilinelog
168
+ tmpFile = Tempfile.new("in_tail_multiline-")
169
+ begin
170
+ d = create_driver %[
171
+ path #{tmpFile.path}
172
+ tag test
173
+ format /^s (?<message1>[^\\n]+)(\\nf (?<message2>[^\\n]+))?(\\nf (?<message3>.*))?/
174
+ format_firstline /^[s]/
175
+ rawdata_key rawdata
176
+ ]
177
+ d.run do
178
+ File.open(tmpFile.path, "w") {|f|
179
+ f.puts "f test1"
180
+ f.puts "s test2"
181
+ f.puts "f test3"
182
+ f.puts "f test4"
183
+ f.puts "s test5"
184
+ f.puts "s test6"
185
+ f.puts "f test7"
186
+ f.puts "s test8"
187
+ }
188
+ sleep 1
189
+ end
190
+
191
+ emits = d.emits
192
+ assert_equal(true, emits.length > 0)
193
+ n = -1
194
+ assert_equal({"message1"=>"test2","message2"=>"test3","message3"=>"test4","rawdata"=>"s test2\nf test3\nf test4"}, emits[0][2])
195
+ assert_equal({"message1"=>"test5","rawdata"=>"s test5"}, emits[1][2])
196
+ assert_equal({"message1"=>"test6","message2"=>"test7","rawdata"=>"s test6\nf test7"}, emits[2][2])
197
+ assert_equal({"message1"=>"test8","rawdata"=>"s test8"}, emits[3][2])
198
+ ensure
199
+ tmpFile.close(true)
200
+ end
201
+ end
202
+
203
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-tail-multiline
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tomohisa Ota
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-03-04 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'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ! '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: ''
28
+ email:
29
+ - tomohisa.ota+github@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - .gitignore
35
+ - Gemfile
36
+ - LICENSE.txt
37
+ - README.md
38
+ - Rakefile
39
+ - fluent-plugin-tail-multiline.gemspec
40
+ - lib/fluent/plugin/in_tail_multiline.rb
41
+ - test/helper.rb
42
+ - test/plugin/test_in_tail_multiline.rb
43
+ homepage: http://github.com/tomohisaota/fluent-plugin-tail-multiline
44
+ licenses: []
45
+ metadata: {}
46
+ post_install_message:
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ! '>='
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ requirements: []
61
+ rubyforge_project:
62
+ rubygems_version: 2.0.0
63
+ signing_key:
64
+ specification_version: 4
65
+ summary: ''
66
+ test_files:
67
+ - test/helper.rb
68
+ - test/plugin/test_in_tail_multiline.rb