fluent-plugin-detect-exceptions-with-error 0.0.1a

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,302 @@
1
+ # Copyright 2016 Google Inc. All rights reserved.
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.
14
+ #
15
+ module Fluent
16
+ Struct.new('Rule', :from_state, :pattern, :to_state)
17
+
18
+ # Configuration of the state machine that detects exceptions.
19
+ module ExceptionDetectorConfig
20
+ # Rule for a state transition: if pattern matches go to the given state.
21
+ class RuleTarget
22
+ attr_accessor :pattern, :to_state
23
+
24
+ def initialize(p, s)
25
+ @pattern = p
26
+ @to_state = s
27
+ end
28
+
29
+ def ==(other)
30
+ other.class == self.class && other.state == state
31
+ end
32
+
33
+ alias eql? ==
34
+
35
+ def hash
36
+ state.hash
37
+ end
38
+
39
+ def state
40
+ [@pattern, @to_state]
41
+ end
42
+ end
43
+
44
+ def self.rule(from_state, pattern, to_state)
45
+ Struct::Rule.new(from_state, pattern, to_state)
46
+ end
47
+
48
+ def self.supported
49
+ RULES_BY_LANG.keys
50
+ end
51
+
52
+ JAVA_RULES = [
53
+ rule(:start_state, /(?:ERROR)[:\r\n]/, :java),
54
+ rule(:java, /(?:Exception|Error|Throwable|V8 errors stack trace)[:\r\n]/, :java),
55
+ rule(:java, /^[\t ]+(?:eval )?at /, :java),
56
+ rule(:java, /^[\t ]*(?:Caused by|Suppressed):/, :java),
57
+ rule(:java, /^[\t ]*... \d+\ more/, :java)
58
+ ].freeze
59
+
60
+ PYTHON_RULES = [
61
+ rule(:start_state, /Traceback \(most recent call last\)/, :python),
62
+ rule(:python, /^[\t ]+File /, :python_code),
63
+ rule(:python_code, /[^\t ]/, :python),
64
+ rule(:python, /^(?:[^\s.():]+\.)*[^\s.():]+:/, :start_state)
65
+ ].freeze
66
+
67
+ PHP_RULES = [
68
+ rule(:start_state, /
69
+ (?:PHP\ (?:Notice|Parse\ error|Fatal\ error|Warning):)|
70
+ (?:exception\ '[^']+'\ with\ message\ ')/x, :php_stack_begin),
71
+ rule(:php_stack_begin, /^Stack trace:/, :php_stack_frames),
72
+ rule(:php_stack_frames, /^#\d/, :php_stack_frames),
73
+ rule(:php_stack_frames, /^\s+thrown in /, :start_state)
74
+ ].freeze
75
+
76
+ GO_RULES = [
77
+ rule(:start_state, /panic: /, :go_before_goroutine),
78
+ rule(:go_before_goroutine, /^$/, :go_goroutine),
79
+ rule(:go_goroutine, /^goroutine \d+ \[[^\]]+\]:$/, :go_frame_1),
80
+ rule(:go_frame_1, /(?:[^\s.():]+\.)*[^\s.():]\(/, :go_frame_2),
81
+ rule(:go_frame_1, /^$/, :go_before_goroutine),
82
+ rule(:go_frame_2, /^\s/, :go_frame_1)
83
+ ].freeze
84
+
85
+ RUBY_RULES = [
86
+ rule(:start_state, /Error \(.*\):$/, :ruby),
87
+ rule(:ruby, /^[\t ]+.*?\.rb:\d+:in `/, :ruby)
88
+ ].freeze
89
+
90
+ ALL_RULES = (
91
+ JAVA_RULES + PYTHON_RULES + PHP_RULES + GO_RULES + RUBY_RULES).freeze
92
+
93
+ RULES_BY_LANG = {
94
+ java: JAVA_RULES,
95
+ javascript: JAVA_RULES,
96
+ js: JAVA_RULES,
97
+ csharp: JAVA_RULES,
98
+ py: PYTHON_RULES,
99
+ python: PYTHON_RULES,
100
+ php: PHP_RULES,
101
+ go: GO_RULES,
102
+ rb: RUBY_RULES,
103
+ ruby: RUBY_RULES,
104
+ all: ALL_RULES
105
+ }.freeze
106
+
107
+ DEFAULT_FIELDS = %w(message log).freeze
108
+ end
109
+
110
+ # State machine that consumes individual log lines and detects
111
+ # multi-line stack traces.
112
+ class ExceptionDetector
113
+ def initialize(*languages)
114
+ @state = :start_state
115
+ @rules = Hash.new { |h, k| h[k] = [] }
116
+
117
+ languages = [:all] if languages.empty?
118
+
119
+ languages.each do |lang|
120
+ rule_config =
121
+ ExceptionDetectorConfig::RULES_BY_LANG.fetch(lang.downcase) do |_k|
122
+ raise ArgumentError, "Unknown language: #{lang}"
123
+ end
124
+
125
+ rule_config.each do |r|
126
+ target = ExceptionDetectorConfig::RuleTarget.new(r[:pattern],
127
+ r[:to_state])
128
+ @rules[r[:from_state]] << target
129
+ end
130
+ end
131
+
132
+ @rules.each_value(&:uniq!)
133
+ end
134
+
135
+ # Updates the state machine and returns the trace detection status:
136
+ # - no_trace: 'line' does not belong to an exception trace,
137
+ # - start_trace: 'line' starts a detected exception trace,
138
+ # - inside: 'line' is part of a detected exception trace,
139
+ # - end: the detected exception trace ends after 'line'.
140
+ def update(line)
141
+ trace_seen_before = transition(line)
142
+ # If the state machine fell back to the start state because there is no
143
+ # defined transition for 'line', trigger another state transition because
144
+ # 'line' may contain the beginning of another exception.
145
+ transition(line) unless trace_seen_before
146
+ new_state = @state
147
+ trace_seen_after = new_state != :start_state
148
+
149
+ case [trace_seen_before, trace_seen_after]
150
+ when [true, true]
151
+ :inside_trace
152
+ when [true, false]
153
+ :end_trace
154
+ when [false, true]
155
+ :start_trace
156
+ else
157
+ :no_trace
158
+ end
159
+ end
160
+
161
+ def reset
162
+ @state = :start_state
163
+ end
164
+
165
+ private
166
+
167
+ # Executes a transition of the state machine for the given line.
168
+ # Returns false if the line does not match any transition rule and the
169
+ # state machine was reset to the initial state.
170
+ def transition(line)
171
+ @rules[@state].each do |r|
172
+ next unless line =~ r.pattern
173
+ @state = r.to_state
174
+ return true
175
+ end
176
+ @state = :start_state
177
+ false
178
+ end
179
+ end
180
+
181
+ # Buffers and groups log records if they contain exception stack traces.
182
+ class TraceAccumulator
183
+ attr_reader :buffer_start_time
184
+
185
+ # If message_field is nil, the instance is set up to accumulate
186
+ # records that are plain strings (i.e. the whole record is concatenated).
187
+ # Otherwise, the instance accepts records that are dictionaries (usually
188
+ # originating from structured JSON logs) and accumulates just the
189
+ # content of the given message field.
190
+ # message_field may contain the empty string. In this case, the
191
+ # TraceAccumulator 'learns' the field name from the first record by checking
192
+ # for some pre-defined common field names of text logs.
193
+ # The named parameters max_lines and max_bytes limit the maximum amount
194
+ # of data to be buffered. The default value 0 indicates 'no limit'.
195
+ def initialize(message_field, languages, max_lines: 0, max_bytes: 0,
196
+ &emit_callback)
197
+ @exception_detector = Fluent::ExceptionDetector.new(*languages)
198
+ @max_lines = max_lines
199
+ @max_bytes = max_bytes
200
+ @message_field = message_field
201
+ @messages = []
202
+ @buffer_start_time = Time.now
203
+ @buffer_size = 0
204
+ @first_record = nil
205
+ @first_timestamp = nil
206
+ @emit = emit_callback
207
+ end
208
+
209
+ def push(time_sec, record)
210
+ message = extract_message(record)
211
+ if message.nil?
212
+ @exception_detector.reset
213
+ detection_status = :no_trace
214
+ else
215
+ force_flush if @max_bytes > 0 &&
216
+ @buffer_size + message.length > @max_bytes
217
+ detection_status = @exception_detector.update(message)
218
+ end
219
+
220
+ update_buffer(detection_status, time_sec, record, message)
221
+
222
+ force_flush if @max_lines > 0 && @messages.length == @max_lines
223
+ end
224
+
225
+ def flush
226
+ case @messages.length
227
+ when 0
228
+ return
229
+ when 1
230
+ @emit.call(@first_timestamp, @first_record)
231
+ else
232
+ combined_message = @messages.join
233
+ if @message_field.nil?
234
+ output_record = combined_message
235
+ else
236
+ output_record = @first_record
237
+ output_record[@message_field] = combined_message
238
+ end
239
+ @emit.call(@first_timestamp, output_record)
240
+ end
241
+ @messages = []
242
+ @first_record = nil
243
+ @first_timestamp = nil
244
+ @buffer_size = 0
245
+ end
246
+
247
+ def force_flush
248
+ flush
249
+ @exception_detector.reset
250
+ end
251
+
252
+ private
253
+
254
+ def extract_message(record)
255
+ if !@message_field.nil? && @message_field.empty?
256
+ ExceptionDetectorConfig::DEFAULT_FIELDS.each do |f|
257
+ if record.key?(f)
258
+ @message_field = f
259
+ break
260
+ end
261
+ end
262
+ end
263
+ @message_field.nil? ? record : record[@message_field]
264
+ end
265
+
266
+ def update_buffer(detection_status, time_sec, record, message)
267
+ trigger_emit = detection_status == :no_trace ||
268
+ detection_status == :end_trace
269
+ if @messages.empty? && trigger_emit
270
+ @emit.call(time_sec, record)
271
+ return
272
+ end
273
+
274
+ case detection_status
275
+ when :inside_trace
276
+ add(time_sec, record, message)
277
+ when :end_trace
278
+ add(time_sec, record, message)
279
+ flush
280
+ when :no_trace
281
+ flush
282
+ add(time_sec, record, message)
283
+ flush
284
+ when :start_trace
285
+ flush
286
+ add(time_sec, record, message)
287
+ end
288
+ end
289
+
290
+ def add(time_sec, record, message)
291
+ if @messages.empty?
292
+ @first_record = record unless @message_field.nil?
293
+ @first_timestamp = time_sec
294
+ @buffer_start_time = Time.now
295
+ end
296
+ unless message.nil?
297
+ @messages << message
298
+ @buffer_size += message.length
299
+ end
300
+ end
301
+ end
302
+ end
@@ -0,0 +1,137 @@
1
+ #
2
+ # Copyright 2016 Google Inc. All rights reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ require 'fluent/plugin/exception_detector'
17
+ require 'fluent/output'
18
+
19
+ module Fluent
20
+ # This output plugin consumes a log stream of JSON objects which contain
21
+ # single-line log messages. If a consecutive sequence of log messages form
22
+ # an exception stack trace, they forwarded as a single, combined JSON
23
+ # object. Otherwise, the input log data is forwarded as is.
24
+ class DetectExceptionsOutput < Output
25
+ desc 'The field which contains the raw message text in the input JSON data.'
26
+ config_param :message, :string, default: ''
27
+ desc 'The prefix to be removed from the input tag when outputting a record.'
28
+ config_param :remove_tag_prefix, :string, default: ''
29
+ desc 'The interval of flushing the buffer for multiline format.'
30
+ config_param :multiline_flush_interval, :time, default: nil
31
+ desc 'Programming languages for which to detect exceptions. Default: all.'
32
+ config_param :languages, :array, value_type: :string, default: []
33
+ desc 'Maximum number of lines to flush (0 means no limit). Default: 1000.'
34
+ config_param :max_lines, :integer, default: 1000
35
+ desc 'Maximum number of bytes to flush (0 means no limit). Default: 0.'
36
+ config_param :max_bytes, :integer, default: 0
37
+ desc 'Separate log streams by this field in the input JSON data.'
38
+ config_param :stream, :string, default: ''
39
+
40
+ Fluent::Plugin.register_output('detect_exceptions', self)
41
+
42
+ def configure(conf)
43
+ super
44
+
45
+ if multiline_flush_interval
46
+ @check_flush_interval = [multiline_flush_interval * 0.1, 1].max
47
+ end
48
+
49
+ @languages = languages.map(&:to_sym)
50
+
51
+ # Maps log stream tags to a corresponding TraceAccumulator.
52
+ @accumulators = {}
53
+ end
54
+
55
+ def start
56
+ super
57
+
58
+ if multiline_flush_interval
59
+ @flush_buffer_mutex = Mutex.new
60
+ @stop_check = false
61
+ @thread = Thread.new(&method(:check_flush_loop))
62
+ end
63
+ end
64
+
65
+ def before_shutdown
66
+ flush_buffers
67
+ super if defined?(super)
68
+ end
69
+
70
+ def shutdown
71
+ # Before shutdown is not available in older fluentd versions.
72
+ # Hence, we make sure that we flush the buffers here as well.
73
+ flush_buffers
74
+ @thread.join if @multiline_flush_interval
75
+ super
76
+ end
77
+
78
+ def emit(tag, es, chain)
79
+ es.each do |time_sec, record|
80
+ process_record(tag, time_sec, record)
81
+ end
82
+ chain.next
83
+ end
84
+
85
+ private
86
+
87
+ def process_record(tag, time_sec, record)
88
+ synchronize do
89
+ log_id = [tag]
90
+ log_id.push(record.fetch(@stream, '')) unless @stream.empty?
91
+ unless @accumulators.key?(log_id)
92
+ out_tag = tag.sub(/^#{Regexp.escape(@remove_tag_prefix)}\./, '')
93
+ @accumulators[log_id] =
94
+ Fluent::TraceAccumulator.new(@message, @languages,
95
+ max_lines: @max_lines,
96
+ max_bytes: @max_bytes) do |t, r|
97
+ router.emit(out_tag, t, r)
98
+ end
99
+ end
100
+
101
+ @accumulators[log_id].push(time_sec, record)
102
+ end
103
+ end
104
+
105
+ def flush_buffers
106
+ synchronize do
107
+ @stop_check = true
108
+ @accumulators.each_value(&:force_flush)
109
+ end
110
+ end
111
+
112
+ def check_flush_loop
113
+ @flush_buffer_mutex.synchronize do
114
+ loop do
115
+ @flush_buffer_mutex.sleep(@check_flush_interval)
116
+ now = Time.now
117
+ break if @stop_check
118
+ @accumulators.each_value do |acc|
119
+ acc.force_flush if now - acc.buffer_start_time >
120
+ @multiline_flush_interval
121
+ end
122
+ end
123
+ end
124
+ rescue
125
+ log.error 'error in check_flush_loop', error: $ERROR_INFO.to_s
126
+ log.error_backtrace
127
+ end
128
+
129
+ def synchronize(&block)
130
+ if @multiline_flush_interval
131
+ @flush_buffer_mutex.synchronize(&block)
132
+ else
133
+ yield
134
+ end
135
+ end
136
+ end
137
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,46 @@
1
+ # Copyright 2016 Google Inc. All rights reserved.
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.
14
+
15
+ require 'rubygems'
16
+ require 'bundler'
17
+
18
+ begin
19
+ Bundler.setup(:default, :development)
20
+ rescue Bundler::BundlerError => e
21
+ $stderr.puts e.message
22
+ $stderr.puts 'Run `bundle install` to install missing gems'
23
+ exit e.status_code
24
+ end
25
+
26
+ require 'test/unit'
27
+
28
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
29
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
30
+ require 'fluent/test'
31
+
32
+ unless ENV.key?('VERBOSE')
33
+ nulllogger = Object.new
34
+ nulllogger.instance_eval do |_|
35
+ def respond_to_missing?
36
+ true
37
+ end
38
+
39
+ def method_missing(_method, *_args) # rubocop:disable Style/MethodMissing
40
+ end
41
+ end
42
+ # global $log variable is used by fluentd
43
+ $log = nulllogger # rubocop:disable Style/GlobalVars
44
+ end
45
+
46
+ require 'fluent/plugin/out_detect_exceptions'
@@ -0,0 +1,73 @@
1
+ # Copyright 2016 Google Inc. All rights reserved.
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.
14
+
15
+ require 'benchmark'
16
+
17
+ require 'fluent/plugin/exception_detector'
18
+
19
+ size_in_m = 25
20
+ line_length = 50
21
+
22
+ size = size_in_m << 20
23
+
24
+ JAVA_EXC = <<END.freeze
25
+ Jul 09, 2015 3:23:29 PM com.google.devtools.search.cloud.feeder.MakeLog: RuntimeException: Run from this message!
26
+ at com.my.app.Object.do$a1(MakeLog.java:50)
27
+ at java.lang.Thing.call(Thing.java:10)
28
+ at com.my.app.Object.help(MakeLog.java:40)
29
+ at sun.javax.API.method(API.java:100)
30
+ at com.jetty.Framework.main(MakeLog.java:30)
31
+ END
32
+
33
+ PYTHON_EXC = <<END.freeze
34
+ Traceback (most recent call last):
35
+ File "/base/data/home/runtimes/python27/python27_lib/versions/third_party/webapp2-2.5.2/webapp2.py", line 1535, in __call__
36
+ rv = self.handle_exception(request, response, e)
37
+ File "/base/data/home/apps/s~nearfieldspy/1.378705245900539993/nearfieldspy.py", line 17, in start
38
+ return get()
39
+ File "/base/data/home/apps/s~nearfieldspy/1.378705245900539993/nearfieldspy.py", line 5, in get
40
+ raise Exception('spam', 'eggs')
41
+ Exception: ('spam', 'eggs')
42
+ END
43
+
44
+ chars = [('a'..'z'), ('A'..'Z')].map(&:to_a).flatten
45
+
46
+ random_text = (1..(size / line_length)).collect do
47
+ (0...line_length).map { chars[rand(chars.length)] }.join
48
+ end
49
+
50
+ exceptions = {
51
+ java: (JAVA_EXC * (size / JAVA_EXC.length)).lines,
52
+ python: (PYTHON_EXC * (size / PYTHON_EXC.length)).lines
53
+ }
54
+
55
+ puts "Start benchmark. Input size #{size_in_m}M."
56
+ Benchmark.bm do |x|
57
+ languages = Fluent::ExceptionDetectorConfig::RULES_BY_LANG.keys
58
+ languages.each do |lang|
59
+ buffer = Fluent::TraceAccumulator.new(nil, lang) {}
60
+ x.report("#{lang}_detector_random_text") do
61
+ random_text.each { |l| buffer.push(0, l) }
62
+ end
63
+ end
64
+ [:java, :python, :all].each do |detector_lang|
65
+ buffer = Fluent::TraceAccumulator.new(nil, detector_lang) {}
66
+ exc_languages = detector_lang == :all ? exceptions.keys : [detector_lang]
67
+ exc_languages.each do |exc_lang|
68
+ x.report("#{detector_lang}_detector_#{exc_lang}_stacks") do
69
+ exceptions[exc_lang].each { |l| buffer.push(0, l) }
70
+ end
71
+ end
72
+ end
73
+ end