fluent-plugin-datadog-log 0.1.0.rc1 → 0.1.0.rc2

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: c5156471f5c6f9fa323d2843a7ae8e291d639cc9
4
- data.tar.gz: f9f60fa52dc48eef464da9de7231253f996462b7
3
+ metadata.gz: 2ec628db3ffd3258f75a1ec766c763f3d4ad70ea
4
+ data.tar.gz: 9dc3f96c5ff194ab5cdb17e3e7ed1ac7ccfcecfd
5
5
  SHA512:
6
- metadata.gz: 5b359716b9eb9fbcda4024db5aa0376ba0655081d1def3d9b57a41cf74c35e6e93fcdf0b10d944c2a675f4102b597d2c645aa444d7dece4d92b02af0546c45cb
7
- data.tar.gz: 5fcfc226bd44d985616a4bc67e9e603f7ebde31f4ccd468e72cae2ec13dc054c71e5a5785a764f8a0f69ca13e10263e3df2f47b811c72a47568411f1a0a699fc
6
+ metadata.gz: 5a97cd2159cb172acd76cb1ed9141176cac53758dd2d384b6bcd7c77dacf0f45f37e81edc932b092e2c86847b65c3a552f8a2756057ae456364aacfafc3b1f6a
7
+ data.tar.gz: e26b6ef4808d0d76fc56f0d63b545f7df3497658ed9353fa7aa1cdb00d20f834227a825bb7e3c45f5dfe127a18bad7add8d55b258cca983c103f70a9fd092329
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fluent-plugin-datadog-log (0.1.0.rc1)
4
+ fluent-plugin-datadog-log (0.1.0.rc2)
5
5
  fluentd (~> 0.14)
6
6
  json (~> 1.8)
7
7
 
@@ -8,7 +8,7 @@ eos
8
8
  gem.homepage = \
9
9
  'https://github.com/mumoshu/fluent-plugin-datadog-log'
10
10
  gem.license = 'Apache-2.0'
11
- gem.version = '0.1.0.rc1'
11
+ gem.version = '0.1.0.rc2'
12
12
  gem.authors = ['Yusuke KUOKA']
13
13
  gem.email = ['ykuoka@gmail.com']
14
14
  gem.required_ruby_version = Gem::Requirement.new('>= 2.0')
@@ -8,7 +8,7 @@ eos
8
8
  gem.homepage = \
9
9
  'https://github.com/mumoshu/fluent-plugin-datadog-log'
10
10
  gem.license = 'Apache-2.0'
11
- gem.version = '0.1.0'
11
+ gem.version = '0.1.0.rc1'
12
12
  gem.authors = ['Yusuke KUOKA']
13
13
  gem.email = ['ykuoka@gmail.com']
14
14
  gem.required_ruby_version = Gem::Requirement.new('>= 2.0')
@@ -128,9 +128,9 @@ module Fluent::Plugin
128
128
  super
129
129
 
130
130
  if @api_key.size == 0
131
- @api_key = ENV['DD_LOG_API_KEY']
131
+ @api_key = ENV['DD_API_KEY']
132
132
  if @api_key == '' || @api_key.nil?
133
- error_message = 'Unable to obtain api_key from DD_LOG_API_KEY'
133
+ error_message = 'Unable to obtain api_key from DD_API_KEY'
134
134
  fail Fluent::ConfigError, error_message
135
135
  end
136
136
  end
@@ -249,10 +249,26 @@ module Fluent::Plugin
249
249
  tags << "#{tag_key}=#{kube[json_key]}" if kube.key? json_key
250
250
  end
251
251
 
252
- if kube.key? 'labels'
253
- labels = kube['labels']
254
- labels.each do |k, v|
255
- tags << "kube_#{k}=#{v}"
252
+ kube_labels = kube['labels']
253
+ unless kube_labels.nil?
254
+ kube_labels.each do |k, v|
255
+ k2 = k.dup
256
+ k2.gsub!(/[\,\.]/, '_')
257
+ k2.gsub!(%r{/}, '-')
258
+ tags << "kube_#{k2}=#{v}"
259
+ end
260
+ end
261
+
262
+ if kube.key? 'annotations'
263
+ annotations = kube['annotations']
264
+ created_by_str = annotations['kubernetes.io/created-by']
265
+ unless created_by_str.nil?
266
+ created_by = JSON.parse(created_by_str)
267
+ ref = created_by['reference'] unless created_by.nil?
268
+ kind = ref['kind'] unless ref.nil?
269
+ name = ref['name'] unless ref.nil?
270
+ kind = kind.downcase unless kind.nil?
271
+ tags << "kube_#{kind}=#{name}" if !kind.nil? && !name.nil?
256
272
  end
257
273
  end
258
274
 
@@ -264,6 +280,14 @@ module Fluent::Plugin
264
280
 
265
281
  tags.concat(@default_tags)
266
282
 
283
+ service = kube_labels['app'] || kube_labels['k8s-app'] unless kube_labels.nil?
284
+ source = kube['pod_name']
285
+ source_category = kube['container_name']
286
+
287
+ service = @service if service.nil?
288
+ source = @source if source.nil?
289
+ source_category = @source_category if source_category.nil?
290
+
267
291
  datetime = Time.at(Fluent::EventTime.new(time).to_r).utc.to_datetime
