gitlab-fluent-plugin-detect-exceptions 0.0.15

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