Skip to content

Commit

Permalink
Merge branch 'develop-logging-attributes' into feature-loguru-attrs
Browse files Browse the repository at this point in the history
  • Loading branch information
TimPansino committed Jan 8, 2024
2 parents 860eb13 + 9cd6af2 commit 5974d7b
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 160 deletions.
69 changes: 0 additions & 69 deletions .github/actions/update-rpm-config/action.yml

This file was deleted.

10 changes: 0 additions & 10 deletions .github/workflows/deploy-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,3 @@ jobs:
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}

- name: Update RPM Config
uses: ./.github/actions/update-rpm-config
with:
agent-language: "python"
target-system: "all"
agent-version: "${{ github.ref_name }}"
dry-run: "false"
production-api-key: ${{ secrets.NEW_RELIC_API_KEY_PRODUCTION }}
staging-api-key: ${{ secrets.NEW_RELIC_API_KEY_STAGING }}
5 changes: 3 additions & 2 deletions newrelic/api/asgi_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ def should_insert_html(self, headers):
content_type = None

for header_name, header_value in headers:
# assume header names are lower cased in accordance with ASGI spec
# ASGI spec (https://asgi.readthedocs.io/en/latest/specs/www.html#http) states
# header names should be lower cased, but not required
header_name = header_name.lower()
if header_name == b"content-type":
content_type = header_value
elif header_name == b"content-encoding":
Expand Down Expand Up @@ -318,7 +320,6 @@ async def nr_async_asgi(receive, send):
send=send,
source=wrapped,
) as transaction:

# Record details of framework against the transaction for later
# reporting as supportability metrics.
if framework:
Expand Down
24 changes: 23 additions & 1 deletion newrelic/api/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from newrelic.api.time_trace import get_linking_metadata
from newrelic.api.transaction import current_transaction, record_log_event
from newrelic.common import agent_http
from newrelic.common.encoding_utils import safe_json_encode
from newrelic.common.encoding_utils import json_encode
from newrelic.common.object_names import parse_exc_info
from newrelic.core.attribute import truncate
from newrelic.core.config import global_settings, is_expected_error
Expand All @@ -44,6 +44,28 @@ def format_exc_info(exc_info):
return formatted


def safe_json_encode(obj, ignore_string_types=False, **kwargs):
# Performs the same operation as json_encode but replaces unserializable objects with a string containing their class name.
# If ignore_string_types is True, do not encode string types further.
# Currently used for safely encoding logging attributes.

if ignore_string_types and isinstance(obj, (six.string_types, six.binary_type)):
return obj

# Attempt to run through JSON serialization
try:
return json_encode(obj, **kwargs)
except Exception:
pass

# If JSON serialization fails then return a repr
try:
return repr(obj)
except Exception:
# If repr fails then default to an unprinatable object name
return "<unprintable %s object>" % type(obj).__name__


class NewRelicContextFormatter(Formatter):
DEFAULT_LOG_RECORD_KEYS = frozenset(set(vars(LogRecord("", 0, "", 0, "", (), None))) | {"message"})

