Skip to content

Commit 6b8fb08

Browse files
authored
Merge pull request #1240 from newrelic/feature-log-event-labels
Log Event Label Attributes
2 parents a5d0de3 + a4b352b commit 6b8fb08

9 files changed

+215
-33
lines changed

newrelic/config.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ def _map_feature_flag(s):
164164
return set(s.split())
165165

166166

167-
def _map_labels(s):
167+
def _map_as_mapping(s):
168168
return newrelic.core.config._environ_as_mapping(name="", default=s)
169169

170170

@@ -210,6 +210,10 @@ def _map_inc_excl_attributes(s):
210210
return newrelic.core.config._parse_attributes(s)
211211

212212

213+
def _map_case_insensitive_excl_labels(s):
214+
return [v.lower() for v in newrelic.core.config._parse_attributes(s)]
215+
216+
213217
def _map_default_host_value(license_key):
214218
# If the license key is region aware, we should override the default host
215219
# to be the region aware host
@@ -311,7 +315,7 @@ def _process_setting(section, option, getter, mapper):
311315
def _process_configuration(section):
312316
_process_setting(section, "feature_flag", "get", _map_feature_flag)
313317
_process_setting(section, "app_name", "get", None)
314-
_process_setting(section, "labels", "get", _map_labels)
318+
_process_setting(section, "labels", "get", _map_as_mapping)
315319
_process_setting(section, "license_key", "get", _map_default_host_value)
316320
_process_setting(section, "api_key", "get", None)
317321
_process_setting(section, "host", "get", None)
@@ -542,6 +546,9 @@ def _process_configuration(section):
542546
_process_setting(section, "application_logging.enabled", "getboolean", None)
543547
_process_setting(section, "application_logging.forwarding.max_samples_stored", "getint", None)
544548
_process_setting(section, "application_logging.forwarding.enabled", "getboolean", None)
549+
_process_setting(section, "application_logging.forwarding.custom_attributes", "get", _map_as_mapping)
550+
_process_setting(section, "application_logging.forwarding.labels.enabled", "getboolean", None)
551+
_process_setting(section, "application_logging.forwarding.labels.exclude", "get", _map_case_insensitive_excl_labels)
545552
_process_setting(section, "application_logging.forwarding.context_data.enabled", "getboolean", None)
546553
_process_setting(section, "application_logging.forwarding.context_data.include", "get", _map_inc_excl_attributes)
547554
_process_setting(section, "application_logging.forwarding.context_data.exclude", "get", _map_inc_excl_attributes)

