fluent-plugin-kubernetes_metadata_filter 2.5.2 → 2.11.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,9 +20,8 @@
18
20
  #
19
21
  module KubernetesMetadata
20
22
  module Common
21
-
22
23
  class GoneError < StandardError
23
- def initialize(msg="410 Gone")
24
+ def initialize(msg = '410 Gone')
24
25
  super
25
26
  end
26
27
  end
@@ -38,71 +39,81 @@ module KubernetesMetadata
38
39
  end
39
40
 
40
41
  def parse_namespace_metadata(namespace_object)
41
- labels = String.new
42
- 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
43
44
 
44
- 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))
45
46
  if @de_dot
46
- self.de_dot!(labels) unless @skip_labels
47
- self.de_dot!(annotations)
47
+ de_dot!(labels) unless @skip_labels
48
+ de_dot!(annotations)
49
+ end
50
+ if @de_slash
51
+ de_slash!(labels) unless @skip_labels
52
+ de_slash!(annotations)
48
53
  end
49
54
  kubernetes_metadata = {
50
- 'namespace_id' => namespace_object['metadata']['uid'],
51
- 'creation_timestamp' => namespace_object['metadata']['creationTimestamp']
55
+ 'namespace_id' => namespace_object[:metadata][:uid],
56
+ 'creation_timestamp' => namespace_object[:metadata][:creationTimestamp]
52
57
  }
53
58
  kubernetes_metadata['namespace_labels'] = labels unless labels.empty?
54
59
  kubernetes_metadata['namespace_annotations'] = annotations unless annotations.empty?
55
- return kubernetes_metadata
60
+ kubernetes_metadata
56
61
  end
57
62
 
58
63
  def parse_pod_metadata(pod_object)
59
- labels = String.new
60
- labels = syms_to_strs(pod_object['metadata']['labels'].to_h) unless @skip_labels
64
+ labels = ''
65
+ labels = syms_to_strs(pod_object[:metadata][:labels].to_h) unless @skip_labels
61
66
 
62
- annotations = match_annotations(syms_to_strs(pod_object['metadata']['annotations'].to_h))
67
+ annotations = match_annotations(syms_to_strs(pod_object[:metadata][:annotations].to_h))
63
68
  if @de_dot
64
- self.de_dot!(labels) unless @skip_labels
65
- self.de_dot!(annotations)
69
+ de_dot!(labels) unless @skip_labels
70
+ de_dot!(annotations)
71
+ end
72
+ if @de_slash
73
+ de_slash!(labels) unless @skip_labels
74
+ de_slash!(annotations)
66
75
  end
67
76
 
68
- # collect container informations
77
+ # collect container information
69
78
  container_meta = {}
70
79
  begin
71
- pod_object['status']['containerStatuses'].each do|container_status|
72
- # get plain container id (eg. docker://hash -> hash)
73
- container_id = container_status['containerID'].sub /^[-_a-zA-Z0-9]+:\/\//, ''
74
- unless @skip_container_metadata
75
- container_meta[container_id] = {
76
- 'name' => container_status['name'],
77
- 'image' => container_status['image'],
78
- 'image_id' => container_status['imageID']
79
- }
80
- else
81
- container_meta[container_id] = {
82
- 'name' => container_status['name']
83
- }
84
- end
85
- end
86
- rescue
87
- log.debug("parsing container meta information failed for: #{pod_object['metadata']['namespace']}/#{pod_object['metadata']['name']} ")
80
+ pod_object[:status][:containerStatuses].each do |container_status|
81
+ container_id = (container_status[:containerID]||"").sub(%r{^[-_a-zA-Z0-9]+://}, '')
82
+ key = container_status[:name]
83
+ container_meta[key] = if @skip_container_metadata
84
+ {
85
+ 'name' => container_status[:name]
86
+ }
87
+ else
88
+ {
89
+ 'name' => container_status[:name],
90
+ 'image' => container_status[:image],
91
+ 'image_id' => container_status[:imageID],
92
+ :containerID => container_id
93
+ }
94
+ end
95
+ end if pod_object[:status] && pod_object[:status][:containerStatuses]
96
+ rescue StandardError=>e
97
+ log.warn("parsing container meta information failed for: #{pod_object[:metadata][:namespace]}/#{pod_object[:metadata][:name]}: #{e}")
88
98
  end
89
99
 
90
100
  kubernetes_metadata = {
91
- 'namespace_name' => pod_object['metadata']['namespace'],
92
- 'pod_id' => pod_object['metadata']['uid'],
93
- 'pod_name' => pod_object['metadata']['name'],
94
- 'containers' => syms_to_strs(container_meta),
95
- 'host' => pod_object['spec']['nodeName']
101
+ 'namespace_name' => pod_object[:metadata][:namespace],
102
+ 'pod_id' => pod_object[:metadata][:uid],
103
+ 'pod_name' => pod_object[:metadata][:name],
104
+ 'pod_ip' => pod_object[:status][:podIP],
105
+ 'containers' => syms_to_strs(container_meta),
106
+ 'host' => pod_object[:spec][:nodeName]
96
107
  }
97
108
  kubernetes_metadata['annotations'] = annotations unless annotations.empty?
98
109
  kubernetes_metadata['labels'] = labels unless labels.empty?
99
110
  kubernetes_metadata['master_url'] = @kubernetes_url unless @skip_master_url
100
- return kubernetes_metadata
111
+ kubernetes_metadata
101
112
  end
102
113
 
103
114
  def syms_to_strs(hsh)
104
115
  newhsh = {}
105
- hsh.each_pair do |kk,vv|
116
+ hsh.each_pair do |kk, vv|
106
117
  if vv.is_a?(Hash)
107
118
  vv = syms_to_strs(vv)
108
119
  end
@@ -114,32 +125,5 @@ module KubernetesMetadata
114
125
  end
115
126
  newhsh
116
127
  end
117
-
118
- end
119
- end
120
-
121
- # copied from activesupport
122
- class Object
123
- # An object is blank if it's false, empty, or a whitespace string.
124
- # For example, +nil+, '', ' ', [], {}, and +false+ are all blank.
125
- #
126
- # This simplifies
127
- #
128
- # !address || address.empty?
129
- #
130
- # to
131
- #
132
- # address.blank?
133
- #
134
- # @return [true, false]
135
- def blank?
136
- respond_to?(:empty?) ? !!empty? : !self
137
- end
138
-
139
- # An object is present if it's not blank.
140
- #
141
- # @return [true, false]
142
- def present?
143
- !blank?
144
128
  end
145
129
  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, unused, options)
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, options)
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,26 +37,33 @@ 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 GoneError => e
43
- # Expected error. Quietly go back through the loop in order to
44
- # start watching from the latest resource versions
45
- @stats.bump(:namespace_watch_gone_errors)
46
- log.info("410 Gone encountered. Restarting namespace watch to reset resource versions.", e)
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 KubeException => e
50
+ if e.error_code == 401
51
+ # recreate client to refresh token
52
+ log.info("Encountered '401 Unauthorized' exception in watch, recreating client to refresh token")
53
+ create_client()
47
54
  namespace_watcher = nil
