fluent-plugin-kubernetes_metadata_filter-rh 2.6.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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +57 -0
  3. data/.gitignore +19 -0
  4. data/.rubocop.yml +57 -0
  5. data/Gemfile +9 -0
  6. data/Gemfile.lock +156 -0
  7. data/LICENSE.txt +201 -0
  8. data/README.md +253 -0
  9. data/Rakefile +41 -0
  10. data/fluent-plugin-kubernetes_metadata_filter.gemspec +34 -0
  11. data/lib/fluent/plugin/filter_kubernetes_metadata.rb +378 -0
  12. data/lib/fluent/plugin/kubernetes_metadata_cache_strategy.rb +102 -0
  13. data/lib/fluent/plugin/kubernetes_metadata_common.rb +120 -0
  14. data/lib/fluent/plugin/kubernetes_metadata_stats.rb +46 -0
  15. data/lib/fluent/plugin/kubernetes_metadata_util.rb +40 -0
  16. data/lib/fluent/plugin/kubernetes_metadata_watch_namespaces.rb +154 -0
  17. data/lib/fluent/plugin/kubernetes_metadata_watch_pods.rb +172 -0
  18. data/test/cassettes/invalid_api_server_config.yml +53 -0
  19. data/test/cassettes/kubernetes_docker_metadata_annotations.yml +205 -0
  20. data/test/cassettes/kubernetes_docker_metadata_dotted_labels.yml +197 -0
  21. data/test/cassettes/kubernetes_get_api_v1.yml +193 -0
  22. data/test/cassettes/kubernetes_get_api_v1_using_token.yml +195 -0
  23. data/test/cassettes/kubernetes_get_namespace_default.yml +69 -0
  24. data/test/cassettes/kubernetes_get_namespace_default_using_token.yml +71 -0
  25. data/test/cassettes/kubernetes_get_pod.yml +146 -0
  26. data/test/cassettes/kubernetes_get_pod_using_token.yml +148 -0
  27. data/test/cassettes/metadata_from_tag_and_journald_fields.yml +153 -0
  28. data/test/cassettes/metadata_from_tag_journald_and_kubernetes_fields.yml +285 -0
  29. data/test/cassettes/valid_kubernetes_api_server.yml +55 -0
  30. data/test/cassettes/valid_kubernetes_api_server_using_token.yml +57 -0
  31. data/test/helper.rb +82 -0
  32. data/test/plugin/test.token +1 -0
  33. data/test/plugin/test_cache_stats.rb +33 -0
  34. data/test/plugin/test_cache_strategy.rb +194 -0
  35. data/test/plugin/test_filter_kubernetes_metadata.rb +1012 -0
  36. data/test/plugin/test_utils.rb +56 -0
  37. data/test/plugin/test_watch_namespaces.rb +245 -0
  38. data/test/plugin/test_watch_pods.rb +344 -0
  39. data/test/plugin/watch_test.rb +74 -0
  40. metadata +269 -0
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Fluentd Kubernetes Metadata Filter Plugin - Enrich Fluentd events with
5
+ # Kubernetes metadata
6
+ #
7
+ # Copyright 2017 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 CacheStrategy
23
+ def get_pod_metadata(key, namespace_name, pod_name, record_create_time, batch_miss_cache)
24
+ metadata = {}
25
+ ids = @id_cache[key]
26
+ if ids.nil?
27
+ # FAST PATH
28
+ # Cache hit, fetch metadata from the cache
29
+ @stats.bump(:id_cache_miss)
30
+ return batch_miss_cache["#{namespace_name}_#{pod_name}"] if batch_miss_cache.key?("#{namespace_name}_#{pod_name}")
31
+
32
+ pod_metadata = fetch_pod_metadata(namespace_name, pod_name)
33
+ if @skip_namespace_metadata
34
+ ids = { pod_id: pod_metadata['pod_id'] }
35
+ @id_cache[key] = ids
36
+ return pod_metadata
37
+ end
38
+ namespace_metadata = fetch_namespace_metadata(namespace_name)
39
+ ids = { pod_id: pod_metadata['pod_id'], namespace_id: namespace_metadata['namespace_id'] }
40
+ if !ids[:pod_id].nil? && !ids[:namespace_id].nil?
41
+ # pod found and namespace found
42
+ metadata = pod_metadata
43
+ metadata.merge!(namespace_metadata)
44
+ else
45
+ if ids[:pod_id].nil? && !ids[:namespace_id].nil?
46
+ # pod not found, but namespace found
47
+ @stats.bump(:id_cache_pod_not_found_namespace)
48
+ ns_time = Time.parse(namespace_metadata['creation_timestamp'])
49
+ if ns_time <= record_create_time
50
+ # namespace is older then record for pod
51
+ ids[:pod_id] = key
52
+ metadata = @cache.fetch(ids[:pod_id]) do
53
+ { 'pod_id' => ids[:pod_id] }
54
+ end
55
+ end
56
+ metadata.merge!(namespace_metadata)
57
+ else
58
+ if !ids[:pod_id].nil? && ids[:namespace_id].nil?
59
+ # pod found, but namespace NOT found
60
+ # this should NEVER be possible since pod meta can
61
+ # only be retrieved with a namespace
62
+ @stats.bump(:id_cache_namespace_not_found_pod)
63
+ else
64
+ # nothing found
65
+ @stats.bump(:id_cache_orphaned_record)
66
+ end
67
+ if @allow_orphans
68
+ log.trace("orphaning message for: #{namespace_name}/#{pod_name} ") if log.trace?
69
+ metadata = {
70
+ 'orphaned_namespace' => namespace_name,
71
+ 'namespace_name' => @orphaned_namespace_name,
72
+ 'namespace_id' => @orphaned_namespace_id
73
+ }
74
+ else
75
+ metadata = {}
76
+ end
77
+ batch_miss_cache["#{namespace_name}_#{pod_name}"] = metadata
78
+ end
79
+ end
80
+ @id_cache[key] = ids unless batch_miss_cache.key?("#{namespace_name}_#{pod_name}")
81
+ else
82
+ # SLOW PATH
83
+ metadata = @cache.fetch(ids[:pod_id]) do
84
+ @stats.bump(:pod_cache_miss)
85
+ m = fetch_pod_metadata(namespace_name, pod_name)
86
+ m.nil? || m.empty? ? { 'pod_id' => ids[:pod_id] } : m
87
+ end
88
+ metadata.merge!(@namespace_cache.fetch(ids[:namespace_id]) do
89
+ m = unless @skip_namespace_metadata
90
+ @stats.bump(:namespace_cache_miss)
91
+ fetch_namespace_metadata(namespace_name)
92
+ end
93
+ m.nil? || m.empty? ? { 'namespace_id' => ids[:namespace_id] } : m
94
+ end)
95
+ end
96
+
97
+ # remove namespace info that is only used for comparison
98
+ metadata.delete('creation_timestamp')
99
+ metadata.delete_if { |_k, v| v.nil? }
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Fluentd Kubernetes Metadata Filter Plugin - Enrich Fluentd events with
5
+ # Kubernetes metadata
6
+ #
7
+ # Copyright 2017 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 Common
23
+ class GoneError < StandardError
24
+ def initialize(msg = '410 Gone')
25
+ super
26
+ end
27
+ end
28
+
29
+ def match_annotations(annotations)
30
+ result = {}
31
+ @annotations_regexps.each do |regexp|
32
+ annotations.each do |key, value|
33
+ if ::Fluent::StringUtil.match_regexp(regexp, key.to_s)
34
+ result[key] = value
35
+ end
36
+ end
37
+ end
38
+ result
39
+ end
40
+
41
+ def parse_namespace_metadata(namespace_object)
42
+ labels = ''
43
+ labels = syms_to_strs(namespace_object[:metadata][:labels].to_h) unless @skip_labels
44
+
45
+ annotations = match_annotations(syms_to_strs(namespace_object[:metadata][:annotations].to_h))
46
+ if @de_dot
47
+ de_dot!(labels) unless @skip_labels
48
+ de_dot!(annotations)
49
+ end
50
+ kubernetes_metadata = {
51
+ 'namespace_id' => namespace_object[:metadata][:uid],
52
+ 'creation_timestamp' => namespace_object[:metadata][:creationTimestamp]
53
+ }
54
+ kubernetes_metadata['namespace_labels'] = labels unless labels.empty?
55
+ kubernetes_metadata['namespace_annotations'] = annotations unless annotations.empty?
56
+ kubernetes_metadata
57
+ end
58
+
59
+ def parse_pod_metadata(pod_object)
60
+ labels = ''
61
+ labels = syms_to_strs(pod_object[:metadata][:labels].to_h) unless @skip_labels
62
+
63
+ annotations = match_annotations(syms_to_strs(pod_object[:metadata][:annotations].to_h))
64
+ if @de_dot
65
+ de_dot!(labels) unless @skip_labels
66
+ de_dot!(annotations)
67
+ end
68
+
69
+ # collect container information
70
+ container_meta = {}
71
+ begin
72
+ pod_object[:status][:containerStatuses].each do |container_status|
73
+ # get plain container id (eg. docker://hash -> hash)
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
86
+ end
87
+ rescue StandardError
88
+ log.debug("parsing container meta information failed for: #{pod_object[:metadata][:namespace]}/#{pod_object[:metadata][:name]} ")
89
+ end
90
+
91
+ kubernetes_metadata = {
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]
98
+ }
99
+ kubernetes_metadata['annotations'] = annotations unless annotations.empty?
100
+ kubernetes_metadata['labels'] = labels unless labels.empty?
101
+ kubernetes_metadata['master_url'] = @kubernetes_url unless @skip_master_url
102
+ kubernetes_metadata
103
+ end
104
+
105
+ def syms_to_strs(hsh)
106
+ newhsh = {}
107
+ hsh.each_pair do |kk, vv|
108
+ if vv.is_a?(Hash)
109
+ vv = syms_to_strs(vv)
110
+ end
111
+ if kk.is_a?(Symbol)
112
+ newhsh[kk.to_s] = vv
113
+ else
114
+ newhsh[kk] = vv
115
+ end
116
+ end
117
+ newhsh
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Fluentd Kubernetes Metadata Filter Plugin - Enrich Fluentd events with
5
+ # Kubernetes metadata
6
+ #
7
+ # Copyright 2017 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 'lru_redux'
22
+ module KubernetesMetadata
23
+ class Stats
24
+ def initialize
25
+ @stats = ::LruRedux::TTL::ThreadSafeCache.new(1000, 3600)
26
+ end
27
+
28
+ def bump(key)
29
+ @stats[key] = @stats.getset(key) { 0 } + 1
30
+ end
31
+
32
+ def set(key, value)
33
+ @stats[key] = value
34
+ end
35
+
36
+ def [](key)
37
+ @stats[key]
38
+ end
39
+
40
+ def to_s
41
+ 'stats - ' + [].tap do |a|
42
+ @stats.each { |k, v| a << "#{k}: #{v}" }
43
+ end.join(', ')
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,40 @@
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
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Fluentd Kubernetes Metadata Filter Plugin - Enrich Fluentd events with
5
+ # Kubernetes metadata
6
+ #
7
+ # Copyright 2017 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
+ # TODO: this is mostly copy-paste from kubernetes_metadata_watch_pods.rb unify them
22
+ require_relative 'kubernetes_metadata_common'
23
+
24
+ module KubernetesMetadata
25
+ module WatchNamespaces
26
+ include ::KubernetesMetadata::Common
27
+
28
+ def set_up_namespace_thread
29
+ # Any failures / exceptions in the initial setup should raise
30
+ # Fluent:ConfigError, so that users can inspect potential errors in
31
+ # the configuration.
32
+ namespace_watcher = start_namespace_watch
33
+ Thread.current[:namespace_watch_retry_backoff_interval] = @watch_retry_interval
34
+ Thread.current[:namespace_watch_retry_count] = 0
35
+
36
+ # Any failures / exceptions in the followup watcher notice
37
+ # processing will be swallowed and retried. These failures /
38
+ # exceptions could be caused by Kubernetes API being temporarily
39
+ # down. We assume the configuration is correct at this point.
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
73
+ end
74
+ end
75
+ end
76
+
77
+ def start_namespace_watch
78
+ get_namespaces_and_start_watcher
79
+ rescue StandardError => e
80
+ message = 'start_namespace_watch: Exception encountered setting up ' \
81
+ "namespace watch from Kubernetes API #{@apiVersion} endpoint " \
82
+ "#{@kubernetes_url}: #{e.message}"
83
+ message += " (#{e.response})" if e.respond_to?(:response)
84
+ log.debug(message)
85
+
86
+ raise Fluent::ConfigError, message
87
+ end
88
+
89
+ # List all namespaces, record the resourceVersion and return a watcher
90
+ # starting from that resourceVersion.
91
+ def get_namespaces_and_start_watcher
92
+ options = {
93
+ resource_version: '0' # Fetch from API server cache instead of etcd quorum read
94
+ }
95
+ namespaces = @client.get_namespaces(options)
96
+ namespaces[:items].each do |namespace|
97
+ cache_key = namespace[:metadata][:uid]
98
+ @namespace_cache[cache_key] = parse_namespace_metadata(namespace)
99
+ @stats.bump(:namespace_cache_host_updates)
100
+ end
101
+
102
+ # continue watching from most recent resourceVersion
103
+ options[:resource_version] = namespaces[:metadata][:resourceVersion]
104
+
105
+ watcher = @client.watch_namespaces(options)
106
+ reset_namespace_watch_retry_stats
107
+ watcher
108
+ end
109
+
110
+ # Reset namespace watch retry count and backoff interval as there is a
111
+ # successful watch notice.
112
+ def reset_namespace_watch_retry_stats
113
+ Thread.current[:namespace_watch_retry_count] = 0
114
+ Thread.current[:namespace_watch_retry_backoff_interval] = @watch_retry_interval
115
+ end
116
+
117
+ # Process a watcher notice and potentially raise an exception.
118
+ def process_namespace_watcher_notices(watcher)
119
+ watcher.each do |notice|
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
141
+ @stats.bump(:namespace_watch_error_type_notices)
142
+ message = notice[:object][:message] if notice[:object] && notice[:object][:message]
143
+ raise "Error while watching namespaces: #{message}"
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)
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end