fluent-plugin-kubernetes_metadata_filter_v0.14 0.24.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,417 @@
1
+ #
2
+ # Fluentd Kubernetes Metadata Filter Plugin - Enrich Fluentd events with
3
+ # Kubernetes metadata
4
+ #
5
+ # Copyright 2015 Red Hat, Inc.
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+ module Fluent
20
+ class KubernetesMetadataFilter < Fluent::Filter
21
+ K8_POD_CA_CERT = 'ca.crt'
22
+ K8_POD_TOKEN = 'token'
23
+
24
+ Fluent::Plugin.register_filter('kubernetes_metadata', self)
25
+
26
+ config_param :kubernetes_url, :string, default: nil
27
+ config_param :cache_size, :integer, default: 1000
28
+ config_param :cache_ttl, :integer, default: 60 * 60
29
+ config_param :watch, :bool, default: true
30
+ config_param :apiVersion, :string, default: 'v1'
31
+ config_param :client_cert, :string, default: nil
32
+ config_param :client_key, :string, default: nil
33
+ config_param :ca_file, :string, default: nil
34
+ config_param :verify_ssl, :bool, default: true
35
+ config_param :tag_to_kubernetes_name_regexp,
36
+ :string,
37
+ :default => 'var\.log\.containers\.(?<pod_name>[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)_(?<namespace>[^_]+)_(?<container_name>.+)-(?<docker_id>[a-z0-9]{64})\.log$'
38
+ config_param :bearer_token_file, :string, default: nil
39
+ config_param :merge_json_log, :bool, default: true
40
+ config_param :preserve_json_log, :bool, default: true
41
+ config_param :include_namespace_id, :bool, default: false
42
+ config_param :secret_dir, :string, default: '/var/run/secrets/kubernetes.io/serviceaccount'
43
+ config_param :de_dot, :bool, default: true
44
+ config_param :de_dot_separator, :string, default: '_'
45
+ # if reading from the journal, the record will contain the following fields in the following
46
+ # format:
47
+ # CONTAINER_NAME=k8s_$containername.$containerhash_$podname_$namespacename_$poduuid_$rand32bitashex
48
+ # CONTAINER_FULL_ID=dockeridassha256hexvalue
49
+ config_param :use_journal, :bool, default: false
50
+ # Field 2 is the container_hash, field 5 is the pod_id, and field 6 is the pod_randhex
51
+ # I would have included them as named groups, but you can't have named groups that are
52
+ # non-capturing :P
53
+ config_param :container_name_to_kubernetes_regexp,
54
+ :string,
55
+ :default => '^k8s_(?<container_name>[^\.]+)\.[^_]+_(?<pod_name>[^_]+)_(?<namespace>[^_]+)_[^_]+_[a-f0-9]{8}$'
56
+
57
+ config_param :annotation_match, :array, default: []
58
+
59
+ def syms_to_strs(hsh)
60
+ newhsh = {}
61
+ hsh.each_pair do |kk,vv|
62
+ if vv.is_a?(Hash)
63
+ vv = syms_to_strs(vv)
64
+ end
65
+ if kk.is_a?(Symbol)
66
+ newhsh[kk.to_s] = vv
67
+ else
68
+ newhsh[kk] = vv
69
+ end
70
+ end
71
+ newhsh
72
+ end
73
+
74
+ def get_metadata(namespace_name, pod_name, container_name)
75
+ begin
76
+ metadata = @client.get_pod(pod_name, namespace_name)
77
+ return if !metadata
78
+ labels = syms_to_strs(metadata['metadata']['labels'].to_h)
79
+ annotations = match_annotations(syms_to_strs(metadata['metadata']['annotations'].to_h))
80
+ if @de_dot
81
+ self.de_dot!(labels)
82
+ end
83
+ kubernetes_metadata = {
84
+ 'namespace_name' => namespace_name,
85
+ 'pod_id' => metadata['metadata']['uid'],
86
+ 'pod_name' => pod_name,
87
+ 'container_name' => container_name,
88
+ 'labels' => labels,
89
+ 'host' => metadata['spec']['nodeName']
90
+ }
91
+ kubernetes_metadata['annotations'] = annotations unless annotations.empty?
92
+ return kubernetes_metadata
93
+ rescue KubeException
94
+ nil
95
+ end
96
+ end
97
+
98
+ def initialize
99
+ super
100
+ end
101
+
102
+ def filter(tag, time, record)
103
+ record
104
+ end
105
+
106
+ def configure(conf)
107
+ super
108
+
109
+ require 'kubeclient'
110
+ require 'active_support/core_ext/object/blank'
111
+ require 'lru_redux'
112
+
113
+ if @de_dot && (@de_dot_separator =~ /\./).present?
114
+ raise Fluent::ConfigError, "Invalid de_dot_separator: cannot be or contain '.'"
115
+ end
116
+
117
+ if @cache_ttl < 0
118
+ @cache_ttl = :none
119
+ end
120
+ @cache = LruRedux::TTL::ThreadSafeCache.new(@cache_size, @cache_ttl)
121
+ if @include_namespace_id
122
+ @namespace_cache = LruRedux::TTL::ThreadSafeCache.new(@cache_size, @cache_ttl)
123
+ end
124
+ @tag_to_kubernetes_name_regexp_compiled = Regexp.compile(@tag_to_kubernetes_name_regexp)
125
+ @container_name_to_kubernetes_regexp_compiled = Regexp.compile(@container_name_to_kubernetes_regexp)
126
+
127
+ # Use Kubernetes default service account if we're in a pod.
128
+ if @kubernetes_url.nil?
129
+ env_host = ENV['KUBERNETES_SERVICE_HOST']
130
+ env_port = ENV['KUBERNETES_SERVICE_PORT']
131
+ if env_host.present? && env_port.present?
132
+ @kubernetes_url = "https://#{env_host}:#{env_port}/api"
133
+ end
134
+ end
135
+
136
+ # Use SSL certificate and bearer token from Kubernetes service account.
137
+ if Dir.exist?(@secret_dir)
138
+ ca_cert = File.join(@secret_dir, K8_POD_CA_CERT)
139
+ pod_token = File.join(@secret_dir, K8_POD_TOKEN)
140
+
141
+ if !@ca_file.present? and File.exist?(ca_cert)
142
+ @ca_file = ca_cert
143
+ end
144
+
145
+ if !@bearer_token_file.present? and File.exist?(pod_token)
146
+ @bearer_token_file = pod_token
147
+ end
148
+ end
149
+
150
+ if @kubernetes_url.present?
151
+
152
+ ssl_options = {
153
+ client_cert: @client_cert.present? ? OpenSSL::X509::Certificate.new(File.read(@client_cert)) : nil,
154
+ client_key: @client_key.present? ? OpenSSL::PKey::RSA.new(File.read(@client_key)) : nil,
155
+ ca_file: @ca_file,
156
+ verify_ssl: @verify_ssl ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
157
+ }
158
+
159
+ auth_options = {}
160
+
161
+ if @bearer_token_file.present?
162
+ bearer_token = File.read(@bearer_token_file)
163
+ auth_options[:bearer_token] = bearer_token
164
+ end
165
+
166
+ @client = Kubeclient::Client.new @kubernetes_url, @apiVersion,
167
+ ssl_options: ssl_options,
168
+ auth_options: auth_options
169
+
170
+ begin
171
+ @client.api_valid?
172
+ rescue KubeException => kube_error
173
+ raise Fluent::ConfigError, "Invalid Kubernetes API #{@apiVersion} endpoint #{@kubernetes_url}: #{kube_error.message}"
174
+ end
175
+
176
+ if @watch
177
+ thread = Thread.new(self) { |this| this.start_watch }
178
+ thread.abort_on_exception = true
179
+ if @include_namespace_id
180
+ namespace_thread = Thread.new(self) { |this| this.start_namespace_watch }
181
+ namespace_thread.abort_on_exception = true
182
+ end
183
+ end
184
+ end
185
+ if @use_journal
186
+ @merge_json_log_key = 'MESSAGE'
187
+ self.class.class_eval { alias_method :filter_stream, :filter_stream_from_journal }
188
+ else
189
+ @merge_json_log_key = 'log'
190
+ self.class.class_eval { alias_method :filter_stream, :filter_stream_from_files }
191
+ end
192
+
193
+ @annotations_regexps = []
194
+ @annotation_match.each do |regexp|
195
+ begin
196
+ @annotations_regexps << Regexp.compile(regexp)
197
+ rescue RegexpError => e
198
+ log.error "Error: invalid regular expression in annotation_match: #{e}"
199
+ end
200
+ end
201
+
202
+ end
203
+
204
+ def filter_stream_from_files(tag, es)
205
+ new_es = MultiEventStream.new
206
+
207
+ match_data = tag.match(@tag_to_kubernetes_name_regexp_compiled)
208
+
209
+ if match_data
210
+ metadata = {
211
+ 'docker' => {
212
+ 'container_id' => match_data['docker_id']
213
+ },
214
+ 'kubernetes' => {
215
+ 'namespace_name' => match_data['namespace'],
216
+ 'pod_name' => match_data['pod_name'],
217
+ 'container_name' => match_data['container_name']
218
+ }
219
+ }
220
+
221
+ if @kubernetes_url.present?
222
+ cache_key = "#{metadata['kubernetes']['namespace_name']}_#{metadata['kubernetes']['pod_name']}_#{metadata['kubernetes']['container_name']}"
223
+
224
+ this = self
225
+ metadata = @cache.getset(cache_key) {
226
+ if metadata
227
+ kubernetes_metadata = this.get_metadata(
228
+ metadata['kubernetes']['namespace_name'],
229
+ metadata['kubernetes']['pod_name'],
230
+ metadata['kubernetes']['container_name']
231
+ )
232
+ metadata['kubernetes'] = kubernetes_metadata if kubernetes_metadata
233
+ metadata
234
+ end
235
+ }
236
+ if @include_namespace_id
237
+ namespace_name = metadata['kubernetes']['namespace_name']
238
+ namespace_id = @namespace_cache.getset(namespace_name) {
239
+ namespace = @client.get_namespace(namespace_name)
240
+ namespace['metadata']['uid'] if namespace
241
+ }
242
+ metadata['kubernetes']['namespace_id'] = namespace_id if namespace_id
243
+ end
244
+ end
245
+ end
246
+
247
+ es.each { |time, record|
248
+ record = merge_json_log(record) if @merge_json_log
249
+
250
+ record = record.merge(metadata) if metadata
251
+
252
+ new_es.add(time, record)
253
+ }
254
+
255
+ new_es
256
+ end
257
+
258
+ def filter_stream_from_journal(tag, es)
259
+ new_es = MultiEventStream.new
260
+
261
+ es.each { |time, record|
262
+ record = merge_json_log(record) if @merge_json_log
263
+
264
+ metadata = nil
265
+ if record.has_key?('CONTAINER_NAME') && record.has_key?('CONTAINER_ID_FULL')
266
+ metadata = record['CONTAINER_NAME'].match(@container_name_to_kubernetes_regexp_compiled) do |match_data|
267
+ metadata = {
268
+ 'docker' => {
269
+ 'container_id' => record['CONTAINER_ID_FULL']
270
+ },
271
+ 'kubernetes' => {
272
+ 'namespace_name' => match_data['namespace'],
273
+ 'pod_name' => match_data['pod_name'],
274
+ 'container_name' => match_data['container_name']
275
+ }
276
+ }
277
+ if @kubernetes_url.present?
278
+ cache_key = "#{metadata['kubernetes']['namespace_name']}_#{metadata['kubernetes']['pod_name']}_#{metadata['kubernetes']['container_name']}"
279
+
280
+ this = self
281
+ metadata = @cache.getset(cache_key) {
282
+ if metadata
283
+ kubernetes_metadata = this.get_metadata(
284
+ metadata['kubernetes']['namespace_name'],
285
+ metadata['kubernetes']['pod_name'],
286
+ metadata['kubernetes']['container_name']
287
+ )
288
+ metadata['kubernetes'] = kubernetes_metadata if kubernetes_metadata
289
+ metadata
290
+ end
291
+ }
292
+ if @include_namespace_id
293
+ namespace_name = metadata['kubernetes']['namespace_name']
294
+ namespace_id = @namespace_cache.getset(namespace_name) {
295
+ namespace = @client.get_namespace(namespace_name)
296
+ namespace['metadata']['uid'] if namespace
297
+ }
298
+ metadata['kubernetes']['namespace_id'] = namespace_id if namespace_id
299
+ end
300
+ end
301
+ metadata
302
+ end
303
+ unless metadata
304
+ log.debug "Error: could not match CONTAINER_NAME from record #{record}"
305
+ end
306
+ elsif record.has_key?('CONTAINER_NAME') && record['CONTAINER_NAME'].start_with?('k8s_')
307
+ log.debug "Error: no container name and id in record #{record}"
308
+ end
309
+
310
+ if metadata
311
+ record = record.merge(metadata)
312
+ end
313
+
314
+ new_es.add(time, record)
315
+ }
316
+
317
+ new_es
318
+ end
319
+
320
+ def merge_json_log(record)
321
+ if record.has_key?(@merge_json_log_key)
322
+ log = record[@merge_json_log_key].strip
323
+ if log[0].eql?('{') && log[-1].eql?('}')
324
+ begin
325
+ record = JSON.parse(log).merge(record)
326
+ unless @preserve_json_log
327
+ record.delete(@merge_json_log_key)
328
+ end
329
+ rescue JSON::ParserError
330
+ end
331
+ end
332
+ end
333
+ record
334
+ end
335
+
336
+ def de_dot!(h)
337
+ h.keys.each do |ref|
338
+ if h[ref] && ref =~ /\./
339
+ v = h.delete(ref)
340
+ newref = ref.to_s.gsub('.', @de_dot_separator)
341
+ h[newref] = v
342
+ end
343
+ end
344
+ end
345
+
346
+ def match_annotations(annotations)
347
+ result = {}
348
+ @annotations_regexps.each do |regexp|
349
+ annotations.each do |key, value|
350
+ if ::Fluent::StringUtil.match_regexp(regexp, key.to_s)
351
+ result[key] = value
352
+ end
353
+ end
354
+ end
355
+ result
356
+ end
357
+
358
+ def start_watch
359
+ begin
360
+ resource_version = @client.get_pods.resourceVersion
361
+ watcher = @client.watch_pods(resource_version)
362
+ rescue Exception => e
363
+ raise Fluent::ConfigError, "Exception encountered fetching metadata from Kubernetes API endpoint: #{e.message}"
364
+ end
365
+
366
+ watcher.each do |notice|
367
+ case notice.type
368
+ when 'MODIFIED'
369
+ if notice.object.status.containerStatuses
370
+ pod_cache_key = "#{notice.object['metadata']['namespace']}_#{notice.object['metadata']['name']}"
371
+ notice.object.status.containerStatuses.each { |container_status|
372
+ cache_key = "#{pod_cache_key}_#{container_status['name']}"
373
+ cached = @cache[cache_key]
374
+ if cached
375
+ # Only thing that can be modified is labels and (possibly) annotations
376
+ labels = syms_to_strs(notice.object.metadata.labels.to_h)
377
+ annotations = match_annotations(syms_to_strs(notice.object.metadata.annotations.to_h))
378
+ if @de_dot
379
+ self.de_dot!(labels)
380
+ end
381
+ cached['kubernetes']['labels'] = labels
382
+ cached['kubernetes']['annotations'] = annotations unless annotations.empty?
383
+ @cache[cache_key] = cached
384
+ end
385
+ }
386
+ end
387
+ when 'DELETED'
388
+ if notice.object.status.containerStatuses
389
+ pod_cache_key = "#{notice.object['metadata']['namespace']}_#{notice.object['metadata']['name']}"
390
+ notice.object.status.containerStatuses.each { |container_status|
391
+ cache_key = "#{pod_cache_key}_#{container_status['name']}"
392
+ @cache.delete(cache_key)
393
+ }
394
+ end
395
+ else
396
+ # Don't pay attention to creations, since the created pod may not
397
+ # end up on this node.
398
+ end
399
+ end
400
+ end
401
+
402
+ def start_namespace_watch
403
+ resource_version = @client.get_namespaces.resourceVersion
404
+ watcher = @client.watch_namespaces(resource_version)
405
+ watcher.each do |notice|
406
+ puts notice
407
+ case notice.type
408
+ when 'DELETED'
409
+ @namespace_cache.delete(notice.object['metadata']['name'])
410
+ else
411
+ # We only care about each namespace's name and UID, neither of which
412
+ # is modifiable, so we only have to care about deletions.
413
+ end
414
+ end
415
+ end
416
+ end
417
+ end
@@ -0,0 +1,53 @@
1
+ #
2
+ # Fluentd Kubernetes Metadata Filter Plugin - Enrich Fluentd events with
3
+ # Kubernetes metadata
4
+ #
5
+ # Copyright 2015 Red Hat, Inc.
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+ ---
20
+ http_interactions:
21
+ - request:
22
+ method: get
23
+ uri: https://localhost:8443/api
24
+ body:
25
+ encoding: US-ASCII
26
+ string: ''
27
+ headers:
28
+ Accept:
29
+ - "*/*; q=0.5, application/xml"
30
+ Accept-Encoding:
31
+ - gzip, deflate
32
+ User-Agent:
33
+ - Ruby
34
+ Authorization:
35
+ - Bearer YzYyYzFlODMtODdhNS00ZTMyLWIzMmItNmY4NDc4OTI1ZWF
36
+ response:
37
+ status:
38
+ code: 401
39
+ message: Unauthorized
40
+ headers:
41
+ Content-Type:
42
+ - text/plain; charset=utf-8
43
+ Date:
44
+ - Sat, 09 May 2015 14:04:39 GMT
45
+ Content-Length:
46
+ - '13'
47
+ body:
48
+ encoding: UTF-8
49
+ string: |
50
+ Unauthorized
51
+ http_version:
52
+ recorded_at: Sat, 09 May 2015 14:04:39 GMT
53
+ recorded_with: VCR 2.9.3