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

NewRelicContextFormatter update to support adding a stack trace #1168

Merged
merged 13 commits into from
Jul 11, 2024
Merged

NewRelicContextFormatter update to support adding a stack trace #1168

merged 13 commits into from
Jul 11, 2024

Conversation

fritzdj
Copy link
Contributor

@fritzdj fritzdj commented Jun 25, 2024

Overview

Currently the NewRelicContextFormatter implementation doesn't support including the stack trace. It would be convenient if there was an option because it would allow stack frames to be included as an attribute for an log in NR (instead of them being broken up by frame, which is the default if structured logs are not used). This PR is adding opt-in support for including stack traces.

Related Issue

#1169

@fritzdj fritzdj requested a review from a team June 25, 2024 13:15
@CLAassistant
Copy link

CLAassistant commented Jun 25, 2024

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
2 out of 3 committers have signed the CLA.

✅ TimPansino
✅ fritzdj
❌ mergify[bot]
You have signed the CLA already but the status is still pending? Let us recheck it.

Comment on lines 72 to 65
def __init__(self, *args, **kwargs):
super(NewRelicContextFormatter, self).__init__()
def __init__(self, *args, stack_trace_limit: Optional[int] = 0, **kwargs):
"""
:param Optional[int] stack_trace_limit:
Specifies the maximum number of frames to include for stack traces.
Defaults to `0` to suppress stack traces.
Setting this to `None` will make it so all available frames are included.
"""
super(NewRelicContextFormatter, self).__init__(*args, **kwargs)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reviewer note: *args and **kwargs were not being passed through to logging.Formatter init. This might have been a bug.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's likely true.

formatted["error.expected"] = expected

if self._stack_trace_limit is None or self._stack_trace_limit > 0:
stack_trace: Optional[str]
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
stack_trace: Optional[str]

We still have Python 2 wheels at the moment, so this line is a syntax issue. Should be removed.

assert filename.endswith("/test_logs_in_context.py")
assert isinstance(line_number, int)
assert isinstance(stack_trace, str)
assert stack_trace == expected_stack_trace
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
assert stack_trace == expected_stack_trace
assert stack_trace and stack_trace == expected_stack_trace

Same as above

assert filename.endswith("/test_logs_in_context.py")
assert isinstance(line_number, int)
assert isinstance(stack_trace, str)
assert stack_trace == expected_stack_trace
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
assert stack_trace == expected_stack_trace
assert stack_trace and stack_trace == expected_stack_trace

I like this strategy for checking stack traces, but would like to make sure they're not None or empty just in case.

Comment on lines 72 to 65
def __init__(self, *args, **kwargs):
super(NewRelicContextFormatter, self).__init__()
def __init__(self, *args, stack_trace_limit: Optional[int] = 0, **kwargs):
"""
:param Optional[int] stack_trace_limit:
Specifies the maximum number of frames to include for stack traces.
Defaults to `0` to suppress stack traces.
Setting this to `None` will make it so all available frames are included.
"""
super(NewRelicContextFormatter, self).__init__(*args, **kwargs)
Copy link
Contributor

Choose a reason for hiding this comment

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

That's likely true.

Comment on lines 82 to 83

@classmethod
def log_record_to_dict(cls, record):
expected = is_expected_error(exc_info)
Copy link
Contributor

Choose a reason for hiding this comment

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

An issue here, we're taking log_record_to_dict() which is a classmethod and changing it to an instance method. This would be a breaking change to the public API.

The same goes for format_exc_info() changing from a top level function to an instance method.

We definitely have customers who use these functions, or at least log_record_to_dict() without instantiating a class for convenience.

I'll push a commit with my proposal, but I'd like to convert these both into class functions, and export the names to the top level of the module so they can be used like functions. I think passing in the stack trace limit as an argument to the functions should preserve the most backwards compatibility for other users.

@@ -69,11 +55,46 @@ def safe_json_encode(obj, ignore_string_types=False, **kwargs):
class NewRelicContextFormatter(logging.Formatter):
DEFAULT_LOG_RECORD_KEYS = frozenset(set(vars(logging.LogRecord("", 0, "", 0, "", (), None))) | {"message"})

def __init__(self, *args, **kwargs):
super(NewRelicContextFormatter, self).__init__()
def __init__(self, *args, stack_trace_limit: Optional[int] = 0, **kwargs):
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
def __init__(self, *args, stack_trace_limit: Optional[int] = 0, **kwargs):
def __init__(self, *args, stack_trace_limit=0, **kwargs):

We still have Python 2 wheels at the moment, so this line is a syntax issue. Should be removed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually having a default keyword argument after *args isn't supported in Python 2 at all, I'll remove it for now like we do for our trace APIs and just attempt to pop it off the kwargs list if it exists.

@TimPansino
Copy link
Contributor

I've several pushed commits for both @fritzdj and our team to review. Here's the summary of my changes.

  • Python 2 issues in the code and tests fixed with transparent changes.
  • The public APIs for log_record_to_dict() and format_exc_info were originally changed in a backwards incompatible manner.
    • These have been changed to be class methods of the NewRelicContextFormatter to maintain compatibility, and exported as top level names in the newrelic.api.log module for the same reason.
    • As part of this change, stack_trace_limit was added as a function argument to both of these APIs and will be supplied automatically by calls to format() if using the Formatter as an object, or can be supplied otherwise.

I think this will achieve the most flexible compatibility possible with existing customer uses, while making this feature possible. It's worth noting that in all cases the default behavior will remain unchanged, where no stack traces are attached.

@fritzdj
Copy link
Contributor Author

fritzdj commented Jun 28, 2024

Thanks @TimPansino, these changes look good to me! Good catches on the breaking change to the public API and Python 2 compatibility issues.

@mergify mergify bot added the tests-failing label Jul 3, 2024
@mergify mergify bot removed the tests-failing label Jul 10, 2024
@lrafeei lrafeei merged commit 47581e9 into newrelic:main Jul 11, 2024
45 of 49 checks passed
@mergify mergify bot removed the tests-failing label Jul 11, 2024
@hmstepanek hmstepanek added this to the v9.12.0 milestone Jul 11, 2024
@rafaelclp
Copy link
Contributor

rafaelclp commented Jul 17, 2024

We extend the NewRelicContextFormatter to add more data to the log record dict:

class OurNewRelicContextFormatter(NewRelicContextFormatter):
    @classmethod
    def log_record_to_dict(cls, record) -> dict[str, Any]:
        log_dict = super().log_record_to_dict(record)
        # ... add more stuff to log_dict here ...
        return log_dict

When we updated to newrelic==9.12, we started getting errors because log_record_to_dict "takes 2 positional arguments but 3 were given".

We already fixed this from our side, by taking in *args, **kwargs and redirecting them to the super call, but wondering if we are the only ones affected...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants