fluent-plugin-google-cloud 0.5.3.grpc.alpha.3 → 0.5.3.grpc.alpha.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cdae7652026e7ff5ad8ac5593c6ecf016948f41f
4
- data.tar.gz: 85162c0a5db2c340faf21e60256a8c3fa66be30c
3
+ metadata.gz: 29718f443dea67bcf817ee44390e61373a307db7
4
+ data.tar.gz: 54b6472916c8657230db649ff339ab1d31b5362d
5
5
  SHA512:
6
- metadata.gz: bdc518fb676f756a572d16d0589ab7297bf2d67a78aac6ffb1772dafa0c2025ad33641c8290ac11c6d467704ab4bcb92b59bf6dc65cff2d23939636e470f88be
7
- data.tar.gz: a209e99c4390c0661438cb24eeaa9628615743096d0021e001a327eac0a4ba2e1abc573adabfa98d0ba0e78ad779521ff046adcb3570e747037bd47b704481c2
6
+ metadata.gz: 1d5695d8834eafc6f059c5a591ff9d3188739814fd7dcc4e2dc976324c8e45f3b5f0b7d914062c9581b2242f76dfd75d0356178c5a6b7f2cbadd3d49b064ec61
7
+ data.tar.gz: 46c8d492364314a4220f3ea61a81814fbd9db64c3af06331e025853582404d33806a5bcacc7be64379aa2a646f415d5e6507e6fcae6d95085fafe838b4f5e19a
data/README.rdoc CHANGED
@@ -3,7 +3,7 @@
3
3
  fluent-plugin-google-cloud is an
