fluent-plugin-group-exceptions 0.0.14

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,307 @@
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.freeze
26
+ remove_tag_prefix prefix
27
+ END
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.freeze
36
+ SomeException: foo
37
+ at bar
38
+ Caused by: org.AnotherException
39
+ at bar2
40
+ at bar3
41
+ END
42
+
43
+ PHP_EXC = <<END.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
50
+
51
+ PYTHON_EXC = <<END.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
57
+
58
+ RUBY_EXC = <<END.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
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(driver, t, *messages, stream: nil)
80
+ count = 0
81
+ messages.each do |m|
82
+ m.each_line do |line|
83
+ driver.emit(log_entry(line, count, stream), t + count)
84
+ count += 1
85
+ end
86
+ end
87
+ end
88
+
89
+ def run_driver(driver, *messages)
90
+ t = Time.now.to_i
91
+ driver.run do
92
+ feed_lines(driver, t, *messages)
93
+ end
94
+ end
95
+
96
+ def make_logs(t, *messages, stream: nil)
97
+ count = 0
98
+ logs = []
99
+ messages.each do |m|
100
+ logs << [t + count, log_entry(m, count, stream)]
101
+ count += m.lines.count
102
+ end
103
+ logs
104
+ end
105
+
106
+ def test_configure
107
+ assert_nothing_raised do
108
+ create_driver
109
+ end
110
+ end
111
+
112
+ def test_exception_detection
113
+ d = create_driver
114
+ t = Time.now.to_i
115
+ messages = [ARBITRARY_TEXT, JAVA_EXC, ARBITRARY_TEXT]
116
+ d.run do
117
+ feed_lines(d, t, *messages)
118
+ end
119
+ assert_equal(make_logs(t, *messages), d.events)
120
+ end
121
+
122
+ def test_ignore_nested_exceptions
123
+ test_cases = {
124
+ 'php' => PHP_EXC,
125
+ 'python' => PYTHON_EXC,
126
+ 'ruby' => RUBY_EXC
127
+ }
128
+
129
+ test_cases.each do |language, exception|
130
+ cfg = %(
131
+ #{CONFIG}
132
+ languages #{language})
133
+ d = create_driver(cfg)
134
+ t = Time.now.to_i
135
+
136
+ # Convert exception to a single line to simplify the test case.
137
+ single_line_exception = exception.gsub("\n", '\\n')
138
+
139
+ # There is a nested exception within the body, we should ignore those!
140
+ json_line_with_exception = {
141
+ 'timestamp' => {
142
+ 'nanos' => 998_152_494,
143
+ 'seconds' => 1_496_420_064
144
+ },
145
+ 'message' => single_line_exception,
146
+ 'thread' => 139_658_267_147_048,
147
+ 'severity' => 'ERROR'
148
+ }.to_json + "\n"
149
+ json_line_without_exception = {
150
+ 'timestamp' => {
151
+ 'nanos' => 5_990_266,
152
+ 'seconds' => 1_496_420_065
153
+ },
154
+ 'message' => 'next line',
155
+ 'thread' => 139_658_267_147_048,
156
+ 'severity' => 'INFO'
157
+ }.to_json + "\n"
158
+
159
+ router_mock = flexmock('router')
160
+
161
+ # Validate that each line received is emitted separately as expected.
162
+ router_mock.should_receive(:emit)
163
+ .once.with(DEFAULT_TAG_STRIPPED, Integer,
164
+ 'message' => json_line_with_exception,
165
+ 'count' => 0)
166
+
167
+ router_mock.should_receive(:emit)
168
+ .once.with(DEFAULT_TAG_STRIPPED, Integer,
169
+ 'message' => json_line_without_exception,
170
+ 'count' => 1)
171
+
172
+ d.instance.router = router_mock
173
+
174
+ d.run do
175
+ feed_lines(d, t, json_line_with_exception + json_line_without_exception)
176
+ end
177
+ end
178
+ end
179
+
180
+ def test_single_language_config
181
+ cfg = %(
182
+ #{CONFIG}
183
+ languages java)
184
+ d = create_driver(cfg)
185
+ t = Time.now.to_i
186
+ d.run do
187
+ feed_lines(d, t, ARBITRARY_TEXT, JAVA_EXC, PYTHON_EXC)
188
+ end
189
+ expected = ARBITRARY_TEXT.lines + [JAVA_EXC] + PYTHON_EXC.lines
190
+ assert_equal(make_logs(t, *expected), d.events)
191
+ end
192
+
193
+ def test_multi_language_config
194
+ cfg = %(
195
+ #{CONFIG}
196
+ languages python, 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]
203
+ assert_equal(make_logs(t, *expected), d.events)
204
+ end
205
+
206
+ def test_split_exception_after_timeout
207
+ cfg = %(
208
+ #{CONFIG}
209
+ multiline_flush_interval 1)
210
+ d = create_driver(cfg)
211
+ t1 = 0
212
+ t2 = 0
213
+ d.run do
214
+ t1 = Time.now.to_i
215
+ feed_lines(d, t1, JAVA_EXC)
216
+ sleep 2
217
+ t2 = Time.now.to_i
218
+ feed_lines(d, t2, " at x\n at y\n")
219
+ end
220
+ assert_equal(make_logs(t1, JAVA_EXC) +
221
+ make_logs(t2, " at x\n", " at y\n"),
222
+ d.events)
223
+ end
224
+
225
+ def test_do_not_split_exception_after_pause
226
+ d = create_driver
227
+ t1 = 0
228
+ t2 = 0
229
+ d.run do
230
+ t1 = Time.now.to_i
231
+ feed_lines(d, t1, JAVA_EXC)
232
+ sleep 1
233
+ t2 = Time.now.to_i
234
+ feed_lines(d, t2, " at x\n at y\n")
235
+ d.instance.before_shutdown
236
+ end
237
+ assert_equal(make_logs(t1, JAVA_EXC + " at x\n at y\n"), d.events)
238
+ end
239
+
240
+ def test_remove_tag_prefix_is_required
241
+ cfg = ''
242
+ e = assert_raises(Fluent::ConfigError) { create_driver(cfg) }
243
+ assert_match(/remove_tag_prefix/, e.message)
244
+ end
245
+
246
+ def get_out_tags(remove_tag_prefix, original_tag)
247
+ cfg = "remove_tag_prefix #{remove_tag_prefix}"
248
+ d = create_driver(cfg, original_tag)
249
+ run_driver(d, ARBITRARY_TEXT, JAVA_EXC, ARBITRARY_TEXT)
250
+ d.emits.collect { |e| e[0] }.sort.uniq
251
+ end
252
+
253
+ def test_remove_tag_prefix
254
+ tags = get_out_tags('prefix.plus', 'prefix.plus.rest.of.the.tag')
255
+ assert_equal(['rest.of.the.tag'], tags)
256
+ tags = get_out_tags('prefix.pl', 'prefix.plus.rest.of.the.tag')
257
+ assert_equal(['prefix.plus.rest.of.the.tag'], tags)
258
+ tags = get_out_tags('does.not.occur', 'prefix.plus.rest.of.the.tag')
259
+ assert_equal(['prefix.plus.rest.of.the.tag'], tags)
260
+ end
261
+
262
+ def test_flush_after_max_lines
263
+ cfg = %(
264
+ #{CONFIG}
265
+ max_lines 2)
266
+ d = create_driver(cfg)
267
+ t = Time.now.to_i
268
+ d.run do
269
+ feed_lines(d, t, PYTHON_EXC, JAVA_EXC)
270
+ end
271
+ # Expected: the first two lines of the exception are buffered and combined.
272
+ # Then the max_lines setting kicks in and the rest of the Python exception
273
+ # is logged line-by-line (since it's not an exception stack in itself).
274
+ # For the following Java stack trace, the two lines of the first exception
275
+ # are buffered and combined. So are the first two lines of the second
276
+ # exception. Then the rest is logged line-by-line.
277
+ expected = [PYTHON_EXC.lines[0..1].join] + PYTHON_EXC.lines[2..-1] + \
278
+ [JAVA_EXC.lines[0..1].join] + [JAVA_EXC.lines[2..3].join] + \
279
+ JAVA_EXC.lines[4..-1]
280
+ assert_equal(make_logs(t, *expected), d.events)
281
+ end
282
+
283
+ def test_separate_streams
284
+ cfg = %(
285
+ #{CONFIG}
286
+ stream stream)
287
+ d = create_driver(cfg)
288
+ t = Time.now.to_i
289
+ d.run do
290
+ feed_lines(d, t, JAVA_EXC.lines[0], stream: 'java')
291
+ feed_lines(d, t, PYTHON_EXC.lines[0..1].join, stream: 'python')
292
+ feed_lines(d, t, JAVA_EXC.lines[1..-1].join, stream: 'java')
293
+ feed_lines(d, t, JAVA_EXC, stream: 'java')
294
+ feed_lines(d, t, PYTHON_EXC.lines[2..-1].join, stream: 'python')
295
+ feed_lines(d, t, 'something else', stream: 'java')
296
+ end
297
+ # Expected: the Python and the Java exceptions are handled separately
298
+ # because they belong to different streams.
299
+ # Note that the Java exception is only detected when 'something else'
300
+ # is processed.
301
+ expected = make_logs(t, JAVA_EXC, stream: 'java') +
302
+ make_logs(t, PYTHON_EXC, stream: 'python') +
303
+ make_logs(t, JAVA_EXC, stream: 'java') +
304
+ make_logs(t, 'something else', stream: 'java')
305
+ assert_equal(expected, d.events)
306
+ end
307
+ end
metadata ADDED
@@ -0,0 +1,136 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-group-exceptions
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.14
5
+ platform: ruby
6
+ authors:
7
+ - Deloitte Agents
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-10-15 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: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '='
46
+ - !ruby/object:Gem::Version
47
+ version: 0.42.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '='
53
+ - !ruby/object:Gem::Version
54
+ version: 0.42.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: test-unit
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: flexmock
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.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 Not an official Google Ruby gem. Added Multiworker to true
88
+ email:
89
+ - gatolgaj@gmail.com
90
+ executables: []
91
+ extensions: []
92
+ extra_rdoc_files: []
93
+ files:
94
+ - CONTRIBUTING
95
+ - Gemfile
96
+ - Gemfile.lock
97
+ - LICENSE
98
+ - README.rdoc
99
+ - Rakefile
100
+ - fluent-plugin-group-exceptions.gemspec
101
+ - fluent-plugin-multi-exceptions-0.0.13.gem
102
+ - fluent-plugin-multi-exceptions-0.0.14.gem
103
+ - lib/fluent/plugin/exception_detector.rb
104
+ - lib/fluent/plugin/out_detect_exceptions.rb
105
+ - test/helper.rb
106
+ - test/plugin/bench_exception_detector.rb
107
+ - test/plugin/test_exception_detector.rb
108
+ - test/plugin/test_out_detect_exceptions.rb
109
+ homepage: https://github.com/GoogleCloudPlatform/fluent-plugin-group-exceptions
110
+ licenses:
111
+ - Apache-2.0
112
+ metadata: {}
113
+ post_install_message:
114
+ rdoc_options: []
115
+ require_paths:
116
+ - lib
117
+ required_ruby_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '2.0'
122
+ required_rubygems_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ requirements: []
128
+ rubygems_version: 3.0.3
129
+ signing_key:
130
+ specification_version: 4
131
+ summary: fluentd output plugin for combining stack traces as multi-line JSON logs
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.rb