fluent-plugin-datadog-log 0.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,173 @@
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 'helper'
16
+ require 'mocha/test_unit'
17
+ require 'webmock/test_unit'
18
+ require 'prometheus/client'
19
+ require 'fluent/test/driver/output'
20
+ require 'fluent/test/helpers'
21
+
22
+ require_relative 'constants'
23
+
24
+ # Unit tests for Datadog plugin
25
+ module BaseTest
26
+ include Constants
27
+ include Fluent::Test::Helpers
28
+
29
+ def setup
30
+ Fluent::Test.setup
31
+ require 'fluent/plugin/out_datadog_log'
32
+ @logs_sent = []
33
+ end
34
+
35
+ def create_driver(conf = APPLICATION_DEFAULT_CONFIG)
36
+ Fluent::Test::Driver::Output.new(Fluent::Plugin::DatadogOutput)
37
+ .configure(conf)
38
+ end
39
+
40
+ private
41
+
42
+ def setup_no_metadata_service_stubs
43
+ # Simulate a machine with no metadata service present
44
+ stub_request(:any, %r{http://169.254.169.254/.*})
45
+ .to_raise(Errno::EHOSTUNREACH)
46
+ end
47
+
48
+ def setup_ec2_metadata_stubs
49
+ # Stub the root, used for platform detection.
50
+ stub_request(:get, 'http://169.254.169.254')
51
+ .to_return(status: 200, headers: { 'Server' => 'EC2ws' })
52
+
53
+ # Stub the identity document lookup made by the agent.
54
+ stub_request(:get, 'http://169.254.169.254/latest/dynamic/' \
55
+ 'instance-identity/document')
56
+ .to_return(body: EC2_IDENTITY_DOCUMENT, status: 200,
57
+ headers: { 'Content-Length' => EC2_IDENTITY_DOCUMENT.length })
58
+ end
59
+
60
+ def setup_logging_stubs
61
+ yield
62
+ end
63
+
64
+ def setup_prometheus
65
+ Prometheus::Client.registry.instance_variable_set('@metrics', {})
66
+ end
67
+
68
+ # Provide a stub context that initializes @logs_sent, executes the block and
69
+ # resets WebMock at the end.
70
+ def new_stub_context
71
+ @logs_sent = []
72
+ yield
73
+ WebMock.reset!
74
+ end
75
+
76
+ # Container.
77
+
78
+ def container_tag_with_container_name(container_name)
79
+ "kubernetes.#{CONTAINER_POD_NAME}_#{CONTAINER_NAMESPACE_NAME}_" \
80
+ "#{container_name}"
81
+ end
82
+
83
+ def container_log_entry_with_metadata(
84
+ log, container_name = CONTAINER_CONTAINER_NAME)
85
+ {
86
+ log: log,
87
+ stream: CONTAINER_STREAM,
88
+ time: CONTAINER_TIMESTAMP,
89
+ kubernetes: {
90
+ namespace_id: CONTAINER_NAMESPACE_ID,
91
+ namespace_name: CONTAINER_NAMESPACE_NAME,
92
+ pod_id: CONTAINER_POD_ID,
93
+ pod_name: CONTAINER_POD_NAME,
94
+ container_name: container_name,
95
+ labels: {
96
+ CONTAINER_LABEL_KEY => CONTAINER_LABEL_VALUE
97
+ }
98
+ }
99
+ }
100
+ end
101
+
102
+ def container_log_entry(log, stream = CONTAINER_STREAM)
103
+ {
104
+ log: log,
105
+ stream: stream,
106
+ time: CONTAINER_TIMESTAMP
107
+ }
108
+ end
109
+
110
+ # Docker Container.
111
+
112
+ def docker_container_stdout_stderr_log_entry(
113
+ log, stream = DOCKER_CONTAINER_STREAM_STDOUT)
114
+ severity = if stream == DOCKER_CONTAINER_STREAM_STDOUT
115
+ 'INFO'
116
+ else
117
+ 'ERROR'
118
+ end
119
+ {
120
+ log: log,
121
+ source: stream,
122
+ severity: severity
123
+ }
124
+ end
125
+
126
+ def docker_container_application_log_entry(log)
127
+ {
128
+ log: log,
129
+ time: DOCKER_CONTAINER_TIMESTAMP
130
+ }
131
+ end
132
+
133
+ def log_entry(i)
134
+ "test log entry #{i}"
135
+ end
136
+
137
+ # This module expects the methods below to be overridden.
138
+
139
+ def assert_prometheus_metric_value(metric_name, expected_value, labels = {})
140
+ metric = Prometheus::Client.registry.get(metric_name)
141
+ assert_not_nil(metric)
142
+ assert_equal(expected_value, metric.get(labels))
143
+ end
144
+
145
+ # Get the fields of the payload.
146
+ def get_fields(_payload)
147
+ _undefined
148
+ end
149
+
150
+ # Get the value of a struct field.
151
+ def get_struct(_field)
152
+ _undefined
153
+ end
154
+
155
+ # Get the value of a string field.
156
+ def get_string(_field)
157
+ _undefined
158
+ end
159
+
160
+ # Get the value of a number field.
161
+ def get_number(_field)
162
+ _undefined
163
+ end
164
+
165
+ # The null value.
166
+ def null_value(_field)
167
+ _undefined
168
+ end
169
+
170
+ def _undefined
171
+ fail "Method #{__callee__} is unimplemented and needs to be overridden."
172
+ end
173
+ end
@@ -0,0 +1,188 @@
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
+ # Constants used by unit tests for Datadog plugin.
16
+ module Constants
17
+ # Generic attributes.
18
+ HOSTNAME = Socket.gethostname
19
+
20
+ # TODO(qingling128) Separate constants into different submodules.
21
+ # Attributes used for the GCE metadata service.
22
+ ZONE = 'us-central1-b'
23
+ VM_ID = '9876543210'
24
+
25
+ # Attributes used for the Metadata Agent resources.
26
+ METADATA_ZONE = 'us-central1-c'
27
+ METADATA_VM_ID = '0123456789'
28
+
29
+ # Attributes used for custom (overridden) configs.
30
+ CUSTOM_PROJECT_ID = 'test-custom-project-id'
31
+ CUSTOM_ZONE = 'us-custom-central1-b'
32
+ CUSTOM_VM_ID = 'C9876543210'
33
+ CUSTOM_HOSTNAME = 'custom.hostname.org'
34
+
35
+ # Attributes used for the EC2 metadata service.
36
+ EC2_PROJECT_ID = 'test-ec2-project-id'
37
+ EC2_ZONE = 'us-west-2b'
38
+ EC2_PREFIXED_ZONE = 'aws:' + EC2_ZONE
39
+ EC2_VM_ID = 'i-81c16767'
40
+ EC2_ACCOUNT_ID = '123456789012'
41
+
42
+ # The formatting here matches the format used on the VM.
43
+ EC2_IDENTITY_DOCUMENT = %({
44
+ "accountId" : "#{EC2_ACCOUNT_ID}",
45
+ "availabilityZone" : "#{EC2_ZONE}",
46
+ "instanceId" : "#{EC2_VM_ID}"
47
+ })
48
+
49
+ # Managed VMs specific labels.
50
+ MANAGED_VM_BACKEND_NAME = 'default'
51
+ MANAGED_VM_BACKEND_VERSION = 'guestbook2.0'
52
+
53
+ # Docker Container labels.
54
+ DOCKER_CONTAINER_ID = '0d0f03ff8d3c42688692536d1af77a28cd135c0a5c531f25a31'
55
+ DOCKER_CONTAINER_NAME = 'happy_hippo'
56
+ DOCKER_CONTAINER_STREAM_STDOUT = 'stdout'
57
+ DOCKER_CONTAINER_STREAM_STDERR = 'stderr'
58
+ # Timestamp for 1234567890 seconds and 987654321 nanoseconds since epoch.
59
+ DOCKER_CONTAINER_TIMESTAMP = '2009-02-13T23:31:30.987654321Z'
60
+ DOCKER_CONTAINER_SECONDS_EPOCH = 1_234_567_890
61
+ DOCKER_CONTAINER_NANOS = 987_654_321
62
+
63
+ # Container Engine / Kubernetes specific labels.
64
+ CONTAINER_CLUSTER_NAME = 'cluster-1'
65
+ CONTAINER_NAMESPACE_ID = '898268c8-4a36-11e5-9d81-42010af0194c'
66
+ CONTAINER_NAMESPACE_NAME = 'kube-system'
67
+ CONTAINER_POD_ID = 'cad3c3c4-4b9c-11e5-9d81-42010af0194c'
68
+ CONTAINER_POD_NAME = 'redis-master-c0l82.foo.bar'
69
+ CONTAINER_CONTAINER_NAME = 'redis'
70
+ CONTAINER_LABEL_KEY = 'component'
71
+ CONTAINER_LABEL_VALUE = 'redis-component'
72
+ CONTAINER_STREAM = 'stdout'
73
+ CONTAINER_SEVERITY = 'INFO'
74
+ # Timestamp for 1234567890 seconds and 987654321 nanoseconds since epoch.
75
+ CONTAINER_TIMESTAMP = '2009-02-13T23:31:30.987654321Z'
76
+ CONTAINER_SECONDS_EPOCH = 1_234_567_890
77
+ CONTAINER_NANOS = 987_654_321
78
+
79
+ # Cloud Functions specific labels.
80
+ CLOUDFUNCTIONS_FUNCTION_NAME = '$My_Function.Name-@1'
81
+ CLOUDFUNCTIONS_REGION = 'us-central1'
82
+ CLOUDFUNCTIONS_EXECUTION_ID = '123-0'
83
+ CLOUDFUNCTIONS_CLUSTER_NAME = 'cluster-1'
84
+ CLOUDFUNCTIONS_NAMESPACE_NAME = 'default'
85
+ CLOUDFUNCTIONS_POD_NAME = 'd.dc.myu.uc.functionp.pc.name-a.a1.987-c0l82'
86
+ CLOUDFUNCTIONS_CONTAINER_NAME = 'worker'
87
+
88
+ # Dataflow specific labels.
89
+ DATAFLOW_REGION = 'us-central1'
90
+ DATAFLOW_JOB_NAME = 'job_name_1'
91
+ DATAFLOW_JOB_ID = 'job_id_1'
92
+ DATAFLOW_STEP_ID = 'step_1'
93
+ DATAFLOW_TAG = 'dataflow-worker'
94
+
95
+ # Dataproc specific labels.
96
+ DATAPROC_CLUSTER_NAME = 'test-cluster'
97
+ DATAPROC_CLUSTER_UUID = '00000000-0000-0000-0000-000000000000'
98
+ DATAPROC_REGION = 'unittest'
99
+
100
+ # ML specific labels.
101
+ ML_REGION = 'us-central1'
102
+ ML_JOB_ID = 'job_name_1'
103
+ ML_TASK_NAME = 'task_name_1'
104
+ ML_TRIAL_ID = 'trial_id_1'
105
+ ML_LOG_AREA = 'log_area_1'
106
+ ML_TAG = 'master-replica-0'
107
+
108
+ # Parameters used for authentication.
109
+ AUTH_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
110
+ FAKE_AUTH_TOKEN = 'abc123'
111
+
112
+ # Information about test credentials files.
113
+ # path: Path to the credentials file.
114
+ # project_id: ID of the project, which must correspond to the file contents.
115
+ IAM_CREDENTIALS = {
116
+ path: 'test/plugin/data/iam-credentials.json',
117
+ project_id: 'fluent-test-project'
118
+ }
119
+ LEGACY_CREDENTIALS = {
120
+ path: 'test/plugin/data/credentials.json',
121
+ project_id: '847859579879'
122
+ }
123
+ INVALID_CREDENTIALS = {
124
+ path: 'test/plugin/data/invalid_credentials.json',
125
+ project_id: ''
126
+ }
127
+
128
+ # Configuration files for various test scenarios.
129
+ APPLICATION_DEFAULT_CONFIG = %(
130
+
131
+ )
132
+
133
+ DETECT_JSON_CONFIG = %(
134
+ detect_json true
135
+ )
136
+
137
+ PARTIAL_SUCCESS_CONFIG = %(
138
+ partial_success true
139
+ )
140
+
141
+ REQUIRE_VALID_TAGS_CONFIG = %(
142
+ require_valid_tags true
143
+ )
144
+
145
+ NO_METADATA_SERVICE_CONFIG = %(
146
+ use_metadata_service false
147
+ )
148
+
149
+ PROMETHEUS_ENABLE_CONFIG = %(
150
+ enable_monitoring true
151
+ monitoring_type prometheus
152
+ )
153
+
154
+ CUSTOM_METADATA_CONFIG = %(
155
+ zone #{CUSTOM_ZONE}
156
+ vm_id #{CUSTOM_VM_ID}
157
+ vm_name #{CUSTOM_HOSTNAME}
158
+ )
159
+
160
+ # Service configurations for various services.
161
+
162
+ CUSTOM_LABELS_MESSAGE = {
163
+ 'customKey' => 'value'
164
+ }
165
+ # Tags and their sanitized and encoded version.
166
+ VALID_TAGS = {
167
+ 'test' => 'test',
168
+ 'germanß' => 'german%C3%9F',
169
+ 'chinese中' => 'chinese%E4%B8%AD',
170
+ 'specialCharacter/_-.' => 'specialCharacter%2F_-.',
171
+ 'abc@&^$*' => 'abc%40%26%5E%24%2A',
172
+ '@&^$*' => '%40%26%5E%24%2A'
173
+ }
174
+ INVALID_TAGS = {
175
+ # Non-string tags.
176
+ 123 => '123',
177
+ 1.23 => '1.23',
178
+ [1, 2, 3] => '%5B1%2C%202%2C%203%5D',
179
+ { key: 'value' } => '%7B%22key%22%3D%3E%22value%22%7D',
180
+ # Non-utf8 string tags.
181
+ "nonutf8#{[0x92].pack('C*')}" => 'nonutf8%20',
182
+ "abc#{[0x92].pack('C*')}" => 'abc%20',
183
+ "#{[0x92].pack('C*')}" => '%20',
184
+ # Empty string tag.
185
+ '' => '_'
186
+ }
187
+ ALL_TAGS = VALID_TAGS.merge(INVALID_TAGS)
188
+ 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