fluent-plugin-group-exceptions 0.0.14

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,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