4
4
  {output plugin for fluentd}[http://docs.fluentd.org/articles/output-plugin-overview]
5
5
  which sends logs to the
6
- {Google Cloud Logging API}[https://cloud.google.com/logging/docs/api/].
6
+ {Stackdriver Logging API}[https://cloud.google.com/logging/docs/api/].
7
7
 
8
8
  This is an official Google Ruby gem.
9
9
 
@@ -1,16 +1,16 @@
1
1
  Gem::Specification.new do |gem|
2
2
  gem.name = 'fluent-plugin-google-cloud'
3
3
  gem.description = <<-eos
4
- Fluentd output plugin for the Google Cloud Logging API, which will make
4
+ Fluentd output plugin for the Stackdriver Logging API, which will make
5
5
  logs viewable in the Developer Console's log viewer and can optionally
6
6
  store them in Google Cloud Storage and/or BigQuery.
7
7
  This is an official Google Ruby gem.
8
8
  eos
9
- gem.summary = 'fluentd output plugin for the Google Cloud Logging API'
9
+ gem.summary = 'fluentd output plugin for the Stackdriver Logging API'
10
10
  gem.homepage = \
11
11
  'https://github.com/GoogleCloudPlatform/fluent-plugin-google-cloud'
12
12
  gem.license = 'Apache-2.0'
13
- gem.version = '0.5.3.grpc.alpha.3'
13
+ gem.version = '0.5.3.grpc.alpha.4'
14
14
  gem.authors = ['Todd Derr', 'Alex Robinson']
15
15
  gem.email = ['salty@google.com']
16
16
  gem.required_ruby_version = Gem::Requirement.new('>= 2.0')
@@ -19,7 +19,7 @@ eos
19
19
  gem.test_files = gem.files.grep(/^(test)/)
20
20
  gem.require_paths = ['lib']
21
21
 
22
- gem.add_runtime_dependency 'fluentd', '~> 0.10', '<= 0.13'
22
+ gem.add_runtime_dependency 'fluentd', '~> 0.10'
23
23
  gem.add_runtime_dependency 'googleapis-common-protos', '~> 1.3'
24
24
  gem.add_runtime_dependency 'google-api-client', '> 0.9'
25
25
  gem.add_runtime_dependency 'googleauth', '~> 0.4'
@@ -28,7 +28,7 @@ eos
28
28
 
29
29
  gem.add_development_dependency 'mocha', '~> 1.1'
30
30
  gem.add_development_dependency 'rake', '~> 10.3'
31
- gem.add_development_dependency 'rubocop', '= 0.35.0'
31
+ gem.add_development_dependency 'rubocop', '~> 0.35.0'
32
32
  gem.add_development_dependency 'webmock', '~> 1.17'
33
33
  gem.add_development_dependency 'test-unit', '~> 3.0'
34
34
  end
@@ -24,13 +24,22 @@ require 'google/logging/v1/logging_services_pb'
24
24
  require 'google/logging/v1/log_entry_pb'
25
25
  require 'googleauth'
26
26
 
27
+ module Google
28
+ module Protobuf
29
+ # Alias the has_key? method to have the same interface as a regular map.
30
+ class Map
31
+ alias_method :key?, :has_key?
32
+ end
33
+ end
34
+ end
35
+
27
36
  module Fluent
28
- # fluentd output plugin for the Google Cloud Logging API
37
+ # fluentd output plugin for the Stackdriver Logging API
29
38
  class GoogleCloudOutput < BufferedOutput
30
39
  Fluent::Plugin.register_output('google_cloud', self)
31
40
 
32
41
  PLUGIN_NAME = 'Fluentd Google Cloud Logging plugin'
33
- PLUGIN_VERSION = '0.5.3.grpc.alpha.3'
42
+ PLUGIN_VERSION = '0.5.3.grpc.alpha.4'
34
43
 
35
44
  # Constants for service names.
36
45
  APPENGINE_SERVICE = 'appengine.googleapis.com'
@@ -502,7 +511,7 @@ module Fluent
502
511
  # to aid with verification and troubleshooting.
503
512
  unless @successful_call
504
513
  @successful_call = true
505
- @log.info 'Successfully sent gRPC to Google Cloud Logging API.'
514
+ @log.info 'Successfully sent gRPC to Stackdriver Logging API.'
506
515
  end
507
516
 
508
517
  rescue GRPC::Cancelled => error
@@ -564,7 +573,7 @@ module Fluent
564
573
  # to aid with verification and troubleshooting.
565
574
  unless @successful_call
566
575
  @successful_call = true
567
- @log.info 'Successfully sent to Google Cloud Logging API.'
576
+ @log.info 'Successfully sent to Stackdriver Logging API.'
568
577
  end
569
578
 
570
579
  rescue Google::Apis::ServerError => error
@@ -746,7 +755,7 @@ module Fluent
746
755
  # # to aid with verification and troubleshooting.
747
756
  # unless @successful_call
748
757
  # @successful_call = true
749
- # @log.info 'Successfully sent gRPC to Google Cloud Logging API.'
758
+ # @log.info 'Successfully sent gRPC to Stackdriver Logging API.'
750
759
  # end
751
760
  #
752
761
  # rescue GRPC::Cancelled => error
@@ -895,7 +904,7 @@ module Fluent
895
904
  # # to aid with verification and troubleshooting.
896
905
  # unless @successful_call
897
906
  # @successful_call = true
898
- # @log.info 'Successfully sent to Google Cloud Logging API.'
907
+ # @log.info 'Successfully sent to Stackdriver Logging API.'
899
908
  # end
900
909
  #
901
910
  # rescue Google::Apis::ServerError => error
@@ -1078,8 +1087,8 @@ module Fluent
1078
1087
  elsif @service_name == CLOUDFUNCTIONS_SERVICE &&
1079
1088
  @cloudfunctions_log_match
1080
1089
  timestamp = DateTime.parse(@cloudfunctions_log_match['timestamp'])
1081
- ts_secs = timestamp.strftime('%s')
1082
- ts_nanos = timestamp.strftime('%N')
1090
+ ts_secs = timestamp.strftime('%s').to_i
1091
+ ts_nanos = timestamp.strftime('%N').to_i
1083
1092
  elsif record.key?('time')
1084
1093
  # k8s ISO8601 timestamp
1085
1094
  begin
@@ -1150,17 +1159,27 @@ module Fluent
1150
1159
  return nil unless record['httpRequest'].is_a?(Hash)
1151
1160
  input = record['httpRequest']
1152
1161
  output = Google::Logging::Type::HttpRequest.new
1153
- output.request_method = input.delete('requestMethod')
1154
- output.request_url = input.delete('requestUrl')
1155
- output.request_size = input.delete('requestSize').to_i
1156
- output.status = input.delete('status').to_i
1157
- output.response_size = input.delete('responseSize').to_i
1158
- output.user_agent = input.delete('userAgent')
1159
- output.remote_ip = input.delete('remoteIp')
1160
- output.referer = input.delete('referer')
1161
- output.cache_hit = input.delete('cacheHit') == 'true'
1162
- output.validated_with_origin_server = \
1163
- input.delete('validatedWithOriginServer') == 'true'
1162
+ output.request_method = input.delete('requestMethod') if
1163
+ input.key?('requestMethod')
1164
+ output.request_url = input.delete('requestUrl') if
1165
+ input.key?('requestUrl')
1166
+ output.request_size = input.delete('requestSize').to_i if
1167
+ input.key?('requestSize')
1168
+ output.status = input.delete('status').to_i if
1169
+ input.key?('status')
1170
+ output.response_size = input.delete('responseSize').to_i if
1171
+ input.key?('responseSize')
1172
+ output.user_agent = input.delete('userAgent') if
1173
+ input.key?('userAgent')
1174
+ output.remote_ip = input.delete('remoteIp') if
1175
+ input.key?('remoteIp')
1176
+ output.referer = input.delete('referer') if
1177
+ input.key?('referer')
1178
+ output.cache_hit = input.delete('cacheHit') if
1179
+ input.key?('cacheHit')
1180
+ output.cache_validated_with_origin_server = \
1181
+ input.delete('cacheValidatedWithOriginServer') if
1182
+ input.key?('cacheValidatedWithOriginServer')
1164
1183
  record.delete('httpRequest') if input.empty?
1165
1184
  entry.http_request = output
1166
1185
  end
@@ -1179,6 +1198,9 @@ module Fluent
1179
1198
  'FINE' => 'DEBUG',
1180
1199
  'FINER' => 'DEBUG',
1181
1200
  'FINEST' => 'DEBUG',
1201
+ # nginx levels (only missing ones from above listed).
1202
+ 'CRIT' => 'CRITICAL',
1203
+ 'EMERG' => 'EMERGENCY',
1182
1204
  # single-letter levels. Note E->ERROR and D->DEBUG.
1183
1205
  'D' => 'DEBUG',
1184
1206
  'I' => 'INFO',
@@ -0,0 +1,1308 @@
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 'google/apis'
16
+ require 'helper'
17
+ require 'mocha/test_unit'
18
+ require 'webmock/test_unit'
19
+
20
+ # Unit tests for Google Cloud Logging plugin
21
+ module BaseTest
22
+ def setup
23
+ Fluent::Test.setup
24
+ # delete environment variables that googleauth uses to find credentials.
25
+ ENV.delete('GOOGLE_APPLICATION_CREDENTIALS')
26
+ # service account env.
27
+ ENV.delete('PRIVATE_KEY_VAR')
28
+ ENV.delete('CLIENT_EMAIL_VAR')
29
+ # authorized_user env.
30
+ ENV.delete('CLIENT_ID_VAR')
31
+ ENV.delete('CLIENT_SECRET_VAR')
32
+ ENV.delete('REFRESH_TOKEN_VAR')
33
+ # home var, which is used to find $HOME/.gcloud/...
34
+ ENV.delete('HOME')
35
+
36
+ setup_auth_stubs
37
+ @logs_sent = []
38
+ end
39
+
40
+ # generic attributes
41
+ HOSTNAME = Socket.gethostname
42
+
43
+ # attributes used for the GCE metadata service
44
+ PROJECT_ID = 'test-project-id'
45
+ ZONE = 'us-central1-b'
46
+ FULLY_QUALIFIED_ZONE = 'projects/' + PROJECT_ID + '/zones/' + ZONE
47
+ VM_ID = '9876543210'
48
+
49
+ # attributes used for custom (overridden) configs
50
+ CUSTOM_PROJECT_ID = 'test-custom-project-id'
51
+ CUSTOM_ZONE = 'us-custom-central1-b'
52
+ CUSTOM_FULLY_QUALIFIED_ZONE = 'projects/' + PROJECT_ID + '/zones/' + ZONE
53
+ CUSTOM_VM_ID = 'C9876543210'
54
+ CUSTOM_HOSTNAME = 'custom.hostname.org'
55
+
56
+ # attributes used for the EC2 metadata service
57
+ EC2_PROJECT_ID = 'test-ec2-project-id'
58
+ EC2_ZONE = 'us-west-2b'
59
+ EC2_PREFIXED_ZONE = 'aws:' + EC2_ZONE
60
+ EC2_VM_ID = 'i-81c16767'
61
+ EC2_ACCOUNT_ID = '123456789012'
62
+
63
+ # The formatting here matches the format used on the VM.
64
+ EC2_IDENTITY_DOCUMENT = %({
65
+ "accountId" : "#{EC2_ACCOUNT_ID}",
66
+ "availabilityZone" : "#{EC2_ZONE}",
67
+ "instanceId" : "#{EC2_VM_ID}"
68
+ })
69
+
70
+ # Managed VMs specific labels
71
+ MANAGED_VM_BACKEND_NAME = 'default'
72
+ MANAGED_VM_BACKEND_VERSION = 'guestbook2.0'
73
+
74
+ # Container Engine / Kubernetes specific labels
75
+ CONTAINER_CLUSTER_NAME = 'cluster-1'
76
+ CONTAINER_NAMESPACE_ID = '898268c8-4a36-11e5-9d81-42010af0194c'
77
+ CONTAINER_NAMESPACE_NAME = 'kube-system'
78
+ CONTAINER_POD_ID = 'cad3c3c4-4b9c-11e5-9d81-42010af0194c'
79
+ CONTAINER_POD_NAME = 'redis-master-c0l82.foo.bar'
80
+ CONTAINER_CONTAINER_NAME = 'redis'
81
+ CONTAINER_LABEL_KEY = 'component'
82
+ CONTAINER_LABEL_VALUE = 'redis-component'
83
+ CONTAINER_STREAM = 'stdout'
84
+ CONTAINER_SEVERITY = 'INFO'
85
+ # Timestamp for 1234567890 seconds and 987654321 nanoseconds since epoch
86
+ CONTAINER_TIMESTAMP = '2009-02-13T23:31:30.987654321Z'
87
+ CONTAINER_SECONDS_EPOCH = 1_234_567_890
88
+ CONTAINER_NANOS = 987_654_321
89
+
90
+ # Cloud Functions specific labels
91
+ CLOUDFUNCTIONS_FUNCTION_NAME = '$My_Function.Name-@1'
92
+ CLOUDFUNCTIONS_REGION = 'us-central1'
93
+ CLOUDFUNCTIONS_EXECUTION_ID = '123-0'
94
+ CLOUDFUNCTIONS_CLUSTER_NAME = 'cluster-1'
95
+ CLOUDFUNCTIONS_NAMESPACE_NAME = 'default'
96
+ CLOUDFUNCTIONS_POD_NAME = 'd.dc.myu.uc.functionp.pc.name-a.a1.987-c0l82'
97
+ CLOUDFUNCTIONS_CONTAINER_NAME = 'worker'
98
+
99
+ # Parameters used for authentication
100
+ AUTH_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
101
+ FAKE_AUTH_TOKEN = 'abc123'
102
+
103
+ # Information about test credentials files.
104
+ # path: Path to the credentials file.
105
+ # project_id: ID of the project, which must correspond to the file contents.
106
+ IAM_CREDENTIALS = {
107
+ path: 'test/plugin/data/iam-credentials.json',
108
+ project_id: 'fluent-test-project'
109
+ }
110
+ LEGACY_CREDENTIALS = {
111
+ path: 'test/plugin/data/credentials.json',
112
+ project_id: '847859579879'
113
+ }
114
+ INVALID_CREDENTIALS = {
115
+ path: 'test/plugin/data/invalid_credentials.json',
116
+ project_id: ''
117
+ }
118
+
119
+ # Configuration files for various test scenarios
120
+ APPLICATION_DEFAULT_CONFIG = %(
121
+ )
122
+
123
+ # rubocop:disable Metrics/LineLength
124
+ PRIVATE_KEY_CONFIG = %(
125
+ auth_method private_key
126
+ private_key_email 271661262351-ft99kc9kjro9rrihq3k2n3s2inbplu0q@developer.gserviceaccount.com
127
+ private_key_path test/plugin/data/c31e573fd7f62ed495c9ca3821a5a85cb036dee1-privatekey.p12
128
+ )
129
+ # rubocop:enable Metrics/LineLength
130
+
131
+ NO_METADATA_SERVICE_CONFIG = %(
132
+ use_metadata_service false
133
+ )
134
+
135
+ NO_DETECT_SUBSERVICE_CONFIG = %(
136
+ detect_subservice false
137
+ )
138
+
139
+ CUSTOM_METADATA_CONFIG = %(
140
+ project_id #{CUSTOM_PROJECT_ID}
141
+ zone #{CUSTOM_ZONE}
142
+ vm_id #{CUSTOM_VM_ID}
143
+ vm_name #{CUSTOM_HOSTNAME}
144
+ )
145
+
146
+ CONFIG_MISSING_METADATA_PROJECT_ID = %(
147
+ zone #{CUSTOM_ZONE}
148
+ vm_id #{CUSTOM_VM_ID}
149
+ )
150
+ CONFIG_MISSING_METADATA_ZONE = %(
151
+ project_id #{CUSTOM_PROJECT_ID}
152
+ vm_id #{CUSTOM_VM_ID}
153
+ )
154
+ CONFIG_MISSING_METADATA_VM_ID = %(
155
+ project_id #{CUSTOM_PROJECT_ID}
156
+ zone #{CUSTOM_ZONE}
157
+ )
158
+ CONFIG_MISSING_METADATA_ALL = %(
159
+ )
160
+
161
+ CONFIG_EC2_PROJECT_ID = %(
162
+ project_id #{EC2_PROJECT_ID}
163
+ )
164
+
165
+ CONFIG_EC2_PROJECT_ID_AND_CUSTOM_VM_ID = %(
166
+ project_id #{EC2_PROJECT_ID}
167
+ vm_id #{CUSTOM_VM_ID}
168
+ )
169
+
170
+ # Service configurations for various services
171
+ COMPUTE_SERVICE_NAME = 'compute.googleapis.com'
172
+ APPENGINE_SERVICE_NAME = 'appengine.googleapis.com'
173
+ CONTAINER_SERVICE_NAME = 'container.googleapis.com'
174
+ CLOUDFUNCTIONS_SERVICE_NAME = 'cloudfunctions.googleapis.com'
175
+ EC2_SERVICE_NAME = 'ec2.amazonaws.com'
176
+
177
+ COMPUTE_PARAMS = {
178
+ service_name: COMPUTE_SERVICE_NAME,
179
+ log_name: 'test',
180
+ project_id: PROJECT_ID,
181
+ zone: ZONE,
182
+ labels: {
183
+ "#{COMPUTE_SERVICE_NAME}/resource_type" => 'instance',
184
+ "#{COMPUTE_SERVICE_NAME}/resource_id" => VM_ID,
185
+ "#{COMPUTE_SERVICE_NAME}/resource_name" => HOSTNAME
186
+ }
187
+ }
188
+
189
+ VMENGINE_PARAMS = {
190
+ service_name: APPENGINE_SERVICE_NAME,
191
+ log_name: "#{APPENGINE_SERVICE_NAME}%2Ftest",
192
+ project_id: PROJECT_ID,
193
+ zone: ZONE,
194
+ labels: {
195
+ "#{APPENGINE_SERVICE_NAME}/module_id" => MANAGED_VM_BACKEND_NAME,
196
+ "#{APPENGINE_SERVICE_NAME}/version_id" => MANAGED_VM_BACKEND_VERSION,
197
+ "#{COMPUTE_SERVICE_NAME}/resource_type" => 'instance',
198
+ "#{COMPUTE_SERVICE_NAME}/resource_id" => VM_ID,
199
+ "#{COMPUTE_SERVICE_NAME}/resource_name" => HOSTNAME
200
+ }
201
+ }
202
+
203
+ CONTAINER_TAG = "kubernetes.#{CONTAINER_POD_NAME}_" \
204
+ "#{CONTAINER_NAMESPACE_NAME}_#{CONTAINER_CONTAINER_NAME}"
205
+
206
+ CONTAINER_FROM_METADATA_PARAMS = {
207
+ service_name: CONTAINER_SERVICE_NAME,
208
+ log_name: CONTAINER_CONTAINER_NAME,
209
+ project_id: PROJECT_ID,
210
+ zone: ZONE,
211
+ labels: {
212
+ "#{CONTAINER_SERVICE_NAME}/instance_id" => VM_ID,
213
+ "#{CONTAINER_SERVICE_NAME}/cluster_name" => CONTAINER_CLUSTER_NAME,
214
+ "#{CONTAINER_SERVICE_NAME}/namespace_name" => CONTAINER_NAMESPACE_NAME,
215
+ "#{CONTAINER_SERVICE_NAME}/namespace_id" => CONTAINER_NAMESPACE_ID,
216
+ "#{CONTAINER_SERVICE_NAME}/pod_name" => CONTAINER_POD_NAME,
217
+ "#{CONTAINER_SERVICE_NAME}/pod_id" => CONTAINER_POD_ID,
218
+ "#{CONTAINER_SERVICE_NAME}/container_name" => CONTAINER_CONTAINER_NAME,
219
+ "#{CONTAINER_SERVICE_NAME}/stream" => CONTAINER_STREAM,
220
+ "label/#{CONTAINER_LABEL_KEY}" => CONTAINER_LABEL_VALUE,
221
+ "#{COMPUTE_SERVICE_NAME}/resource_type" => 'instance',
222
+ "#{COMPUTE_SERVICE_NAME}/resource_id" => VM_ID,
223
+ "#{COMPUTE_SERVICE_NAME}/resource_name" => HOSTNAME
224
+ }
225
+ }
226
+
227
+ # Almost the same as from metadata, but missing namespace_id and pod_id.
228
+ CONTAINER_FROM_TAG_PARAMS = {
229
+ service_name: CONTAINER_SERVICE_NAME,
230
+ log_name: CONTAINER_CONTAINER_NAME,
231
+ project_id: PROJECT_ID,
232
+ zone: ZONE,
233
+ labels: {
234
+ "#{CONTAINER_SERVICE_NAME}/instance_id" => VM_ID,
235
+ "#{CONTAINER_SERVICE_NAME}/cluster_name" => CONTAINER_CLUSTER_NAME,
236
+ "#{CONTAINER_SERVICE_NAME}/namespace_name" => CONTAINER_NAMESPACE_NAME,
237
+ "#{CONTAINER_SERVICE_NAME}/pod_name" => CONTAINER_POD_NAME,
238
+ "#{CONTAINER_SERVICE_NAME}/container_name" => CONTAINER_CONTAINER_NAME,
239
+ "#{CONTAINER_SERVICE_NAME}/stream" => CONTAINER_STREAM,
240
+ "#{COMPUTE_SERVICE_NAME}/resource_type" => 'instance',
241
+ "#{COMPUTE_SERVICE_NAME}/resource_id" => VM_ID,
242
+ "#{COMPUTE_SERVICE_NAME}/resource_name" => HOSTNAME
243
+ }
244
+ }
245
+
246
+ CLOUDFUNCTIONS_TAG = "kubernetes.#{CLOUDFUNCTIONS_POD_NAME}_" \
247
+ "#{CLOUDFUNCTIONS_NAMESPACE_NAME}_" \
248
+ "#{CLOUDFUNCTIONS_CONTAINER_NAME}"
249
+
250
+ CLOUDFUNCTIONS_PARAMS = {
251
+ service_name: CLOUDFUNCTIONS_SERVICE_NAME,
252
+ log_name: 'cloud-functions',
253
+ project_id: PROJECT_ID,
254
+ zone: ZONE,
255
+ labels: {
256
+ 'execution_id' => CLOUDFUNCTIONS_EXECUTION_ID,
257
+ "#{CLOUDFUNCTIONS_SERVICE_NAME}/function_name" =>
258
+ CLOUDFUNCTIONS_FUNCTION_NAME,
259
+ "#{CLOUDFUNCTIONS_SERVICE_NAME}/region" => CLOUDFUNCTIONS_REGION,
260
+ "#{CONTAINER_SERVICE_NAME}/instance_id" => VM_ID,
261
+ "#{CONTAINER_SERVICE_NAME}/cluster_name" => CLOUDFUNCTIONS_CLUSTER_NAME,
262
+ "#{COMPUTE_SERVICE_NAME}/resource_type" => 'instance',
263
+ "#{COMPUTE_SERVICE_NAME}/resource_id" => VM_ID,
264
+ "#{COMPUTE_SERVICE_NAME}/resource_name" => HOSTNAME
265
+ }
266
+ }
267
+
268
+ CLOUDFUNCTIONS_TEXT_NOT_MATCHED_PARAMS = {
269
+ service_name: CLOUDFUNCTIONS_SERVICE_NAME,
270
+ log_name: 'cloud-functions',
271
+ project_id: PROJECT_ID,
272
+ zone: ZONE,
273
+ labels: {
274
+ "#{CLOUDFUNCTIONS_SERVICE_NAME}/function_name" =>
275
+ CLOUDFUNCTIONS_FUNCTION_NAME,
276
+ "#{CLOUDFUNCTIONS_SERVICE_NAME}/region" => CLOUDFUNCTIONS_REGION,
277
+ "#{CONTAINER_SERVICE_NAME}/instance_id" => VM_ID,
278
+ "#{CONTAINER_SERVICE_NAME}/cluster_name" => CLOUDFUNCTIONS_CLUSTER_NAME,
279
+ "#{COMPUTE_SERVICE_NAME}/resource_type" => 'instance',
280
+ "#{COMPUTE_SERVICE_NAME}/resource_id" => VM_ID,
281
+ "#{COMPUTE_SERVICE_NAME}/resource_name" => HOSTNAME
282
+ }
283
+ }
284
+
285
+ CUSTOM_PARAMS = {
286
+ service_name: COMPUTE_SERVICE_NAME,
287
+ log_name: 'test',
288
+ project_id: CUSTOM_PROJECT_ID,
289
+ zone: CUSTOM_ZONE,
290
+ labels: {
291
+ "#{COMPUTE_SERVICE_NAME}/resource_type" => 'instance',
292
+ "#{COMPUTE_SERVICE_NAME}/resource_id" => CUSTOM_VM_ID,
293
+ "#{COMPUTE_SERVICE_NAME}/resource_name" => CUSTOM_HOSTNAME
294
+ }
295
+ }
296
+
297
+ EC2_PARAMS = {
298
+ service_name: EC2_SERVICE_NAME,
299
+ log_name: 'test',
300
+ project_id: EC2_PROJECT_ID,
301
+ zone: EC2_PREFIXED_ZONE,
302
+ labels: {
303
+ "#{EC2_SERVICE_NAME}/resource_type" => 'instance',
304
+ "#{EC2_SERVICE_NAME}/resource_id" => EC2_VM_ID,
305
+ "#{EC2_SERVICE_NAME}/account_id" => EC2_ACCOUNT_ID,
306
+ "#{EC2_SERVICE_NAME}/resource_name" => HOSTNAME
307
+ }
308
+ }
309
+
310
+ # Shared tests.
311
+
312
+ def test_configure_service_account_application_default
313
+ setup_gce_metadata_stubs
314
+ d = create_driver
315
+ assert_equal HOSTNAME, d.instance.vm_name
316
+ end
317
+
318
+ def test_configure_service_account_private_key
319
+ # Using out-of-date config method.
320
+ exception_count = 0
321
+ begin
322
+ create_driver(PRIVATE_KEY_CONFIG)
323
+ rescue Fluent::ConfigError => error
324
+ assert error.message.include? 'Please remove configuration parameters'
325
+ exception_count += 1
326
+ end
327
+ assert_equal 1, exception_count
328
+ end
329
+
330
+ def test_configure_custom_metadata
331
+ setup_no_metadata_service_stubs
332
+ d = create_driver(CUSTOM_METADATA_CONFIG)
333
+ assert_equal CUSTOM_PROJECT_ID, d.instance.project_id
334
+ assert_equal CUSTOM_ZONE, d.instance.zone
335
+ assert_equal CUSTOM_VM_ID, d.instance.vm_id
336
+ end
337
+
338
+ def test_configure_invalid_metadata_missing_parts
339
+ setup_no_metadata_service_stubs
340
+ Fluent::GoogleCloudOutput::CredentialsInfo.stubs(:project_id).returns(nil)
341
+ { CONFIG_MISSING_METADATA_PROJECT_ID => ['project_id'],
342
+ CONFIG_MISSING_METADATA_ZONE => ['zone'],
343
+ CONFIG_MISSING_METADATA_VM_ID => ['vm_id'],
344
+ CONFIG_MISSING_METADATA_ALL => %w(project_id zone vm_id)
345
+ }.each_with_index do |(config, parts), index|
346
+ exception_count = 0
347
+ begin
348
+ create_driver(config)
349
+ rescue Fluent::ConfigError => error
350
+ assert error.message.include?('Unable to obtain metadata parameters:'),
351
+ "Index #{index} failed."
352
+ parts.each do |part|
353
+ assert error.message.include?(part), "Index #{index} failed."
354
+ end
355
+ exception_count += 1
356
+ end
357
+ assert_equal 1, exception_count, "Index #{index} failed."
358
+ end
359
+ end
360
+
361
+ def test_metadata_loading
362
+ setup_gce_metadata_stubs
363
+ d = create_driver
364
+ d.run
365
+ assert_equal PROJECT_ID, d.instance.project_id
366
+ assert_equal ZONE, d.instance.zone
367
+ assert_equal VM_ID, d.instance.vm_id
368
+ assert_equal false, d.instance.running_on_managed_vm
369
+ end
370
+
371
+ def test_managed_vm_metadata_loading
372
+ setup_gce_metadata_stubs
373
+ setup_managed_vm_metadata_stubs
374
+ d = create_driver
375
+ d.run
376
+ assert_equal PROJECT_ID, d.instance.project_id
377
+ assert_equal ZONE, d.instance.zone
378
+ assert_equal VM_ID, d.instance.vm_id
379
+ assert_equal true, d.instance.running_on_managed_vm
380
+ assert_equal MANAGED_VM_BACKEND_NAME, d.instance.gae_backend_name
381
+ assert_equal MANAGED_VM_BACKEND_VERSION, d.instance.gae_backend_version
382
+ end
383
+
384
+ def test_gce_metadata_does_not_load_when_use_metadata_service_is_false
385
+ Fluent::GoogleCloudOutput.any_instance.expects(:fetch_metadata).never
386
+ d = create_driver(NO_METADATA_SERVICE_CONFIG + CUSTOM_METADATA_CONFIG)
387
+ d.run
388
+ assert_equal CUSTOM_PROJECT_ID, d.instance.project_id
389
+ assert_equal CUSTOM_ZONE, d.instance.zone
390
+ assert_equal CUSTOM_VM_ID, d.instance.vm_id
391
+ assert_equal false, d.instance.running_on_managed_vm
392
+ end
393
+
394
+ def test_gce_used_when_detect_subservice_is_false
395
+ setup_gce_metadata_stubs
396
+ # This would cause the service to be container.googleapis.com if not for the
397
+ # detect_subservice=false config.
398
+ setup_container_metadata_stubs
399
+ d = create_driver(NO_DETECT_SUBSERVICE_CONFIG)
400
+ d.run
401
+ assert_equal COMPUTE_SERVICE_NAME, d.instance.service_name
402
+ end
403
+
404
+ def test_metadata_overrides
405
+ {
406
+ # In this case we are overriding all configured parameters so we should
407
+ # see all "custom" values rather than the ones from the metadata server.
408
+ CUSTOM_METADATA_CONFIG =>
409
+ ['gce', CUSTOM_PROJECT_ID, CUSTOM_ZONE, CUSTOM_VM_ID],
410
+ # Similar to above, but we are not overriding project_id in this config so
411
+ # we should see the metadata value for project_id and "custom" otherwise.
412
+ CONFIG_MISSING_METADATA_PROJECT_ID =>
413
+ ['gce', PROJECT_ID, CUSTOM_ZONE, CUSTOM_VM_ID],
414
+ CONFIG_EC2_PROJECT_ID =>
415
+ ['ec2', EC2_PROJECT_ID, EC2_PREFIXED_ZONE, EC2_VM_ID],
416
+ CONFIG_EC2_PROJECT_ID_AND_CUSTOM_VM_ID =>
417
+ ['ec2', EC2_PROJECT_ID, EC2_PREFIXED_ZONE, CUSTOM_VM_ID]
418
+ }.each_with_index do |(config, parts), index|
419
+ send("setup_#{parts[0]}_metadata_stubs")
420
+ d = create_driver(config)
421
+ d.run
422
+ assert_equal parts[1], d.instance.project_id, "Index #{index} failed."
423
+ assert_equal parts[2], d.instance.zone, "Index #{index} failed."
424
+ assert_equal parts[3], d.instance.vm_id, "Index #{index} failed."
425
+ assert_equal false, d.instance.running_on_managed_vm,
426
+ "Index #{index} failed."
427
+ end
428
+ end
429
+
430
+ def test_ec2_metadata_requires_project_id
431
+ setup_ec2_metadata_stubs
432
+ exception_count = 0
433
+ Fluent::GoogleCloudOutput::CredentialsInfo.stubs(:project_id).returns(nil)
434
+ begin
435
+ create_driver
436
+ rescue Fluent::ConfigError => error
437
+ assert error.message.include? 'Unable to obtain metadata parameters:'
438
+ assert error.message.include? 'project_id'
439
+ exception_count += 1
440
+ end
441
+ assert_equal 1, exception_count
442
+ end
443
+
444
+ def test_ec2_metadata_project_id_from_credentials
445
+ setup_ec2_metadata_stubs
446
+ [IAM_CREDENTIALS, LEGACY_CREDENTIALS].each do |creds|
447
+ ENV['GOOGLE_APPLICATION_CREDENTIALS'] = creds[:path]
448
+ d = create_driver
449
+ d.run
450
+ assert_equal creds[:project_id], d.instance.project_id
451
+ end
452
+ end
453
+
454
+ def test_one_log
455
+ setup_gce_metadata_stubs
456
+ setup_logging_stubs do
457
+ d = create_driver
458
+ d.emit('message' => log_entry(0))
459
+ d.run
460
+ end
461
+ verify_log_entries(1, COMPUTE_PARAMS)
462
+ end
463
+
464
+ def test_one_log_with_json_credentials
465
+ setup_gce_metadata_stubs
466
+ ENV['GOOGLE_APPLICATION_CREDENTIALS'] = IAM_CREDENTIALS[:path]
467
+ setup_logging_stubs do
468
+ d = create_driver
469
+ d.emit('message' => log_entry(0))
470
+ d.run
471
+ end
472
+ verify_log_entries(1, COMPUTE_PARAMS)
473
+ end
474
+
475
+ def test_one_log_with_invalid_json_credentials
476
+ setup_gce_metadata_stubs
477
+ ENV['GOOGLE_APPLICATION_CREDENTIALS'] = INVALID_CREDENTIALS[:path]
478
+ setup_logging_stubs do
479
+ d = create_driver
480
+ d.emit('message' => log_entry(0))
481
+ exception_count = 0
482
+ begin
483
+ d.run
484
+ rescue RuntimeError => error
485
+ assert error.message.include? 'Unable to read the credential file'
486
+ exception_count += 1
487
+ end
488
+ assert_equal 1, exception_count
489
+ end
490
+ end
491
+
492
+ def test_one_log_custom_metadata
493
+ # don't set up any metadata stubs, so the test will fail if we try to
494
+ # fetch metadata (and explicitly check this as well).
495
+ Fluent::GoogleCloudOutput.any_instance.expects(:fetch_metadata).never
496
+ ENV['GOOGLE_APPLICATION_CREDENTIALS'] = IAM_CREDENTIALS[:path]
497
+ setup_logging_stubs do
498
+ d = create_driver(NO_METADATA_SERVICE_CONFIG + CUSTOM_METADATA_CONFIG)
499
+ d.emit('message' => log_entry(0))
500
+ d.run
501
+ end
502
+ verify_log_entries(1, CUSTOM_PARAMS)
503
+ end
504
+
505
+ def test_one_log_ec2
506
+ ENV['GOOGLE_APPLICATION_CREDENTIALS'] = IAM_CREDENTIALS[:path]
507
+ setup_ec2_metadata_stubs
508
+ setup_logging_stubs do
509
+ d = create_driver(CONFIG_EC2_PROJECT_ID)
510
+ d.emit('message' => log_entry(0))
511
+ d.run
512
+ end
513
+ verify_log_entries(1, EC2_PARAMS)
514
+ end
515
+
516
+ def test_struct_payload_log
517
+ setup_gce_metadata_stubs
518
+ setup_logging_stubs do
519
+ d = create_driver
520
+ d.emit('msg' => log_entry(0), 'tag2' => 'test', 'data' => 5000)
521
+ d.run
522
+ end
523
+ verify_log_entries(1, COMPUTE_PARAMS, 'structPayload') do |entry|
524
+ fields = get_fields(entry['structPayload'])
525
+ assert_equal 3, fields.size, entry
526
+ assert_equal 'test log entry 0', get_string(fields['msg']), entry
527
+ assert_equal 'test', get_string(fields['tag2']), entry
528
+ assert_equal 5000, get_number(fields['data']), entry
529
+ end
530
+ end
531
+
532
+ def test_struct_payload_json_log
533
+ setup_gce_metadata_stubs
534
+ setup_logging_stubs do
535
+ d = create_driver
536
+ json_string = '{"msg": "test log entry 0", "tag2": "test", "data": 5000}'
537
+ d.emit('message' => 'notJSON ' + json_string)
538
+ d.emit('message' => json_string)
539
+ d.emit('message' => "\t" + json_string)
540
+ d.emit('message' => ' ' + json_string)
541
+ d.run
542
+ end
543
+ verify_log_entries(4, COMPUTE_PARAMS, '') do |entry|
544
+ assert entry.key?('textPayload'), 'Entry did not have textPayload'
545
+ end
546
+ end
547
+
548
+ def test_struct_payload_json_container_log
549
+ setup_gce_metadata_stubs
550
+ setup_container_metadata_stubs
551
+ setup_logging_stubs do
552
+ d = create_driver(APPLICATION_DEFAULT_CONFIG, CONTAINER_TAG)
553
+ json_string = '{"msg": "test log entry 0", "tag2": "test", "data": 5000}'
554
+ d.emit(container_log_entry_with_metadata('notJSON' + json_string))
555
+ d.emit(container_log_entry_with_metadata(json_string))
556
+ d.emit(container_log_entry_with_metadata(" \r\n \t" + json_string))
557
+ d.run
558
+ end
559
+ log_index = 0
560
+ verify_log_entries(
561
+ 3, CONTAINER_FROM_METADATA_PARAMS, '') do |entry|
562
+ log_index += 1
563
+ if log_index == 1
564
+ assert entry.key?('textPayload'), 'Entry did not have textPayload'
565
+ else
566
+ assert entry.key?('structPayload'), 'Entry did not have structPayload'
567
+ fields = get_fields(entry['structPayload'])
568
+ assert_equal 3, fields.size, entry
569
+ assert_equal 'test log entry 0', get_string(fields['msg']), entry
570
+ assert_equal 'test', get_string(fields['tag2']), entry
571
+ assert_equal 5000, get_number(fields['data']), entry
572
+ end
573
+ end
574
+ end
575
+
576
+ def test_timestamps
577
+ setup_gce_metadata_stubs
578
+ expected_ts = []
579
+ emit_index = 0
580
+ setup_logging_stubs do
581
+ [Time.at(123_456.789), Time.at(0), Time.now].each do |ts|
582
+ d = create_driver
583
+ # Test the "native" fluentd timestamp as well as our nanosecond tags.
584
+ d.emit({ 'message' => log_entry(emit_index) }, ts.to_f)
585
+ expected_ts.push(ts)
586
+ emit_index += 1
587
+ d.emit('message' => log_entry(emit_index),
588
+ 'timeNanos' => ts.tv_sec * 1_000_000_000 + ts.tv_nsec)
589
+ expected_ts.push(ts)
590
+ emit_index += 1
591
+ d.emit('message' => log_entry(emit_index),
592
+ 'timestamp' => { 'seconds' => ts.tv_sec, 'nanos' => ts.tv_nsec })
593
+ expected_ts.push(ts)
594
+ emit_index += 1
595
+ d.emit('message' => log_entry(emit_index),
596
+ 'timestampSeconds' => ts.tv_sec, 'timestampNanos' => ts.tv_nsec)
597
+ expected_ts.push(ts)
598
+ emit_index += 1
599
+ d.run
600
+ end
601
+ end
602
+ verify_index = 0
603
+ verify_log_entries(emit_index, COMPUTE_PARAMS) do |entry|
604
+ assert_equal_with_default entry['metadata']['timestamp']['seconds'],
605
+ expected_ts[verify_index].tv_sec, 0, entry
606
+ assert_equal_with_default entry['metadata']['timestamp']['nanos'],
607
+ expected_ts[verify_index].tv_nsec, 0, entry do
608
+ # Fluentd v0.14 onwards supports nanosecond timestamp values.
609
+ # Added in 600 ns delta to avoid flaky tests introduced
610
+ # due to rounding error in double-precision floating-point numbers
611
+ # (to account for the missing 9 bits of precision ~ 512 ns).
612
+ # See http://wikipedia.org/wiki/Double-precision_floating-point_format
613
+ assert_in_delta expected_ts[verify_index].tv_nsec,
614
+ entry['metadata']['timestamp']['nanos'], 600, entry
615
+ end
616
+ verify_index += 1
617
+ end
618
+ end
619
+
620
+ def test_malformed_timestamp
621
+ setup_gce_metadata_stubs
622
+ setup_logging_stubs do
623
+ d = create_driver
624
+ # if timestamp is not a hash it is passed through to the struct payload.
625
+ d.emit('message' => log_entry(0), 'timestamp' => 'not-a-hash')
626
+ d.run
627
+ end
628
+ verify_log_entries(1, COMPUTE_PARAMS, 'structPayload') do |entry|
629
+ fields = get_fields(entry['structPayload'])
630
+ assert_equal 2, fields.size, entry
631
+ assert_equal 'not-a-hash', get_string(fields['timestamp']), entry
632
+ end
633
+ end
634
+
635
+ # Make parse_severity public so we can test it.
636
+ class Fluent::GoogleCloudOutput # rubocop:disable Style/ClassAndModuleChildren
637
+ public :parse_severity
638
+ end
639
+
640
+ def test_label_map_without_field_present
641
+ setup_gce_metadata_stubs
642
+ setup_logging_stubs do
643
+ config = %(label_map { "label_field": "sent_label" })
644
+ d = create_driver(config)
645
+ d.emit('message' => log_entry(0))
646
+ d.run
647
+ # No additional labels should be present
648
+ end
649
+ verify_log_entries(1, COMPUTE_PARAMS)
650
+ end
651
+
652
+ def test_label_map_with_field_present
653
+ setup_gce_metadata_stubs
654
+ setup_logging_stubs do
655
+ config = %(label_map { "label_field": "sent_label" })
656
+ d = create_driver(config)
657
+ d.emit('message' => log_entry(0), 'label_field' => 'label_value')
658
+ d.run
659
+ end
660
+ # make a deep copy of COMPUTE_PARAMS and add the parsed label.
661
+ params = Marshal.load(Marshal.dump(COMPUTE_PARAMS))
662
+ params[:labels]['sent_label'] = 'label_value'
663
+ verify_log_entries(1, params)
664
+ end
665
+
666
+ def test_label_map_with_numeric_field
667
+ setup_gce_metadata_stubs
668
+ setup_logging_stubs do
669
+ config = %(label_map { "label_field": "sent_label" })
670
+ d = create_driver(config)
671
+ d.emit('message' => log_entry(0), 'label_field' => 123_456_789)
672
+ d.run
673
+ end
674
+ # make a deep copy of COMPUTE_PARAMS and add the parsed label.
675
+ params = Marshal.load(Marshal.dump(COMPUTE_PARAMS))
676
+ params[:labels]['sent_label'] = '123456789'
677
+ verify_log_entries(1, params)
678
+ end
679
+
680
+ def test_label_map_with_hash_field
681
+ setup_gce_metadata_stubs
682
+ setup_logging_stubs do
683
+ config = %(label_map { "label_field": "sent_label" })
684
+ d = create_driver(config)
685
+ # I'm not sure this actually makes sense for a user to do, but make
686
+ # sure that it works if they try it.
687
+ d.emit('message' => log_entry(0),
688
+ 'label_field' => { 'k1' => 10, 'k2' => 'val' })
689
+ d.run
690
+ end
691
+ # make a deep copy of COMPUTE_PARAMS and add the parsed label.
692
+ params = Marshal.load(Marshal.dump(COMPUTE_PARAMS))
693
+ params[:labels]['sent_label'] = '{"k1"=>10, "k2"=>"val"}'
694
+ verify_log_entries(1, params)
695
+ end
696
+
697
+ def test_label_map_with_multiple_fields
698
+ setup_gce_metadata_stubs
699
+ setup_logging_stubs do
700
+ config = %(
701
+ label_map {
702
+ "label1": "sent_label_1",
703
+ "label_number_two": "foo.googleapis.com/bar",
704
+ "label3": "label3"
705
+ }
706
+ )
707
+ d = create_driver(config)
708
+ # not_a_label passes through to the struct payload
709
+ d.emit('message' => log_entry(0),
710
+ 'label1' => 'value1',
711
+ 'label_number_two' => 'value2',
712
+ 'not_a_label' => 'value4',
713
+ 'label3' => 'value3')
714
+ d.run
715
+ end
716
+ # make a deep copy of COMPUTE_PARAMS and add the parsed labels.
717
+ params = Marshal.load(Marshal.dump(COMPUTE_PARAMS))
718
+ params[:labels]['sent_label_1'] = 'value1'
719
+ params[:labels]['foo.googleapis.com/bar'] = 'value2'
720
+ params[:labels]['label3'] = 'value3'
721
+ verify_log_entries(1, params, 'structPayload') do |entry|
722
+ fields = get_fields(entry['structPayload'])
723
+ assert_equal 2, fields.size, entry
724
+ assert_equal 'test log entry 0', get_string(fields['message']), entry
725
+ assert_equal 'value4', get_string(fields['not_a_label']), entry
726
+ end
727
+ end
728
+
729
+ def test_multiple_logs
730
+ setup_gce_metadata_stubs
731
+ # Only test a few values because otherwise the test can take minutes.
732
+ [2, 3, 5, 11, 50].each do |n|
733
+ setup_logging_stubs do
734
+ d = create_driver
735
+ # The test driver doesn't clear its buffer of entries after running, so
736
+ # do it manually here.
737
+ d.instance_variable_get('@entries').clear
738
+ @logs_sent = []
739
+ n.times { |i| d.emit('message' => log_entry(i)) }
740
+ d.run
741
+ end
742
+ verify_log_entries(n, COMPUTE_PARAMS)
743
+ end
744
+ end
745
+
746
+ def test_malformed_log
747
+ setup_gce_metadata_stubs
748
+ setup_logging_stubs do
749
+ d = create_driver
750
+ # if the entry is not a hash, the plugin should silently drop it.
751
+ d.emit('a string is not a valid message')
752
+ d.run
753
+ end
754
+ assert @logs_sent.empty?
755
+ end
756
+
757
+ def test_one_managed_vm_log
758
+ setup_gce_metadata_stubs
759
+ setup_managed_vm_metadata_stubs
760
+ setup_logging_stubs do
761
+ d = create_driver
762
+ d.emit('message' => log_entry(0))
763
+ d.run
764
+ end
765
+ verify_log_entries(1, VMENGINE_PARAMS)
766
+ end
767
+
768
+ def test_multiple_managed_vm_logs
769
+ setup_gce_metadata_stubs
770
+ setup_managed_vm_metadata_stubs
771
+ [2, 3, 5, 11, 50].each do |n|
772
+ setup_logging_stubs do
773
+ d = create_driver
774
+ # The test driver doesn't clear its buffer of entries after running, so
775
+ # do it manually here.
776
+ d.instance_variable_get('@entries').clear
777
+ @logs_sent = []
778
+ n.times { |i| d.emit('message' => log_entry(i)) }
779
+ d.run
780
+ end
781
+ verify_log_entries(n, VMENGINE_PARAMS)
782
+ end
783
+ end
784
+
785
+ def test_one_container_log_metadata_from_plugin
786
+ setup_gce_metadata_stubs
787
+ setup_container_metadata_stubs
788
+ setup_logging_stubs do
789
+ d = create_driver(APPLICATION_DEFAULT_CONFIG, CONTAINER_TAG)
790
+ d.emit(container_log_entry_with_metadata(log_entry(0)))
791
+ d.run
792
+ end
793
+ verify_log_entries(1, CONTAINER_FROM_METADATA_PARAMS) do |entry|
794
+ assert_equal CONTAINER_SECONDS_EPOCH, \
795
+ entry['metadata']['timestamp']['seconds'], entry
796
+ assert_equal CONTAINER_NANOS, \
797
+ entry['metadata']['timestamp']['nanos'], entry
798
+ assert_equal CONTAINER_SEVERITY, entry['metadata']['severity'], entry
799
+ end
800
+ end
801
+
802
+ def test_multiple_container_logs_metadata_from_plugin
803
+ setup_gce_metadata_stubs
804
+ setup_container_metadata_stubs
805
+ [2, 3, 5, 11, 50].each do |n|
806
+ @logs_sent = []
807
+ setup_logging_stubs do
808
+ d = create_driver(APPLICATION_DEFAULT_CONFIG, CONTAINER_TAG)
809
+ # The test driver doesn't clear its buffer of entries after running, so
810
+ # do it manually here.
811
+ d.instance_variable_get('@entries').clear
812
+ n.times { |i| d.emit(container_log_entry_with_metadata(log_entry(i))) }
813
+ d.run
814
+ end
815
+ verify_log_entries(n, CONTAINER_FROM_METADATA_PARAMS) do |entry|
816
+ assert_equal CONTAINER_SECONDS_EPOCH, \
817
+ entry['metadata']['timestamp']['seconds'], entry
818
+ assert_equal CONTAINER_NANOS, \
819
+ entry['metadata']['timestamp']['nanos'], entry
820
+ assert_equal CONTAINER_SEVERITY, entry['metadata']['severity'], entry
821
+ end
822
+ end
823
+ end
824
+
825
+ def test_multiple_container_logs_metadata_from_tag
826
+ setup_gce_metadata_stubs
827
+ setup_container_metadata_stubs
828
+ [2, 3, 5, 11, 50].each do |n|
829
+ @logs_sent = []
830
+ setup_logging_stubs do
831
+ d = create_driver(APPLICATION_DEFAULT_CONFIG, CONTAINER_TAG)
832
+ # The test driver doesn't clear its buffer of entries after running, so
833
+ # do it manually here.
834
+ d.instance_variable_get('@entries').clear
835
+ n.times { |i| d.emit(container_log_entry(log_entry(i))) }
836
+ d.run
837
+ end
838
+ verify_log_entries(n, CONTAINER_FROM_TAG_PARAMS) do |entry|
839
+ assert_equal CONTAINER_SECONDS_EPOCH, \
840
+ entry['metadata']['timestamp']['seconds'], entry
841
+ assert_equal CONTAINER_NANOS, \
842
+ entry['metadata']['timestamp']['nanos'], entry
843
+ assert_equal CONTAINER_SEVERITY, entry['metadata']['severity'], entry
844
+ end
845
+ end
846
+ end
847
+
848
+ def test_one_container_log_metadata_from_tag
849
+ setup_gce_metadata_stubs
850
+ setup_container_metadata_stubs
851
+ setup_logging_stubs do
852
+ d = create_driver(APPLICATION_DEFAULT_CONFIG, CONTAINER_TAG)
853
+ d.emit(container_log_entry(log_entry(0)))
854
+ d.run
855
+ end
856
+ verify_log_entries(1, CONTAINER_FROM_TAG_PARAMS) do |entry|
857
+ assert_equal CONTAINER_SECONDS_EPOCH, \
858
+ entry['metadata']['timestamp']['seconds'], entry
859
+ assert_equal CONTAINER_NANOS, \
860
+ entry['metadata']['timestamp']['nanos'], entry
861
+ assert_equal CONTAINER_SEVERITY, entry['metadata']['severity'], entry
862
+ end
863
+ end
864
+
865
+ def test_one_container_log_from_tag_stderr
866
+ setup_gce_metadata_stubs
867
+ setup_container_metadata_stubs
868
+ setup_logging_stubs do
869
+ d = create_driver(APPLICATION_DEFAULT_CONFIG, CONTAINER_TAG)
870
+ d.emit(container_log_entry(log_entry(0), 'stderr'))
871
+ d.run
872
+ end
873
+ expected_params = CONTAINER_FROM_TAG_PARAMS.merge(
874
+ labels: { "#{CONTAINER_SERVICE_NAME}/stream" => 'stderr' }
875
+ ) { |_, oldval, newval| oldval.merge(newval) }
876
+ verify_log_entries(1, expected_params) do |entry|
877
+ assert_equal CONTAINER_SECONDS_EPOCH, \
878
+ entry['metadata']['timestamp']['seconds'], entry
879
+ assert_equal CONTAINER_NANOS, \
880
+ entry['metadata']['timestamp']['nanos'], entry
881
+ assert_equal 'ERROR', entry['metadata']['severity'], entry
882
+ end
883
+ end
884
+
885
+ def test_struct_container_log_metadata_from_plugin
886
+ setup_gce_metadata_stubs
887
+ setup_container_metadata_stubs
888
+ setup_logging_stubs do
889
+ d = create_driver(APPLICATION_DEFAULT_CONFIG, CONTAINER_TAG)
890
+ d.emit(container_log_entry_with_metadata('{"msg": "test log entry 0", ' \
891
+ '"tag2": "test", "data": ' \
892
+ '5000, "severity": "WARNING"}'))
893
+ d.run
894
+ end
895
+ verify_log_entries(1, CONTAINER_FROM_METADATA_PARAMS,
896
+ 'structPayload') do |entry|
897
+ fields = get_fields(entry['structPayload'])
898
+ assert_equal 3, fields.size, entry
899
+ assert_equal 'test log entry 0', get_string(fields['msg']), entry
900
+ assert_equal 'test', get_string(fields['tag2']), entry
901
+ assert_equal 5000, get_number(fields['data']), entry
902
+ assert_equal CONTAINER_SECONDS_EPOCH, \
903
+ entry['metadata']['timestamp']['seconds'], entry
904
+ assert_equal CONTAINER_NANOS, \
905
+ entry['metadata']['timestamp']['nanos'], entry
906
+ assert_equal 'WARNING', entry['metadata']['severity'], entry
907
+ end
908
+ end
909
+
910
+ def test_struct_container_log_metadata_from_tag
911
+ setup_gce_metadata_stubs
912
+ setup_container_metadata_stubs
913
+ setup_logging_stubs do
914
+ d = create_driver(APPLICATION_DEFAULT_CONFIG, CONTAINER_TAG)
915
+ d.emit(container_log_entry('{"msg": "test log entry 0", ' \
916
+ '"tag2": "test", "data": 5000, ' \
917
+ '"severity": "W"}'))
918
+ d.run
919
+ end
920
+ verify_log_entries(1, CONTAINER_FROM_TAG_PARAMS,
921
+ 'structPayload') do |entry|
922
+ fields = get_fields(entry['structPayload'])
923
+ assert_equal 3, fields.size, entry
924
+ assert_equal 'test log entry 0', get_string(fields['msg']), entry
925
+ assert_equal 'test', get_string(fields['tag2']), entry
926
+ assert_equal 5000, get_number(fields['data']), entry
927
+ assert_equal CONTAINER_SECONDS_EPOCH, \
928
+ entry['metadata']['timestamp']['seconds'], entry
929
+ assert_equal CONTAINER_NANOS, \
930
+ entry['metadata']['timestamp']['nanos'], entry
931
+ assert_equal 'WARNING', entry['metadata']['severity'], entry
932
+ end
933
+ end
934
+
935
+ def test_cloudfunctions_log
936
+ setup_gce_metadata_stubs
937
+ setup_cloudfunctions_metadata_stubs
938
+ [1, 2, 3, 5, 11, 50].each do |n|
939
+ setup_logging_stubs do
940
+ d = create_driver(APPLICATION_DEFAULT_CONFIG, CLOUDFUNCTIONS_TAG)
941
+ # The test driver doesn't clear its buffer of entries after running, so
942
+ # do it manually here.
943
+ d.instance_variable_get('@entries').clear
944
+ @logs_sent = []
945
+ n.times { |i| d.emit(cloudfunctions_log_entry(i)) }
946
+ d.run
947
+ end
948
+ verify_log_entries(n, CLOUDFUNCTIONS_PARAMS) do |entry|
949
+ assert_equal 'DEBUG', entry['metadata']['severity'],
950
+ "Test with #{n} logs failed. \n#{entry}"
951
+ end
952
+ end
953
+ end
954
+
955
+ def test_cloudfunctions_logs_text_not_matched
956
+ setup_gce_metadata_stubs
957
+ setup_cloudfunctions_metadata_stubs
958
+ [1, 2, 3, 5, 11, 50].each do |n|
959
+ @logs_sent = []
960
+ setup_logging_stubs do
961
+ d = create_driver(APPLICATION_DEFAULT_CONFIG, CLOUDFUNCTIONS_TAG)
962
+ # The test driver doesn't clear its buffer of entries after running, so
963
+ # do it manually here.
964
+ d.instance_variable_get('@entries').clear
965
+ n.times { |i| d.emit(cloudfunctions_log_entry_text_not_matched(i)) }
966
+ d.run
967
+ end
968
+ verify_log_entries(
969
+ n, CLOUDFUNCTIONS_TEXT_NOT_MATCHED_PARAMS) do |entry|
970
+ assert_equal 'INFO', entry['metadata']['severity'],
971
+ "Test with #{n} logs failed. \n#{entry}"
972
+ end
973
+ end
974
+ end
975
+
976
+ def test_multiple_cloudfunctions_logs_tag_not_matched
977
+ setup_gce_metadata_stubs
978
+ setup_cloudfunctions_metadata_stubs
979
+ [1, 2, 3, 5, 11, 50].each do |n|
980
+ @logs_sent = []
981
+ setup_logging_stubs do
982
+ d = create_driver(APPLICATION_DEFAULT_CONFIG, CONTAINER_TAG)
983
+ # The test driver doesn't clear its buffer of entries after running, so
984
+ # do it manually here.
985
+ d.instance_variable_get('@entries').clear
986
+ n.times { |i| d.emit(cloudfunctions_log_entry(i)) }
987
+ d.run
988
+ end
989
+ i = 0
990
+ verify_log_entries(n, CONTAINER_FROM_TAG_PARAMS, '') do |entry|
991
+ assert_equal '[D][2015-09-25T12:34:56.789Z][123-0] test log entry ' \
992
+ "#{i}", entry['textPayload'],
993
+ "Test with #{n} logs failed. \n#{entry}"
994
+ i += 1
995
+ end
996
+ end
997
+ end
998
+
999
+ def test_http_request_from_record
1000
+ setup_gce_metadata_stubs
1001
+ setup_logging_stubs do
1002
+ d = create_driver
1003
+ d.emit('httpRequest' => http_request_message)
1004
+ d.run
1005
+ end
1006
+ verify_log_entries(1, COMPUTE_PARAMS, 'httpRequest') do |entry|
1007
+ assert_equal http_request_message, entry['httpRequest'], entry
1008
+ assert_nil get_fields(entry['structPayload'])['httpRequest'], entry
1009
+ end
1010
+ end
1011
+
1012
+ def test_http_request_partial_from_record
1013
+ setup_gce_metadata_stubs
1014
+ setup_logging_stubs do
1015
+ d = create_driver
1016
+ d.emit('httpRequest' => http_request_message.merge(
1017
+ 'otherKey' => 'value'))
1018
+ d.run
1019
+ end
1020
+ verify_log_entries(1, COMPUTE_PARAMS, 'httpRequest') do |entry|
1021
+ assert_equal http_request_message, entry['httpRequest'], entry
1022
+ fields = get_fields(entry['structPayload'])
1023
+ request = get_fields(get_struct(fields['httpRequest']))
1024
+ assert_equal 'value', get_string(request['otherKey']), entry
1025
+ end
1026
+ end
1027
+
1028
+ def test_http_request_without_referer_from_record
1029
+ setup_gce_metadata_stubs
1030
+ setup_logging_stubs do
1031
+ d = create_driver
1032
+ d.emit('httpRequest' => http_request_message_without_referer)
1033
+ d.run
1034
+ end
1035
+ verify_log_entries(1, COMPUTE_PARAMS, 'httpRequest') do |entry|
1036
+ assert_equal http_request_message_without_referer, entry['httpRequest'],
1037
+ entry
1038
+ assert_nil get_fields(entry['structPayload'])['httpRequest'], entry
1039
+ end
1040
+ end
1041
+
1042
+ def test_http_request_when_not_hash
1043
+ setup_gce_metadata_stubs
1044
+ setup_logging_stubs do
1045
+ d = create_driver
1046
+ d.emit('httpRequest' => 'a_string')
1047
+ d.run
1048
+ end
1049
+ verify_log_entries(1, COMPUTE_PARAMS, 'structPayload') do |entry|
1050
+ fields = get_fields(entry['structPayload'])
1051
+ assert_equal 'a_string', get_string(fields['httpRequest']), entry
1052
+ assert_nil entry['httpRequest'], entry
1053
+ end
1054
+ end
1055
+
1056
+ private
1057
+
1058
+ def uri_for_log(params)
1059
+ 'https://logging.googleapis.com/v1beta3/projects/' + params[:project_id] +
1060
+ '/logs/' + params[:log_name] + '/entries:write'
1061
+ end
1062
+
1063
+ def stub_metadata_request(metadata_path, response_body)
1064
+ stub_request(:get, 'http://169.254.169.254/computeMetadata/v1/' +
1065
+ metadata_path)
1066
+ .to_return(body: response_body, status: 200,
1067
+ headers: { 'Content-Length' => response_body.length })
1068
+ end
1069
+
1070
+ def setup_no_metadata_service_stubs
1071
+ # Simulate a machine with no metadata service present
1072
+ stub_request(:any, %r{http://169.254.169.254/.*})
1073
+ .to_raise(Errno::EHOSTUNREACH)
1074
+ end
1075
+
1076
+ def setup_gce_metadata_stubs
1077
+ # Stub the root, used for platform detection by the plugin and 'googleauth'.
1078
+ stub_request(:get, 'http://169.254.169.254')
1079
+ .to_return(status: 200, headers: { 'Metadata-Flavor' => 'Google' })
1080
+
1081
+ # Create stubs for all the GCE metadata lookups the agent needs to make.
1082
+ stub_metadata_request('project/project-id', PROJECT_ID)
1083
+ stub_metadata_request('instance/zone', FULLY_QUALIFIED_ZONE)
1084
+ stub_metadata_request('instance/id', VM_ID)
1085
+ stub_metadata_request('instance/attributes/',
1086
+ "attribute1\nattribute2\nattribute3")
1087
+
1088
+ # Used by 'googleauth' to fetch the default service account credentials.
1089
+ stub_request(:get, 'http://169.254.169.254/computeMetadata/v1/' \
1090
+ 'instance/service-accounts/default/token')
1091
+ .to_return(body: %({"access_token": "#{FAKE_AUTH_TOKEN}"}),
1092
+ status: 200,
1093
+ headers: { 'Content-Length' => FAKE_AUTH_TOKEN.length,
1094
+ 'Content-Type' => 'application/json' })
1095
+ end
1096
+
1097
+ def setup_ec2_metadata_stubs
1098
+ # Stub the root, used for platform detection.
1099
+ stub_request(:get, 'http://169.254.169.254')
1100
+ .to_return(status: 200, headers: { 'Server' => 'EC2ws' })
1101
+
1102
+ # Stub the identity document lookup made by the agent.
1103
+ stub_request(:get, 'http://169.254.169.254/latest/dynamic/' \
1104
+ 'instance-identity/document')
1105
+ .to_return(body: EC2_IDENTITY_DOCUMENT, status: 200,
1106
+ headers: { 'Content-Length' => EC2_IDENTITY_DOCUMENT.length })
1107
+ end
1108
+
1109
+ def setup_auth_stubs
1110
+ # Used when loading credentials from a JSON file.
1111
+ stub_request(:post, 'https://www.googleapis.com/oauth2/v3/token')
1112
+ .with(body: hash_including(grant_type: AUTH_GRANT_TYPE))
1113
+ .to_return(body: %({"access_token": "#{FAKE_AUTH_TOKEN}"}),
1114
+ status: 200,
1115
+ headers: { 'Content-Length' => FAKE_AUTH_TOKEN.length,
1116
+ 'Content-Type' => 'application/json' })
1117
+
1118
+ stub_request(:post, 'https://www.googleapis.com/oauth2/v3/token')
1119
+ .with(body: hash_including(grant_type: 'refresh_token'))
1120
+ .to_return(body: %({"access_token": "#{FAKE_AUTH_TOKEN}"}),
1121
+ status: 200,
1122
+ headers: { 'Content-Length' => FAKE_AUTH_TOKEN.length,
1123
+ 'Content-Type' => 'application/json' })
1124
+ end
1125
+
1126
+ def setup_managed_vm_metadata_stubs
1127
+ stub_metadata_request(
1128
+ 'instance/attributes/',
1129
+ "attribute1\ngae_backend_name\ngae_backend_version\nlast_attribute")
1130
+ stub_metadata_request('instance/attributes/gae_backend_name',
1131
+ MANAGED_VM_BACKEND_NAME)
1132
+ stub_metadata_request('instance/attributes/gae_backend_version',
1133
+ MANAGED_VM_BACKEND_VERSION)
1134
+ end
1135
+
1136
+ def setup_container_metadata_stubs
1137
+ stub_metadata_request(
1138
+ 'instance/attributes/',
1139
+ "attribute1\nkube-env\nlast_attribute")
1140
+ stub_metadata_request('instance/attributes/kube-env',
1141
+ "ENABLE_NODE_LOGGING: \"true\"\n"\
1142
+ 'INSTANCE_PREFIX: '\
1143
+ "gke-#{CONTAINER_CLUSTER_NAME}-740fdafa\n"\
1144
+ 'KUBE_BEARER_TOKEN: AoQiMuwkNP2BMT0S')
1145
+ end
1146
+
1147
+ def setup_cloudfunctions_metadata_stubs
1148
+ stub_metadata_request(
1149
+ 'instance/attributes/',
1150
+ "attribute1\nkube-env\ngcf_region\nlast_attribute")
1151
+ stub_metadata_request('instance/attributes/kube-env',
1152
+ "ENABLE_NODE_LOGGING: \"true\"\n"\
1153
+ 'INSTANCE_PREFIX: '\
1154
+ "gke-#{CLOUDFUNCTIONS_CLUSTER_NAME}-740fdafa\n"\
1155
+ 'KUBE_BEARER_TOKEN: AoQiMuwkNP2BMT0S')
1156
+ stub_metadata_request('instance/attributes/gcf_region',
1157
+ CLOUDFUNCTIONS_REGION)
1158
+ end
1159
+
1160
+ def container_log_entry_with_metadata(log)
1161
+ {
1162
+ log: log,
1163
+ stream: CONTAINER_STREAM,
1164
+ time: CONTAINER_TIMESTAMP,
1165
+ kubernetes: {
1166
+ namespace_id: CONTAINER_NAMESPACE_ID,
1167
+ namespace_name: CONTAINER_NAMESPACE_NAME,
1168
+ pod_id: CONTAINER_POD_ID,
1169
+ pod_name: CONTAINER_POD_NAME,
1170
+ container_name: CONTAINER_CONTAINER_NAME,
1171
+ labels: {
1172
+ CONTAINER_LABEL_KEY => CONTAINER_LABEL_VALUE
1173
+ }
1174
+ }
1175
+ }
1176
+ end
1177
+
1178
+ def container_log_entry(log, stream = CONTAINER_STREAM)
1179
+ {
1180
+ log: log,
1181
+ stream: stream,
1182
+ time: CONTAINER_TIMESTAMP
1183
+ }
1184
+ end
1185
+
1186
+ def cloudfunctions_log_entry(i)
1187
+ {
1188
+ stream: 'stdout',
1189
+ log: '[D][2015-09-25T12:34:56.789Z][123-0] ' + log_entry(i)
1190
+ }
1191
+ end
1192
+
1193
+ def cloudfunctions_log_entry_text_not_matched(i)
1194
+ {
1195
+ stream: 'stdout',
1196
+ log: log_entry(i)
1197
+ }
1198
+ end
1199
+
1200
+ def log_entry(i)
1201
+ 'test log entry ' + i.to_s
1202
+ end
1203
+
1204
+ def check_labels(entry, common_labels, expected_labels)
1205
+ # TODO(salty) test/handle overlap between common_labels and entry labels
1206
+ all_labels ||= common_labels
1207
+ all_labels.merge!(entry['metadata']['labels'] || {})
1208
+ all_labels.each do |key, value|
1209
+ assert value.is_a?(String), "Value #{value} for label #{key} " \
1210
+ 'is not a string: ' + value.class.name
1211
+ assert expected_labels.key?(key), "Unexpected label #{key} => #{value}"
1212
+ assert_equal expected_labels[key], value, 'Value mismatch - expected ' \
1213
+ "#{expected_labels[key]} in #{key} => #{value}"
1214
+ end
1215
+ assert_equal expected_labels.length, all_labels.length, 'Expected ' \
1216
+ "#{expected_labels.length} labels, got #{all_labels.length}"
1217
+ end
1218
+
1219
+ # The caller can optionally provide a block which is called for each entry.
1220
+ def verify_json_log_entries(n, params, payload_type = 'textPayload')
1221
+ i = 0
1222
+ @logs_sent.each do |batch|
1223
+ batch['entries'].each do |entry|
1224
+ unless payload_type.empty?
1225
+ assert entry.key?(payload_type), 'Entry did not contain expected ' \
1226
+ "#{payload_type} key: " + entry.to_s
1227
+ # Check the payload for textPayload, otherwise it's up to the caller.
1228
+ if payload_type == 'textPayload'
1229
+ assert_equal "test log entry #{i}", entry['textPayload'], batch
1230
+ end
1231
+ end
1232
+
1233
+ assert_equal params[:zone], entry['metadata']['zone']
1234
+ assert_equal params[:service_name], entry['metadata']['serviceName']
1235
+ check_labels entry, batch['commonLabels'], params[:labels]
1236
+ yield(entry) if block_given?
1237
+ i += 1
1238
+ assert i <= n, "Number of entries #{i} exceeds expected number #{n}"
1239
+ end
1240
+ end
1241
+ assert i == n, "Number of entries #{i} does not match expected number #{n}"
1242
+ end
1243
+
1244
+ # This module expects the methods below to be overridden.
1245
+
1246
+ # Create a Fluentd output test driver with the Google Cloud Output plugin.
1247
+ def create_driver(_conf = APPLICATION_DEFAULT_CONFIG, _tag = 'test')
1248
+ _undefined
1249
+ end
1250
+
1251
+ # Set up http or grpc stubs to mock the external calls.
1252
+ def setup_logging_stubs
1253
+ _undefined
1254
+ end
1255
+
1256
+ # Verify the number and the content of the log entries match the expectation.
1257
+ # The caller can optionally provide a block which is called for each entry.
1258
+ def verify_log_entries(_n, _params, _payload_type = 'textPayload', &_block)
1259
+ _undefined
1260
+ end
1261
+
1262
+ # For an optional field with default values, Protobuf omits the field when it
1263
+ # is deserialized to json. So we need to add an extra check for gRPC which
1264
+ # uses Protobuf.
1265
+ #
1266
+ # An optional block can be passed in if we need to assert something other than
1267
+ # a plain equal. e.g. assert_in_delta.
1268
+ def assert_equal_with_default(_field, _expected_value, _default_value, _entry)
1269
+ _undefined
1270
+ end
1271
+
1272
+ # A wrapper around the constant HTTP_REQUEST_MESSAGE, so the definition can be
1273
+ # skipped in the shared module here and defined in the test class later.
1274
+ def http_request_message
1275
+ _undefined
1276
+ end
1277
+
1278
+ # A wrapper around the constant HTTP_REQUEST_MESSAGE_WITHOUT_REFERER, so the
1279
+ # definition can be skipped in the shared module and defined in the test
1280
+ # classes later.
1281
+ def http_request_message_without_referer
1282
+ _undefined
1283
+ end
1284
+
1285
+ # Get the fields of the struct payload.
1286
+ def get_fields(_struct_payload)
1287
+ _undefined
1288
+ end
1289
+
1290
+ # Get the value of a struct field.
1291
+ def get_struct(_field)
1292
+ _undefined
1293
+ end
1294
+
1295
+ # Get the value of a string field.
1296
+ def get_string(_field)
1297
+ _undefined
1298
+ end
1299
+
1300
+ # Get the value of a number field.
1301
+ def get_number(_field)
1302
+ _undefined
1303
+ end
1304
+
1305
+ def _undefined
1306
+ fail "Method #{__callee__} is unimplemented and needs to be overridden."
1307
+ end
1308
+ end