Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add configuration options to apply labels to log events #2925

Merged
merged 12 commits into from
Nov 15, 2024
15 changes: 15 additions & 0 deletions lib/new_relic/agent/configuration/default_source.rb
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,21 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil)
:allowed_from_server => false,
:description => 'A hash with key/value pairs to add as custom attributes to all log events forwarded to New Relic. If sending using an environment variable, the value must be formatted like: "key1=value1,key2=value2"'
},
:'application_logging.forwarding.labels.enabled' => {
:default => false,
:public => true,
:type => Boolean,
:allowed_from_server => false,
:description => 'If `true`, the agent attaches [labels](https://docs.newrelic.com/docs/apm/agents/ruby-agent/configuration/ruby-agent-configuration/#labels) to log records.'
},
:'application_logging.forwarding.labels.exclude' => {
:default => [],
:public => true,
:type => Array,
:transform => DefaultSource.method(:convert_to_list),
:allowed_from_server => false,
:description => 'A case-insensitive array or comma-delimited string containing the labels to exclude from log records.'
},
:'application_logging.forwarding.max_samples_stored' => {
:default => 10000,
:public => true,
Expand Down
31 changes: 29 additions & 2 deletions lib/new_relic/agent/log_event_aggregator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class LogEventAggregator < EventAggregator
METRICS_SUPPORTABILITY_FORMAT = 'Supportability/Logging/Metrics/Ruby/%s'.freeze
FORWARDING_SUPPORTABILITY_FORMAT = 'Supportability/Logging/Forwarding/Ruby/%s'.freeze
DECORATING_SUPPORTABILITY_FORMAT = 'Supportability/Logging/LocalDecorating/Ruby/%s'.freeze
LABELS_SUPPORTABILITY_FORMAT = 'Supportability/Logging/Labels/Ruby/%s'.freeze
MAX_BYTES = 32768 # 32 * 1024 bytes (32 kibibytes)

named :LogEventAggregator
Expand All @@ -38,6 +39,7 @@ class LogEventAggregator < EventAggregator
METRICS_ENABLED_KEY = :'application_logging.metrics.enabled'
FORWARDING_ENABLED_KEY = :'application_logging.forwarding.enabled'
DECORATING_ENABLED_KEY = :'application_logging.local_decorating.enabled'
LABELS_ENABLED_KEY = :'application_logging.forwarding.labels.enabled'
LOG_LEVEL_KEY = :'application_logging.forwarding.log_level'
CUSTOM_ATTRIBUTES_KEY = :'application_logging.forwarding.custom_attributes'

Expand All @@ -51,6 +53,7 @@ def initialize(events)
@high_security = NewRelic::Agent.config[:high_security]
@instrumentation_logger_enabled = NewRelic::Agent::Instrumentation::Logger.enabled?
@attributes = NewRelic::Agent::LogEventAttributes.new

register_for_done_configuring(events)
end

Expand Down Expand Up @@ -186,6 +189,10 @@ def add_custom_attributes(custom_attributes)
attributes.add_custom_attributes(custom_attributes)
end

def labels
@labels ||= create_labels
end

# Because our transmission format (MELT) is different than historical
# agent payloads, extract the munging here to keep the service focused
# on the general harvest + transmit instead of the format.
Expand All @@ -201,8 +208,9 @@ def self.payload_to_melt_format(data)
# To save on unnecessary data transmission, trim the entity.type
# sent by classic logs-in-context
common_attributes.delete(ENTITY_TYPE_KEY)

common_attributes.merge!(NewRelic::Agent.agent.log_event_aggregator.attributes.custom_attributes)
aggregator = NewRelic::Agent.agent.log_event_aggregator
common_attributes.merge!(aggregator.attributes.custom_attributes)
common_attributes.merge!(aggregator.labels)
Comment on lines +211 to +213
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This leaves me wondering if we should leverage the LogEventAttributes class for the labels work too, rather than leaving all the labels work inside the aggregator. However, this may make labels more difficult to remove when the aggregator is reset. Thoughts?


_, items = data
payload = [{
Expand All @@ -222,6 +230,7 @@ def reset!
@counter_lock.synchronize do
@seen = 0
@seen_by_severity.clear
remove_instance_variable(:@labels) if instance_variable_defined?(:@labels)
kaylareopelle marked this conversation as resolved.
Show resolved Hide resolved
end

super
Expand All @@ -247,6 +256,7 @@ def register_for_done_configuring(events)
record_configuration_metric(METRICS_SUPPORTABILITY_FORMAT, METRICS_ENABLED_KEY)
record_configuration_metric(FORWARDING_SUPPORTABILITY_FORMAT, FORWARDING_ENABLED_KEY)
record_configuration_metric(DECORATING_SUPPORTABILITY_FORMAT, DECORATING_ENABLED_KEY)
record_configuration_metric(LABELS_SUPPORTABILITY_FORMAT, LABELS_ENABLED_KEY)

add_custom_attributes(NewRelic::Agent.config[CUSTOM_ATTRIBUTES_KEY])
end
Expand Down Expand Up @@ -327,6 +337,23 @@ def severity_too_low?(severity)

Logger::Severity.const_get(severity_constant) < Logger::Severity.const_get(configured_log_level_constant)
end

def create_labels
return NewRelic::EMPTY_HASH unless NewRelic::Agent.config[LABELS_ENABLED_KEY]

downcased_exclusions = NewRelic::Agent.config[:'application_logging.forwarding.labels.exclude'].map(&:downcase)
log_labels = {}

NewRelic::Agent.config.parsed_labels.each do |parsed_label|
next if downcased_exclusions.include?(parsed_label['label_type'].downcase)

# labels are referred to as tags in the UI, so prefix the
# label-related attributes with 'tags.*'
log_labels["tags.#{parsed_label['label_type']}"] = parsed_label['label_value']
end

log_labels
end
end
end
end
137 changes: 129 additions & 8 deletions test/new_relic/agent/log_event_aggregator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ def test_records_enabled_metrics_on_startup
LogEventAggregator::OVERALL_ENABLED_KEY => true,
LogEventAggregator::METRICS_ENABLED_KEY => true,
LogEventAggregator::FORWARDING_ENABLED_KEY => true,
LogEventAggregator::DECORATING_ENABLED_KEY => true
LogEventAggregator::DECORATING_ENABLED_KEY => true,
LogEventAggregator::LABELS_ENABLED_KEY => true
) do
NewRelic::Agent.config.notify_server_source_added

Expand All @@ -61,7 +62,8 @@ def test_records_enabled_metrics_on_startup
'Supportability/Logging/Ruby/LogStasher/enabled' => {:call_count => 1},
'Supportability/Logging/Metrics/Ruby/enabled' => {:call_count => 1},
'Supportability/Logging/Forwarding/Ruby/enabled' => {:call_count => 1},
'Supportability/Logging/LocalDecorating/Ruby/enabled' => {:call_count => 1}
'Supportability/Logging/LocalDecorating/Ruby/enabled' => {:call_count => 1},
'Supportability/Logging/Labels/Ruby/enabled' => {:call_count => 1}
},
:ignore_filter => %r{^Supportability/API/})
end
Expand All @@ -72,7 +74,8 @@ def test_records_disabled_metrics_on_startup
LogEventAggregator::OVERALL_ENABLED_KEY => false,
LogEventAggregator::METRICS_ENABLED_KEY => false,
LogEventAggregator::FORWARDING_ENABLED_KEY => false,
LogEventAggregator::DECORATING_ENABLED_KEY => false
LogEventAggregator::DECORATING_ENABLED_KEY => false,
LogEventAggregator::LABELS_ENABLED_KEY => false
) do
NewRelic::Agent.config.notify_server_source_added