268
292
 
269
293
  payload =
@@ -271,9 +295,9 @@ module Fluent::Plugin
271
295
  logset: @logset,
272
296
  msg: msg,
273
297
  datetime: datetime,
274
- service: @service,
275
- source: @source,
276
- source_category: @source_category,
298
+ service: service,
299
+ source: source,
300
+ source_category: source_category,
277
301
  tags: tags
278
302
  )
279
303
 
@@ -290,12 +314,19 @@ module Fluent::Plugin
290
314
  end
291
315
 
292
316
  rescue => error
293
- increment_failed_requests_count
294
- increment_retried_entries_count(entries_count)
295
- # RPC cancelled, so retry via re-raising the error.
296
- @log.debug "Retrying #{entries_count} log message(s) later.",
297
- error: error.to_s
298
317
  raise error
318
+ increment_failed_requests_count
319
+ if entries_count.nil?
320
+ increment_dropped_entries_count(1)
321
+ @log.error 'Not retrying a log message later',
322
+ error: error.to_s
323
+ else
324
+ increment_retried_entries_count(entries_count)
325
+ # RPC cancelled, so retry via re-raising the error.
326
+ @log.debug "Retrying #{entries_count} log message(s) later.",
327
+ error: error.to_s
328
+ raise error
329
+ end
299
330
  end
300
331
  end
301
332
  end
