fluent-plugin-kubernetes_metadata_filter_v0.14 0.24.1

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