fluent-plugin-vadimberezniker-gcp 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,465 @@
1
+ # Copyright 2014 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 'fluent/test/startup_shutdown'
16
+ require 'net/http'
17
+
18
+ require_relative 'base_test'
19
+ require_relative 'test_driver'
20
+
21
+ # Unit tests for Google Cloud Logging plugin
22
+ class GoogleCloudOutputTest < Test::Unit::TestCase
23
+ include BaseTest
24
+ extend Fluent::Test::StartupShutdown
25
+
26
+ def test_configure_use_grpc
27
+ setup_gce_metadata_stubs
28
+ d = create_driver
29
+ assert_false d.instance.instance_variable_get(:@use_grpc)
30
+ end
31
+
32
+ def test_user_agent
33
+ setup_gce_metadata_stubs
34
+ user_agent = nil
35
+ stub_request(:post, WRITE_LOG_ENTRIES_URI).to_return do |request|
36
+ user_agent = request.headers['User-Agent']
37
+ { body: '' }
38
+ end
39
+ d = create_driver
40
+ d.emit('message' => log_entry(0))
41
+ d.run
42
+ assert_match Regexp.new("#{Fluent::GoogleCloudOutput::PLUGIN_NAME}/" \
43
+ "#{Fluent::GoogleCloudOutput::PLUGIN_VERSION}"), \
44
+ user_agent
45
+ end
46
+
47
+ def test_client_status400
48
+ setup_gce_metadata_stubs
49
+ # The API Client should not retry this and the plugin should consume
50
+ # the exception.
51
+ stub_request(:post, WRITE_LOG_ENTRIES_URI)
52
+ .to_return(status: 400, body: 'Bad Request')
53
+ d = create_driver
54
+ d.emit('message' => log_entry(0))
55
+ d.run
56
+ assert_requested(:post, WRITE_LOG_ENTRIES_URI, times: 1)
57
+ end
58
+
59
+ # All credentials errors resolve to a 401.
60
+ def test_client_status401
61
+ setup_gce_metadata_stubs
62
+ stub_request(:post, WRITE_LOG_ENTRIES_URI)
63
+ .to_return(status: 401, body: 'Unauthorized')
64
+ d = create_driver
65
+ d.emit('message' => log_entry(0))
66
+ begin
67
+ d.run
68
+ rescue Google::Apis::AuthorizationError => e
69
+ assert_equal 'Unauthorized', e.message
70
+ end
71
+ assert_requested(:post, WRITE_LOG_ENTRIES_URI, times: 2)
72
+ end
73
+
74
+ def test_partial_success
75
+ setup_gce_metadata_stubs
76
+ clear_metrics
77
+ # The API Client should not retry this and the plugin should consume
78
+ # the exception.
79
+ root_error_code = PARTIAL_SUCCESS_RESPONSE_BODY['error']['code']
80
+ stub_request(:post, WRITE_LOG_ENTRIES_URI)
81
+ .to_return(status: root_error_code,
82
+ body: PARTIAL_SUCCESS_RESPONSE_BODY.to_json)
83
+ d = create_driver(ENABLE_PROMETHEUS_CONFIG)
84
+ 4.times do |i|
85
+ d.emit('message' => log_entry(i.to_s))
86
+ end
87
+ d.run
88
+ assert_prometheus_metric_value(
89
+ :stackdriver_successful_requests_count, 1,
90
+ 'agent.googleapis.com/agent', OpenCensus::Stats::Aggregation::Sum, d,
91
+ grpc: false, code: 200
92
+ )
93
+ assert_prometheus_metric_value(
94
+ :stackdriver_ingested_entries_count, 1,
95
+ 'agent.googleapis.com/agent', OpenCensus::Stats::Aggregation::Sum, d,
96
+ grpc: false, code: 200
97
+ )
98
+ assert_prometheus_metric_value(
99
+ :stackdriver_dropped_entries_count, 2,
100
+ 'agent.googleapis.com/agent', OpenCensus::Stats::Aggregation::Sum, d,
101
+ grpc: false, code: 3
102
+ )
103
+ assert_prometheus_metric_value(
104
+ :stackdriver_dropped_entries_count, 1,
105
+ 'agent.googleapis.com/agent', OpenCensus::Stats::Aggregation::Sum, d,
106
+ grpc: false, code: 7
107
+ )
108
+ assert_requested(:post, WRITE_LOG_ENTRIES_URI, times: 1)
109
+ end
110
+
111
+ def test_non_api_error
112
+ setup_gce_metadata_stubs
113
+ clear_metrics
114
+ # The API Client should not retry this and the plugin should consume
115
+ # the exception.
116
+ root_error_code = PARSE_ERROR_RESPONSE_BODY['error']['code']
117
+ stub_request(:post, WRITE_LOG_ENTRIES_URI)
118
+ .to_return(status: root_error_code,
119
+ body: PARSE_ERROR_RESPONSE_BODY.to_json)
120
+ d = create_driver(ENABLE_PROMETHEUS_CONFIG)
121
+ d.emit('message' => log_entry(0))
122
+ d.run
123
+ assert_prometheus_metric_value(
124
+ :stackdriver_successful_requests_count, 0,
125
+ 'agent.googleapis.com/agent', OpenCensus::Stats::Aggregation::Sum, d,
126
+ grpc: false, code: 200
127
+ )
128
+ assert_prometheus_metric_value(
129
+ :stackdriver_failed_requests_count, 1,
130
+ 'agent.googleapis.com/agent', OpenCensus::Stats::Aggregation::Sum, d,
131
+ grpc: false, code: 400
132
+ )
133
+ assert_prometheus_metric_value(
134
+ :stackdriver_ingested_entries_count, 0,
135
+ 'agent.googleapis.com/agent', OpenCensus::Stats::Aggregation::Sum, d,
136
+ grpc: false, code: 200
137
+ )
138
+ assert_prometheus_metric_value(
139
+ :stackdriver_dropped_entries_count, 1,
140
+ 'agent.googleapis.com/agent', OpenCensus::Stats::Aggregation::Sum, d,
141
+ grpc: false, code: 400
142
+ )
143
+ assert_requested(:post, WRITE_LOG_ENTRIES_URI, times: 1)
144
+ end
145
+
146
+ def test_server_error
147
+ setup_gce_metadata_stubs
148
+ # The API client should retry this once, then throw an exception which
149
+ # gets propagated through the plugin.
150
+ stub_request(:post, WRITE_LOG_ENTRIES_URI)
151
+ .to_return(status: 500, body: 'Server Error')
152
+ d = create_driver
153
+ d.emit('message' => log_entry(0))
154
+ exception_count = 0
155
+ begin
156
+ d.run
157
+ rescue Google::Apis::ServerError => e
158
+ assert_equal 'Server error', e.message
159
+ exception_count += 1
160
+ end
161
+ assert_requested(:post, WRITE_LOG_ENTRIES_URI, times: 1)
162
+ assert_equal 1, exception_count
163
+ end
164
+
165
+ # This test looks similar between the grpc and non-grpc paths except that when
166
+ # parsing "105", the grpc path responds with "DEBUG", while the non-grpc path
167
+ # responds with "100".
168
+ #
169
+ # TODO(lingshi) consolidate the tests between the grpc path and the non-grpc
170
+ # path, or at least split into two tests, one with string severities and one
171
+ # with numeric severities.
172
+ def test_severities
173
+ setup_gce_metadata_stubs
174
+ expected_severity = []
175
+ emit_index = 0
176
+ setup_logging_stubs do
177
+ d = create_driver
178
+ # Array of pairs of [parsed_severity, expected_severity]
179
+ [%w[INFO INFO], %w[warn WARNING], %w[E ERROR], %w[BLAH DEFAULT],
180
+ ['105', 100], ['', 'DEFAULT']].each do |sev|
181
+ d.emit('message' => log_entry(emit_index), 'severity' => sev[0])
182
+ expected_severity.push(sev[1])
183
+ emit_index += 1
184
+ end
185
+ d.run
186
+ end
187
+ verify_index = 0
188
+ verify_log_entries(emit_index, COMPUTE_PARAMS) do |entry|
189
+ assert_equal expected_severity[verify_index],
190
+ entry['severity'], entry
191
+ verify_index += 1
192
+ end
193
+ end
194
+
195
+ def test_parse_severity
196
+ test_obj = Fluent::GoogleCloudOutput.new
197
+
198
+ # known severities should translate to themselves, regardless of case
199
+ %w[DEFAULT DEBUG INFO NOTICE WARNING ERROR CRITICAL ALERT EMERGENCY].each \
200
+ do |severity|
201
+ assert_equal(severity, test_obj.parse_severity(severity))
202
+ assert_equal(severity, test_obj.parse_severity(severity.downcase))
203
+ assert_equal(severity, test_obj.parse_severity(severity.capitalize))
204
+ end
205
+
206
+ # numeric levels
207
+ assert_equal(0, test_obj.parse_severity('0'))
208
+ assert_equal(100, test_obj.parse_severity('100'))
209
+ assert_equal(200, test_obj.parse_severity('200'))
210
+ assert_equal(300, test_obj.parse_severity('300'))
211
+ assert_equal(400, test_obj.parse_severity('400'))
212
+ assert_equal(500, test_obj.parse_severity('500'))
213
+ assert_equal(600, test_obj.parse_severity('600'))
214
+ assert_equal(700, test_obj.parse_severity('700'))
215
+ assert_equal(800, test_obj.parse_severity('800'))
216
+
217
+ assert_equal(800, test_obj.parse_severity('900'))
218
+ assert_equal(0, test_obj.parse_severity('1'))
219
+ assert_equal(100, test_obj.parse_severity('105'))
220
+ assert_equal(400, test_obj.parse_severity('420'))
221
+ assert_equal(700, test_obj.parse_severity('799'))
222
+
223
+ assert_equal(100, test_obj.parse_severity('105 '))
224
+ assert_equal(100, test_obj.parse_severity(' 105'))
225
+ assert_equal(100, test_obj.parse_severity(' 105 '))
226
+
227
+ assert_equal(100, test_obj.parse_severity(100))
228
+
229
+ assert_equal('DEFAULT', test_obj.parse_severity('-100'))
230
+ assert_equal('DEFAULT', test_obj.parse_severity('105 100'))
231
+
232
+ # synonyms for existing log levels
233
+ assert_equal('ERROR', test_obj.parse_severity('ERR'))
234
+ assert_equal('ERROR', test_obj.parse_severity('SEVERE'))
235
+ assert_equal('WARNING', test_obj.parse_severity('WARN'))
236
+ assert_equal('CRITICAL', test_obj.parse_severity('FATAL'))
237
+ assert_equal('DEBUG', test_obj.parse_severity('TRACE'))
238
+ assert_equal('DEBUG', test_obj.parse_severity('TRACE_INT'))
239
+ assert_equal('DEBUG', test_obj.parse_severity('FINE'))
240
+ assert_equal('DEBUG', test_obj.parse_severity('FINER'))
241
+ assert_equal('DEBUG', test_obj.parse_severity('FINEST'))
242
+ assert_equal('DEBUG', test_obj.parse_severity('CONFIG'))
243
+
244
+ # single letters.
245
+ assert_equal('DEBUG', test_obj.parse_severity('D'))
246
+ assert_equal('INFO', test_obj.parse_severity('I'))
247
+ assert_equal('NOTICE', test_obj.parse_severity('N'))
248
+ assert_equal('WARNING', test_obj.parse_severity('W'))
249
+ assert_equal('ERROR', test_obj.parse_severity('E'))
250
+ assert_equal('CRITICAL', test_obj.parse_severity('C'))
251
+ assert_equal('ALERT', test_obj.parse_severity('A'))
252
+ assert_equal('ERROR', test_obj.parse_severity('e'))
253
+
254
+ assert_equal('DEFAULT', test_obj.parse_severity('x'))
255
+ assert_equal('DEFAULT', test_obj.parse_severity('-'))
256
+
257
+ # leading/trailing whitespace should be stripped
258
+ assert_equal('ERROR', test_obj.parse_severity(' ERROR'))
259
+ assert_equal('ERROR', test_obj.parse_severity('ERROR '))
260
+ assert_equal('ERROR', test_obj.parse_severity(' ERROR '))
261
+ assert_equal('ERROR', test_obj.parse_severity("\t ERROR "))
262
+ # space in the middle should not be stripped.
263
+ assert_equal('DEFAULT', test_obj.parse_severity('ER ROR'))
264
+
265
+ # anything else should translate to 'DEFAULT'
266
+ assert_equal('DEFAULT', test_obj.parse_severity(nil))
267
+ assert_equal('DEFAULT', test_obj.parse_severity(Object.new))
268
+ assert_equal('DEFAULT', test_obj.parse_severity({}))
269
+ assert_equal('DEFAULT', test_obj.parse_severity([]))
270
+ assert_equal('DEFAULT', test_obj.parse_severity(100.0))
271
+ assert_equal('DEFAULT', test_obj.parse_severity(''))
272
+ assert_equal('DEFAULT', test_obj.parse_severity('garbage'))
273
+ assert_equal('DEFAULT', test_obj.parse_severity('er'))
274
+ end
275
+
276
+ def test_non_integer_timestamp
277
+ setup_gce_metadata_stubs
278
+ time = Time.now
279
+ [
280
+ { 'seconds' => nil, 'nanos' => nil },
281
+ { 'seconds' => nil, 'nanos' => time.tv_nsec },
282
+ { 'seconds' => 'seconds', 'nanos' => time.tv_nsec },
283
+ { 'seconds' => time.tv_sec, 'nanos' => 'nanos' },
284
+ { 'seconds' => time.tv_sec, 'nanos' => nil }
285
+ ].each do |timestamp|
286
+ setup_logging_stubs do
287
+ d = create_driver
288
+ @logs_sent = []
289
+ d.emit('message' => log_entry(0), 'timestamp' => timestamp)
290
+ d.run
291
+ end
292
+ verify_log_entries(1, COMPUTE_PARAMS) do |entry|
293
+ assert_equal timestamp, entry['timestamp'], 'Test with timestamp ' \
294
+ "'#{timestamp}' failed for entry: '#{entry}'."
295
+ end
296
+ end
297
+ end
298
+
299
+ def test_statusz_endpoint
300
+ setup_gce_metadata_stubs
301
+ WebMock.disable_net_connect!(allow_localhost: true)
302
+ # TODO(davidbtucker): Consider searching for an unused port
303
+ # instead of hardcoding a constant here.
304
+ d = create_driver(CONFIG_STATUSZ)
305
+ d.run do
306
+ resp = Net::HTTP.get('127.0.0.1', '/statusz', 5678)
307
+ must_match = [
308
+ '<h1>Status for .*</h1>.*',
309
+ '\bStarted: .*<br>',
310
+ '\bUp \d+ hr \d{2} min \d{2} sec<br>',
311
+
312
+ '\badjust_invalid_timestamps\b.*\bfalse\b',
313
+ '\bautoformat_stackdriver_trace\b.*\bfalse\b',
314
+ '\bcoerce_to_utf8\b.*\bfalse\b',
315
+ '\bdetect_json\b.*\btrue\b',
316
+ '\bdetect_subservice\b.*\bfalse\b',
317
+ '\benable_monitoring\b.*\btrue\b',
318
+ '\bhttp_request_key\b.*\btest_http_request_key\b',
319
+ '\binsert_id_key\b.*\btest_insert_id_key\b',
320
+ '\bk8s_cluster_location\b.*\btest-k8s-cluster-location\b',
321
+ '\bk8s_cluster_name\b.*\btest-k8s-cluster-name\b',
322
+ '\bkubernetes_tag_regexp\b.*\b.*test-regexp.*\b',
323
+ '\blabel_map\b.*{"label_map_key"=>"label_map_value"}',
324
+ '\blabels_key\b.*\btest_labels_key\b',
325
+ '\blabels\b.*{"labels_key"=>"labels_value"}',
326
+ '\blogging_api_url\b.*\bhttp://localhost:52000\b',
327
+ '\bmonitoring_type\b.*\bnot_prometheus\b',
328
+ '\bnon_utf8_replacement_string\b.*\bzzz\b',
329
+ '\boperation_key\b.*\btest_operation_key\b',
330
+ '\bproject_id\b.*\btest-project-id-123\b',
331
+ '\brequire_valid_tags\b.*\btrue\b',
332
+ '\bsource_location_key\b.*\btest_source_location_key\b',
333
+ '\bspan_id_key\b.*\btest_span_id_key\b',
334
+ '\bsplit_logs_by_tag\b.*\btrue\b',
335
+ '\bstatusz_port\b.*\b5678\b',
336
+ '\bsubservice_name\b.*\btest_subservice_name\b',
337
+ '\btrace_key\b.*\btest_trace_key\b',
338
+ '\btrace_sampled_key\b.*\btest_trace_sampled_key\b',
339
+ '\buse_aws_availability_zone\b.*\bfalse\b',
340
+ '\buse_grpc\b.*\btrue\b',
341
+ '\buse_metadata_service\b.*\bfalse\b',
342
+ '\bvm_id\b.*\b12345\b',
343
+ '\bvm_name\b.*\btest.hostname.org\b',
344
+ '\bzone\b.*\basia-east2\b',
345
+
346
+ '^</html>$'
347
+ ]
348
+ must_match.each do |re|
349
+ assert_match Regexp.new(re), resp
350
+ end
351
+ end
352
+ end
353
+
354
+ private
355
+
356
+ WRITE_LOG_ENTRIES_URI =
357
+ 'https://logging.googleapis.com/v2/entries:write'.freeze
358
+
359
+ def rename_key(hash, old_key, new_key)
360
+ hash.merge(new_key => hash[old_key]).reject { |k, _| k == old_key }
361
+ end
362
+
363
+ # Set up http stubs to mock the external calls.
364
+ def setup_logging_stubs(_error = nil, code = nil, message = 'some message')
365
+ stub_request(:post, WRITE_LOG_ENTRIES_URI).to_return do |request|
366
+ @logs_sent << JSON.parse(request.body)
367
+ { status: code, body: message }
368
+ end
369
+ yield
370
+ end
371
+
372
+ # Whether this is the grpc path
373
+ def use_grpc
374
+ false
375
+ end
376
+
377
+ # The OK status code for the grpc path.
378
+ def ok_status_code
379
+ 200
380
+ end
381
+
382
+ # A client side error status code for the grpc path.
383
+ def client_error_status_code
384
+ 401
385
+ end
386
+
387
+ # A server side error status code for the grpc path.
388
+ def server_error_status_code
389
+ 500
390
+ end
391
+
392
+ # The parent error type to expect in the mock
393
+ def mock_error_type
394
+ Google::Apis::Error
395
+ end
396
+
397
+ # The conversions from user input to output.
398
+ def latency_conversion
399
+ {
400
+ '32 s' => { 'seconds' => 32 },
401
+ '32s' => { 'seconds' => 32 },
402
+ '0.32s' => { 'nanos' => 320_000_000 },
403
+ ' 123 s ' => { 'seconds' => 123 },
404
+ '1.3442 s' => { 'seconds' => 1, 'nanos' => 344_200_000 },
405
+
406
+ # Test whitespace.
407
+ # \t: tab. \r: carriage return. \n: line break.
408
+ # \v: vertical whitespace. \f: form feed.
409
+ "\t123.5\ts\t" => { 'seconds' => 123, 'nanos' => 500_000_000 },
410
+ "\r123.5\rs\r" => { 'seconds' => 123, 'nanos' => 500_000_000 },
411
+ "\n123.5\ns\n" => { 'seconds' => 123, 'nanos' => 500_000_000 },
412
+ "\v123.5\vs\v" => { 'seconds' => 123, 'nanos' => 500_000_000 },
413
+ "\f123.5\fs\f" => { 'seconds' => 123, 'nanos' => 500_000_000 },
414
+ "\r123.5\ts\f" => { 'seconds' => 123, 'nanos' => 500_000_000 }
415
+ }
416
+ end
417
+
418
+ # Create a Fluentd output test driver with the Google Cloud Output plugin.
419
+ def create_driver(conf = APPLICATION_DEFAULT_CONFIG,
420
+ tag = 'test',
421
+ multi_tags = false)
422
+ driver = if multi_tags
423
+ Fluent::Test::MultiTagBufferedOutputTestDriver.new(
424
+ Fluent::GoogleCloudOutput
425
+ )
426
+ else
427
+ Fluent::Test::BufferedOutputTestDriver.new(
428
+ Fluent::GoogleCloudOutput, tag
429
+ )
430
+ end
431
+ driver.configure(conf, true)
432
+ end
433
+
434
+ # Verify the number and the content of the log entries match the expectation.
435
+ # The caller can optionally provide a block which is called for each entry.
436
+ def verify_log_entries(expected_count, params, payload_type = 'textPayload',
437
+ check_exact_entry_labels = true, &block)
438
+ verify_json_log_entries(expected_count, params, payload_type,
439
+ check_exact_entry_labels, &block)
440
+ end
441
+
442
+ # For an optional field with default values, Protobuf omits the field when it
443
+ # is deserialized to json. So we need to add an extra check for gRPC which
444
+ # uses Protobuf.
445
+ #
446
+ # An optional block can be passed in if we need to assert something other than
447
+ # a plain equal. e.g. assert_in_delta.
448
+ def assert_equal_with_default(field, expected_value, _default_value, entry)
449
+ if block_given?
450
+ yield
451
+ else
452
+ assert_equal expected_value, field, entry
453
+ end
454
+ end
455
+
456
+ def expected_operation_message2
457
+ OPERATION_MESSAGE2
458
+ end
459
+
460
+ # Directly return the timestamp value, which should be a hash two keys:
461
+ # "seconds" and "nanos".
462
+ def timestamp_parse(timestamp)
463
+ timestamp
464
+ end
465
+ end