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

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