Expand All @@ -81,7 +84,8 @@ def test_records_disabled_metrics_on_startup
'Supportability/Logging/Ruby/LogStasher/disabled' => {:call_count => 1},
'Supportability/Logging/Metrics/Ruby/disabled' => {:call_count => 1},
'Supportability/Logging/Forwarding/Ruby/disabled' => {:call_count => 1},
'Supportability/Logging/LocalDecorating/Ruby/disabled' => {:call_count => 1}
'Supportability/Logging/LocalDecorating/Ruby/disabled' => {:call_count => 1},
'Supportability/Logging/Labels/Ruby/disabled' => {:call_count => 1}
},
:ignore_filter => %r{^Supportability/API/})
end
Expand Down Expand Up @@ -324,6 +328,35 @@ def test_records_metrics_on_harvest
end
end

def test_seen_and_sent_zeroed_out_on_reset!
kaylareopelle marked this conversation as resolved.
Show resolved Hide resolved
with_config(CAPACITY_KEY => 3) do
3.times { @aggregator.record('Are you counting this?', 'DEBUG') }
@aggregator.harvest! # seen/sent are counted on harvest, this zeroes things out

assert_metrics_recorded_exclusive({
'Logging/lines' => {:call_count => 3},
'Logging/lines/DEBUG' => {:call_count => 3},
'Logging/Forwarding/Dropped' => {:call_count => 0},
'Supportability/Logging/Forwarding/Seen' => {:call_count => 3},
'Supportability/Logging/Forwarding/Sent' => {:call_count => 3}
},
:ignore_filter => %r{^Supportability/API/})

