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 Dictionary Log Message Support #1014

Merged
merged 20 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 40 additions & 8 deletions newrelic/api/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -1540,17 +1540,49 @@ def record_log_event(self, message, level=None, timestamp=None, attributes=None,
timestamp = timestamp if timestamp is not None else time.time()
level = str(level) if level is not None else "UNKNOWN"

if not message or message.isspace():
_logger.debug("record_log_event called where message was missing. No log event will be sent.")
# Unpack message and attributes from dict inputs
if isinstance(message, dict):
message_attributes = {k: v for k, v in message.items() if k != "message"}
message = str(message.get("message", ""))
TimPansino marked this conversation as resolved.
Show resolved Hide resolved
else:
message_attributes = None

# Exit early for invalid message type after unpacking
is_string = isinstance(message, six.string_types)
if message is not None and not is_string:
_logger.debug("record_log_event called where message was not found. No log event will be sent.")
TimPansino marked this conversation as resolved.
Show resolved Hide resolved
return

message = truncate(message, MAX_LOG_MESSAGE_LENGTH)
# Exit early if no message or attributes found
no_message = not message or message.isspace()
if not attributes and not message_attributes and no_message:
TimPansino marked this conversation as resolved.
Show resolved Hide resolved
_logger.debug("record_log_event called where message was missing, and no attributes found. No log event will be sent.")
return

# Truncate the now unpacked and string converted message
if is_string:
message = truncate(message, MAX_LOG_MESSAGE_LENGTH)

# Collect attributes from linking metadata, context data, and message attributes
collected_attributes = {}
if settings and settings.application_logging.forwarding.context_data.enabled:
if attributes:
context_attributes = resolve_logging_context_attributes(attributes, settings.attribute_filter, "context.")
if context_attributes:
collected_attributes.update(context_attributes)

if message_attributes:
message_attributes = resolve_logging_context_attributes(message_attributes, settings.attribute_filter, "message.")
if message_attributes:
collected_attributes.update(message_attributes)

# Exit early if no message or attributes found after filtering
if not collected_attributes and no_message:
TimPansino marked this conversation as resolved.
Show resolved Hide resolved
_logger.debug("record_log_event called where message was missing, and no attributes found. No log event will be sent.")
return

collected_attributes = get_linking_metadata()
if attributes and (settings and settings.application_logging.forwarding.context_data.enabled):
context_attributes = resolve_logging_context_attributes(attributes, settings.attribute_filter, "context.")
if context_attributes:
collected_attributes.update(context_attributes)
# Finally, add in linking attributes after checking that there is a valid message or at least 1 attribute
TimPansino marked this conversation as resolved.
Show resolved Hide resolved
collected_attributes.update(get_linking_metadata())

event = LogEventNode(
timestamp=timestamp,
Expand Down
13 changes: 6 additions & 7 deletions newrelic/core/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -943,13 +943,12 @@ def record_log_event(self, message, level=None, timestamp=None, attributes=None,
if not self._active_session:
return

if message:
with self._stats_custom_lock:
event = self._stats_engine.record_log_event(
message, level, timestamp, attributes=attributes, priority=priority
)
if event:
self._global_events_account += 1
with self._stats_custom_lock:
event = self._stats_engine.record_log_event(
message, level, timestamp, attributes=attributes, priority=priority
)
if event:
self._global_events_account += 1

def record_transaction(self, data):
"""Record a single transaction against this application."""
Expand Down
48 changes: 40 additions & 8 deletions newrelic/core/stats_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -1233,17 +1233,49 @@ def record_log_event(self, message, level=None, timestamp=None, attributes=None,
timestamp = timestamp if timestamp is not None else time.time()
level = str(level) if level is not None else "UNKNOWN"

if not message or message.isspace():
_logger.debug("record_log_event called where message was missing. No log event will be sent.")
# Unpack message and attributes from dict inputs
TimPansino marked this conversation as resolved.
Show resolved Hide resolved
if isinstance(message, dict):
message_attributes = {k: v for k, v in message.items() if k != "message"}
message = str(message.get("message", ""))
else:
message_attributes = None

# Exit early for invalid message type after unpacking
is_string = isinstance(message, six.string_types)
if message is not None and not is_string:
_logger.debug("record_log_event called where message was not found. No log event will be sent.")
return

# Exit early if no message or attributes found
no_message = not message or message.isspace()
if not attributes and not message_attributes and no_message:
_logger.debug("record_log_event called where message was missing, and no attributes found. No log event will be sent.")
return

message = truncate(message, MAX_LOG_MESSAGE_LENGTH)
# Truncate the now unpacked and string converted message
if is_string:
message = truncate(message, MAX_LOG_MESSAGE_LENGTH)

# Collect attributes from linking metadata, context data, and message attributes
collected_attributes = {}
if settings and settings.application_logging.forwarding.context_data.enabled:
if attributes:
context_attributes = resolve_logging_context_attributes(attributes, settings.attribute_filter, "context.")
if context_attributes:
collected_attributes.update(context_attributes)

if message_attributes:
message_attributes = resolve_logging_context_attributes(message_attributes, settings.attribute_filter, "message.")
if message_attributes:
collected_attributes.update(message_attributes)

# Exit early if no message or attributes found after filtering
if not collected_attributes and no_message:
_logger.debug("record_log_event called where message was missing, and no attributes found. No log event will be sent.")
return

collected_attributes = get_linking_metadata()
if attributes and (settings and settings.application_logging.forwarding.context_data.enabled):
context_attributes = resolve_logging_context_attributes(attributes, settings.attribute_filter, "context.")
if context_attributes:
collected_attributes.update(context_attributes)
# Finally, add in linking attributes after checking that there is a valid message or at least 1 attribute
collected_attributes.update(get_linking_metadata())

event = LogEventNode(
timestamp=timestamp,
Expand Down
138 changes: 95 additions & 43 deletions tests/agent_features/test_log_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,20 @@ def exercise_record_log_event():
record_log_event("keyword_arguments", timestamp=1234, level="ERROR", attributes={"key": "value"})
record_log_event("positional_arguments", "WARNING", 2345, {"key": "value"})
record_log_event("serialized_attributes", attributes=_serialized_attributes)
record_log_event(None, attributes={"attributes_only": "value"})
record_log_event({"attributes_only": "value"})
record_log_event({"message": "dict_message"})

# Unsent due to message content missing
record_log_event("")
record_log_event(" ")
record_log_event({"message": ""})
record_log_event({"filtered_attribute": "should_be_removed"})
record_log_event(None)
record_log_event(None, attributes={"attributes_only": "value"})


enable_log_forwarding = override_application_settings(
{"application_logging.forwarding.enabled": True, "application_logging.forwarding.context_data.enabled": True}
{"application_logging.forwarding.enabled": True, "application_logging.forwarding.context_data.enabled": True, "application_logging.forwarding.context_data.exclude": ["filtered_attribute"]}
)
disable_log_forwarding = override_application_settings({"application_logging.forwarding.enabled": False})

Expand Down Expand Up @@ -123,6 +127,9 @@ def exercise_record_log_event():
"context.non_serializable_attr": "<NonSerializableObject object>",
"context.attr_value_too_long": "*" * 255,
},
{"context.attributes_only": "value"},
{"message.attributes_only": "value"},
{"message": "dict_message"},
]
_exercise_record_log_event_inside_transaction_events = [
combine_dicts(_common_attributes_trace_linking, log) for log in _exercise_record_log_event_events
Expand Down Expand Up @@ -252,41 +259,37 @@ def test():
test()


_test_record_log_event_attribute_filtering_params = [
("", "", "context.A", True),
("", "A", "context.A", False),
("", "A", "context.B", True),
("A B", "*", "context.A", True),
("A B", "*", "context.B", True),
("A B", "*", "context.C", False),
("A B", "C", "context.A", True),
("A B", "C", "context.C", False),
("A B", "B", "context.A", True),
("A B", "B", "context.B", False),
("A", "A *", "context.A", False),
("A", "A *", "context.B", False),
("A*", "", "context.A", True),
("A*", "", "context.AB", True),
("", "A*", "context.A", False),
("", "A*", "context.B", True),
("A*", "AB", "context.AC", True),
("A*", "AB", "context.AB", False),
("AB", "A*", "context.AB", True),
("A*", "AB*", "context.ACB", True),
("A*", "AB*", "context.ABC", False),
# Linking attributes not affected by filtering
("", "", "entity.name", True),
("A", "*", "entity.name", True),
("", "*", "entity.name", True),
_test_record_log_event_context_attribute_filtering_params = [
("", "", "A", True),
("", "A", "A", False),
("", "A", "B", True),
("A B", "*", "A", True),
("A B", "*", "B", True),
("A B", "*", "C", False),
("A B", "C", "A", True),
("A B", "C", "C", False),
("A B", "B", "A", True),
("A B", "B", "B", False),
("A", "A *", "A", False),
("A", "A *", "B", False),
("A*", "", "A", True),
("A*", "", "AB", True),
("", "A*", "A", False),
("", "A*", "B", True),
("A*", "AB", "AC", True),
("A*", "AB", "AB", False),
("AB", "A*", "AB", True),
("A*", "AB*", "ACB", True),
("A*", "AB*", "ABC", False),
]


@pytest.mark.parametrize("include,exclude,attr,expected", _test_record_log_event_attribute_filtering_params)
def test_record_log_event_attribute_filtering_inside_transaction(include, exclude, attr, expected):
@pytest.mark.parametrize("prefix", ("context", "message"))
@pytest.mark.parametrize("include,exclude,attr,expected", _test_record_log_event_context_attribute_filtering_params)
def test_record_log_event_context_attribute_filtering_inside_transaction(include, exclude, attr, expected, prefix):
if expected:
expected_event = {"required_attrs": [attr]}
expected_event = {"required_attrs": [".".join((prefix, attr))]}
else:
expected_event = {"forgone_attrs": [attr]}
expected_event = {"forgone_attrs": [".".join((prefix, attr))]}

@override_application_settings(
{
Expand All @@ -300,21 +303,22 @@ def test_record_log_event_attribute_filtering_inside_transaction(include, exclud
@validate_log_event_count(1)
@background_task()
def test():
if attr.startswith("context."):
record_log_event("A", attributes={attr.lstrip("context."): 1})
if prefix == "context":
record_log_event("A", attributes={attr: 1})
else:
record_log_event("A")
record_log_event({"message": "A", attr: 1})

test()


@pytest.mark.parametrize("include,exclude,attr,expected", _test_record_log_event_attribute_filtering_params)
@pytest.mark.parametrize("prefix", ("context", "message"))
@pytest.mark.parametrize("include,exclude,attr,expected", _test_record_log_event_context_attribute_filtering_params)
@reset_core_stats_engine()
def test_record_log_event_attribute_filtering_outside_transaction(include, exclude, attr, expected):
def test_record_log_event_context_attribute_filtering_outside_transaction(include, exclude, attr, expected, prefix):
if expected:
expected_event = {"required_attrs": [attr]}
expected_event = {"required_attrs": [".".join((prefix, attr))]}
else:
expected_event = {"forgone_attrs": [attr]}
expected_event = {"forgone_attrs": [".".join((prefix, attr))]}

@override_application_settings(
{
Expand All @@ -327,9 +331,57 @@ def test_record_log_event_attribute_filtering_outside_transaction(include, exclu
@validate_log_events_outside_transaction(**expected_event)
@validate_log_event_count_outside_transaction(1)
def test():
if attr.startswith("context."):
record_log_event("A", attributes={attr.lstrip("context."): 1})
if prefix == "context":
record_log_event("A", attributes={attr: 1})
else:
record_log_event("A")
record_log_event({"message": "A", attr: 1})

test()


_test_record_log_event_linking_attribute_no_filtering_params = [
("", ""),
("", "entity.name"),
("", "*"),
]

@pytest.mark.parametrize("include,exclude", _test_record_log_event_linking_attribute_no_filtering_params)
def test_record_log_event_linking_attribute_no_filtering_inside_transaction(include, exclude):
attr = "entity.name"

@override_application_settings(
{
"application_logging.forwarding.enabled": True,
"application_logging.forwarding.context_data.enabled": True,
"application_logging.forwarding.context_data.include": _parse_attributes(include),
"application_logging.forwarding.context_data.exclude": _parse_attributes(exclude),
}
)
@validate_log_events(required_attrs=[attr])
@validate_log_event_count(1)
@background_task()
def test():
record_log_event("A")

test()


@pytest.mark.parametrize("include,exclude", _test_record_log_event_linking_attribute_no_filtering_params)
@reset_core_stats_engine()
def test_record_log_event_linking_attribute_filtering_outside_transaction(include, exclude):
attr = "entity.name"

@override_application_settings(
{
"application_logging.forwarding.enabled": True,
"application_logging.forwarding.context_data.enabled": True,
"application_logging.forwarding.context_data.include": _parse_attributes(include),
"application_logging.forwarding.context_data.exclude": _parse_attributes(exclude),
}
)
@validate_log_events_outside_transaction(required_attrs=[attr])
@validate_log_event_count_outside_transaction(1)
def test():
record_log_event("A")

test()
Loading