newrelic/core/application.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,9 @@ def connect_to_data_collector(self, activate_agent):
548548
application_logging_local_decorating = (
549549
configuration.application_logging.enabled and configuration.application_logging.local_decorating.enabled
550550
)
551-
ai_monitoring_streaming = configuration.ai_monitoring.streaming.enabled
551+
application_logging_labels = (
552+
application_logging_forwarding and configuration.application_logging.forwarding.labels.enabled
553+
)
552554
internal_metric(
553555
f"Supportability/Logging/Forwarding/Python/{'enabled' if application_logging_forwarding else 'disabled'}",
554556
1,
@@ -561,6 +563,13 @@ def connect_to_data_collector(self, activate_agent):
561563
f"Supportability/Logging/Metrics/Python/{'enabled' if application_logging_metrics else 'disabled'}",
562564
1,
563565
)
566+
internal_metric(
567+
f"Supportability/Logging/Labels/Python/{'enabled' if application_logging_labels else 'disabled'}",
568+
1,
569+
)
570+
571+
# AI monitoring feature toggle metrics
572+
ai_monitoring_streaming = configuration.ai_monitoring.streaming.enabled
564573
if not ai_monitoring_streaming:
565574
internal_metric(
566575
"Supportability/Python/ML/Streaming/Disabled",

newrelic/core/config.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,10 @@ class ApplicationLoggingForwardingSettings(Settings):
323323
pass
324324

325325

326+
class ApplicationLoggingForwardingLabelsSettings(Settings):
327+
pass
328+
329+
326330
class ApplicationLoggingForwardingContextDataSettings(Settings):
327331
pass
328332

@@ -424,6 +428,7 @@ class EventHarvestConfigHarvestLimitSettings(Settings):
424428
_settings.agent_limits = AgentLimitsSettings()
425429
_settings.application_logging = ApplicationLoggingSettings()
426430
_settings.application_logging.forwarding = ApplicationLoggingForwardingSettings()
431+
_settings.application_logging.forwarding.labels = ApplicationLoggingForwardingLabelsSettings()
427432
_settings.application_logging.forwarding.context_data = ApplicationLoggingForwardingContextDataSettings()
428433
_settings.application_logging.metrics = ApplicationLoggingMetricsSettings()
429434
_settings.application_logging.local_decorating = ApplicationLoggingLocalDecoratingSettings()
@@ -935,6 +940,17 @@ def default_otlp_host(host):
935940
_settings.application_logging.forwarding.enabled = _environ_as_bool(
936941
"NEW_RELIC_APPLICATION_LOGGING_FORWARDING_ENABLED", default=True
937942
)
943+
_settings.application_logging.forwarding.custom_attributes = _environ_as_mapping(
944+
"NEW_RELIC_APPLICATION_LOGGING_FORWARDING_CUSTOM_ATTRIBUTES", default=""
945+
)
946+
947+
_settings.application_logging.forwarding.labels.enabled = _environ_as_bool(
948+
"NEW_RELIC_APPLICATION_LOGGING_FORWARDING_LABELS_ENABLED", default=False
949+
)
950+
_settings.application_logging.forwarding.labels.exclude = set(
951+
v.lower() for v in _environ_as_set("NEW_RELIC_APPLICATION_LOGGING_FORWARDING_LABELS_EXCLUDE", default="")
952+
)
953+
938954
_settings.application_logging.forwarding.context_data.enabled = _environ_as_bool(
939955
"NEW_RELIC_APPLICATION_LOGGING_FORWARDING_CONTEXT_DATA_ENABLED", default=False
940956
)
@@ -1096,7 +1112,7 @@ def global_settings_dump(settings_object=None, serializable=False):
10961112
if not isinstance(key, str):
10971113
del settings[key]
10981114

1099-
if not isinstance(value, str) and not isinstance(value, float) and not isinstance(value, int):
1115+
if not isinstance(value, (str, float, int)):
11001116
settings[key] = repr(value)
11011117

11021118
return settings

newrelic/core/data_collector.py

+51
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
from newrelic.core.config import global_settings
3333
from newrelic.core.otlp_utils import encode_metric_data, encode_ml_event_data
3434

35+
from newrelic.core.attribute import process_user_attribute, MAX_NUM_USER_ATTRIBUTES
36+
3537
_logger = logging.getLogger(__name__)
3638

3739

@@ -154,10 +156,59 @@ def send_dimensional_metric_data(self, start_time, end_time, metric_data):
154156
payload = encode_metric_data(metric_data, start_time, end_time)
155157
return self._otlp_protocol.send("dimensional_metric_data", payload, path="/v1/metrics")
156158

159+
def get_log_events_common_block(self):
160+
""" "Generate common block for log events."""
161+
common = {}
162+
163+
try:
164+
# Add global custom log attributes to common block
165+
if self.configuration.application_logging.forwarding.custom_attributes:
166+
# Retrieve and process attrs
167+
custom_attributes = {}
168+
for attr_name, attr_value in self.configuration.application_logging.forwarding.custom_attributes:
169+
if len(custom_attributes) >= MAX_NUM_USER_ATTRIBUTES:
170+
_logger.debug("Maximum number of custom attributes already added. Dropping attribute: %r=%r", attr_name, attr_value)
171+
break
172+
173+
key, val = process_user_attribute(attr_name, attr_value)
174+
175+
if key is not None:
176+
custom_attributes[key] = val
177+
178+
common.update(custom_attributes)
179+
180+
# Add application labels as tags. prefixed attributes to common block
181+
labels = self.configuration.labels
182+
if not labels or not self.configuration.application_logging.forwarding.labels.enabled:
183+
return common
184+
elif not self.configuration.application_logging.forwarding.labels.exclude:
185+
common.update({
186+
f"tags.{label['label_type']}": label['label_value']
187+
for label in labels
188+
})
189+
else:
190+
common.update({
191+
f"tags.{label['label_type']}": label['label_value']
192+
for label in labels
193+
if label['label_type'].lower() not in self.configuration.application_logging.forwarding.labels.exclude
194+
})
195+
196+
except Exception:
197+
_logger.exception("Cannot generate common block for log events.")
198+
return {}
199+
else:
200+
return common
201+
157202
def send_log_events(self, sampling_info, log_event_data):
158203
"""Called to submit sample set for log events."""
159204

160205
payload = ({"logs": tuple(log._asdict() for log in log_event_data)},)
206+
207+
# Add common block attributes if not empty
208+
common = self.get_log_events_common_block()
209+
if common:
210+
payload[0]["common"] = {"attributes": common}
211+
161212
return self._protocol.send("log_event_data", payload)
162213

163214
def get_agent_commands(self):

tests/agent_features/test_collector_payloads.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,6 @@ def test_custom_event_json():
8181
custom_event_application.get("/")
8282

8383

84-
@pytest.mark.xfail(reason="Unwritten validator")
85-
@validate_log_event_collector_json
84+
@validate_log_event_collector_json()
8685
def test_log_event_json():
8786
normal_application.get("/")
88-
raise NotImplementedError("Fix my validator")

tests/agent_features/test_log_events.py

+105
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,9 @@ def exercise_record_log_event():
153153
]
154154