@@ -0,0 +1,549 @@
1
+ # Copyright 2017 Yusuke KUOKA 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
+ require 'erb'
15
+ require 'json'
16
+ require 'open-uri'
17
+ require 'socket'
18
+ require 'time'
19
+ require 'yaml'
20
+ require 'fluent/plugin/output'
21
+ require 'datadog/log'
22
+
23
+ require_relative 'monitoring'
24
+
25
+ module Fluent::Plugin
26
+ # fluentd output plugin for the Datadog Log Intake API
27
+ class DatadogOutput < ::Fluent::Plugin::Output
28
+ Fluent::Plugin.register_output('datadog', self)
29
+
30
+ helpers :compat_parameters, :inject
31
+
32
+ include ::Datadog::Log
33
+
34
+ DEFAULT_BUFFER_TYPE = 'memory'
35
+
36
+ PLUGIN_NAME = 'Fluentd Datadog plugin'
37
+ PLUGIN_VERSION = '0.1.0'
38
+
39
+ # Address of the metadata service.
40
+ METADATA_SERVICE_ADDR = '169.254.169.254'
41
+
42
+ # Disable this warning to conform to fluentd config_param conventions.
43
+ # rubocop:disable Style/HashSyntax
44
+
45
+ # see https://github.com/DataDog/datadog-log-agent/blob/db13b53dfdd036d43acfb15089a43eb31548f09f/pkg/logagent/logsagent.go#L26-L30
46
+ # see https://github.com/DataDog/datadog-log-agent/blob/db13b53dfdd036d43acfb15089a43eb31548f09f/pkg/config/config.go#L52-L56
47
+ config_param :log_dd_url, :string, default: 'intake.logs.datadoghq.com'
48
+ config_param :log_dd_port, :integer, default: 10516
49
+ config_param :skip_ssl_validation, default: false
50
+ config_param :api_key, :string, default: ''
51
+ config_param :logset, :string, default: 'main'
52
+
53
+ # e.g. ['env:prod', 'app:myapp']
54
+ # see https://github.com/DataDog/datadog-log-agent/blob/db13b53dfdd036d43acfb15089a43eb31548f09f/pkg/logagent/etc/conf.d/integration.yaml.example
55
+ config_param :tags, :array, default: [], value_type: :string
56
+ config_param :service, :string, default: '-'
57
+ # e.g. 'nginx'
58
+ config_param :source, :string, default: ''
59
+ config_param :source_category, :string, default: ''
60
+
61
+ config_section :buffer do
62
+ config_set_default :@type, DEFAULT_BUFFER_TYPE
63
+ end
64
+
65
+ # e.g. 'http_access'
66
+ # config_param :source_category, :string, default: ''
67
+
68
+ # Specify project/instance metadata.
69
+ #
70
+ # project_id, zone, and vm_id are required to have valid values, which
71
+ # can be obtained from the metadata service or set explicitly.
72
+ # Otherwise, the plugin will fail to initialize.
73
+ #
74
+ # Note that while 'project id' properly refers to the alphanumeric name
75
+ # of the project, the logging service will also accept the project number,
76
+ # so either one is acceptable in this context.
77
+ #
78
+ # Whether to attempt to obtain metadata from the local metadata service.
79
+ # It is safe to specify 'true' even on platforms with no metadata service.
80
+ config_param :use_metadata_service, :bool, :default => true
81
+ # These parameters override any values obtained from the metadata service.
82
+ config_param :project_id, :string, :default => nil
83
+ config_param :zone, :string, :default => nil
84
+ config_param :vm_id, :string, :default => nil
85
+ config_param :vm_name, :string, :default => nil
86
+
87
+ # TODO: Correlate log messages to corresponding Datadog APM spans
88
+ # config_param :trace_key, :string, :default => DEFAULT_TRACE_KEY
89
+
90
+ # Whether to try to detect if the record is a text log entry with JSON
91
+ # content that needs to be parsed.
92
+ config_param :detect_json, :bool, :default => false
93
+
94
+ # Whether to reject log entries with invalid tags. If this option is set to
95
+ # false, tags will be made valid by converting any non-string tag to a
96
+ # string, and sanitizing any non-utf8 or other invalid characters.
97
+ config_param :require_valid_tags, :bool, :default => false
98
+
99
+ # Whether to allow non-UTF-8 characters in user logs. If set to true, any
100
+ # non-UTF-8 character would be replaced by the string specified by
101
+ # 'non_utf8_replacement_string'. If set to false, any non-UTF-8 character
102
+ # would trigger the plugin to error out.
103
+ config_param :coerce_to_utf8, :bool, :default => true
104
+
105
+ # If 'coerce_to_utf8' is set to true, any non-UTF-8 character would be
106
+ # replaced by the string specified here.
107
+ config_param :non_utf8_replacement_string, :string, :default => ' '
108
+
109
+ # Whether to collect metrics about the plugin usage. The mechanism for
110
+ # collecting and exposing metrics is controlled by the monitoring_type
111
+ # parameter.
112
+ config_param :enable_monitoring, :bool, :default => false
113
+ config_param :monitoring_type, :string, :default => 'prometheus'
114
+
115
+ # rubocop:enable Style/HashSyntax
116
+
117
+ attr_reader :zone
118
+ attr_reader :vm_id
119
+
120
+ def initialize
121
+ super
122
+ # use the global logger
123
+ @log = $log # rubocop:disable Style/GlobalVars
124
+ end
125
+
126
+ def configure(conf)
127
+ compat_parameters_convert(conf, :buffer, :inject)
128
+ super
129
+
130
+ if @api_key.size == 0
131
+ @api_key = ENV['DD_LOG_API_KEY']
132
+ if @api_key == '' || @api_key.nil?
133
+ error_message = 'Unable to obtain api_key from DD_LOG_API_KEY'
134
+ fail Fluent::ConfigError, error_message
135
+ end
136
+ end
137
+
138
+ # If monitoring is enabled, register metrics in the default registry
139
+ # and store metric objects for future use.
140
+ if @enable_monitoring
141
+ registry = Monitoring::MonitoringRegistryFactory.create @monitoring_type
142
+ @successful_requests_count = registry.counter(
143
+ :datadog_successful_requests_count,
144
+ 'A number of successful requests to the Datadog Log Intake API')
145
+ @failed_requests_count = registry.counter(
146
+ :datadog_failed_requests_count,
147
+ 'A number of failed requests to the Datadog Log Intake API,'\
148
+ ' broken down by the error code')
149
+ @ingested_entries_count = registry.counter(
150
+ :datadog_ingested_entries_count,
151
+ 'A number of log entries ingested by Datadog Log Intake')
152
+ @dropped_entries_count = registry.counter(
153
+ :datadog_dropped_entries_count,
154
+ 'A number of log entries dropped by the Stackdriver output plugin')
155
+ @retried_entries_count = registry.counter(
156
+ :datadog_retried_entries_count,
157
+ 'The number of log entries that failed to be ingested by the'\
158
+ ' Stackdriver output plugin due to a transient error and were'\
159
+ ' retried')
160
+ end
161
+
162
+ @platform = detect_platform
163
+
164
+ # Set required variables: @project_id, @vm_id, @vm_name and @zone.
165
+ set_required_metadata_variables
166
+
167
+ @default_tags = build_default_tags
168
+
169
+ # The resource and labels are now set up; ensure they can't be modified
170
+ # without first duping them.
171
+ @default_tags.freeze
172
+
173
+ # Log an informational message containing the Logs viewer URL
174
+ @log.info 'Logs viewer address: https://example.com/logs/'
175
+ end
176
+
177
+ def start
178
+ super
179
+ init_api_client
180
+ @successful_call = false
181
+ @timenanos_warning = false
182
+ end
183
+
184
+ def shutdown
185
+ super
186
+ @conn.shutdown
187
+ end
188
+
189
+ def format(tag, time, record)
190
+ record = inject_values_to_record(tag, time, record)
191
+ [tag, time, record].to_msgpack
192
+ end
193
+
194
+ def formatted_to_msgpack_binary?
195
+ true
196
+ end
197
+
198
+ def multi_workers_ready?
199
+ true
200
+ end
201
+
202
+ def write(chunk)
203
+ each_valid_record(chunk) do |_tag, time, record|
204
+ if @detect_json
205
+ # Save the timestamp and severity if available, then clear it out to
206
+ # allow for determining whether we should parse the log or message
207
+ # field.
208
+ timestamp = record.delete('time')
209
+ severity = record.delete('severity')
210
+
211
+ # If the log is json, we want to export it as a structured log
212
+ # unless there is additional metadata that would be lost.
213
+ record_json = nil
214
+ if record.length == 1
215
+ %w(log message msg).each do |field|
216
+ if record.key?(field)
217
+ record_json = parse_json_or_nil(record[field])
218
+ end
219
+ end
220
+ end
221
+ record = record_json unless record_json.nil?
222
+ # Restore timestamp and severity if necessary. Note that we don't
223
+ # want to override these keys in the JSON we've just parsed.
224
+ record['time'] ||= timestamp if timestamp
225
+ record['severity'] ||= severity if severity
226
+ end
227
+
228
+ # TODO: Correlate Datadog APM spans with log messages
229
+ # fq_trace_id = record.delete(@trace_key)
230
+ # entry.trace = fq_trace_id if fq_trace_id
231
+
232
+ begin
233
+ msg = nil
234
+ %w(log message msg).each do |field|
235
+ msg = record[field] if record.key?(field)
236
+ end
237
+
238
+ tags = []
239
+
240
+ kube = record['kubernetes'] || {}
241
+
242
+ mappings = {
243
+ 'pod_name' => 'pod_name',
244
+ 'container_name' => 'container_name',
245
+ 'namespace_name' => 'kube_namespace'
246
+ }
247
+
248
+ mappings.each do |json_key, tag_key|
249
+ tags << "#{tag_key}=#{kube[json_key]}" if kube.key? json_key
250
+ end
251
+
252
+ if kube.key? 'labels'
253
+ labels = kube['labels']
254
+ labels.each do |k, v|
255
+ tags << "kube_#{k}=#{v}"
256
+ end
257
+ end
258
+
259
+ # TODO: Include K8S tags like
260
+ # - kube_daemon_set=$daemonset_name
261
+ # - kube_deployment=$deployment_name
262
+ # - kube_replica_set=$replicaset_name
263
+ # -
264
+
265
+ tags.concat(@default_tags)
266
+
267
+ datetime = Time.at(Fluent::EventTime.new(time).to_r).utc.to_datetime
268
+
269
+ payload =
270
+ @conn.send_payload(
271
+ logset: @logset,
272
+ msg: msg,
273
+ datetime: datetime,
274
+ service: @service,
275
+ source: @source,
276
+ source_category: @source_category,
277
+ tags: tags
278
+ )
279
+
280
+ entries_count = 1
281
+ @log.debug 'Sent payload to Datadog.', payload: payload
282
+ increment_successful_requests_count
283
+ increment_ingested_entries_count(entries_count)
284
+
285
+ # Let the user explicitly know when the first call succeeded, to aid
286
+ # with verification and troubleshooting.
287
+ unless @successful_call
288
+ @successful_call = true
289
+ @log.info 'Successfully sent to Datadog.'
290
+ end
291
+
292
+ rescue => error
293
+ increment_failed_requests_count
294
+ increment_retried_entries_count(entries_count)
295
+ # RPC cancelled, so retry via re-raising the error.
296
+ @log.debug "Retrying #{entries_count} log message(s) later.",
297
+ error: error.to_s
298
+ raise error
299
+ end
300
+ end
301
+ end
302
+
303
+ private
304
+
305
+ def init_api_client
306
+ @conn = ::Datadog::Log::Client.new(
307
+ log_dd_url: @log_dd_uri,
308
+ log_dd_port: @log_dd_port,
309
+ api_key: @api_key,
310
+ hostname: @vm_id,
311
+ skip_ssl_validation: @skip_ssl_validation
312
+ )
313
+ end
314
+
315
+ def parse_json_or_nil(input)
316
+ # Only here to please rubocop...
317
+ return nil if input.nil?
318
+
319
+ input.each_codepoint do |c|
320
+ if c == 123
321
+ # left curly bracket (U+007B)
322
+ begin
323
+ return JSON.parse(input)
324
+ rescue JSON::ParserError
325
+ return nil
326
+ end
327
+ else
328
+ # Break (and return nil) unless the current character is whitespace,
329
+ # in which case we continue to look for a left curly bracket.
330
+ # Whitespace as per the JSON spec are: tabulation (U+0009),
331
+ # line feed (U+000A), carriage return (U+000D), and space (U+0020).
332
+ break unless c == 9 || c == 10 || c == 13 || c == 32
333
+ end # case
334
+ end # do
335
+ nil
336
+ end
337
+
338
+ # "enum" of Platform values
339
+ module Platform
340
+ OTHER = 0 # Other/unkown platform
341
+ GCE = 1 # Google Compute Engine
342
+ EC2 = 2 # Amazon EC2
343
+ end
344
+
345
+ # Determine what platform we are running on by consulting the metadata
346
+ # service (unless the user has explicitly disabled using that).
347
+ def detect_platform
348
+ unless @use_metadata_service
349
+ @log.info 'use_metadata_service is false; not detecting platform'
350
+ return Platform::OTHER
351
+ end
352
+
353
+ begin
354
+ open('http://' + METADATA_SERVICE_ADDR) do |f|
355
+ if f.meta['metadata-flavor'] == 'Google'
356
+ @log.info 'Detected GCE platform'
357
+ return Platform::GCE
358
+ end
359
+ if f.meta['server'] == 'EC2ws'
360
+ @log.info 'Detected EC2 platform'
361
+ return Platform::EC2
362
+ end
363
+ end
364
+ rescue StandardError => e
365
+ @log.error 'Failed to access metadata service: ', error: e
366
+ end
367
+
368
+ @log.info 'Unable to determine platform'
369
+ Platform::OTHER
370
+ end
371
+
372
+ def fetch_gce_metadata(metadata_path)
373
+ fail "Called fetch_gce_metadata with platform=#{@platform}" unless
374
+ @platform == Platform::GCE
375
+ # See https://cloud.google.com/compute/docs/metadata
376
+ open('http://' + METADATA_SERVICE_ADDR + '/computeMetadata/v1/' +
377
+ metadata_path, 'Metadata-Flavor' => 'Google', &:read)
378
+ end
379
+
380
+ # EC2 Metadata server returns everything in one call. Store it after the
381
+ # first fetch to avoid making multiple calls.
382
+ def ec2_metadata
383
+ fail "Called ec2_metadata with platform=#{@platform}" unless
384
+ @platform == Platform::EC2
385
+ unless @ec2_metadata
386
+ # See http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html
387
+ open('http://' + METADATA_SERVICE_ADDR +
388
+ '/latest/dynamic/instance-identity/document') do |f|
389
+ contents = f.read
390
+ @ec2_metadata = JSON.parse(contents)
391
+ end
392
+ end
393
+
394
+ @ec2_metadata
395
+ end
396
+
397
+ # Set required variables like @vm_id, @vm_name and @zone.
398
+ def set_required_metadata_variables
399
+ set_vm_id
400
+ set_vm_name
401
+ set_zone
402
+
403
+ # All metadata parameters must now be set.
404
+ missing = []
405
+ missing << 'zone' unless @zone
406
+ missing << 'vm_id' unless @vm_id
407
+ missing << 'vm_name' unless @vm_name
408
+ return if missing.empty?
409
+ fail Fluent::ConfigError, 'Unable to obtain metadata parameters: ' +
410
+ missing.join(' ')
411
+ end
412
+
413
+ # 1. Return the value if it is explicitly set in the config already.
414
+ # 2. If not, try to retrieve it by calling metadata servers directly.
415
+ def set_vm_id
416
+ @vm_id ||= ec2_metadata['instanceId'] if @platform == Platform::EC2
417
+ rescue StandardError => e
418
+ @log.error 'Failed to obtain vm_id: ', error: e
419
+ end
420
+
421
+ # 1. Return the value if it is explicitly set in the config already.
422
+ # 2. If not, try to retrieve it locally.
423
+ def set_vm_name
424
+ @vm_name ||= Socket.gethostname
425
+ rescue StandardError => e
426
+ @log.error 'Failed to obtain vm name: ', error: e
427
+ end
428
+
429
+ # 1. Return the value if it is explicitly set in the config already.
430
+ # 2. If not, try to retrieve it locally.
431
+ def set_zone
432
+ @zone ||= 'aws:' + ec2_metadata['availabilityZone'] if
433
+ @platform == Platform::EC2 && ec2_metadata.key?('availabilityZone')
434
+ rescue StandardError => e
435
+ @log.error 'Failed to obtain location: ', error: e
436
+ end
437
+
438
+ # Determine agent level monitored resource labels based on the resource
439
+ # type. Each resource type has its own labels that need to be filled in.
440
+ def build_default_tags
441
+ aws_account_id = ec2_metadata['accountId'] if
442
+ ec2_metadata.key?('accountId')
443
+ # #host:i-09fbfed2672d2c6bf
444
+ %W(host=#{@vm_id} zone=#{@zone} aws_account_id=#{aws_account_id})
445
+ .concat @tags
446
+ end
447
+
448
+ # Filter out invalid non-Hash entries.
449
+ def each_valid_record(chunk)
450
+ chunk.msgpack_each do |event|
451
+ record = event.last
452
+ unless record.is_a?(Hash)
453
+ @log.warn 'Dropping log entries with malformed record: ' \
454
+ "'#{record.inspect}'. " \
455
+ 'A log record should be in JSON format.'
456
+ next
457
+ end
458
+ tag = record.first
459
+ sanitized_tag = sanitize_tag(tag)
460
+ if sanitized_tag.nil?
461
+ @log.warn "Dropping log entries with invalid tag: '#{tag.inspect}'." \
462
+ ' A tag should be a string with utf8 characters.'
463
+ next
464
+ end
465
+ yield event
466
+ end
467
+ end
468
+
469
+ # Given a tag, returns the corresponding valid tag if possible, or nil if
470
+ # the tag should be rejected. If 'require_valid_tags' is false, non-string
471
+ # tags are converted to strings, and invalid characters are sanitized;
472
+ # otherwise such tags are rejected.
473
+ def sanitize_tag(tag)
474
+ if @require_valid_tags &&
475
+ (!tag.is_a?(String) || tag == '' || convert_to_utf8(tag) != tag)
476
+ return nil
477
+ end
478
+ tag = convert_to_utf8(tag.to_s)
479
+ tag = '_' if tag == ''
480
+ tag
481
+ end
482
+
483
+ # Encode as UTF-8. If 'coerce_to_utf8' is set to true in the config, any
484
+ # non-UTF-8 character would be replaced by the string specified by
485
+ # 'non_utf8_replacement_string'. If 'coerce_to_utf8' is set to false, any
486
+ # non-UTF-8 character would trigger the plugin to error out.
487
+ def convert_to_utf8(input)
488
+ if @coerce_to_utf8
489
+ input.encode(
490
+ 'utf-8',
491
+ invalid: :replace,
492
+ undef: :replace,
493
+ replace: @non_utf8_replacement_string)
494
+ else
495
+ begin
496
+ input.encode('utf-8')
497
+ rescue EncodingError
498
+ @log.error 'Encountered encoding issues potentially due to non ' \
499
+ 'UTF-8 characters. To allow non-UTF-8 characters and ' \
500
+ 'replace them with spaces, please set "coerce_to_utf8" ' \
501
+ 'to true.'
502
+ raise
503
+ end
504
+ end
505
+ end
506
+
507
+ def ensure_array(value)
508
+ Array.try_convert(value) || (fail JSON::ParserError, "#{value.class}")
509
+ end
510
+
511
+ def ensure_hash(value)
512
+ Hash.try_convert(value) || (fail JSON::ParserError, "#{value.class}")
513
+ end
514
+
515
+ # Increment the metric for the number of successful requests.
516
+ def increment_successful_requests_count
517
+ return unless @successful_requests_count
518
+ @successful_requests_count.increment
519
+ end
520
+
521
+ # Increment the metric for the number of failed requests, labeled by
522
+ # the provided status code.
523
+ def increment_failed_requests_count
524
+ return unless @failed_requests_count
525
+ @failed_requests_count.increment
526
+ end
527
+
528
+ # Increment the metric for the number of log entries, successfully
529
+ # ingested by the Datadog Log Intake API.
530
+ def increment_ingested_entries_count(count)
531
+ return unless @ingested_entries_count
532
+ @ingested_entries_count.increment({}, count)
533
+ end
534
+
535
+ # Increment the metric for the number of log entries that were dropped
536
+ # and not ingested by the Datadog Log Intake API.
537
+ def increment_dropped_entries_count(count)
538
+ return unless @dropped_entries_count
539
+ @dropped_entries_count.increment({}, count)
540
+ end
541
+
542
+ # Increment the metric for the number of log entries that were dropped
543
+ # and not ingested by the Datadog Log Intake API.
544
+ def increment_retried_entries_count(count)
545
+ return unless @retried_entries_count
546
+ @retried_entries_count.increment({}, count)
547
+ end
548
+ end
549
+ end
@@ -35,7 +35,70 @@ class DatadogLogOutputTest < Test::Unit::TestCase
35
35
  end
36
36
  end
37
37
 
38
+ def test_configure_with_env
39
+ new_stub_context do
40
+ setup_ec2_metadata_stubs
41
+
42
+ ENV.stubs(:[])
43
+ .with('DD_API_KEY')
44
+ .returns('myapikey_from_env')
45
+
46
+ ENV.stubs(:[])
47
+ .with(Not equals 'DD_API_KEY')
48
+ .returns('')
49
+ .times(3)
50
+
51
+ d = create_driver(<<-EOC)
52
+ type datadog_log
53
+ service myservice
54
+ source mysource
55
+ EOC
56
+
57
+ assert_equal 'myapikey_from_env', d.instance.api_key
58
+ assert_equal 'myservice', d.instance.service
59
+ assert_equal 'mysource', d.instance.source
60
+ end
61
+ end
62
+
38
63
  def test_write
64
+ new_stub_context do
65
+ setup_ec2_metadata_stubs
66
+
67
+ timestamp_str = '2006-01-02T15:04:05.000000+00:00'
68
+ t = DateTime.rfc3339(timestamp_str).to_time
69
+ time = Fluent::EventTime.from_time(t)
70
+ d = create_driver(<<-EOC)
71
+ type datadog_log
72
+ api_key myapikey
73
+ service myservice
74
+ source mysource
75
+ source_category mysourcecategory
76
+ logset mylogset
77
+ log_level debug
78
+ EOC
79
+ conn = StubConn.new
80
+ fluentd_tag = 'mytag'
81
+ Net::TCPClient.stubs(:new)
82
+ .with(server: ':10516', ssl: true)
83
+ .returns(conn)
84
+ d.run(default_tag: fluentd_tag) do
85
+ record = {
86
+ 'log' => 'mymsg'
87
+ }
88
+ d.feed(time, record)
89
+ end
90
+
91
+ # fail d.logs.inspect
92
+ assert_equal(1, d.logs.count { |l| l =~ /Sent payload to Datadog/ })
93
+ assert_equal(1, conn.sent.size)
94
+ # rubocop:disable LineLength
95
+ payload = %(myapikey/mylogset <46>0 2006-01-02T15:04:05.000000+00:00 i-81c16767 myservice - - [dd ddsource="mysource"][dd ddsourcecategory="mysourcecategory"][dd ddtags="host=i-81c16767,zone=aws:us-west-2b,aws_account_id=123456789012"] mymsg\n)
96
+ # rubocop:enable LineLength
97
+ assert_equal(payload, conn.sent.first)
98
+ end
99
+ end
100
+
101
+ def test_write_kube
39
102
  new_stub_context do
40
103
  setup_ec2_metadata_stubs
41
104
 
@@ -59,12 +122,20 @@ class DatadogLogOutputTest < Test::Unit::TestCase
59
122
  d.run(default_tag: fluentd_tag) do
60
123
  record = {
61
124
  'log' => 'mymsg',
125
+ 'docker' => {
126
+ 'container_id' => 'myfullcontainerid'
127
+ },
62
128
  'kubernetes' => {
63
129
  'namespace' => 'myns',
64
130
  'pod_name' => 'mypod',
65
131
  'container_name' => 'mycontainer',
66
132
  'labels' => {
67
133
  'k8s-app' => 'myapp'
134
+ },
135
+ 'annotations' => {
136
+ # rubocop:disable LineLength
137
+ 'kubernetes.io/created-by' => '{"kind":"SerializedReference","apiVersion":"v1","reference":{"kind":"Deployment","namespace":"default","name":"myapp","uid":"d67e8857-c2dc-11e7-aed9-066d23381f8c","apiVersion":"extensions","resourceVersion":"289"}}'
138
+ # rubocop:enable LineLength
68
139
  }
69
140
  }
70
141
  }
@@ -75,7 +146,7 @@ class DatadogLogOutputTest < Test::Unit::TestCase
75
146
  assert_equal(1, d.logs.count { |l| l =~ /Sent payload to Datadog/ })
76
147
  assert_equal(1, conn.sent.size)
77
148
  # rubocop:disable LineLength
78
- payload = %(myapikey/mylogset <46>0 2006-01-02T15:04:05.000000+00:00 i-81c16767 myservice - - [dd ddsource="mysource"][dd ddsourcecategory="mysourcecategory"][dd ddtags="pod_name=mypod,container_name=mycontainer,kube_k8s-app=myapp,host=i-81c16767,zone=aws:us-west-2b,aws_account_id=123456789012"] mymsg\n)
149
+ payload = %(myapikey/mylogset <46>0 2006-01-02T15:04:05.000000+00:00 i-81c16767 myapp - - [dd ddsource="mypod"][dd ddsourcecategory="mycontainer"][dd ddtags="pod_name=mypod,container_name=mycontainer,kube_k8s-app=myapp,kube_deployment=myapp,host=i-81c16767,zone=aws:us-west-2b,aws_account_id=123456789012"] mymsg\n)
79
150
  # rubocop:enable LineLength
80
151
  assert_equal(payload, conn.sent.first)
81
152
  end
@@ -0,0 +1,206 @@
1
+ # Copyright 2017 Yusuke KUOKA 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_relative 'base_test'
16
+
17
+ # Unit tests for Datadog Log plugin
18
+ class DatadogLogOutputTest < Test::Unit::TestCase
19
+ include BaseTest
20
+
21
+ def test_configure
22
+ new_stub_context do
23
+ setup_ec2_metadata_stubs
24
+
25
+ d = create_driver(<<-EOC)
26
+ type datadog_log
27
+ api_key myapikey
28
+ service myservice
29
+ source mysource
30
+ EOC
31
+
32
+ assert_equal 'myapikey', d.instance.api_key
33
+ assert_equal 'myservice', d.instance.service
34
+ assert_equal 'mysource', d.instance.source
35
+ end
36
+ end
37
+
38
+ def test_write
39
+ new_stub_context do
40
+ setup_ec2_metadata_stubs
41
+
42
+ timestamp_str = '2006-01-02T15:04:05.000000+00:00'
43
+ t = DateTime.rfc3339(timestamp_str).to_time
44
+ time = Fluent::EventTime.from_time(t)
45
+ d = create_driver(<<-EOC)
46
+ type datadog_log
47
+ api_key myapikey
48
+ service myservice
49
+ source mysource
50
+ source_category mysourcecategory
51
+ logset mylogset
52
+ log_level debug
53
+ EOC
54
+ conn = StubConn.new
55
+ fluentd_tag = 'mytag'
56
+ Net::TCPClient.stubs(:new)
57
+ .with(server: ':10516', ssl: true)
58
+ .returns(conn)
59
+ d.run(default_tag: fluentd_tag) do
60
+ record = {
61
+ 'log' => 'mymsg',
62
+ 'kubernetes' => {
63
+ 'namespace' => 'myns',
64
+ 'pod_name' => 'mypod',
65
+ 'container_name' => 'mycontainer',
66
+ 'labels' => {
67
+ 'k8s-app' => 'myapp'
68
+ }
69
+ }
70
+ }
71
+ d.feed(time, record)
72
+ end
73
+
74
+ # fail d.logs.inspect
75
+ assert_equal(1, d.logs.count { |l| l =~ /Sent payload to Datadog/ })
76
+ assert_equal(1, conn.sent.size)
77
+ # rubocop:disable LineLength
78
+ payload = %(myapikey/mylogset <46>0 2006-01-02T15:04:05.000000+00:00 i-81c16767 myservice - - [dd ddsource="mysource"][dd ddsourcecategory="mysourcecategory"][dd ddtags="pod_name=mypod,container_name=mycontainer,kube_k8s-app=myapp,host=i-81c16767,zone=aws:us-west-2b,aws_account_id=123456789012"] mymsg\n)
79
+ # rubocop:enable LineLength
80
+ assert_equal(payload, conn.sent.first)
81
+ end
82
+ end
83
+
84
+ def test_prometheus_metrics
85
+ new_stub_context do
86
+ setup_ec2_metadata_stubs
87
+ timestamp_str = '2006-01-02T15:04:05.000000+00:00'
88
+ t = DateTime.rfc3339(timestamp_str).to_time
89
+ time = Fluent::EventTime.from_time(t)
90
+ [
91
+ # Single successful request.
92
+ [false, 0, 1, 1, [1, 0, 1, 0, 0]],
93
+ # Several successful requests.
94
+ [false, 0, 2, 1, [2, 0, 2, 0, 0]]
95
+ ].each do |_should_fail, _code, request_count, entry_count, metric_values|
96
+ setup_prometheus
97
+ (1..request_count).each do
98
+ d = create_driver(<<-EOC)
99
+ type datadog_log
100
+ api_key myapikey
101
+ service myservice
102
+ source mysource
103
+ source_category mysourcecategory
104
+ logset mylogset
105
+ log_level debug
106
+ enable_monitoring true
107
+ EOC
108
+ conn = StubConn.new
109
+ Net::TCPClient.stubs(:new)
110
+ .with(server: ':10516', ssl: true)
111
+ .returns(conn)
112
+ d.run(default_tag: 'mytag') do
113
+ (1..entry_count).each do |i|
114
+ d.feed time, 'message' => log_entry(i.to_s)
115
+ end
116
+ end
117
+ end
118
+ successful_requests_count, failed_requests_count,
119
+ ingested_entries_count, dropped_entries_count,
120
+ retried_entries_count = metric_values
121
+ assert_prometheus_metric_value(:datadog_successful_requests_count,
122
+ successful_requests_count)
123
+ assert_prometheus_metric_value(:datadog_failed_requests_count,
124
+ failed_requests_count)
125
+ assert_prometheus_metric_value(:datadog_ingested_entries_count,
126
+ ingested_entries_count)
127
+ assert_prometheus_metric_value(:datadog_dropped_entries_count,
128
+ dropped_entries_count)
129
+ assert_prometheus_metric_value(:datadog_retried_entries_count,
130
+ retried_entries_count)
131
+ end
132
+ end
133
+ end
134
+
135
+ def test_struct_payload_non_utf8_log
136
+ # d.emit('msg' => log_entry(0),
137
+ # 'normal_key' => "test#{non_utf8_character}non utf8",
138
+ # "non_utf8#{non_utf8_character}key" => 5000,
139
+ # 'nested_struct' => { "non_utf8#{non_utf8_character}key" => \
140
+ # "test#{non_utf8_character}non utf8" },
141
+ # 'null_field' => nil)
142
+ end
143
+
144
+ class StubConn
145
+ attr_reader :sent
146
+
147
+ def initialize
148
+ @sent = []
149
+ end
150
+
151
+ def write(payload)
152
+ @sent << payload
153
+ end
154
+
155
+ def close
156
+ end
157
+ end
158
+
159
+ private
160
+
161
+ # Use the right single quotation mark as the sample non-utf8 character.
162
+ def non_utf8_character
163
+ [0x92].pack('C*')
164
+ end
165
+
166
+ # For an optional field with default values, Protobuf omits the field when it
167
+ # is deserialized to json. So we need to add an extra check for gRPC which
168
+ # uses Protobuf.
169
+ #
170
+ # An optional block can be passed in if we need to assert something other than
171
+ # a plain equal. e.g. assert_in_delta.
172
+ def assert_equal_with_default(field, expected_value, default_value, entry)
173
+ if expected_value == default_value
174
+ assert_nil field
175
+ elsif block_given?
176
+ yield
177
+ else
178
+ assert_equal expected_value, field, entry
179
+ end
180
+ end
181
+
182
+ # Get the fields of the payload.
183
+ def get_fields(payload)
184
+ payload['fields']
185
+ end
186
+
187
+ # Get the value of a struct field.
188
+ def get_struct(field)
189
+ field['structValue']
190
+ end
191
+
192
+ # Get the value of a string field.
193
+ def get_string(field)
194
+ field['stringValue']
195
+ end
196
+
197
+ # Get the value of a number field.
198
+ def get_number(field)
199
+ field['numberValue']
200
+ end
201
+
202
+ # The null value.
203
+ def null_value
204
+ { 'nullValue' => 'NULL_VALUE' }
205
+ end
206
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fluent-plugin-datadog-log
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.rc1
4
+ version: 0.1.0.rc2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yusuke KUOKA
@@ -156,12 +156,16 @@ files:
156
156
  - lib/datadog/log.rb
157
157
  - lib/fluent/plugin/monitoring.rb
158
158
  - lib/fluent/plugin/out_datadog_log.rb
159
+ - lib/fluent/plugin/out_datadog_log.rb~
159
160
  - pkg/fluent-plugin-datadog-0.1.0.gem
160
161
  - pkg/fluent-plugin-datadog-log-0.1.0.gem
162
+ - pkg/fluent-plugin-datadog-log-0.1.0.rc1.gem
163
+ - pkg/fluent-plugin-datadog-log-0.1.0.rc2.gem
161
164
  - test/helper.rb
162
165
  - test/plugin/base_test.rb
163
166
  - test/plugin/constants.rb
164
167
  - test/plugin/test_out_datadog_log.rb
168
+ - test/plugin/test_out_datadog_log.rb~
165
169
  homepage: https://github.com/mumoshu/fluent-plugin-datadog-log
166
170
  licenses:
167
171
  - Apache-2.0
@@ -191,3 +195,4 @@ test_files:
191
195
  - test/plugin/base_test.rb
192
196
  - test/plugin/constants.rb
193
197
  - test/plugin/test_out_datadog_log.rb
198
+ - test/plugin/test_out_datadog_log.rb~