48
- rescue Exception => e
55
+ else
56
+ # treat all other errors the same as StandardError, log, swallow and reset
49
57
  @stats.bump(:namespace_watch_failures)
50
58
  if Thread.current[:namespace_watch_retry_count] < @watch_retry_max_times
51
59
  # Instead of raising exceptions and crashing Fluentd, swallow
52
60
  # the exception and reset the watcher.
53
61
  log.info(
54
- "Exception encountered parsing namespace watch event. " \
55
- "The connection might have been closed. Sleeping for " \
62
+ 'Exception encountered parsing namespace watch event. ' \
63
+ 'The connection might have been closed. Sleeping for ' \
56
64
  "#{Thread.current[:namespace_watch_retry_backoff_interval]} " \
57
- "seconds and resetting the namespace watcher.", e)
65
+ 'seconds and resetting the namespace watcher.', e
66
+ )
58
67
  sleep(Thread.current[:namespace_watch_retry_backoff_interval])
59
68
  Thread.current[:namespace_watch_retry_count] += 1
60
69
  Thread.current[:namespace_watch_retry_backoff_interval] *= @watch_retry_exponential_backoff_base
@@ -63,20 +72,45 @@ module KubernetesMetadata
63
72
  # Since retries failed for many times, log as errors instead
64
73
  # of info and raise exceptions and trigger Fluentd to restart.
65
74
  message =
66
- "Exception encountered parsing namespace watch event. The " \
67
- "connection might have been closed. Retried " \
75
+ 'Exception encountered parsing namespace watch event. The ' \
76
+ 'connection might have been closed. Retried ' \
68
77
  "#{@watch_retry_max_times} times yet still failing. Restarting."
69
78
  log.error(message, e)
70
- raise Fluent::UnrecoverableError.new(message)
79
+ raise Fluent::UnrecoverableError, message
71
80
  end
72
81
  end
82
+ rescue StandardError => e
83
+ @stats.bump(:namespace_watch_failures)
84
+ if Thread.current[:namespace_watch_retry_count] < @watch_retry_max_times
85
+ # Instead of raising exceptions and crashing Fluentd, swallow
86
+ # the exception and reset the watcher.
87
+ log.info(
88
+ 'Exception encountered parsing namespace watch event. ' \
89
+ 'The connection might have been closed. Sleeping for ' \
90
+ "#{Thread.current[:namespace_watch_retry_backoff_interval]} " \
91
+ 'seconds and resetting the namespace watcher.', e
92
+ )
93
+ sleep(Thread.current[:namespace_watch_retry_backoff_interval])
94
+ Thread.current[:namespace_watch_retry_count] += 1
95
+ Thread.current[:namespace_watch_retry_backoff_interval] *= @watch_retry_exponential_backoff_base
96
+ namespace_watcher = nil
97
+ else
98
+ # Since retries failed for many times, log as errors instead
99
+ # of info and raise exceptions and trigger Fluentd to restart.
100
+ message =
101
+ 'Exception encountered parsing namespace watch event. The ' \
102
+ 'connection might have been closed. Retried ' \
103
+ "#{@watch_retry_max_times} times yet still failing. Restarting."
104
+ log.error(message, e)
105
+ raise Fluent::UnrecoverableError, message
106
+ end
73
107
  end
