fluent-plugin-detect-exceptions-with-webflux-support 0.0.13

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,352 @@
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 'flexmock/test_unit'
16
+ require_relative '../helper'
17
+ require 'fluent/plugin/out_detect_exceptions'
18
+ require 'json'
19
+
20
+ class DetectExceptionsOutputTest < Test::Unit::TestCase
21
+ def setup
22
+ Fluent::Test.setup
23
+ end
24
+
25
+ CONFIG = <<~END_CONFIG.freeze
26
+ remove_tag_prefix prefix
27
+ END_CONFIG
28
+
29
+ DEFAULT_TAG = 'prefix.test.tag'.freeze
30
+
31
+ DEFAULT_TAG_STRIPPED = 'test.tag'.freeze
32
+
33
+ ARBITRARY_TEXT = 'This line is not an exception.'.freeze
34
+
35
+ JAVA_EXC = <<~END_JAVA.freeze
36
+ SomeException: foo
37
+ at bar
38
+ Caused by: org.AnotherException
39
+ at bar2
40
+ at bar3
41
+ END_JAVA
42
+
43
+ PHP_EXC = <<~END_PHP.freeze
44
+ exception 'Exception' with message 'Custom exception' in /home/joe/work/test-php/test.php:5
45
+ Stack trace:
46
+ #0 /home/joe/work/test-php/test.php(9): func1()
47
+ #1 /home/joe/work/test-php/test.php(13): func2()
48
+ #2 {main}
49
+ END_PHP
50
+
51
+ PYTHON_EXC = <<~END_PYTHON.freeze
52
+ Traceback (most recent call last):
53
+ File "/base/data/home/runtimes/python27/python27_lib/versions/third_party/webapp2-2.5.2/webapp2.py", line 1535, in __call__
54
+ rv = self.handle_exception(request, response, e)
55
+ Exception: ('spam', 'eggs')
56
+ END_PYTHON
57
+
58
+ RUBY_EXC = <<~END_RUBY.freeze
59
+ examble.rb:18:in `thrower': An error has occurred. (RuntimeError)
60
+ from examble.rb:14:in `caller'
61
+ from examble.rb:10:in `helper'
62
+ from examble.rb:6:in `writer'
63
+ from examble.rb:2:in `runner'
64
+ from examble.rb:21:in `<main>'
65
+ END_RUBY
66
+
67
+ def create_driver(conf = CONFIG, tag = DEFAULT_TAG)
68
+ d = Fluent::Test::OutputTestDriver.new(Fluent::DetectExceptionsOutput, tag)
69
+ d.configure(conf)
70
+ d
71
+ end
72
+
73
+ def log_entry(message, count, stream)
74
+ log_entry = { 'message' => message, 'count' => count }
75
+ log_entry['stream'] = stream unless stream.nil?
76
+ log_entry
77
+ end
78
+
79
+ def feed_lines_without_line_breaks(driver, timestamp, *messages, stream: nil)
80
+ count = 0
81
+ messages.each do |m|
82
+ m.each_line do |line|
83
+ line.delete!("\n")
84
+ driver.emit(log_entry(line, count, stream), timestamp + count)
85
+ count += 1
86
+ end
87
+ end
88
+ end
89
+
90
+ def feed_lines(driver, timestamp, *messages, stream: nil)
91
+ count = 0
92
+ messages.each do |m|
93
+ m.each_line do |line|
94
+ driver.emit(log_entry(line, count, stream), timestamp + count)
95
+ count += 1
96
+ end
97
+ end
98
+ end
99
+
100
+ def run_driver(driver, *messages)
101
+ t = Time.now.to_i
102
+ driver.run do
103
+ feed_lines(driver, t, *messages)
104
+ end
105
+ end
106
+
107
+ def make_logs(timestamp, *messages, stream: nil)
108
+ count = 0
109
+ logs = []
110
+ messages.each do |m|
111
+ logs << [timestamp + count, log_entry(m, count, stream)]
112
+ count += m.lines.count
113
+ end
114
+ logs
115
+ end
116
+
117
+ def test_configure
118
+ assert_nothing_raised do
119
+ create_driver
120
+ end
121
+ end
122
+
123
+ def test_exception_detection
124
+ d = create_driver
125
+ t = Time.now.to_i
126
+ messages = [ARBITRARY_TEXT, JAVA_EXC, ARBITRARY_TEXT]
127
+ d.run do
128
+ feed_lines(d, t, *messages)
129
+ end
130
+ assert_equal(make_logs(t, *messages), d.events)
131
+ end
132
+
133
+ def test_ignore_nested_exceptions
134
+ test_cases = {
135
+ 'php' => PHP_EXC,
136
+ 'python' => PYTHON_EXC,
137
+ 'ruby' => RUBY_EXC
138
+ }
139
+
140
+ test_cases.each do |language, exception|
141
+ cfg = %(
142
+ #{CONFIG}
143
+ languages #{language})
144
+ d = create_driver(cfg)
145
+ t = Time.now.to_i
146
+
147
+ # Convert exception to a single line to simplify the test case.
148
+ single_line_exception = exception.gsub("\n", '\\n')
149
+
150
+ # There is a nested exception within the body, we should ignore those!
151
+ json_with_exception = {
152
+ 'timestamp' => {
153
+ 'nanos' => 998_152_494,
154
+ 'seconds' => 1_496_420_064
155
+ },
156
+ 'message' => single_line_exception,
157
+ 'thread' => 139_658_267_147_048,
158
+ 'severity' => 'ERROR'
159
+ }
160
+ json_line_with_exception = "#{json_with_exception.to_json}\n"
161
+ json_without_exception = {
162
+ 'timestamp' => {
163
+ 'nanos' => 5_990_266,
164
+ 'seconds' => 1_496_420_065
165
+ },
166
+ 'message' => 'next line',
167
+ 'thread' => 139_658_267_147_048,
168
+ 'severity' => 'INFO'
169
+ }
170
+ json_line_without_exception = "#{json_without_exception.to_json}\n"
171
+
172
+ router_mock = flexmock('router')
173
+
174
+ # Validate that each line received is emitted separately as expected.
175
+ router_mock.should_receive(:emit)
176
+ .once.with(DEFAULT_TAG_STRIPPED, Integer,
177
+ 'message' => json_line_with_exception,
178
+ 'count' => 0)
179
+
180
+ router_mock.should_receive(:emit)
181
+ .once.with(DEFAULT_TAG_STRIPPED, Integer,
182
+ 'message' => json_line_without_exception,
183
+ 'count' => 1)
184
+
185
+ d.instance.router = router_mock
186
+
187
+ d.run do
188
+ feed_lines(d, t, json_line_with_exception + json_line_without_exception)
189
+ end
190
+ end
191
+ end
192
+
193
+ def test_single_language_config
194
+ cfg = %(
195
+ #{CONFIG}
196
+ languages java)
197
+ d = create_driver(cfg)
198
+ t = Time.now.to_i
199
+ d.run do
200
+ feed_lines(d, t, ARBITRARY_TEXT, JAVA_EXC, PYTHON_EXC)
201
+ end
202
+ expected = ARBITRARY_TEXT.lines + [JAVA_EXC] + PYTHON_EXC.lines
203
+ assert_equal(make_logs(t, *expected), d.events)
204
+ end
205
+
206
+ def test_multi_language_config
207
+ cfg = %(
208
+ #{CONFIG}
209
+ languages python, java)
210
+ d = create_driver(cfg)
211
+ t = Time.now.to_i
212
+ d.run do
213
+ feed_lines(d, t, ARBITRARY_TEXT, JAVA_EXC, PYTHON_EXC)
214
+ end
215
+ expected = ARBITRARY_TEXT.lines + [JAVA_EXC] + [PYTHON_EXC]
216
+ assert_equal(make_logs(t, *expected), d.events)
217
+ end
218
+
219
+ def test_split_exception_after_timeout
220
+ cfg = %(
221
+ #{CONFIG}
222
+ multiline_flush_interval 1)
223
+ d = create_driver(cfg)
224
+ t1 = 0
225
+ t2 = 0
226
+ d.run do
227
+ t1 = Time.now.to_i
228
+ feed_lines(d, t1, JAVA_EXC)
229
+ sleep 2
230
+ t2 = Time.now.to_i
231
+ feed_lines(d, t2, " at x\n at y\n")
232
+ end
233
+ assert_equal(make_logs(t1, JAVA_EXC) +
234
+ make_logs(t2, " at x\n", " at y\n"),
235
+ d.events)
236
+ end
237
+
238
+ def test_do_not_split_exception_after_pause
239
+ d = create_driver
240
+ t1 = 0
241
+ t2 = 0
242
+ d.run do
243
+ t1 = Time.now.to_i
244
+ feed_lines(d, t1, JAVA_EXC)
245
+ sleep 1
246
+ t2 = Time.now.to_i
247
+ feed_lines(d, t2, " at x\n at y\n")
248
+ d.instance.before_shutdown
249
+ end
250
+ assert_equal(make_logs(t1, "#{JAVA_EXC} at x\n at y\n"), d.events)
251
+ end
252
+
253
+ def test_remove_tag_prefix_is_required
254
+ cfg = ''
255
+ e = assert_raises(Fluent::ConfigError) { create_driver(cfg) }
256
+ assert_match(/remove_tag_prefix/, e.message)
257
+ end
258
+
259
+ def get_out_tags(remove_tag_prefix, original_tag)
260
+ cfg = "remove_tag_prefix #{remove_tag_prefix}"
261
+ d = create_driver(cfg, original_tag)
262
+ run_driver(d, ARBITRARY_TEXT, JAVA_EXC, ARBITRARY_TEXT)
263
+ d.emits.collect { |e| e[0] }.sort.uniq
264
+ end
265
+
266
+ def test_remove_tag_prefix
267
+ tags = get_out_tags('prefix.plus', 'prefix.plus.rest.of.the.tag')
268
+ assert_equal(['rest.of.the.tag'], tags)
269
+ tags = get_out_tags('prefix.pl', 'prefix.plus.rest.of.the.tag')
270
+ assert_equal(['prefix.plus.rest.of.the.tag'], tags)
271
+ tags = get_out_tags('does.not.occur', 'prefix.plus.rest.of.the.tag')
272
+ assert_equal(['prefix.plus.rest.of.the.tag'], tags)
273
+ end
274
+
275
+ def test_force_line_breaks_false
276
+ cfg = %(
277
+ #{CONFIG}
278
+ force_line_breaks true)
279
+ d = create_driver(cfg)
280
+ t = Time.now.to_i
281
+ d.run do
282
+ feed_lines(d, t, JAVA_EXC)
283
+ end
284
+ expected = JAVA_EXC
285
+ assert_equal(make_logs(t, *expected), d.events)
286
+ end
287
+
288
+ def test_force_line_breaks_true
289
+ cfg = %(
290
+ #{CONFIG}
291
+ force_line_breaks true)
292
+ d = create_driver(cfg)
293
+ t = Time.now.to_i
294
+ d.run do
295
+ feed_lines_without_line_breaks(d, t, JAVA_EXC)
296
+ end
297
+ # Expected: the first two lines of the exception are buffered and combined.
298
+ # Then the max_lines setting kicks in and the rest of the Python exception
299
+ # is logged line-by-line (since it's not an exception stack in itself).
300
+ # For the following Java stack trace, the two lines of the first exception
301
+ # are buffered and combined. So are the first two lines of the second
302
+ # exception. Then the rest is logged line-by-line.
303
+ expected = JAVA_EXC.chomp
304
+ assert_equal(make_logs(t, *expected), d.events)
305
+ end
306
+
307
+ def test_flush_after_max_lines
308
+ cfg = %(
309
+ #{CONFIG}
310
+ max_lines 2)
311
+ d = create_driver(cfg)
312
+ t = Time.now.to_i
313
+ d.run do
314
+ feed_lines(d, t, PYTHON_EXC, JAVA_EXC)
315
+ end
316
+ # Expected: the first two lines of the exception are buffered and combined.
317
+ # Then the max_lines setting kicks in and the rest of the Python exception
318
+ # is logged line-by-line (since it's not an exception stack in itself).
319
+ # For the following Java stack trace, the two lines of the first exception
320
+ # are buffered and combined. So are the first two lines of the second
321
+ # exception. Then the rest is logged line-by-line.
322
+ expected = [PYTHON_EXC.lines[0..1].join] + PYTHON_EXC.lines[2..] + \
323
+ [JAVA_EXC.lines[0..1].join] + [JAVA_EXC.lines[2..3].join] + \
324
+ JAVA_EXC.lines[4..]
325
+ assert_equal(make_logs(t, *expected), d.events)
326
+ end
327
+
328
+ def test_separate_streams
329
+ cfg = %(
330
+ #{CONFIG}
331
+ stream stream)
332
+ d = create_driver(cfg)
333
+ t = Time.now.to_i
334
+ d.run do
335
+ feed_lines(d, t, JAVA_EXC.lines[0], stream: 'java')
336
+ feed_lines(d, t, PYTHON_EXC.lines[0..1].join, stream: 'python')
337
+ feed_lines(d, t, JAVA_EXC.lines[1..].join, stream: 'java')
338
+ feed_lines(d, t, JAVA_EXC, stream: 'java')
339
+ feed_lines(d, t, PYTHON_EXC.lines[2..].join, stream: 'python')
340
+ feed_lines(d, t, 'something else', stream: 'java')
341
+ end
342
+ # Expected: the Python and the Java exceptions are handled separately
343
+ # because they belong to different streams.
344
+ # Note that the Java exception is only detected when 'something else'
345
+ # is processed.
346
+ expected = make_logs(t, JAVA_EXC, stream: 'java') +
347
+ make_logs(t, PYTHON_EXC, stream: 'python') +
348
+ make_logs(t, JAVA_EXC, stream: 'java') +
349
+ make_logs(t, 'something else', stream: 'java')
350
+ assert_equal(expected, d.events)
351
+ end
352
+ end
metadata ADDED
@@ -0,0 +1,136 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-detect-exceptions-with-webflux-support
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.13
5
+ platform: ruby
6
+ authors:
7
+ - Stackdriver Agents
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-05-08 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'
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'
27
+ - !ruby/object:Gem::Dependency
28
+ name: flexmock
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.3'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '='
60
+ - !ruby/object:Gem::Version
61
+ version: 1.48.1
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '='
67
+ - !ruby/object:Gem::Version
68
+ version: 1.48.1
69
+ - !ruby/object:Gem::Dependency
70
+ name: test-unit
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ description: |2
84
+ Fluentd output plugin which detects exception stack traces in a stream of
85
+ JSON log messages and combines all single-line messages that belong to the
86
+ same stack trace into one multi-line message.
87
+ This is an official Google Ruby gem.
88
+ NOTE: This is a fork that tries to support spring webflux stack
89
+ email:
90
+ - stackdriver-agents@google.com
91
+ executables: []
92
+ extensions: []
93
+ extra_rdoc_files: []
94
+ files:
95
+ - CONTRIBUTING
96
+ - Gemfile
97
+ - LICENSE
98
+ - README.rdoc
99
+ - Rakefile
100
+ - fluent-plugin-detect-exceptions-with-webflux-support-0.0.12.gem
101
+ - fluent-plugin-detect-exceptions.gemspec
102
+ - lib/fluent/plugin/exception_detector.rb
103
+ - lib/fluent/plugin/out_detect_exceptions_with_webflux_support.rb
104
+ - test/helper.rb
105
+ - test/plugin/bench_exception_detector.rb
106
+ - test/plugin/test_exception_detector.rb
107
+ - test/plugin/test_out_detect_exceptions_with_webflux_support.rb
108
+ homepage: https://github.com/faisaltheparttimecoder/fluent-plugin-detect-exceptions_with_webflux_support
109
+ licenses:
110
+ - Apache-2.0
111
+ metadata: {}
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '2.6'
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubygems_version: 3.2.3
128
+ signing_key:
129
+ specification_version: 4
130
+ summary: fluentd output plugin for combining stack traces as multi-line JSON logs
131
+ with webflux support
132
+ test_files:
133
+ - test/helper.rb
134
+ - test/plugin/bench_exception_detector.rb
135
+ - test/plugin/test_exception_detector.rb
136
+ - test/plugin/test_out_detect_exceptions_with_webflux_support.rb