Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 4 additions & 2 deletions newrelic/api/cat_header_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class CatHeaderMixin(object):
cat_transaction_key = 'X-NewRelic-Transaction'
cat_appdata_key = 'X-NewRelic-App-Data'
cat_synthetics_key = 'X-NewRelic-Synthetics'
cat_synthetics_info_key = 'X-NewRelic-Synthetics-Info'
cat_metadata_key = 'x-newrelic-trace'
cat_distributed_trace_key = 'newrelic'
settings = None
Expand Down Expand Up @@ -105,8 +106,9 @@ def generate_request_headers(cls, transaction):
(cls.cat_transaction_key, encoded_transaction))

if transaction.synthetics_header:
nr_headers.append(
(cls.cat_synthetics_key, transaction.synthetics_header))
nr_headers.append((cls.cat_synthetics_key, transaction.synthetics_header))
if transaction.synthetics_info_header:
nr_headers.append((cls.cat_synthetics_info_key, transaction.synthetics_info_header))

return nr_headers

Expand Down
1 change: 1 addition & 0 deletions newrelic/api/message_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class MessageTrace(CatHeaderMixin, TimeTrace):
cat_transaction_key = "NewRelicTransaction"
cat_appdata_key = "NewRelicAppData"
cat_synthetics_key = "NewRelicSynthetics"
cat_synthetics_info_key = "NewRelicSyntheticsInfo"

def __init__(self, library, operation, destination_type, destination_name, params=None, terminal=True, **kwargs):
parent = kwargs.pop("parent", None)
Expand Down
22 changes: 22 additions & 0 deletions newrelic/api/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
json_decode,
json_encode,
obfuscate,
snake_case,
)
from newrelic.core.attribute import (
MAX_ATTRIBUTE_LENGTH,
Expand Down Expand Up @@ -303,10 +304,17 @@ def __init__(self, application, enabled=None, source=None):
self._alternate_path_hashes = {}
self.is_part_of_cat = False

# Synthetics Header
self.synthetics_resource_id = None
self.synthetics_job_id = None
self.synthetics_monitor_id = None
self.synthetics_header = None

# Synthetics Info Header
self.synthetics_type = None
self.synthetics_initiator = None
self.synthetics_attributes = None
self.synthetics_info_header = None

self._custom_metrics = CustomMetrics()
self._dimensional_metrics = DimensionalMetrics()
Expand Down Expand Up @@ -603,6 +611,10 @@ def __exit__(self, exc, value, tb):
synthetics_job_id=self.synthetics_job_id,
synthetics_monitor_id=self.synthetics_monitor_id,
synthetics_header=self.synthetics_header,
synthetics_type=self.synthetics_type,
synthetics_initiator=self.synthetics_initiator,
synthetics_attributes=self.synthetics_attributes,
synthetics_info_header=self.synthetics_info_header,
is_part_of_cat=self.is_part_of_cat,
trip_id=self.trip_id,
path_hash=self.path_hash,
Expand Down Expand Up @@ -840,6 +852,16 @@ def trace_intrinsics(self):
i_attrs["synthetics_job_id"] = self.synthetics_job_id
if self.synthetics_monitor_id:
i_attrs["synthetics_monitor_id"] = self.synthetics_monitor_id
if self.synthetics_type:
i_attrs["synthetics_type"] = self.synthetics_type
if self.synthetics_initiator:
i_attrs["synthetics_initiator"] = self.synthetics_initiator
if self.synthetics_attributes:
# Add all synthetics attributes
for k, v in self.synthetics_attributes.items():
if k:
i_attrs["synthetics_%s" % snake_case(k)] = v

if self.total_time:
i_attrs["totalTime"] = self.total_time
if self._loop_time:
Expand Down
39 changes: 38 additions & 1 deletion newrelic/api/web_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,27 @@ def _parse_synthetics_header(header):
return synthetics


def _parse_synthetics_info_header(header):
# Return a dictionary of values from SyntheticsInfo header
# Returns empty dict, if version is not supported.

synthetics_info = {}
version = None

try:
version = int(header.get("version"))

if version == 1:
synthetics_info['version'] = version
synthetics_info['type'] = header.get("type")
synthetics_info['initiator'] = header.get("initiator")
synthetics_info['attributes'] = header.get("attributes")
except Exception:
return

return synthetics_info


def _remove_query_string(url):
url = ensure_str(url)
out = urlparse.urlsplit(url)
Expand Down Expand Up @@ -231,6 +252,7 @@ def _process_synthetics_header(self):
settings.trusted_account_ids and \
settings.encoding_key:

# Synthetics Header
encoded_header = self._request_headers.get('x-newrelic-synthetics')
encoded_header = encoded_header and ensure_str(encoded_header)
if not encoded_header:
Expand All @@ -241,18 +263,33 @@ def _process_synthetics_header(self):
settings.encoding_key)
synthetics = _parse_synthetics_header(decoded_header)

# Synthetics Info Header
encoded_info_header = self._request_headers.get('x-newrelic-synthetics-info')
encoded_info_header = encoded_info_header and ensure_str(encoded_info_header)

decoded_info_header = decode_newrelic_header(
encoded_info_header,
settings.encoding_key)
synthetics_info = _parse_synthetics_info_header(decoded_info_header)

if synthetics and \
synthetics['account_id'] in \
settings.trusted_account_ids:

# Save obfuscated header, because we will pass it along
# Save obfuscated headers, because we will pass them along
# unchanged in all external requests.

