fluent-plugin-kubernetes_metadata_filter 2.5.1 → 2.7.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.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #
2
4
  # Fluentd Kubernetes Metadata Filter Plugin - Enrich Fluentd events with
3
5
  # Kubernetes metadata
@@ -18,6 +20,11 @@
18
20
  #
19
21
  module KubernetesMetadata
20
22
  module Common
23
+ class GoneError < StandardError
24
+ def initialize(msg = '410 Gone')
25
+ super
26
+ end
27
+ end
21
28
 
22
29
  def match_annotations(annotations)
23
30
  result = {}
@@ -32,71 +39,72 @@ module KubernetesMetadata
32
39
  end
33
40
 
34
41
  def parse_namespace_metadata(namespace_object)
35
- labels = String.new
36
- labels = syms_to_strs(namespace_object['metadata']['labels'].to_h) unless @skip_labels
42
+ labels = ''
43
+ labels = syms_to_strs(namespace_object[:metadata][:labels].to_h) unless @skip_labels
37
44
 
38
- annotations = match_annotations(syms_to_strs(namespace_object['metadata']['annotations'].to_h))
45
+ annotations = match_annotations(syms_to_strs(namespace_object[:metadata][:annotations].to_h))
39
46
  if @de_dot
40
- self.de_dot!(labels) unless @skip_labels
41
- self.de_dot!(annotations)
47
+ de_dot!(labels) unless @skip_labels
48
+ de_dot!(annotations)
42
49
  end
43
50
  kubernetes_metadata = {
44
- 'namespace_id' => namespace_object['metadata']['uid'],
45
- 'creation_timestamp' => namespace_object['metadata']['creationTimestamp']
51
+ 'namespace_id' => namespace_object[:metadata][:uid],
52
+ 'creation_timestamp' => namespace_object[:metadata][:creationTimestamp]
46
53
  }
47
54
  kubernetes_metadata['namespace_labels'] = labels unless labels.empty?
48
55
  kubernetes_metadata['namespace_annotations'] = annotations unless annotations.empty?
49
- return kubernetes_metadata
56
+ kubernetes_metadata
50
57
  end
51
58
 
52
59
  def parse_pod_metadata(pod_object)
53
- labels = String.new
54
- labels = syms_to_strs(pod_object['metadata']['labels'].to_h) unless @skip_labels
60
+ labels = ''
61
+ labels = syms_to_strs(pod_object[:metadata][:labels].to_h) unless @skip_labels
55
62
 
56
- annotations = match_annotations(syms_to_strs(pod_object['metadata']['annotations'].to_h))
63
+ annotations = match_annotations(syms_to_strs(pod_object[:metadata][:annotations].to_h))
57
64
  if @de_dot
58
- self.de_dot!(labels) unless @skip_labels
59
- self.de_dot!(annotations)
65
+ de_dot!(labels) unless @skip_labels
66
+ de_dot!(annotations)
60
67
  end
61
68
 
62
- # collect container informations
69
+ # collect container information
63
70
  container_meta = {}
64
71
  begin
65
- pod_object['status']['containerStatuses'].each do|container_status|
72
+ pod_object[:status][:containerStatuses].each do |container_status|
66
73
  # get plain container id (eg. docker://hash -> hash)
