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.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rspec +2 -0
- data/.travis.yml +14 -0
- data/Gemfile +4 -0
- data/LICENSE +202 -0
- data/README.md +484 -0
- data/Rakefile +7 -0
- data/fluent-plugin-prometheus.gemspec +22 -0
- data/lib/fluent/plugin/filter_prometheus.rb +30 -0
- data/lib/fluent/plugin/in_prometheus.rb +222 -0
- data/lib/fluent/plugin/in_prometheus_monitor.rb +99 -0
- data/lib/fluent/plugin/in_prometheus_output_monitor.rb +202 -0
- data/lib/fluent/plugin/in_prometheus_tail_monitor.rb +95 -0
- data/lib/fluent/plugin/out_prometheus.rb +29 -0
- data/lib/fluent/plugin/prometheus.rb +296 -0
- data/lib/fluent/plugin/prometheus/placeholder_expander.rb +132 -0
- data/lib/fluent/plugin/prometheus_metrics.rb +77 -0
- data/misc/fluentd_sample.conf +170 -0
- data/misc/nginx_proxy.conf +22 -0
- data/misc/prometheus.yaml +13 -0
- data/misc/prometheus_alerts.yaml +59 -0
- data/spec/fluent/plugin/filter_prometheus_spec.rb +48 -0
- data/spec/fluent/plugin/in_prometheus_monitor_spec.rb +42 -0
- data/spec/fluent/plugin/in_prometheus_spec.rb +225 -0
- data/spec/fluent/plugin/in_prometheus_tail_monitor_spec.rb +42 -0
- data/spec/fluent/plugin/out_prometheus_spec.rb +43 -0
- data/spec/fluent/plugin/prometheus/placeholder_expander_spec.rb +110 -0
- data/spec/fluent/plugin/prometheus_metrics_spec.rb +138 -0
- data/spec/fluent/plugin/shared.rb +249 -0
- data/spec/spec_helper.rb +10 -0
- metadata +173 -0
@@ -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
|