fluent-plugin-vadimberezniker-gcp 0.1.0

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