67
- container_id = container_status['containerID'].sub /^[-_a-zA-Z0-9]+:\/\//, ''
68
- unless @skip_container_metadata
69
- container_meta[container_id] = {
70
- 'name' => container_status['name'],
71
- 'image' => container_status['image'],
72
- 'image_id' => container_status['imageID']
73
- }
74
- else
75
- container_meta[container_id] = {
76
- 'name' => container_status['name']
77
- }
78
- end
74
+ container_id = container_status[:containerID].sub(%r{^[-_a-zA-Z0-9]+://}, '')
75
+ container_meta[container_id] = if @skip_container_metadata
76
+ {
77
+ 'name' => container_status[:name]
78
+ }
79
+ else
80
+ {
81
+ 'name' => container_status[:name],
82
+ 'image' => container_status[:image],
83
+ 'image_id' => container_status[:imageID]
84
+ }
85
+ end
79
86
  end
80
- rescue
81
- log.debug("parsing container meta information failed for: #{pod_object['metadata']['namespace']}/#{pod_object['metadata']['name']} ")
87
+ rescue StandardError
88
+ log.debug("parsing container meta information failed for: #{pod_object[:metadata][:namespace]}/#{pod_object[:metadata][:name]} ")
82
89
  end
83
90
 
84
91
  kubernetes_metadata = {
85
- 'namespace_name' => pod_object['metadata']['namespace'],
86
- 'pod_id' => pod_object['metadata']['uid'],
87
- 'pod_name' => pod_object['metadata']['name'],
88
- 'containers' => syms_to_strs(container_meta),
89
- 'host' => pod_object['spec']['nodeName']
92
+ 'namespace_name' => pod_object[:metadata][:namespace],
93
+ 'pod_id' => pod_object[:metadata][:uid],
94
+ 'pod_name' => pod_object[:metadata][:name],
95
+ 'pod_ip' => pod_object[:status][:podIP],
96
+ 'containers' => syms_to_strs(container_meta),
97
+ 'host' => pod_object[:spec][:nodeName]
90
98
  }
91
99
  kubernetes_metadata['annotations'] = annotations unless annotations.empty?
92
100
  kubernetes_metadata['labels'] = labels unless labels.empty?
93
101
  kubernetes_metadata['master_url'] = @kubernetes_url unless @skip_master_url
94
- return kubernetes_metadata
102
+ kubernetes_metadata
95
103
  end
96
104
 
97
105
  def syms_to_strs(hsh)
98
106
  newhsh = {}
99
- hsh.each_pair do |kk,vv|
107
+ hsh.each_pair do |kk, vv|
100
108
  if vv.is_a?(Hash)
101
109
  vv = syms_to_strs(vv)
102
110
  end
@@ -108,32 +116,5 @@ module KubernetesMetadata
108
116
  end
109
117
  newhsh
110
118
  end
111
-
112
- end
113
- end
114
-
115
- # copied from activesupport
116
- class Object
117
- # An object is blank if it's false, empty, or a whitespace string.
118
- # For example, +nil+, '', ' ', [], {}, and +false+ are all blank.
119
- #
120
- # This simplifies
121
- #
122
- # !address || address.empty?
123
- #
124
- # to
125
- #
126
- # address.blank?
127
- #
128
- # @return [true, false]
129
- def blank?
130
- respond_to?(:empty?) ? !!empty? : !self
131
- end
132
-
133
- # An object is present if it's not blank.
134
- #
135
- # @return [true, false]
136
- def present?
137
- !blank?
138
119
  end
139
120
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #
2
4
  # Fluentd Kubernetes Metadata Filter Plugin - Enrich Fluentd events with
3
5
  # Kubernetes metadata
@@ -19,17 +21,16 @@
19
21
  require 'lru_redux'
20
22
  module KubernetesMetadata
21
23
  class Stats
22
-
23
24
  def initialize
24
25
  @stats = ::LruRedux::TTL::ThreadSafeCache.new(1000, 3600)
25
26
  end
26
27
 
27
28
  def bump(key)
28
- @stats[key] = @stats.getset(key) { 0 } + 1
29
+ @stats[key] = @stats.getset(key) { 0 } + 1
29
30
  end
30
31
 
31
32
  def set(key, value)
32
- @stats[key] = value
33
+ @stats[key] = value
33
34
  end
34
35
 
35
36
  def [](key)
@@ -37,10 +38,9 @@ module KubernetesMetadata
37
38
  end
38
39
 
39
40
  def to_s
40
- "stats - " + [].tap do |a|
41
- @stats.each {|k,v| a << "#{k.to_s}: #{v}"}
41
+ 'stats - ' + [].tap do |a|
42
+ @stats.each { |k, v| a << "#{k}: #{v}" }
42
43
  end.join(', ')
43
44
  end
44
-
45
45
  end
46
46
  end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Fluentd Kubernetes Metadata Filter Plugin - Enrich Fluentd events with
5
+ # Kubernetes metadata
6
+ #
7
+ # Copyright 2021 Red Hat, Inc.
8
+ #
9
+ # Licensed under the Apache License, Version 2.0 (the "License");
10
+ # you may not use this file except in compliance with the License.
11
+ # You may obtain a copy of the License at
12
+ #
13
+ # http://www.apache.org/licenses/LICENSE-2.0
14
+ #
15
+ # Unless required by applicable law or agreed to in writing, software
16
+ # distributed under the License is distributed on an "AS IS" BASIS,
17
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
+ # See the License for the specific language governing permissions and
19
+ # limitations under the License.
20
+ #
21
+ require 'kubeclient'
22
+
23
+ module KubernetesMetadata
24
+ module TestApiAdapter
25
+
26
+ def api_valid?
27
+ true
28
+ end
29
+ def get_namespace(namespace_name)
30
+ return {
31
+ metadata: {
32
+ name: namespace_name,
33
+ uid: namespace_name + 'uuid',
34
+ labels: {
35
+ foo_ns: 'bar_ns'
36
+ }
37
+ }
38
+ }
39
+ end
40
+
41
+ def get_pod(pod_name, namespace_name)
42
+ return {
43
+ metadata: {
44
+ name: pod_name,
45
+ namespace: namespace_name,
46
+ uid: namespace_name + namespace_name + "uuid",
47
+ labels: {
48
+ foo: 'bar'
49
+ }
50
+ },
51
+ spec: {
52
+ nodeName: 'aNodeName',
53
+ containers: [{
54
+ name: 'foo',
55
+ image: 'bar'
56
+ }, {
57
+ name: 'bar',
58
+ image: 'foo'
59
+ }]
60
+ },
61
+ status: {
62
+ podIP: '172.17.0.8'
63
+ }
64
+ }
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Fluentd Kubernetes Metadata Filter Plugin - Enrich Fluentd events with
5
+ # Kubernetes metadata
6
+ #
7
+ # Copyright 2021 Red Hat, Inc.
8
+ #
9
+ # Licensed under the Apache License, Version 2.0 (the "License");
10
+ # you may not use this file except in compliance with the License.
11
+ # You may obtain a copy of the License at
12
+ #
13
+ # http://www.apache.org/licenses/LICENSE-2.0
14
+ #
15
+ # Unless required by applicable law or agreed to in writing, software
16
+ # distributed under the License is distributed on an "AS IS" BASIS,
17
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
+ # See the License for the specific language governing permissions and
19
+ # limitations under the License.
20
+ #
21
+ module KubernetesMetadata
22
+ module Util
23
+ def create_time_from_record(record, internal_time)
24
+ time_key = @time_fields.detect { |ii| record.key?(ii) }
25
+ time = record[time_key]
26
+ if time.nil? || time.is_a?(String) && time.chop.empty?
27
+ # `internal_time` is a Fluent::EventTime, it can't compare with Time.
28
+ return Time.at(internal_time.to_f)
29
+ end
30
+
31
+ if ['_SOURCE_REALTIME_TIMESTAMP', '__REALTIME_TIMESTAMP'].include?(time_key)
32
+ timei = time.to_i
33
+ return Time.at(timei / 1_000_000, timei % 1_000_000)
34
+ end
35
+ return Time.at(time) if time.is_a?(Numeric)
36
+
37
+ Time.parse(time)
38
+ end
39
+ end
40
+ end
41
+
42
+ #https://stackoverflow.com/questions/5622435/how-do-i-convert-a-ruby-class-name-to-a-underscore-delimited-symbol
43
+ class String
44
+ def underscore
45
+ word = self.dup
46
+ word.gsub!(/::/, '_')
47
+ word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
48
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
49
+ word.tr!("-", "_")
50
+ word.downcase!
51
+ word
52
+ end
53
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #
2
4
  # Fluentd Kubernetes Metadata Filter Plugin - Enrich Fluentd events with
3
5
  # Kubernetes metadata
@@ -16,11 +18,11 @@
16
18
  # See the License for the specific language governing permissions and
17
19
  # limitations under the License.
18
20
  #
21
+ # TODO: this is mostly copy-paste from kubernetes_metadata_watch_pods.rb unify them
19
22
  require_relative 'kubernetes_metadata_common'
20
23
 
21
24
  module KubernetesMetadata
22
25
  module WatchNamespaces
23
-
24
26
  include ::KubernetesMetadata::Common
25
27
 
26
28
  def set_up_namespace_thread
@@ -35,42 +37,47 @@ module KubernetesMetadata
35
37
  # processing will be swallowed and retried. These failures /
36
38
  # exceptions could be caused by Kubernetes API being temporarily
37
39
  # down. We assume the configuration is correct at this point.
38
- while true
39
- begin
40
- namespace_watcher ||= get_namespaces_and_start_watcher
41
- process_namespace_watcher_notices(namespace_watcher)
42
- rescue Exception => e
43
- @stats.bump(:namespace_watch_failures)
44
- if Thread.current[:namespace_watch_retry_count] < @watch_retry_max_times
45
- # Instead of raising exceptions and crashing Fluentd, swallow
46
- # the exception and reset the watcher.
47
- log.info(
48
- "Exception encountered parsing namespace watch event. " \
49
- "The connection might have been closed. Sleeping for " \
50
- "#{Thread.current[:namespace_watch_retry_backoff_interval]} " \
51
- "seconds and resetting the namespace watcher.", e)
52
- sleep(Thread.current[:namespace_watch_retry_backoff_interval])
53
- Thread.current[:namespace_watch_retry_count] += 1
54
- Thread.current[:namespace_watch_retry_backoff_interval] *= @watch_retry_exponential_backoff_base
55
- namespace_watcher = nil
56
- else
57
- # Since retries failed for many times, log as errors instead
58
- # of info and raise exceptions and trigger Fluentd to restart.
59
- message =
60
- "Exception encountered parsing namespace watch event. The " \
61
- "connection might have been closed. Retried " \
62
- "#{@watch_retry_max_times} times yet still failing. Restarting."
63
- log.error(message, e)
64
- raise Fluent::UnrecoverableError.new(message)
65
- end
40
+ loop do
41
+ namespace_watcher ||= get_namespaces_and_start_watcher
42
+ process_namespace_watcher_notices(namespace_watcher)
43
+ rescue GoneError => e
44
+ # Expected error. Quietly go back through the loop in order to
45
+ # start watching from the latest resource versions
46
+ @stats.bump(:namespace_watch_gone_errors)
47
+ log.info('410 Gone encountered. Restarting namespace watch to reset resource versions.', e)
48
+ namespace_watcher = nil
49
+ rescue StandardError => e
50
+ @stats.bump(:namespace_watch_failures)
51
+ if Thread.current[:namespace_watch_retry_count] < @watch_retry_max_times
52
+ # Instead of raising exceptions and crashing Fluentd, swallow
53
+ # the exception and reset the watcher.
54
+ log.info(
55
+ 'Exception encountered parsing namespace watch event. ' \
56
+ 'The connection might have been closed. Sleeping for ' \
57
+ "#{Thread.current[:namespace_watch_retry_backoff_interval]} " \
58
+ 'seconds and resetting the namespace watcher.', e
59
+ )
60
+ sleep(Thread.current[:namespace_watch_retry_backoff_interval])
61
+ Thread.current[:namespace_watch_retry_count] += 1
62
+ Thread.current[:namespace_watch_retry_backoff_interval] *= @watch_retry_exponential_backoff_base
63
+ namespace_watcher = nil
64
+ else
65
+ # Since retries failed for many times, log as errors instead
66
+ # of info and raise exceptions and trigger Fluentd to restart.
67
+ message =
68
+ 'Exception encountered parsing namespace watch event. The ' \
69
+ 'connection might have been closed. Retried ' \
70
+ "#{@watch_retry_max_times} times yet still failing. Restarting."
71
+ log.error(message, e)
72
+ raise Fluent::UnrecoverableError, message
66
73
  end
67
74
  end
68
75
  end
69
76
 
70
77
  def start_namespace_watch
71
- return get_namespaces_and_start_watcher
72
- rescue Exception => e
73
- message = "start_namespace_watch: Exception encountered setting up " \
78
+ get_namespaces_and_start_watcher
79
+ rescue StandardError => e
80
+ message = 'start_namespace_watch: Exception encountered setting up ' \
74
81
  "namespace watch from Kubernetes API #{@apiVersion} endpoint " \
75
82
  "#{@kubernetes_url}: #{e.message}"
76
83
  message += " (#{e.response})" if e.respond_to?(:response)
@@ -83,16 +90,20 @@ module KubernetesMetadata
83
90
  # starting from that resourceVersion.
84
91
  def get_namespaces_and_start_watcher
85
92
  options = {
86
- resource_version: '0' # Fetch from API server.
93
+ resource_version: '0' # Fetch from API server cache instead of etcd quorum read
87
94
  }
88
95
  namespaces = @client.get_namespaces(options)
89
- namespaces.each do |namespace|
90
- cache_key = namespace.metadata['uid']
96
+ namespaces[:items].each do |namespace|
97
+ cache_key = namespace[:metadata][:uid]
91
98
  @namespace_cache[cache_key] = parse_namespace_metadata(namespace)
92
99
  @stats.bump(:namespace_cache_host_updates)
93
100
  end
94
- options[:resource_version] = namespaces.resourceVersion
101
+
102
+ # continue watching from most recent resourceVersion
103
+ options[:resource_version] = namespaces[:metadata][:resourceVersion]
104
+
95
105
  watcher = @client.watch_namespaces(options)
106
+ reset_namespace_watch_retry_stats
96
107
  watcher
97
108
  end
98
109
 
@@ -106,31 +117,36 @@ module KubernetesMetadata
106
117
  # Process a watcher notice and potentially raise an exception.
107
118
  def process_namespace_watcher_notices(watcher)
108
119
  watcher.each do |notice|
109
- case notice.type
110
- when 'MODIFIED'
111
- reset_namespace_watch_retry_stats
112
- cache_key = notice.object['metadata']['uid']
113
- cached = @namespace_cache[cache_key]
114
- if cached
115
- @namespace_cache[cache_key] = parse_namespace_metadata(notice.object)
116
- @stats.bump(:namespace_cache_watch_updates)
117
- else
118
- @stats.bump(:namespace_cache_watch_misses)
119
- end
120
- when 'DELETED'
121
- reset_namespace_watch_retry_stats
122
- # ignore and let age out for cases where
123
- # deleted but still processing logs
124
- @stats.bump(:namespace_cache_watch_deletes_ignored)
125
- when 'ERROR'
120
+ case notice[:type]
121
+ when 'MODIFIED'
122
+ reset_namespace_watch_retry_stats
123
+ cache_key = notice[:object][:metadata][:uid]
124
+ cached = @namespace_cache[cache_key]
125
+ if cached
126
+ @namespace_cache[cache_key] = parse_namespace_metadata(notice[:object])
127
+ @stats.bump(:namespace_cache_watch_updates)
128
+ else
129
+ @stats.bump(:namespace_cache_watch_misses)
130
+ end
131
+ when 'DELETED'
132
+ reset_namespace_watch_retry_stats
133
+ # ignore and let age out for cases where
134
+ # deleted but still processing logs
135
+ @stats.bump(:namespace_cache_watch_deletes_ignored)
136
+ when 'ERROR'
137
+ if notice[:object] && notice[:object][:code] == 410
138
+ @stats.bump(:namespace_watch_gone_notices)
139
+ raise GoneError
140
+ else
126
141
  @stats.bump(:namespace_watch_error_type_notices)
127
- message = notice['object']['message'] if notice['object'] && notice['object']['message']
142
+ message = notice[:object][:message] if notice[:object] && notice[:object][:message]
128
143
  raise "Error while watching namespaces: #{message}"
129
- else
130
- reset_namespace_watch_retry_stats
131
- # Don't pay attention to creations, since the created namespace may not
132
- # be used by any namespace on this node.
133
- @stats.bump(:namespace_cache_watch_ignored)
144
+ end
145
+ else
146
+ reset_namespace_watch_retry_stats
147
+ # Don't pay attention to creations, since the created namespace may not
148
+ # be used by any namespace on this node.
149
+ @stats.bump(:namespace_cache_watch_ignored)
134
150
  end
135
151
  end
136
152
  end