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

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.
@@ -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