Expand Down
53 changes: 29 additions & 24 deletions newrelic/api/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
json_decode,
json_encode,
obfuscate,
safe_json_encode,
snake_case,
)
from newrelic.core.attribute import (
Expand Down Expand Up @@ -1539,46 +1538,52 @@ 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"
context_attributes = attributes # Name reassigned for clarity

# 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", ""))
message = 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
if message is not None:
# Coerce message into a string type
if not isinstance(message, six.string_types):
try:
message = str(message)
except Exception:
# Exit early for invalid message type after unpacking
_logger.debug(
"record_log_event called where message could not be converted to a string type. No log event will be sent."
)
return

# Truncate the now unpacked and string converted message
if is_string:
# Truncate the now unpacked and string converted message
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:
context_attributes = resolve_logging_context_attributes(
context_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.")
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.")
if (not message or message.isspace()) and not context_attributes and not message_attributes:
_logger.debug(
"record_log_event called where no message and no attributes were found. No log event will be sent."
)
return

# Finally, add in linking attributes after checking that there is a valid message or at least 1 attribute
Expand Down Expand Up @@ -1944,17 +1949,17 @@ def add_framework_info(name, version=None):
transaction.add_framework_info(name, version)


def get_browser_timing_header():
def get_browser_timing_header(nonce=None):
transaction = current_transaction()
if transaction and hasattr(transaction, "browser_timing_header"):
return transaction.browser_timing_header()
return transaction.browser_timing_header(nonce)
return ""


def get_browser_timing_footer():
def get_browser_timing_footer(nonce=None):
transaction = current_transaction()
if transaction and hasattr(transaction, "browser_timing_footer"):
return transaction.browser_timing_footer()
return transaction.browser_timing_footer(nonce)
return ""


Expand Down
20 changes: 13 additions & 7 deletions newrelic/api/web_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,8 @@

_logger = logging.getLogger(__name__)

_js_agent_header_fragment = '<script type="text/javascript">%s</script>'
_js_agent_footer_fragment = '<script type="text/javascript">'\
'window.NREUM||(NREUM={});NREUM.info=%s</script>'
_js_agent_header_fragment = '<script type="text/javascript"%s>%s</script>'
_js_agent_footer_fragment = '<script type="text/javascript"%s>window.NREUM||(NREUM={});NREUM.info=%s</script>'

# Seconds since epoch for Jan 1 2000
JAN_1_2000 = time.mktime((2000, 1, 1, 0, 0, 0, 0, 0, 0))
Expand Down Expand Up @@ -156,6 +155,13 @@ def _is_websocket(environ):
return environ.get('HTTP_UPGRADE', '').lower() == 'websocket'


def _encode_nonce(nonce):
if not nonce:
return ""
else:
return ' nonce="%s"' % ensure_str(nonce) # Extra space intentional


class WebTransaction(Transaction):
unicode_error_reported = False
QUEUE_TIME_HEADERS = ('x-request-start', 'x-queue-start')
Expand Down Expand Up @@ -386,7 +392,7 @@ def _update_agent_attributes(self):

return super(WebTransaction, self)._update_agent_attributes()

def browser_timing_header(self):
def browser_timing_header(self, nonce=None):
"""Returns the JavaScript header to be included in any HTML
response to perform real user monitoring. This function returns
the header as a native Python string. In Python 2 native strings
Expand Down Expand Up @@ -437,7 +443,7 @@ def browser_timing_header(self):
# 'none'.

if self._settings.js_agent_loader:
header = _js_agent_header_fragment % self._settings.js_agent_loader
header = _js_agent_header_fragment % (_encode_nonce(nonce), self._settings.js_agent_loader)

# To avoid any issues with browser encodings, we will make sure
# that the javascript we inject for the browser agent is ASCII
Expand Down Expand Up @@ -476,7 +482,7 @@ def browser_timing_header(self):

return header

def browser_timing_footer(self):
def browser_timing_footer(self, nonce=None):
"""Returns the JavaScript footer to be included in any HTML
response to perform real user monitoring. This function returns
the footer as a native Python string. In Python 2 native strings
Expand Down Expand Up @@ -541,7 +547,7 @@ def browser_timing_footer(self):
attributes = obfuscate(json_encode(attributes), obfuscation_key)
footer_data['atts'] = attributes

footer = _js_agent_footer_fragment % json_encode(footer_data)
footer = _js_agent_footer_fragment % (_encode_nonce(nonce), json_encode(footer_data))

# To avoid any issues with browser encodings, we will make sure that
# the javascript we inject for the browser agent is ASCII encodable.
Expand Down
22 changes: 0 additions & 22 deletions newrelic/common/encoding_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,28 +113,6 @@ def json_decode(s, **kwargs):
return json.loads(s, **kwargs)


def safe_json_encode(obj, ignore_string_types=False, **kwargs):
# Performs the same operation as json_encode but replaces unserializable objects with a string containing their class name.
# If ignore_string_types is True, do not encode string types further.
# Currently used for safely encoding logging attributes.

if ignore_string_types and isinstance(obj, (six.string_types, six.binary_type)):
return obj

# Attempt to run through JSON serialization
try:
return json_encode(obj, **kwargs)
except Exception:
pass

# If JSON serialization fails then return a repr
try:
return repr(obj)
except Exception:
# If repr fails then default to an unprinatable object name
return "<unprintable %s object>" % type(obj).__name__


# Functions for obfuscating/deobfuscating text string based on an XOR
# cipher.

Expand Down
Loading

0 comments on commit 5974d7b

Please sign in to comment.