fluent-plugin-prometheus-test 1.7.0

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,95 @@
1
+ require 'fluent/plugin/input'
2
+ require 'fluent/plugin/in_monitor_agent'
3
+ require 'fluent/plugin/prometheus'
4
+
5
+ module Fluent::Plugin
6
+ class PrometheusTailMonitorInput < Fluent::Plugin::Input
7
+ Fluent::Plugin.register_input('prometheus_tail_monitor', self)
8
+ include Fluent::Plugin::PrometheusLabelParser
9
+
10
+ helpers :timer
11
+
12
+ config_param :interval, :time, default: 5
13
+ attr_reader :registry
14
+
15
+ MONITOR_IVARS = [
16
+ :tails,
17
+ ]
18
+
19
+ def initialize
20
+ super
21
+ @registry = ::Prometheus::Client.registry
22
+ end
23
+
24
+ def multi_workers_ready?
25
+ true
26
+ end
27
+
28
+ def configure(conf)
29
+ super
30
+ hostname = Socket.gethostname
31
+ expander_builder = Fluent::Plugin::Prometheus.placeholder_expander(log)
32
+ expander = expander_builder.build({ 'hostname' => hostname, 'worker_id' => fluentd_worker_id })
33
+ @base_labels = parse_labels_elements(conf)
34
+ @base_labels.each do |key, value|
35
+ unless value.is_a?(String)
36
+ raise Fluent::ConfigError, "record accessor syntax is not available in prometheus_tail_monitor"
37
+ end
38
+ @base_labels[key] = expander.expand(value)
39
+ end
40
+
41
+ if defined?(Fluent::Plugin) && defined?(Fluent::Plugin::MonitorAgentInput)
42
+ # from v0.14.6
43
+ @monitor_agent = Fluent::Plugin::MonitorAgentInput.new
44
+ else
45
+ @monitor_agent = Fluent::MonitorAgentInput.new
46
+ end
47
+ end
48
+
49
+ def start
50
+ super
51
+
52
+ @metrics = {
53
+ position: @registry.gauge(
54
+ :fluentd_tail_file_position,
55
+ 'Current position of file.'),
56
+ inode: @registry.gauge(
57
+ :fluentd_tail_file_inode,
58
+ 'Current inode of file.'),
59
+ }
60
+ timer_execute(:in_prometheus_tail_monitor, @interval, &method(:update_monitor_info))
61
+ end
62
+
63
+ def update_monitor_info
64
+ opts = {
65
+ ivars: MONITOR_IVARS,
66
+ }
67
+
68
+ agent_info = @monitor_agent.plugins_info_all(opts).select {|info|
69
+ info['type'] == 'tail'.freeze
70
+ }
71
+
72
+ agent_info.each do |info|
73
+ tails = info['instance_variables'][:tails]
74
+ next if tails.nil?
75
+
76
+ tails.clone.each do |_, watcher|
77
+ # Access to internal variable of internal class...
78
+ # Very fragile implementation
79
+ pe = watcher.instance_variable_get(:@pe)
80
+ label = labels(info, watcher.path)
81
+ @metrics[:inode].set(label, pe.read_inode)
82
+ @metrics[:position].set(label, pe.read_pos)
83
+ end
84
+ end
85
+ end
86
+
87
+ def labels(plugin_info, path)
88
+ @base_labels.merge(
89
+ plugin_id: plugin_info["plugin_id"],
90
+ type: plugin_info["type"],
91
+ path: path,
92
+ )
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,29 @@
1
+ require 'fluent/plugin/output'
2
+ require 'fluent/plugin/prometheus'
3
+
4
+ module Fluent::Plugin
5
+ class PrometheusOutput < Fluent::Plugin::Output
6
+ Fluent::Plugin.register_output('prometheus', self)
7
+ include Fluent::Plugin::PrometheusLabelParser
8
+ include Fluent::Plugin::Prometheus
9
+
10
+ def initialize
11
+ super
12
+ @registry = ::Prometheus::Client.registry
13
+ end
14
+
15
+ def multi_workers_ready?
16
+ true
17
+ end
18
+
19
+ def configure(conf)
20
+ super
21
+ labels = parse_labels_elements(conf)
22
+ @metrics = Fluent::Plugin::Prometheus.parse_metrics_elements(conf, @registry, labels)
23
+ end
24
+
25
+ def process(tag, es)
26
+ instrument(tag, es, @metrics)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,296 @@
1
+ require 'prometheus/client'
2
+ require 'prometheus/client/formats/text'
3
+ require 'fluent/plugin/prometheus/placeholder_expander'
4
+
5
+ module Fluent
6
+ module Plugin
7
+ module PrometheusLabelParser
8
+ def configure(conf)
9
+ super
10
+ # Check if running with multiple workers
11
+ sysconf = if self.respond_to?(:owner) && owner.respond_to?(:system_config)
12
+ owner.system_config
13
+ elsif self.respond_to?(:system_config)
14
+ self.system_config
15
+ else
16
+ nil
17
+ end
18
+ @multi_worker = sysconf && sysconf.workers ? (sysconf.workers > 1) : false
19
+ end
20
+
21
+ def parse_labels_elements(conf)
22
+ base_labels = Fluent::Plugin::Prometheus.parse_labels_elements(conf)
23
+
24
+ if @multi_worker
25
+ base_labels[:worker_id] = fluentd_worker_id.to_s
26
+ end
27
+
28
+ base_labels
29
+ end
30
+ end
31
+
32
+ module Prometheus
33
+ class AlreadyRegisteredError < StandardError; end
34
+
35
+ def self.parse_labels_elements(conf)
36
+ labels = conf.elements.select { |e| e.name == 'labels' }
37
+ if labels.size > 1
38
+ raise ConfigError, "labels section must have at most 1"
39
+ end
40
+
41
+ base_labels = {}
42
+ unless labels.empty?
43
+ labels.first.each do |key, value|
44
+ labels.first.has_key?(key)
45
+
46
+ # use RecordAccessor only for $. and $[ syntax
47
+ # otherwise use the value as is or expand the value by RecordTransformer for ${} syntax
48
+ if value.start_with?('$.') || value.start_with?('$[')
49
+ base_labels[key.to_sym] = PluginHelper::RecordAccessor::Accessor.new(value)
50
+ else
51
+ base_labels[key.to_sym] = value
52
+ end
53
+ end
54
+ end
55
+
56
+ base_labels
57
+ end
58
+
59
+ def self.parse_metrics_elements(conf, registry, labels = {})
60
+ metrics = []
61
+ conf.elements.select { |element|
62
+ element.name == 'metric'
63
+ }.each { |element|
64
+ if element.has_key?('key') && (element['key'].start_with?('$.') || element['key'].start_with?('$['))
65
+ value = element['key']
66
+ element['key'] = PluginHelper::RecordAccessor::Accessor.new(value)
67
+ end
68
+ case element['type']
69
+ when 'summary'
70
+ metrics << Fluent::Plugin::Prometheus::Summary.new(element, registry, labels)
71
+ when 'gauge'
72
+ metrics << Fluent::Plugin::Prometheus::Gauge.new(element, registry, labels)
73
+ when 'counter'
74
+ metrics << Fluent::Plugin::Prometheus::Counter.new(element, registry, labels)
75
+ when 'histogram'
76
+ metrics << Fluent::Plugin::Prometheus::Histogram.new(element, registry, labels)
77
+ else
78
+ raise ConfigError, "type option must be 'counter', 'gauge', 'summary' or 'histogram'"
79
+ end
80
+ }
81
+ metrics
82
+ end
83
+
84
+ def self.placeholder_expander(log)
85
+ Fluent::Plugin::Prometheus::ExpandBuilder.new(log: log)
86
+ end
87
+
88
+ def configure(conf)
89
+ super
90
+ @placeholder_values = {}
91
+ @placeholder_expander_builder = Fluent::Plugin::Prometheus.placeholder_expander(log)
92
+ @hostname = Socket.gethostname
93
+ end
94
+
95
+ def instrument_single(tag, time, record, metrics)
96
+ @placeholder_values[tag] ||= {
97
+ 'tag' => tag,
98
+ 'hostname' => @hostname,
99
+ 'worker_id' => fluentd_worker_id,
100
+ }
101
+
102
+ placeholders = record.merge(@placeholder_values[tag])
103
+ expander = @placeholder_expander_builder.build(placeholders)
104
+ metrics.each do |metric|
105
+ begin
106
+ metric.instrument(record, expander)
107
+ rescue => e
108
+ log.warn "prometheus: failed to instrument a metric.", error_class: e.class, error: e, tag: tag, name: metric.name
109
+ router.emit_error_event(tag, time, record, e)
110
+ end
111
+ end
112
+ end
113
+
114
+ def instrument(tag, es, metrics)
115
+ placeholder_values = {
116
+ 'tag' => tag,
117
+ 'hostname' => @hostname,
118
+ 'worker_id' => fluentd_worker_id,
119
+ }
120
+
121
+ es.each do |time, record|
122
+ placeholders = record.merge(placeholder_values)
123
+ expander = @placeholder_expander_builder.build(placeholders)
124
+ metrics.each do |metric|
125
+ begin
126
+ metric.instrument(record, expander)
127
+ rescue => e
128
+ log.warn "prometheus: failed to instrument a metric.", error_class: e.class, error: e, tag: tag, name: metric.name
129
+ router.emit_error_event(tag, time, record, e)
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ class Metric
136
+ attr_reader :type
137
+ attr_reader :name
138
+ attr_reader :key
139
+ attr_reader :desc
140
+
141
+ def initialize(element, registry, labels)
142
+ ['name', 'desc'].each do |key|
143
+ if element[key].nil?
144
+ raise ConfigError, "metric requires '#{key}' option"
145
+ end
146
+ end
147
+ @type = element['type']
148
+ @name = element['name']
149
+ @key = element['key']
150
+ @desc = element['desc']
151
+
152
+ @base_labels = Fluent::Plugin::Prometheus.parse_labels_elements(element)
153
+ @base_labels = labels.merge(@base_labels)
154
+ end
155
+
156
+ def labels(record, expander)
157
+ label = {}
158
+ @base_labels.each do |k, v|
159
+ if v.is_a?(String)
160
+ label[k] = expander.expand(v)
161
+ else
162
+ label[k] = v.call(record)
163
+ end
164
+ end
165
+ label
166
+ end
167
+
168
+ def self.get(registry, name, type, docstring)
169
+ metric = registry.get(name)
170
+
171
+ # should have same type, docstring
172
+ if metric.type != type
173
+ raise AlreadyRegisteredError, "#{name} has already been registered as #{type} type"
174
+ end
175
+ if metric.docstring != docstring
176
+ raise AlreadyRegisteredError, "#{name} has already been registered with different docstring"
177
+ end
178
+
179
+ metric
180
+ end
181
+ end
182
+
183
+ class Gauge < Metric
184
+ def initialize(element, registry, labels)
185
+ super
186
+ if @key.nil?
187
+ raise ConfigError, "gauge metric requires 'key' option"
188
+ end
189
+
190
+ begin
191
+ @gauge = registry.gauge(element['name'].to_sym, element['desc'])
192
+ rescue ::Prometheus::Client::Registry::AlreadyRegisteredError
193
+ @gauge = Fluent::Plugin::Prometheus::Metric.get(registry, element['name'].to_sym, :gauge, element['desc'])
194
+ end
195
+ end
196
+
197
+ def instrument(record, expander)
198
+ if @key.is_a?(String)
199
+ value = record[@key]
200
+ else
201
+ value = @key.call(record)
202
+ end
203
+ if value
204
+ @gauge.set(labels(record, expander), value)
205
+ end
206
+ end
207
+ end
208
+
209
+ class Counter < Metric
210
+ def initialize(element, registry, labels)
211
+ super
212
+ begin
213
+ @counter = registry.counter(element['name'].to_sym, element['desc'])
214
+ rescue ::Prometheus::Client::Registry::AlreadyRegisteredError
215
+ @counter = Fluent::Plugin::Prometheus::Metric.get(registry, element['name'].to_sym, :counter, element['desc'])
216
+ end
217
+ end
218
+
219
+ def instrument(record, expander)
220
+ # use record value of the key if key is specified, otherwise just increment
221
+ if @key.nil?
222
+ value = 1
223
+ elsif @key.is_a?(String)
224
+ value = record[@key]
225
+ else
226
+ value = @key.call(record)
227
+ end
228
+
229
+ # ignore if record value is nil
230
+ return if value.nil?
231
+
232
+ @counter.increment(labels(record, expander), value)
233
+ end
234
+ end
235
+
236
+ class Summary < Metric
237
+ def initialize(element, registry, labels)
238
+ super
239
+ if @key.nil?
240
+ raise ConfigError, "summary metric requires 'key' option"
241
+ end
242
+
243
+ begin
244
+ @summary = registry.summary(element['name'].to_sym, element['desc'])
245
+ rescue ::Prometheus::Client::Registry::AlreadyRegisteredError
246
+ @summary = Fluent::Plugin::Prometheus::Metric.get(registry, element['name'].to_sym, :summary, element['desc'])
247
+ end
248
+ end
249
+
250
+ def instrument(record, expander)
251
+ if @key.is_a?(String)
252
+ value = record[@key]
253
+ else
254
+ value = @key.call(record)
255
+ end
256
+ if value
257
+ @summary.observe(labels(record, expander), value)
258
+ end
259
+ end
260
+ end
261
+
262
+ class Histogram < Metric
263
+ def initialize(element, registry, labels)
264
+ super
265
+ if @key.nil?
266
+ raise ConfigError, "histogram metric requires 'key' option"
267
+ end
268
+
269
+ begin
270
+ if element['buckets']
271
+ buckets = element['buckets'].split(/,/).map(&:strip).map do |e|
272
+ e[/\A\d+.\d+\Z/] ? e.to_f : e.to_i
273
+ end
274
+ @histogram = registry.histogram(element['name'].to_sym, element['desc'], {}, buckets)
275
+ else
276
+ @histogram = registry.histogram(element['name'].to_sym, element['desc'])
277
+ end
278
+ rescue ::Prometheus::Client::Registry::AlreadyRegisteredError
279
+ @histogram = Fluent::Plugin::Prometheus::Metric.get(registry, element['name'].to_sym, :histogram, element['desc'])
280
+ end
281
+ end
282
+
283
+ def instrument(record, expander)
284
+ if @key.is_a?(String)
285
+ value = record[@key]
286
+ else
287
+ value = @key.call(record)
288
+ end
289
+ if value
290
+ @histogram.observe(labels(record, expander), value)
291
+ end
292
+ end
293
+ end
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,132 @@
1
+ module Fluent
2
+ module Plugin
3
+ module Prometheus
4
+ class ExpandBuilder
5
+ def self.build(placeholder, log:)
6
+ new(log: log).build(placeholder)
7
+ end
8
+
9
+ def initialize(log:)
10
+ @log = log
11
+ end
12
+
13
+ def build(placeholder_values)
14
+ placeholders = {}
15
+ placeholder_values.each do |key, value|
16
+ case value
17
+ when Array
18
+ size = value.size
19
+ value.each_with_index do |v, i|
20
+ placeholders["${#{key}[#{i}]}"] = v
21
+ placeholders["${#{key}[#{i - size}]}"] = v
22
+ end
23
+ when Hash
24
+ value.each do |k, v|
25
+ placeholders[%(${#{key}["#{k}"]})] = v
26
+ end
27
+ else
28
+ if key == 'tag'
29
+ placeholders.merge!(build_tag(value))
30
+ else
31
+ placeholders["${#{key}}"] = value
32
+ end
33
+ end
34
+ end
35
+
36
+ Fluent::Plugin::Prometheus::ExpandBuilder::PlaceholderExpander.new(@log, placeholders)
37
+ end
38
+
39
+ private
40
+
41
+ def build_tag(tag)
42
+ tags = tag.split('.')
43
+
44
+ placeholders = { '${tag}' => tag }
45
+
46
+ size = tags.size
47
+
48
+ tags.each_with_index do |v, i|
49
+ placeholders["${tag_parts[#{i}]}"] = v
50
+ placeholders["${tag_parts[#{i - size}]}"] = v
51
+ end
52
+
53
+ tag_prefix(tags).each_with_index do |v, i|
54
+ placeholders["${tag_prefix[#{i}]}"] = v
55
+ end
56
+
57
+ tag_suffix(tags).each_with_index do |v, i|
58
+ placeholders["${tag_suffix[#{i}]}"] = v
59
+ end
60
+
61
+ placeholders
62
+ end
63
+
64
+ def tag_prefix(tags)
65
+ tags = tags.dup
66
+ return [] if tags.empty?
67
+
68
+ ret = [tags.shift]
69
+ tags.each.with_index(1) do |tag, i|
70
+ ret[i] = "#{ret[i-1]}.#{tag}"
71
+ end
72
+ ret
73
+ end
74
+
75
+ def tag_suffix(tags)
76
+ return [] if tags.empty?
77
+
78
+ tags = tags.dup.reverse
79
+ ret = [tags.shift]
80
+ tags.each.with_index(1) do |tag, i|
81
+ ret[i] = "#{tag}.#{ret[i-1]}"
82
+ end
83
+ ret
84
+ end
85
+
86
+ class PlaceholderExpander
87
+ PLACEHOLDER_REGEX = /(\${[^\[}]+(\[[^\]]+\])?})/.freeze
88
+
89
+ attr_reader :placeholder
90
+
91
+ def initialize(log, placeholder)
92
+ @placeholder = placeholder
93
+ @log = log
94
+ @expander_cache = {}
95
+ end
96
+
97
+ def merge_placeholder(placeholder)
98
+ @placeholder.merge!(placeholder)
99
+ end
100
+
101
+ def expand(str, dynamic_placeholders: nil)
102
+ expander = if dynamic_placeholders
103
+ if @expander_cache[dynamic_placeholders]
104
+ @expander_cache[dynamic_placeholders]
105
+ else
106
+ e = ExpandBuilder.build(dynamic_placeholders, log: @log)
107
+ e.merge_placeholder(@placeholder)
108
+ @expander_cache[dynamic_placeholders] = e
109
+ e
110
+ end
111
+ else
112
+ self
113
+ end
114
+
115
+ expander.expand!(str)
116
+ end
117
+
118
+ protected
119
+
120
+ def expand!(str)
121
+ str.gsub(PLACEHOLDER_REGEX) { |value|
122
+ @placeholder.fetch(value) do
123
+ @log.warn("unknown placeholder `#{value}` found")
124
+ value # return as it is
125
+ end
126
+ }
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end