self.synthetics_header = encoded_header
self.synthetics_resource_id = synthetics['resource_id']
self.synthetics_job_id = synthetics['job_id']
self.synthetics_monitor_id = synthetics['monitor_id']

if synthetics_info:
self.synthetics_info_header = encoded_info_header
self.synthetics_type = synthetics_info['type']
self.synthetics_initiator = synthetics_info['initiator']
self.synthetics_attributes = synthetics_info['attributes']

def _process_context_headers(self):
# Process the New Relic cross process ID header and extract
# the relevant details.
Expand Down
43 changes: 43 additions & 0 deletions newrelic/common/encoding_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -571,3 +571,46 @@ def decode(cls, payload, tk):
data['pr'] = None

return data


def capitalize(string):
"""Capitalize the first letter of a string."""
if not string:
return string
elif len(string) == 1:
return string.capitalize()
else:
return "".join((string[0].upper(), string[1:]))


def camel_case(string, upper=False):
"""
Convert a string of snake case to camel case.

Setting upper=True will capitalize the first letter. Defaults to False, where no change is made to the first letter.
"""
string = ensure_str(string)
split_string = list(string.split("_"))

if len(split_string) < 2:
if upper:
return capitalize(string)
else:
return string
else:
if upper:
camel_cased_string = "".join([capitalize(substr) for substr in split_string])
else:
camel_cased_string = split_string[0] + "".join([capitalize(substr) for substr in split_string[1:]])

return camel_cased_string


_snake_case_re = re.compile(r"([A-Z]+[a-z]*)")
def snake_case(string):
"""Convert a string of camel case to snake case. Assumes no repeated runs of capital letters."""
string = ensure_str(string)
if "_" in string:
return string # Don't touch strings that are already snake cased

return "_".join([s for s in _snake_case_re.split(string) if s]).lower()
14 changes: 14 additions & 0 deletions newrelic/core/transaction_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import newrelic.core.error_collector
import newrelic.core.trace_node
from newrelic.common.encoding_utils import camel_case
from newrelic.common.streaming_utils import SpanProtoAttrs
from newrelic.core.attribute import create_agent_attributes, create_user_attributes
from newrelic.core.attribute_filter import (
Expand Down Expand Up @@ -76,6 +77,10 @@
"synthetics_job_id",
"synthetics_monitor_id",
"synthetics_header",
"synthetics_type",
"synthetics_initiator",
"synthetics_attributes",
"synthetics_info_header",
"is_part_of_cat",
"trip_id",
"path_hash",
Expand Down Expand Up @@ -586,6 +591,15 @@ def _event_intrinsics(self, stats_table):
intrinsics["nr.syntheticsJobId"] = self.synthetics_job_id
intrinsics["nr.syntheticsMonitorId"] = self.synthetics_monitor_id

if self.synthetics_type:
intrinsics["nr.syntheticsType"] = self.synthetics_type
intrinsics["nr.syntheticsInitiator"] = self.synthetics_initiator
if self.synthetics_attributes:
# Add all synthetics attributes
for k, v in self.synthetics_attributes.items():
if k:
intrinsics["nr.synthetics%s" % camel_case(k, upper=True)] = v

def _add_call_time(source, target):
# include time for keys previously added to stats table via
# stats_engine.record_transaction
Expand Down
15 changes: 12 additions & 3 deletions tests/agent_features/test_error_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from testing_support.fixtures import (
cat_enabled,
make_cross_agent_headers,
make_synthetics_header,
make_synthetics_headers,
override_application_settings,
reset_core_stats_engine,
validate_error_event_sample_data,
Expand All @@ -43,6 +43,9 @@
SYNTHETICS_RESOURCE_ID = "09845779-16ef-4fa7-b7f2-44da8e62931c"
SYNTHETICS_JOB_ID = "8c7dd3ba-4933-4cbb-b1ed-b62f511782f4"
SYNTHETICS_MONITOR_ID = "dc452ae9-1a93-4ab5-8a33-600521e9cd00"
SYNTHETICS_TYPE = "scheduled"
SYNTHETICS_INITIATOR = "graphql"
SYNTHETICS_ATTRIBUTES = {"exampleAttribute": "1"}

ERR_MESSAGE = "Transaction had bad value"
ERROR = ValueError(ERR_MESSAGE)
Expand Down Expand Up @@ -135,6 +138,9 @@ def test_transaction_error_cross_agent():
"nr.syntheticsResourceId": SYNTHETICS_RESOURCE_ID,
"nr.syntheticsJobId": SYNTHETICS_JOB_ID,
"nr.syntheticsMonitorId": SYNTHETICS_MONITOR_ID,
"nr.syntheticsType": SYNTHETICS_TYPE,
"nr.syntheticsInitiator": SYNTHETICS_INITIATOR,
"nr.syntheticsExampleAttribute": "1",
}


Expand All @@ -144,12 +150,15 @@ def test_transaction_error_with_synthetics():
"err_message": ERR_MESSAGE,
}
settings = application_settings()
headers = make_synthetics_header(
headers = make_synthetics_headers(
settings.encoding_key,
settings.trusted_account_ids[0],
SYNTHETICS_RESOURCE_ID,
SYNTHETICS_JOB_ID,
SYNTHETICS_MONITOR_ID,
settings.encoding_key,
SYNTHETICS_TYPE,
SYNTHETICS_INITIATOR,
SYNTHETICS_ATTRIBUTES,
)
response = fully_featured_application.get("/", headers=headers, extra_environ=test_environ)

Expand Down
Loading