gitlab-fluent-plugin-detect-exceptions 0.0.15

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,350 @@
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_without_line_breaks(driver, t, *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), t + count)
85
+ count += 1
86
+ end
87
+ end
88
+ end
89
+
90
+ def feed_lines(driver, t, *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), t + 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(t, *messages, stream: nil)
108
+ count = 0
109
+ logs = []
110
+ messages.each do |m|
111
+ logs << [t + 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_line_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
+ }.to_json + "\n"
160
+ json_line_without_exception = {
161
+ 'timestamp' => {
162
+ 'nanos' => 5_990_266,
163
+ 'seconds' => 1_496_420_065
164
+ },
165
+ 'message' => 'next line',
166
+ 'thread' => 139_658_267_147_048,
167
+ 'severity' => 'INFO'
168
+ }.to_json + "\n"
169
+
170
+ router_mock = flexmock('router')
171
+
172
+ # Validate that each line received is emitted separately as expected.
173
+ router_mock.should_receive(:emit)
174
+ .once.with(DEFAULT_TAG_STRIPPED, Integer,
175
+ 'message' => json_line_with_exception,
176
+ 'count' => 0)
177
+
178
+ router_mock.should_receive(:emit)
179
+ .once.with(DEFAULT_TAG_STRIPPED, Integer,
180
+ 'message' => json_line_without_exception,
181
+ 'count' => 1)
182
+
183
+ d.instance.router = router_mock
184
+
185
+ d.run do
186
+ feed_lines(d, t, json_line_with_exception + json_line_without_exception)
187
+ end
188
+ end
189
+ end
190
+
191
+ def test_single_language_config
192
+ cfg = %(
193
+ #{CONFIG}
194
+ languages java)
195
+ d = create_driver(cfg)
196
+ t = Time.now.to_i
197
+ d.run do
198
+ feed_lines(d, t, ARBITRARY_TEXT, JAVA_EXC, PYTHON_EXC)
199
+ end
200
+ expected = ARBITRARY_TEXT.lines + [JAVA_EXC] + PYTHON_EXC.lines
201
+ assert_equal(make_logs(t, *expected), d.events)
202
+ end
203
+
204
+ def test_multi_language_config
205
+ cfg = %(
206
+ #{CONFIG}
207
+ languages python, java)
208
+ d = create_driver(cfg)
209
+ t = Time.now.to_i
210
+ d.run do
211
+ feed_lines(d, t, ARBITRARY_TEXT, JAVA_EXC, PYTHON_EXC)
212
+ end
213
+ expected = ARBITRARY_TEXT.lines + [JAVA_EXC] + [PYTHON_EXC]
214
+ assert_equal(make_logs(t, *expected), d.events)
215
+ end
216
+
217
+ def test_split_exception_after_timeout
218
+ cfg = %(
219
+ #{CONFIG}
220
+ multiline_flush_interval 1)
221
+ d = create_driver(cfg)
222
+ t1 = 0
223
+ t2 = 0
224
+ d.run do
225
+ t1 = Time.now.to_i
226
+ feed_lines(d, t1, JAVA_EXC)
227
+ sleep 2
228
+ t2 = Time.now.to_i
229
+ feed_lines(d, t2, " at x\n at y\n")
230
+ end
231
+ assert_equal(make_logs(t1, JAVA_EXC) +
232
+ make_logs(t2, " at x\n", " at y\n"),
233
+ d.events)
234
+ end
235
+
236
+ def test_do_not_split_exception_after_pause
237
+ d = create_driver
238
+ t1 = 0
239
+ t2 = 0
240
+ d.run do
241
+ t1 = Time.now.to_i
242
+ feed_lines(d, t1, JAVA_EXC)
243
+ sleep 1
244
+ t2 = Time.now.to_i
245
+ feed_lines(d, t2, " at x\n at y\n")
246
+ d.instance.before_shutdown
247
+ end
248
+ assert_equal(make_logs(t1, JAVA_EXC + " at x\n at y\n"), d.events)
249
+ end
250
+
251
+ def test_remove_tag_prefix_is_required
252
+ cfg = ''
253
+ e = assert_raises(Fluent::ConfigError) { create_driver(cfg) }
254
+ assert_match(/remove_tag_prefix/, e.message)
255
+ end
256
+
257
+ def get_out_tags(remove_tag_prefix, original_tag)
258
+ cfg = "remove_tag_prefix #{remove_tag_prefix}"
259
+ d = create_driver(cfg, original_tag)
260
+ run_driver(d, ARBITRARY_TEXT, JAVA_EXC, ARBITRARY_TEXT)
261
+ d.emits.collect { |e| e[0] }.sort.uniq
262
+ end
263
+
264
+ def test_remove_tag_prefix
265
+ tags = get_out_tags('prefix.plus', 'prefix.plus.rest.of.the.tag')
266
+ assert_equal(['rest.of.the.tag'], tags)
267
+ tags = get_out_tags('prefix.pl', 'prefix.plus.rest.of.the.tag')
268
+ assert_equal(['prefix.plus.rest.of.the.tag'], tags)
269
+ tags = get_out_tags('does.not.occur', 'prefix.plus.rest.of.the.tag')
270
+ assert_equal(['prefix.plus.rest.of.the.tag'], tags)
271
+ end
272
+
273
+ def test_force_line_breaks_false
274
+ cfg = %(
275
+ #{CONFIG}
276
+ force_line_breaks true)
277
+ d = create_driver(cfg)
278
+ t = Time.now.to_i
279
+ d.run do
280
+ feed_lines(d, t, JAVA_EXC)
281
+ end
282
+ expected = JAVA_EXC
283
+ assert_equal(make_logs(t, *expected), d.events)
284
+ end
285
+
286
+ def test_force_line_breaks_true
287
+ cfg = %(
288
+ #{CONFIG}
289
+ force_line_breaks true)
290
+ d = create_driver(cfg)
291
+ t = Time.now.to_i
292
+ d.run do
293
+ feed_lines_without_line_breaks(d, t, JAVA_EXC)
294
+ end
295
+ # Expected: the first two lines of the exception are buffered and combined.
296
+ # Then the max_lines setting kicks in and the rest of the Python exception
297
+ # is logged line-by-line (since it's not an exception stack in itself).
298
+ # For the following Java stack trace, the two lines of the first exception
299
+ # are buffered and combined. So are the first two lines of the second
300
+ # exception. Then the rest is logged line-by-line.
301
+ expected = JAVA_EXC.chomp
302
+ assert_equal(make_logs(t, *expected), d.events)
303
+ end
304
+
305
+ def test_flush_after_max_lines
306
+ cfg = %(
307
+ #{CONFIG}
308
+ max_lines 2)
309
+ d = create_driver(cfg)
310
+ t = Time.now.to_i
311
+ d.run do
312
+ feed_lines(d, t, PYTHON_EXC, JAVA_EXC)
313
+ end
314
+ # Expected: the first two lines of the exception are buffered and combined.
315
+ # Then the max_lines setting kicks in and the rest of the Python exception
316
+ # is logged line-by-line (since it's not an exception stack in itself).
317
+ # For the following Java stack trace, the two lines of the first exception
318
+ # are buffered and combined. So are the first two lines of the second
319
+ # exception. Then the rest is logged line-by-line.
320
+ expected = [PYTHON_EXC.lines[0..1].join] + PYTHON_EXC.lines[2..-1] + \
321
+ [JAVA_EXC.lines[0..1].join] + [JAVA_EXC.lines[2..3].join] + \
322
+ JAVA_EXC.lines[4..-1]
323
+ assert_equal(make_logs(t, *expected), d.events)
324
+ end
325
+
326
+ def test_separate_streams
327
+ cfg = %(
328
+ #{CONFIG}
329
+ stream stream)
330
+ d = create_driver(cfg)
331
+ t = Time.now.to_i
332
+ d.run do
333
+ feed_lines(d, t, JAVA_EXC.lines[0], stream: 'java')
334
+ feed_lines(d, t, PYTHON_EXC.lines[0..1].join, stream: 'python')
335
+ feed_lines(d, t, JAVA_EXC.lines[1..-1].join, stream: 'java')
336
+ feed_lines(d, t, JAVA_EXC, stream: 'java')
337
+ feed_lines(d, t, PYTHON_EXC.lines[2..-1].join, stream: 'python')
338
+ feed_lines(d, t, 'something else', stream: 'java')
339
+ end
340
+ # Expected: the Python and the Java exceptions are handled separately
341
+ # because they belong to different streams.
342
+ # Note that the Java exception is only detected when 'something else'
343
+ # is processed.
344
+ expected = make_logs(t, JAVA_EXC, stream: 'java') +
345
+ make_logs(t, PYTHON_EXC, stream: 'python') +
346
+ make_logs(t, JAVA_EXC, stream: 'java') +
347
+ make_logs(t, 'something else', stream: 'java')
348
+ assert_equal(expected, d.events)
349
+ end
350
+ end
metadata ADDED
@@ -0,0 +1,134 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gitlab-fluent-plugin-detect-exceptions
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.15
5
+ platform: ruby
6
+ authors:
7
+ - Stackdriver Agents
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-12-22 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 an official Google Ruby gem.
88
+ email:
89
+ - stackdriver-agents@google.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
+ - gitlab-fluent-plugin-detect-exceptions.gemspec
101
+ - lib/fluent/plugin/exception_detector.rb
102
+ - lib/fluent/plugin/out_detect_exceptions.rb
103
+ - test/helper.rb
104
+ - test/plugin/bench_exception_detector.rb
105
+ - test/plugin/test_exception_detector.rb
106
+ - test/plugin/test_out_detect_exceptions.rb
107
+ homepage: https://gitlab.com/gitlab-org/ruby/gems/gitlab-fluent-plugin-detect-exceptions
108
+ licenses:
109
+ - Apache-2.0
110
+ metadata: {}
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '2.0'
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubygems_version: 3.3.26
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: fluentd output plugin for combining stack traces as multi-line JSON logs
130
+ test_files:
131
+ - test/helper.rb
132
+ - test/plugin/bench_exception_detector.rb
133
+ - test/plugin/test_exception_detector.rb
134
+ - test/plugin/test_out_detect_exceptions.rb