@aggregator.record('Are you counting this?', 'DEBUG')
@aggregator.reset! # set seen/sent to zero, throwing out the latest #record
@aggregator.harvest! # nothing new should be available to count, so use same numbers as before

assert_metrics_recorded_exclusive({
'Logging/lines' => {:call_count => 3},
'Logging/lines/DEBUG' => {:call_count => 3}, # ?
tannalynn marked this conversation as resolved.
Show resolved Hide resolved
'Logging/Forwarding/Dropped' => {:call_count => 0},
'Supportability/Logging/Forwarding/Seen' => {:call_count => 3}, # ?
'Supportability/Logging/Forwarding/Sent' => {:call_count => 3}
},
:ignore_filter => %r{^Supportability/API/})
end
end

def test_high_security_mode
with_config(CAPACITY_KEY => 5, :high_security => true) do
# We refresh the high security setting on this notification
Expand All @@ -343,14 +376,24 @@ def test_high_security_mode
'Supportability/Logging/Ruby/LogStasher/enabled' => {:call_count => 1},
'Supportability/Logging/Metrics/Ruby/enabled' => {:call_count => 1},
'Supportability/Logging/Forwarding/Ruby/enabled' => {:call_count => 1},
'Supportability/Logging/LocalDecorating/Ruby/disabled' => {:call_count => 1}
'Supportability/Logging/LocalDecorating/Ruby/disabled' => {:call_count => 1},
'Supportability/Logging/Labels/Ruby/disabled' => {:call_count => 1}
},
:ignore_filter => %r{^Supportability/API/})
end
end

def test_overall_disabled
with_config(LogEventAggregator::OVERALL_ENABLED_KEY => false) do
# set the overall enabled key to false
# put all other configs as true
with_config(
LogEventAggregator::OVERALL_ENABLED_KEY => false,
:'instrumentation.logger' => 'auto',
LogEventAggregator::METRICS_ENABLED_KEY => true,
LogEventAggregator::FORWARDING_ENABLED_KEY => true,
LogEventAggregator::DECORATING_ENABLED_KEY => true,
LogEventAggregator::LABELS_ENABLED_KEY => true
) do
kaylareopelle marked this conversation as resolved.
Show resolved Hide resolved
# Refresh the value of @enabled on the LogEventAggregator
NewRelic::Agent.config.notify_server_source_added

