Class: NewRelic::Agent::Configuration::Manager

Inherits:
Object
  • Object
show all
Defined in:
lib/new_relic/agent/configuration/manager.rb

Constant Summary collapse

DEPENDENCY_DETECTION_VALUES =
i[prepend chain unsatisfied].freeze
BOOLEAN_MAP =
{
  'true' => true,
  'yes' => true,
  'on' => true,
  'false' => false,
  'no' => false,
  'off' => false
}.freeze
INSTRUMENTATION_VALUES =
%w[chain prepend unsatisfied]
INSTRUMENTATION_DISABLED_VALUES =
%w[false no off].freeze
NUMERIC_TYPES =
[Integer, Float]
STRINGLIKE_TYPES =
[String, Symbol]
TYPE_COERCIONS =
{Integer => {pattern: /^\d+$/, proc: proc { |s| s.to_i }},
                          Float => {pattern: /^\d+\.\d+$/, proc: proc { |s| s.to_f }},
                          Array => {proc: proc { |s| s.split(/\s*,\s*/) }},
                          Hash => {proc: proc { |s| s.split(/\s*,\s*/).each_with_object({}) { |i, h| k, v = i.split(/\s*=\s*/); h[k] = v } }},
                          NewRelic::Agent::Configuration::Boolean => {pattern: /^(?:#{BOOLEAN_MAP.keys.join('|')})$/,
proc: proc { |s| BOOLEAN_MAP[s] }}}.freeze
USER_CONFIG_CLASSES =
[NewRelic::Agent::Configuration::EnvironmentSource, NewRelic::Agent::Configuration::YamlSource]
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

Instance Method Summary collapse

Constructor Details

#initializeManager

Returns a new instance of Manager.



56
57
58
59
60
# File 'lib/new_relic/agent/configuration/manager.rb', line 56

def initialize
  reset_to_defaults
  @callbacks = Hash.new { |hash, key| hash[key] = [] }
  @lock = Mutex.new
end

Instance Method Details

#[](key) ⇒ Object

Defining these explicitly saves object allocations that we incur if we use Forwardable and def_delegators.



44
45
46
# File 'lib/new_relic/agent/configuration/manager.rb', line 44

def [](key)
  @cache[key]
end

#add_config_for_testing(source, level = 0) ⇒ Object



62
63
64
65
66
67
68
69
# File 'lib/new_relic/agent/configuration/manager.rb', line 62

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

#apply_mask(hash) ⇒ Object



361
362
363
364
365
366
# File 'lib/new_relic/agent/configuration/manager.rb', line 361

def apply_mask(hash)
  MASK_DEFAULTS
    .select { |_, proc| proc.call }
    .each { |key, _| hash.delete(key) }
  hash
end

#apply_transformations(key, value) ⇒ Object



265
266
267
268
269
270
271
# File 'lib/new_relic/agent/configuration/manager.rb', line 265

def apply_transformations(key, value)
  return value unless transform = default_source.transform_for(key)

  transform.call(value)
rescue => e
  default_with_warning(key, value, "Error encountered while applying transformation: >>#{e}<<")
end

#boolean?(type, value) ⇒ Boolean



166
167
168
169
170
# File 'lib/new_relic/agent/configuration/manager.rb', line 166

def boolean?(type, value)
  return false unless type == NewRelic::Agent::Configuration::Boolean

  value.class == TrueClass || value.class == FalseClass
end

#break_label_string_into_pairs(labels) ⇒ Object



406
407
408
409
410
411
412
413
414
# File 'lib/new_relic/agent/configuration/manager.rb', line 406

def break_label_string_into_pairs(labels)
  stripped_labels = labels.strip
  stripped_labels = stripped_labels.delete_prefix(';') while stripped_labels.start_with?(';')
  stripped_labels = stripped_labels.delete_suffix(';') while stripped_labels.end_with?(';')

  stripped_labels.split(';').map do |pair|
    pair.split(':').map(&:strip)
  end
end

#config_category(klass) ⇒ Object



150
151
152
153
154
155
156
# File 'lib/new_relic/agent/configuration/manager.rb', line 150

def config_category(klass)
  return :user if USER_CONFIG_CLASSES.include?(klass)
  return :test if [DottedHash, Hash].include?(klass)
  return :manual if klass == ManualSource

  return :nr
end

#config_classes_for_testingObject



554
555
556
# File 'lib/new_relic/agent/configuration/manager.rb', line 554

def config_classes_for_testing
  config_stack.map(&:class)
end

#default_sourceObject



290
291
292
# File 'lib/new_relic/agent/configuration/manager.rb', line 290

def default_source
  NewRelic::Agent::Configuration::DefaultSource
end

#default_with_warning(key, value, msg) ⇒ Object



246
247
248
249
250
251
# File 'lib/new_relic/agent/configuration/manager.rb', line 246

def default_with_warning(key, value, msg)
  default = default_without_warning(key)
  NewRelic::Agent.logger.warn "Received an invalid '#{value}' value for the '#{key}' configuration " \
    "parameter! #{msg} Using the default value of '#{default}'."
  default
end

#default_without_warning(key) ⇒ Object



253
254
255
256
# File 'lib/new_relic/agent/configuration/manager.rb', line 253

def default_without_warning(key)
  default = DEFAULTS.dig(key, :default)
  default.respond_to?(:call) ? default.call : default
end

#delete_all_configs_for_testingObject



540
541
542
543
544
545
546
547
548
# File 'lib/new_relic/agent/configuration/manager.rb', line 540

def delete_all_configs_for_testing
  @high_security_source = nil
  @environment_source = nil
  @server_source = nil
  @manual_source = nil
  @yaml_source = nil
  @default_source = nil
  @configs_for_testing = []
end

#enforce_allowlist(key, value) ⇒ Object



273
274
275
276
277
278
279
280
281
282
# File 'lib/new_relic/agent/configuration/manager.rb', line 273

def enforce_allowlist(key, value)
  return value unless allowlist = default_source.allowlist_for(key)
  return value if allowlist.include?(value)

  match = normalized_case_match(allowlist, value)
  return match unless match.nil?

  default_with_warning(key, value, 'Expected to receive a value found on the following list: ' \
                       ">>#{allowlist}<<, but received '#{value}'.")
end

#evaluate_and_apply_transformations(key, value, category) ⇒ Object



158
159
160
161
162
163
164
# File 'lib/new_relic/agent/configuration/manager.rb', line 158

def evaluate_and_apply_transformations(key, value, category)
  evaluated = value.respond_to?(:call) ? instance_eval(&value) : value
  evaluated = type_coerce(key, evaluated, category)
  evaluated = enforce_allowlist(key, evaluated)

  apply_transformations(key, evaluated)
end

#fetch(key) ⇒ Object



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/new_relic/agent/configuration/manager.rb', line 133

def fetch(key)
  config_stack.each do |config|
    next unless config

    accessor = key.to_sym
    next unless config.has_key?(accessor)

    begin
      return evaluate_and_apply_transformations(accessor, config[accessor], config_category(config.class))
    rescue
      next
    end
  end

  nil
end

#finished_configuring?Boolean



341
342
343
# File 'lib/new_relic/agent/configuration/manager.rb', line 341

def finished_configuring?
  !@server_source.nil?
end

#flattenedObject



345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/new_relic/agent/configuration/manager.rb', line 345

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

#handle_nil_type(key, value, category) ⇒ Object



187
188
189
190
191
192
193
194
195
196
197
# File 'lib/new_relic/agent/configuration/manager.rb', line 187

def handle_nil_type(key, value, category)
  return value if i[manual test].include?(category)

  # TODO: identify all config params such as :web_transactions_apdex
  #       that can exist in the @config hash without having an entry
  #       in the DEFAULTS hash. then warn here when a key is in play
  #       that is not on that allowlist. for now, just permit any key
  #       and return the value.
  #       https://github.com/newrelic/newrelic-ruby-agent/issues/3340
  default_without_warning(key) || value
end

#has_key?(key) ⇒ Boolean



48
49
50
# File 'lib/new_relic/agent/configuration/manager.rb', line 48

def has_key?(key)
  @cache.has_key?(key)
end

#instrumentation?(type, value) ⇒ Boolean

auto-instrumentation configuration params can be symbols or strings and unless we want to refactor the configuration hash to support both types, we handle the special case here



175
176
177
178
179
180
# File 'lib/new_relic/agent/configuration/manager.rb', line 175

def instrumentation?(type, value)
  return false unless type == String || type == Symbol
  return true if INSTRUMENTATION_VALUES.include?(value.to_s)

  false
end

#instrumentation_key?(key) ⇒ Boolean



182
183
184
185
# File 'lib/new_relic/agent/configuration/manager.rb', line 182

def instrumentation_key?(key)
  key.to_s.match?(/\Ainstrumentation\.[^.]+\z/) &&
    DEFAULTS.dig(key, :type) == String
end

#invoke_callbacks(direction, source) ⇒ Object



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
# File 'lib/new_relic/agent/configuration/manager.rb', line 299

def invoke_callbacks(direction, source)
  return unless source
  return if source.respond_to?(:empty?) && source.empty?

  source.keys.each do |key|
    next unless @callbacks.key?(key)

    begin
      evaluated_source = evaluate_and_apply_transformations(key, source[key], config_category(source.class))
    rescue => e
      NewRelic::Agent.logger.warn("Error evaluating callback for direction '#{direction}' with key '#{key}': #{e}")
      next
    end

    evaluated_cache = @cache[key]
    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

#keysObject



52
53
54
# File 'lib/new_relic/agent/configuration/manager.rb', line 52

def keys
  @cache.keys
end

#likely_transformed_already?(key, value) ⇒ Boolean



242
243
244
# File 'lib/new_relic/agent/configuration/manager.rb', line 242

def likely_transformed_already?(key, value)
  DEFAULTS.dig(key, :transformed_type) == value.class
end

#limit_number_of_labels(pairs) ⇒ Object



467
468
469
470
471
472
473
474
# File 'lib/new_relic/agent/configuration/manager.rb', line 467

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

#log_config(direction, source) ⇒ Object



527
528
529
530
531
532
533
534
535
536
537
538
# File 'lib/new_relic/agent/configuration/manager.rb', line 527

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
    source_hash = source.dup.to_h.delete_if { |k, _v| DEFAULTS.fetch(k, {}).fetch(:exclude_from_reported_settings, false) }
    final_hash = flattened.delete_if { |k, _h| DEFAULTS.fetch(k, {}).fetch(:exclude_from_reported_settings, false) }

    "Updating config (#{direction}) from #{source.class} with values: #{source_hash}. \nConfig Stack Results: #{final_hash.inspect}"
  end
end

#make_label_hash(pairs, labels = nil) ⇒ Object



434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
# File 'lib/new_relic/agent/configuration/manager.rb', line 434

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

#new_cacheObject



523
524
525
# File 'lib/new_relic/agent/configuration/manager.rb', line 523

def new_cache
  @cache = Hash.new { |hash, key| hash[key] = self.fetch(key) }
end

#normalized_case_match(allowlist, value) ⇒ Object



284
285
286
287
288
# File 'lib/new_relic/agent/configuration/manager.rb', line 284

def normalized_case_match(allowlist, value)
  return unless STRINGLIKE_TYPES.include?(value.class)

  allowlist.find { |allowlist_value| allowlist_value.to_s.casecmp?(value.to_s) }
end

#notify_finished_configuringObject

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.



337
338
339
# File 'lib/new_relic/agent/configuration/manager.rb', line 337

def notify_finished_configuring
  NewRelic::Agent.instance.events.notify(:initial_configuration_complete)
end

#notify_server_source_addedObject

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.



329
330
331
# File 'lib/new_relic/agent/configuration/manager.rb', line 329

def notify_server_source_added
  NewRelic::Agent.instance.events.notify(:server_source_configuration_added)
end

#num_configs_for_testingObject



550
551
552
# File 'lib/new_relic/agent/configuration/manager.rb', line 550

def num_configs_for_testing
  config_stack.size
end

#numeric_conversion(value) ⇒ Object

permit an int to be supplied for a float based param and vice versa



200
201
202
# File 'lib/new_relic/agent/configuration/manager.rb', line 200

def numeric_conversion(value)
  value.is_a?(Integer) ? value.to_f : value.round
end

#parse_labels_from_dictionaryObject



482
483
484
# File 'lib/new_relic/agent/configuration/manager.rb', line 482

def parse_labels_from_dictionary
  make_label_hash(NewRelic::Agent.config[:labels])
end

#parse_labels_from_stringObject



400
401
402
403
404
# File 'lib/new_relic/agent/configuration/manager.rb', line 400

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

#parsed_labelsObject



388
389
390
391
392
393
394
395
396
397
398
# File 'lib/new_relic/agent/configuration/manager.rb', line 388

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

#register_callback(key) {|| ... } ⇒ Object

Yields:

  • ()


294
295
296
297
# File 'lib/new_relic/agent/configuration/manager.rb', line 294

def register_callback(key, &proc)
  @callbacks[key] << proc
  yield(@cache[key])
end

#remove_config(source) ⇒ Object



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/new_relic/agent/configuration/manager.rb', line 84

def remove_config(source)
  case source
  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

#remove_config_type(sym) ⇒ Object



71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/new_relic/agent/configuration/manager.rb', line 71

def remove_config_type(sym)
  source = case sym
  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

#remove_duplicates(pairs) ⇒ Object

We only take the last value provided for a given label type key



477
478
479
480
# File 'lib/new_relic/agent/configuration/manager.rb', line 477

def remove_duplicates(pairs)
  grouped_by_type = pairs.group_by(&:first)
  grouped_by_type.values.map(&:last)
end

#replace_or_add_config(source) ⇒ Object



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/new_relic/agent/configuration/manager.rb', line 101

def replace_or_add_config(source)
  source.freeze
  was_finished = finished_configuring?

  invoke_callbacks(:add, source)

  case 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

#reset_cacheObject

reset the configuration hash, but do not replace previously auto determined dependency detection values with nil or 'auto'



503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
# File 'lib/new_relic/agent/configuration/manager.rb', line 503

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

#reset_to_defaultsObject

Generally only useful during initial construction and tests



487
488
489
490
491
492
493
494
495
496
497
498
499
# File 'lib/new_relic/agent/configuration/manager.rb', line 487

def reset_to_defaults
  @high_security_source = nil
  @environment_source = EnvironmentSource.new
  log_config(:add, @environment_source) # this is the only place the EnvironmentSource is ever created, so we should log it
  @server_source = nil
  @manual_source = nil
  @yaml_source = nil
  @default_source = DefaultSource.new

  @configs_for_testing = []

  reset_cache
end

#source(key) ⇒ Object



125
126
127
128
129
130
131
# File 'lib/new_relic/agent/configuration/manager.rb', line 125

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

#string_conversion(value) ⇒ Object

permit a symbol to be supplied for a string based param and vice versa



205
206
207
# File 'lib/new_relic/agent/configuration/manager.rb', line 205

def string_conversion(value)
  value.is_a?(Symbol) ? value.to_s : value.to_sym
end

#to_collector_hashObject



368
369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/new_relic/agent/configuration/manager.rb', line 368

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

#truncate(text, key = nil) ⇒ Object



453
454
455
456
457
458
459
460
461
462
463
464
465
# File 'lib/new_relic/agent/configuration/manager.rb', line 453

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

#type_coerce(key, value, category) ⇒ Object



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
# File 'lib/new_relic/agent/configuration/manager.rb', line 209

def type_coerce(key, value, category)
  return validate_nil(key, category) if value.nil?

  type = DEFAULTS.dig(key, :type)
  return handle_nil_type(key, value, category) unless type
  return 'disabled' if instrumentation_key?(key) && INSTRUMENTATION_DISABLED_VALUES.include?(value.to_s.downcase)
  return value if value.is_a?(type) || boolean?(type, value) || instrumentation?(type, value)
  return numeric_conversion(value) if NUMERIC_TYPES.include?(type) && NUMERIC_TYPES.include?(value.class)
  return string_conversion(value) if STRINGLIKE_TYPES.include?(type) && STRINGLIKE_TYPES.include?(value.class)

  # convert bool to string for regex usage and bool hash lookup
  # normalize case for backwards-compatibility
  # fixes: https://github.com/newrelic/newrelic-ruby-agent/issues/3632
  value = value.to_s.downcase if type == Boolean
  if value.class != String
    return value if category == :test || likely_transformed_already?(key, value)

    return default_with_warning(key, value, "Expected to receive a value of type #{type} but " \
                                "received #{value.class}.")
  end

  pattern = TYPE_COERCIONS.dig(type, :pattern)
  if pattern && value !~ pattern
    return default_with_warning(key, value, "Expected to receive a value of type #{type} matching " \
      "pattern '#{pattern}'.")
  end

  procedure = TYPE_COERCIONS.dig(type, :proc)
  return value unless procedure

  procedure.call(value)
end

#valid_label_item?(item) ⇒ Boolean



424
425
426
427
428
429
430
431
432
# File 'lib/new_relic/agent/configuration/manager.rb', line 424

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

#valid_label_pairs?(label_pairs) ⇒ Boolean



416
417
418
419
420
421
422
# File 'lib/new_relic/agent/configuration/manager.rb', line 416

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

#validate_nil(key, category) ⇒ Object



258
259
260
261
262
263
# File 'lib/new_relic/agent/configuration/manager.rb', line 258

def validate_nil(key, category)
  return if DEFAULTS.dig(key, :allow_nil) || category == :test # tests are free to specify nil
  return default_without_warning(key) unless category == :user # only user supplied config raises a warning

  default_with_warning(key, nil, 'Nil values are not permitted for the parameter.')
end