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
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.



34
35
36
37
38
# File 'lib/new_relic/agent/configuration/manager.rb', line 34

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.



22
23
24
# File 'lib/new_relic/agent/configuration/manager.rb', line 22

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

#add_config_for_testing(source, level = 0) ⇒ Object



40
41
42
43
44
45
46
47
# File 'lib/new_relic/agent/configuration/manager.rb', line 40

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



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

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

#apply_transformations(key, value) ⇒ Object



151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/new_relic/agent/configuration/manager.rb', line 151

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

#break_label_string_into_pairs(labels) ⇒ Object



303
304
305
306
307
308
# File 'lib/new_relic/agent/configuration/manager.rb', line 303

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

#config_classes_for_testingObject



447
448
449
# File 'lib/new_relic/agent/configuration/manager.rb', line 447

def config_classes_for_testing
  config_stack.map(&:class)
end

#default_sourceObject



189
190
191
# File 'lib/new_relic/agent/configuration/manager.rb', line 189

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

#delete_all_configs_for_testingObject



432
433
434
435
436
437
438
439
440
441
# File 'lib/new_relic/agent/configuration/manager.rb', line 432

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

#enforce_allowlist(key, value) ⇒ Object



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

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

#enforce_boolean(key, value) ⇒ Object



173
174
175
176
177
178
179
180
181
182
183
# File 'lib/new_relic/agent/configuration/manager.rb', line 173

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

#evaluate_and_apply_transformations(key, value) ⇒ Object



140
141
142
143
144
145
146
147
148
149
# File 'lib/new_relic/agent/configuration/manager.rb', line 140

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

#evaluate_procs(value) ⇒ Object



132
133
134
135
136
137
138
# File 'lib/new_relic/agent/configuration/manager.rb', line 132

def evaluate_procs(value)
  if value.respond_to?(:call)
    instance_eval(&value)
  else
    value
  end
end

#fetch(key) ⇒ Object



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/new_relic/agent/configuration/manager.rb', line 114

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

#finished_configuring?Boolean

Returns:



238
239
240
# File 'lib/new_relic/agent/configuration/manager.rb', line 238

def finished_configuring?
  !@server_source.nil?
end

#flattenedObject



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/new_relic/agent/configuration/manager.rb', line 242

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

#has_key?(key) ⇒ Boolean

Returns:



26
27
28
# File 'lib/new_relic/agent/configuration/manager.rb', line 26

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

#invoke_callbacks(direction, source) ⇒ Object



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/new_relic/agent/configuration/manager.rb', line 198

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

#keysObject



30
31
32
# File 'lib/new_relic/agent/configuration/manager.rb', line 30

def keys
  @cache.keys
end

#limit_number_of_labels(pairs) ⇒ Object



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

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



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

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

#make_label_hash(pairs, labels = nil) ⇒ Object



328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# File 'lib/new_relic/agent/configuration/manager.rb', line 328

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



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

def new_cache
  @cache = Hash.new { |hash, key| hash[key] = self.fetch(key) }
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.



234
235
236
# File 'lib/new_relic/agent/configuration/manager.rb', line 234

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.



226
227
228
# File 'lib/new_relic/agent/configuration/manager.rb', line 226

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

#num_configs_for_testingObject



443
444
445
# File 'lib/new_relic/agent/configuration/manager.rb', line 443

def num_configs_for_testing
  config_stack.size
end

#parse_labels_from_dictionaryObject



376
377
378
# File 'lib/new_relic/agent/configuration/manager.rb', line 376

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

#parse_labels_from_stringObject



297
298
299
300
301
# File 'lib/new_relic/agent/configuration/manager.rb', line 297

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



285
286
287
288
289
290
291
292
293
294
295
# File 'lib/new_relic/agent/configuration/manager.rb', line 285

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:

  • ()


193
194
195
196
# File 'lib/new_relic/agent/configuration/manager.rb', line 193

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

#remove_config(source) ⇒ Object



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/new_relic/agent/configuration/manager.rb', line 63

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

#remove_config_type(sym) ⇒ Object



49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/new_relic/agent/configuration/manager.rb', line 49

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

#remove_duplicates(pairs) ⇒ Object

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



371
372
373
374
# File 'lib/new_relic/agent/configuration/manager.rb', line 371

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

#replace_or_add_config(source) ⇒ Object



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/new_relic/agent/configuration/manager.rb', line 81

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

#reset_cacheObject

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



397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
# File 'lib/new_relic/agent/configuration/manager.rb', line 397

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



381
382
383
384
385
386
387
388
389
390
391
392
393
# File 'lib/new_relic/agent/configuration/manager.rb', line 381

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

#source(key) ⇒ Object



106
107
108
109
110
111
112
# File 'lib/new_relic/agent/configuration/manager.rb', line 106

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

#to_collector_hashObject



265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/new_relic/agent/configuration/manager.rb', line 265

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

#transform_from_default(key) ⇒ Object



185
186
187
# File 'lib/new_relic/agent/configuration/manager.rb', line 185

def transform_from_default(key)
  default_source.transform_for(key)
end

#truncate(text, key = nil) ⇒ Object



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

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

#valid_label_item?(item) ⇒ Boolean

Returns:



318
319
320
321
322
323
324
325
326
# File 'lib/new_relic/agent/configuration/manager.rb', line 318

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

Returns:



310
311
312
313
314
315
316
# File 'lib/new_relic/agent/configuration/manager.rb', line 310

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