74
108
  end
75
109
 
76
110
  def start_namespace_watch
77
- return get_namespaces_and_start_watcher
78
- rescue Exception => e
79
- message = "start_namespace_watch: Exception encountered setting up " \
111
+ get_namespaces_and_start_watcher
112
+ rescue StandardError => e
113
+ message = 'start_namespace_watch: Exception encountered setting up ' \
80
114
  "namespace watch from Kubernetes API #{@apiVersion} endpoint " \
81
115
  "#{@kubernetes_url}: #{e.message}"
82
116
  message += " (#{e.response})" if e.respond_to?(:response)
@@ -89,16 +123,20 @@ module KubernetesMetadata
89
123
  # starting from that resourceVersion.
90
124
  def get_namespaces_and_start_watcher
91
125
  options = {
92
- resource_version: '0' # Fetch from API server.
126
+ resource_version: '0' # Fetch from API server cache instead of etcd quorum read
93
127
  }
94
128
  namespaces = @client.get_namespaces(options)
95
- namespaces.each do |namespace|
96
- cache_key = namespace.metadata['uid']
129
+ namespaces[:items].each do |namespace|
130
+ cache_key = namespace[:metadata][:uid]
97
131
  @namespace_cache[cache_key] = parse_namespace_metadata(namespace)
98
132
  @stats.bump(:namespace_cache_host_updates)
99
133
  end
100
- options[:resource_version] = namespaces.resourceVersion
134
+
135
+ # continue watching from most recent resourceVersion
136
+ options[:resource_version] = namespaces[:metadata][:resourceVersion]
137
+
101
138
  watcher = @client.watch_namespaces(options)
139
+ reset_namespace_watch_retry_stats
102
140
  watcher
103
141
  end
104
142
 
@@ -112,36 +150,36 @@ module KubernetesMetadata
112
150
  # Process a watcher notice and potentially raise an exception.
113
151
  def process_namespace_watcher_notices(watcher)
114
152
  watcher.each do |notice|
115
- case notice.type
116
- when 'MODIFIED'
117
- reset_namespace_watch_retry_stats
118
- cache_key = notice.object['metadata']['uid']
119
- cached = @namespace_cache[cache_key]
120
- if cached
121
- @namespace_cache[cache_key] = parse_namespace_metadata(notice.object)
122
- @stats.bump(:namespace_cache_watch_updates)
123
- else
124
- @stats.bump(:namespace_cache_watch_misses)
125
- end
126
- when 'DELETED'
127
- reset_namespace_watch_retry_stats
128
- # ignore and let age out for cases where
129
- # deleted but still processing logs
130
- @stats.bump(:namespace_cache_watch_deletes_ignored)
131
- when 'ERROR'
132
- if notice.object && notice.object['code'] == 410
133
- @stats.bump(:namespace_watch_gone_notices)
134
- raise GoneError
135
- else
136
- @stats.bump(:namespace_watch_error_type_notices)
137
- message = notice['object']['message'] if notice['object'] && notice['object']['message']
138
- raise "Error while watching namespaces: #{message}"
139
- end
153
+ case notice[:type]
154
+ when 'MODIFIED'
155
+ reset_namespace_watch_retry_stats
156
+ cache_key = notice[:object][:metadata][:uid]
157
+ cached = @namespace_cache[cache_key]
158
+ if cached
159
+ @namespace_cache[cache_key] = parse_namespace_metadata(notice[:object])
160
+ @stats.bump(:namespace_cache_watch_updates)
161
+ else
162
+ @stats.bump(:namespace_cache_watch_misses)
163
+ end
164
+ when 'DELETED'
165
+ reset_namespace_watch_retry_stats
166
+ # ignore and let age out for cases where
167
+ # deleted but still processing logs
168
+ @stats.bump(:namespace_cache_watch_deletes_ignored)
169
+ when 'ERROR'
170
+ if notice[:object] && notice[:object][:code] == 410
171
+ @stats.bump(:namespace_watch_gone_notices)
172
+ raise GoneError
140
173
  else
141
- reset_namespace_watch_retry_stats
142
- # Don't pay attention to creations, since the created namespace may not
143
- # be used by any namespace on this node.
144
- @stats.bump(:namespace_cache_watch_ignored)
174
+ @stats.bump(:namespace_watch_error_type_notices)
175
+ message = notice[:object][:message] if notice[:object] && notice[:object][:message]
176
+ raise "Error while watching namespaces: #{message}"
177
+ end
178
+ else
179
+ reset_namespace_watch_retry_stats
180
+ # Don't pay attention to creations, since the created namespace may not
181
+ # be used by any namespace on this node.
182
+ @stats.bump(:namespace_cache_watch_ignored)
145
183
  end
146
184
  end
147
185
  end