155155

156+
# ================================================
156157
# Test Log Forwarding
158+
# ================================================
157159

158160

159161
@enable_log_forwarding
@@ -193,7 +195,10 @@ def test():
193195
test()
194196

195197

198+
# ================================================
196199
# Test Message Truncation
200+
# ================================================
201+
197202

198203
_test_log_event_truncation_events = [{"message": "A" * 32768}]
199204

@@ -220,7 +225,9 @@ def test():
220225
test()
221226

222227

228+
# ================================================
223229
# Test Log Forwarding Settings
230+
# ================================================
224231

225232

226233
@disable_log_forwarding
@@ -243,7 +250,9 @@ def test():
243250
test()
244251

245252

253+
# ================================================
246254
# Test Log Attribute Settings
255+
# ================================================
247256

248257

249258
@disable_log_attributes
@@ -396,3 +405,99 @@ def test():
396405
record_log_event("A")
397406

398407
test()
408+
409+
410+
# ================================================
411+
# Test Log Event Labels Settings
412+
# ================================================
413+
414+
415+
# Add labels setting value in already processed format
416+
TEST_LABELS = {"testlabel1": "A", "testlabel2": "B", "testlabelexclude": "C"}
417+
TEST_LABELS = [{"label_type": k, "label_value": v} for k, v in TEST_LABELS.items()]
418+
419+
@override_application_settings({
420+
"labels": TEST_LABELS,
421+
"application_logging.forwarding.labels.enabled": True,
422+
})
423+
@background_task()
424+
def test_label_forwarding_enabled():
425+
txn = current_transaction()
426+
session = list(txn.application._agent._applications.values())[0]._active_session
427+
428+
common = session.get_log_events_common_block()
429+
# Excluded label should not appear, and other labels should be prefixed with 'tag.'
430+
assert common == {"tags.testlabel1": "A", "tags.testlabel2": "B", "tags.testlabelexclude": "C"}
431+
432+
433+
@override_application_settings({
434+
"labels": TEST_LABELS,
435+
"application_logging.forwarding.labels.enabled": True,
436+
"application_logging.forwarding.labels.exclude": {"testlabelexclude"},
437+
})
438+
@background_task()
439+
def test_label_forwarding_enabled_exclude():
440+
txn = current_transaction()
441+
session = list(txn.application._agent._applications.values())[0]._active_session
442+
443+
common = session.get_log_events_common_block()
444+
# Excluded label should not appear, and other labels should be prefixed with 'tags.'
445+
assert common == {"tags.testlabel1": "A", "tags.testlabel2": "B"}
446+
447+
448+
@override_application_settings({
449+
"labels": TEST_LABELS,
450+
"application_logging.forwarding.labels.enabled": False,
451+
})
452+
@background_task()
453+
def test_label_forwarding_disabled():
454+
txn = current_transaction()
455+
session = list(txn.application._agent._applications.values())[0]._active_session
456+
457+
common = session.get_log_events_common_block()
458+
# No labels should appear
459+
assert common == {}
460+
461+
462+
# ================================================
463+
# Test Log Event Global Custom Attributes Settings
464+
# ================================================
465+
466+
467+
@override_application_settings({
468+
"application_logging.forwarding.custom_attributes": [("custom_attr_1", "value 1"), ("custom_attr_2", "value 2")],
469+
})
470+
@background_task()
471+
def test_global_custom_attribute_forwarding_enabled():
472+
txn = current_transaction()
473+
session = list(txn.application._agent._applications.values())[0]._active_session
474+
475+
common = session.get_log_events_common_block()
476+
# Both attrs should appear
477+
assert common == {"custom_attr_1": "value 1", "custom_attr_2": "value 2"}
478+
479+
480+
@override_application_settings({
481+
"application_logging.forwarding.custom_attributes": [("custom_attr_1", "a" * 256)],
482+
})
483+
@background_task()
484+
def test_global_custom_attribute_forwarding_truncation():
485+
txn = current_transaction()
486+
session = list(txn.application._agent._applications.values())[0]._active_session
487+
488+
common = session.get_log_events_common_block()
489+
# Attribute value should be truncated to the max user attribute length
490+
assert common == {"custom_attr_1": "a" * 255}
491+
492+
493+
@override_application_settings({
494+
"application_logging.forwarding.custom_attributes": [(f"custom_attr_{i+1}", "value") for i in range(129)],
495+
})
496+
@background_task()
497+
def test_global_custom_attribute_forwarding_max_num_attrs():
498+
txn = current_transaction()
499+
session = list(txn.application._agent._applications.values())[0]._active_session
500+
501+
common = session.get_log_events_common_block()
502+
# Should be truncated to the max number of user attributes
503+
assert common == {f"custom_attr_{i+1}": "value" for i in range(128)}

tests/cross_agent/test_labels_and_rollups.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import os
1717
import pytest
1818

19-
from newrelic.config import _process_labels_setting, _map_labels
19+
from newrelic.config import _process_labels_setting, _map_as_mapping
2020
from newrelic.core.config import global_settings
2121

2222
from testing_support.fixtures import override_application_settings
@@ -41,7 +41,7 @@ def _parametrize_test(test):
4141
@pytest.mark.parametrize('name,labelString,warning,expected', _labels_tests)
4242
def test_labels(name, labelString, warning, expected):
4343

44-
parsed_labels = _map_labels(labelString)
44+
parsed_labels = _map_as_mapping(labelString)
4545
_process_labels_setting(parsed_labels)
4646

4747
settings = global_settings()

tests/testing_support/sample_applications.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,11 @@ def simple_exceptional_app(environ, start_response):
123123

124124
def simple_app_raw(environ, start_response):
125125
status = "200 OK"
126-
127-
_logger.info("Starting response")
126+
127+
logger = logging.getLogger("simple_app_raw")
128+
logger.setLevel(logging.INFO)
129+
logger.info("Starting response")
130+
128131
start_response(status, response_headers=[])
129132

130133
return []

0 commit comments

Comments
 (0)