-
Notifications
You must be signed in to change notification settings - Fork 600
/
manager.rb
477 lines (402 loc) · 15.5 KB
/
manager.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
# This file is distributed under New Relic's license terms.
# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
# frozen_string_literal: true
require 'forwardable'
require 'new_relic/agent/configuration/mask_defaults'
require 'new_relic/agent/configuration/yaml_source'
require 'new_relic/agent/configuration/default_source'
require 'new_relic/agent/configuration/server_source'
require 'new_relic/agent/configuration/environment_source'
require 'new_relic/agent/configuration/high_security_source'
require 'new_relic/agent/configuration/security_policy_source'
module NewRelic
module Agent
module Configuration
class Manager
DEPENDENCY_DETECTION_VALUES = %i[prepend chain unsatisfied].freeze
# Defining these explicitly saves object allocations that we incur
# if we use Forwardable and def_delegators.
def [](key)
@cache[key]
end
def has_key?(key)
@cache.has_key?(key)
end
def keys
@cache.keys
end
def initialize
reset_to_defaults
@callbacks = Hash.new { |hash, key| hash[key] = [] }
@lock = Mutex.new
end
def add_config_for_testing(source, level = 0)
raise 'Invalid config type for testing' unless [Hash, DottedHash].include?(source.class)
invoke_callbacks(:add, source)
@configs_for_testing << [source.freeze, level]
reset_cache
log_config(:add, source)
end
def remove_config_type(sym)
source = case sym
when :security_policy then @security_policy_source
when :high_security then @high_security_source
when :environment then @environment_source
when :server then @server_source
when :manual then @manual_source
when :yaml then @yaml_source
when :default then @default_source
end
remove_config(source)
end
def remove_config(source)
case source
when SecurityPolicySource then @security_policy_source = nil
when HighSecuritySource then @high_security_source = nil
when EnvironmentSource then @environment_source = nil
when ServerSource then @server_source = nil
when ManualSource then @manual_source = nil
when YamlSource then @yaml_source = nil
when DefaultSource then @default_source = nil
else
@configs_for_testing.delete_if { |src, lvl| src == source }
end
reset_cache
invoke_callbacks(:remove, source)
log_config(:remove, source)
end
def replace_or_add_config(source)
source.freeze
was_finished = finished_configuring?
invoke_callbacks(:add, source)
case source
when SecurityPolicySource then @security_policy_source = source
when HighSecuritySource then @high_security_source = source
when EnvironmentSource then @environment_source = source
when ServerSource then @server_source = source
when ManualSource then @manual_source = source
when YamlSource then @yaml_source = source
when DefaultSource then @default_source = source
else
NewRelic::Agent.logger.warn("Invalid config format; config will be ignored: #{source}")
end
reset_cache
log_config(:add, source)
notify_server_source_added if ServerSource === source
notify_finished_configuring if !was_finished && finished_configuring?
end
def source(key)
config_stack.each do |config|
if config.respond_to?(key.to_sym) || config.has_key?(key.to_sym)
return config
end
end
end
def fetch(key)
config_stack.each do |config|
next unless config
accessor = key.to_sym
if config.has_key?(accessor)
begin
return evaluate_and_apply_transformations(accessor, config[accessor])
rescue
next
end
end
end
nil
end
def evaluate_procs(value)
if value.respond_to?(:call)
instance_eval(&value)
else
value
end
end
def evaluate_and_apply_transformations(key, value)
evaluated = evaluate_procs(value)
default = enforce_allowlist(key, evaluated)
return default if default
boolean = enforce_boolean(key, value)
evaluated = boolean if [true, false].include?(boolean)
apply_transformations(key, evaluated)
end
def apply_transformations(key, value)
if transform = transform_from_default(key)
begin
transform.call(value)
rescue => e
NewRelic::Agent.logger.error("Error applying transformation for #{key}, pre-transform value was: #{value}.", e)
raise e
end
else
value
end
end
def enforce_allowlist(key, value)
return unless allowlist = default_source.allowlist_for(key)
return if allowlist.include?(value)
default = default_source.default_for(key)
NewRelic::Agent.logger.warn "Invalid value '#{value}' for #{key}, applying default value of '#{default}'"
default
end
def enforce_boolean(key, value)
type = default_source.value_from_defaults(key, :type)
return unless type == Boolean
bool_value = default_source.boolean_for(key, value)
return bool_value unless bool_value.nil?
default = default_source.default_for(key)
NewRelic::Agent.logger.warn "Invalid value '#{value}' for #{key}, applying default value of '#{default}'"
default
end
def transform_from_default(key)
default_source.transform_for(key)
end
def default_source
NewRelic::Agent::Configuration::DefaultSource
end
def register_callback(key, &proc)
@callbacks[key] << proc
yield(@cache[key])
end
def invoke_callbacks(direction, source)
return unless source
source.keys.each do |key|
begin
# we need to evaluate and apply transformations for the value to deal with procs as values
# this is usually done by the fetch method when accessing config, however the callbacks bypass that
evaluated_cache = evaluate_and_apply_transformations(key, @cache[key])
evaluated_source = evaluate_and_apply_transformations(key, source[key])
rescue
next
end
if evaluated_cache != evaluated_source
@callbacks[key].each do |proc|
if direction == :add
proc.call(evaluated_source)
else
proc.call(evaluated_cache)
end
end
end
end
end
# This event is intended to be fired every time the server source is
# applied. This happens after the agent's initial connect, and again
# on every forced reconnect.
def notify_server_source_added
NewRelic::Agent.instance.events.notify(:server_source_configuration_added)
end
# This event is intended to be fired once during the entire lifespan of
# an agent run, after the server source has been applied for the first
# time. This should indicate that all configuration has been applied,
# and the main functions of the agent are safe to start.
def notify_finished_configuring
NewRelic::Agent.instance.events.notify(:initial_configuration_complete)
end
def finished_configuring?
!@server_source.nil?
end
def flattened
config_stack.reverse.inject({}) do |flat, layer|
thawed_layer = layer.to_hash.dup
thawed_layer.each do |k, v|
begin
thawed_layer[k] = instance_eval(&v) if v.respond_to?(:call)
rescue => e
NewRelic::Agent.logger.debug("#{e.class.name} : #{e.message} - when accessing config key #{k}")
thawed_layer[k] = nil
end
thawed_layer.delete(:config)
end
flat.merge(thawed_layer.to_hash)
end
end
def apply_mask(hash)
MASK_DEFAULTS \
.select { |_, proc| proc.call } \
.each { |key, _| hash.delete(key) }
hash
end
def to_collector_hash
DottedHash.new(apply_mask(flattened)).to_hash.delete_if do |k, _v|
default = DEFAULTS[k]
if default
default[:exclude_from_reported_settings]
else
# In our tests, we add totally bogus configs, because testing.
# In those cases, there will be no default. So we'll just let
# them through.
false
end
end
end
MALFORMED_LABELS_WARNING = 'Skipping malformed labels configuration'
PARSING_LABELS_FAILURE = 'Failure during parsing labels. Ignoring and carrying on with connect.'
MAX_LABEL_COUNT = 64
MAX_LABEL_LENGTH = 255
def parsed_labels
case NewRelic::Agent.config[:labels]
when String
parse_labels_from_string
else
parse_labels_from_dictionary
end
rescue => e
NewRelic::Agent.logger.error(PARSING_LABELS_FAILURE, e)
NewRelic::EMPTY_ARRAY
end
def parse_labels_from_string
labels = NewRelic::Agent.config[:labels]
label_pairs = break_label_string_into_pairs(labels)
make_label_hash(label_pairs, labels)
end
def break_label_string_into_pairs(labels)
stripped_labels = labels.strip.sub(/^;*/, '').sub(/;*$/, '')
stripped_labels.split(';').map do |pair|
pair.split(':').map(&:strip)
end
end
def valid_label_pairs?(label_pairs)
label_pairs.all? do |pair|
pair.length == 2 &&
valid_label_item?(pair.first) &&
valid_label_item?(pair.last)
end
end
def valid_label_item?(item)
case item
when String then !item.empty?
when Numeric then true
when true then true
when false then true
else false
end
end
def make_label_hash(pairs, labels = nil)
# This can accept a hash, so force it down to an array of pairs first
pairs = Array(pairs)
unless valid_label_pairs?(pairs)
NewRelic::Agent.logger.warn("#{MALFORMED_LABELS_WARNING}: #{labels || pairs}")
return NewRelic::EMPTY_ARRAY
end
pairs = limit_number_of_labels(pairs)
pairs = remove_duplicates(pairs)
pairs.map do |key, value|
{
'label_type' => truncate(key),
'label_value' => truncate(value.to_s, key)
}
end
end
def truncate(text, key = nil)
if text.length > MAX_LABEL_LENGTH
if key
msg = "The value for the label '#{key}' is longer than the allowed #{MAX_LABEL_LENGTH} and will be truncated. Value = '#{text}'"
else
msg = "Label name longer than the allowed #{MAX_LABEL_LENGTH} will be truncated. Name = '#{text}'"
end
NewRelic::Agent.logger.warn(msg)
text[0..MAX_LABEL_LENGTH - 1]
else
text
end
end
def limit_number_of_labels(pairs)
if pairs.length > MAX_LABEL_COUNT
NewRelic::Agent.logger.warn("Too many labels defined. Only taking first #{MAX_LABEL_COUNT}")
pairs[0...64]
else
pairs
end
end
# We only take the last value provided for a given label type key
def remove_duplicates(pairs)
grouped_by_type = pairs.group_by(&:first)
grouped_by_type.values.map(&:last)
end
def parse_labels_from_dictionary
make_label_hash(NewRelic::Agent.config[:labels])
end
# Generally only useful during initial construction and tests
def reset_to_defaults
@security_policy_source = nil
@high_security_source = nil
@environment_source = EnvironmentSource.new
@server_source = nil
@manual_source = nil
@yaml_source = nil
@default_source = DefaultSource.new
@configs_for_testing = []
reset_cache
end
# reset the configuration hash, but do not replace previously auto
# determined dependency detection values with nil or 'auto'
def reset_cache
return new_cache unless defined?(@cache) && @cache
# Modifying the @cache hash under JRuby - even with a `synchronize do`
# block and a `Hash#dup` operation - has been known to cause issues
# with JRuby for concurrent access of the hash while it is being
# modified. The hash really only needs to be modified for the benefit
# of the security agent, so if JRuby is in play and the security agent
# is not, don't attempt to modify the hash at all and return early.
return new_cache if NewRelic::LanguageSupport.jruby? && !Agent.config[:'security.agent.enabled']
@lock.synchronize do
preserved = @cache.dup.select { |_k, v| DEPENDENCY_DETECTION_VALUES.include?(v) }
new_cache
preserved.each { |k, v| @cache[k] = v }
end
@cache
end
def new_cache
@cache = Hash.new { |hash, key| hash[key] = self.fetch(key) }
end
def log_config(direction, source)
# Just generating this log message (specifically calling `flattened`)
# is expensive enough that we don't want to do it unless we're
# actually going to be logging the message based on our current log
# level, so use a `do` block.
NewRelic::Agent.logger.debug do
hash = flattened.delete_if { |k, _h| DEFAULTS.fetch(k, {}).fetch(:exclude_from_reported_settings, false) }
"Updating config (#{direction}) from #{source.class}. Results: #{hash.inspect}"
end
end
def delete_all_configs_for_testing
@security_policy_source = nil
@high_security_source = nil
@environment_source = nil
@server_source = nil
@manual_source = nil
@yaml_source = nil
@default_source = nil
@configs_for_testing = []
end
def num_configs_for_testing
config_stack.size
end
def config_classes_for_testing
config_stack.map(&:class)
end
private
def config_stack
stack = [@security_policy_source,
@high_security_source,
@environment_source,
@server_source,
@manual_source,
@yaml_source,
@default_source]
stack.compact!
@configs_for_testing.each do |config, at_start|
if at_start
stack.insert(0, config)
else
stack.push(config)
end
end
stack
end
end
end
end
end