Expand All @@ -366,7 +409,8 @@ def test_overall_disabled
'Supportability/Logging/Ruby/LogStasher/disabled' => {:call_count => 1},
'Supportability/Logging/Metrics/Ruby/disabled' => {:call_count => 1},
'Supportability/Logging/Forwarding/Ruby/disabled' => {:call_count => 1},
'Supportability/Logging/LocalDecorating/Ruby/disabled' => {:call_count => 1}
'Supportability/Logging/LocalDecorating/Ruby/disabled' => {:call_count => 1},
'Supportability/Logging/Labels/Ruby/disabled' => {:call_count => 1}
},
:ignore_filter => %r{^Supportability/API/})
end
Expand All @@ -392,7 +436,8 @@ def test_overall_disabled_in_high_security_mode
'Supportability/Logging/Ruby/LogStasher/disabled' => {:call_count => 1},
'Supportability/Logging/Metrics/Ruby/disabled' => {:call_count => 1},
'Supportability/Logging/Forwarding/Ruby/disabled' => {:call_count => 1},
'Supportability/Logging/LocalDecorating/Ruby/disabled' => {:call_count => 1}
'Supportability/Logging/LocalDecorating/Ruby/disabled' => {:call_count => 1},
'Supportability/Logging/Labels/Ruby/disabled' => {:call_count => 1}
},
:ignore_filter => %r{^Supportability/API/})
end
Expand Down Expand Up @@ -693,5 +738,81 @@ def test_nil_when_record_logstasher_errors
assert_empty results
end
end

def test_labels_added_as_common_attributes_when_enabled
config = {:labels => 'Server:One;Data Center:Primary', :'application_logging.forwarding.labels.enabled' => true}
expected_label_attributes = {'tags.Server' => 'One', 'tags.Data Center' => 'Primary'}

assert_labels(config, expected_label_attributes)
end

def test_labels_not_added_as_common_attributes_when_disabled
config = {:labels => 'Server:One;Data Center:Primary', :'application_logging.forwarding.labels.enabled' => false}
expected_label_attributes = {}

assert_labels(config, expected_label_attributes)
end

def test_labels_not_added_as_common_attributes_when_excluded
config = {
:labels => 'Server:One;Data Center:Primary;Fake:two',
:'application_logging.forwarding.labels.enabled' => true,
:'application_logging.forwarding.labels.exclude' => ['Fake']
}
expected_label_attributes = {'tags.Server' => 'One', 'tags.Data Center' => 'Primary'}

assert_labels(config, expected_label_attributes)
end

def test_labels_excluded_only_for_case_insensitive_match
config = {
:labels => 'Server:One;Data Center:Primary;fake:two;Faker:three',
:'application_logging.forwarding.labels.enabled' => true,
:'application_logging.forwarding.labels.exclude' => ['Fake']
}
expected_label_attributes = {'tags.Server' => 'One', 'tags.Data Center' => 'Primary', 'tags.Faker' => 'three'}

assert_labels(config, expected_label_attributes)
end

def test_labels_undefined_during_reset!
with_config(:'application_logging.forwarding.labels.enabled' => true, :labels => 'Server:One;Data Center:primary') do
assert_equal({'tags.Server' => 'One', 'tags.Data Center' => 'primary'}, @aggregator.labels)
@aggregator.reset!

refute @aggregator.instance_variable_defined?(:@labels)
end
end

private

def assert_labels(config, expected_attributes = {})
# we should only have one log event aggregator per agent instance
# simulate that by resetting @labels between tests
aggregator = NewRelic::Agent.agent.log_event_aggregator
aggregator.remove_instance_variable(:@labels) if aggregator.instance_variable_defined?(:@labels)

with_config(config) do
LinkingMetadata.stub('append_service_linking_metadata', {'entity.guid' => 'GUID', 'entity.name' => 'Hola'}) do
log_data = [
{
events_seen: 0,
reservoir_size: 0
},
[
[{"priority": 1}, {"message": 'This is a mess'}]
]
]

payload, _ = LogEventAggregator.payload_to_melt_format(log_data)
expected = [{
common: {attributes: {'entity.guid' => 'GUID', 'entity.name' => 'Hola'}.merge!(expected_attributes)},
logs: [{"message": 'This is a mess'}]
}]

assert_equal(payload, expected)
end
end
end
end
end
Loading