Skip to content

Commit d61ac06

Browse files
committed
Context propagation/DT enabling
1 parent 7dad3bb commit d61ac06

File tree

10 files changed

+1170
-41
lines changed

10 files changed

+1170
-41
lines changed

newrelic/api/opentelemetry.py

Lines changed: 173 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
from contextlib import contextmanager
1818

1919
from opentelemetry import trace as otel_api_trace
20+
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
21+
from opentelemetry.baggage.propagation import W3CBaggagePropagator
22+
from opentelemetry.propagators.composite import CompositePropagator
23+
from opentelemetry.propagate import set_global_textmap
2024

2125
from newrelic.api.application import application_instance
2226
from newrelic.api.background_task import BackgroundTask
@@ -28,11 +32,137 @@
2832
from newrelic.api.time_trace import current_trace, notice_error
2933
from newrelic.api.transaction import Sentinel, current_transaction
3034
from newrelic.api.web_transaction import WebTransaction
35+
36+
from newrelic.common.encoding_utils import (
37+
W3CTraceState,
38+
NrTraceState,
39+
)
3140
from newrelic.core.otlp_utils import create_resource
3241

3342
_logger = logging.getLogger(__name__)
3443

3544

45+
class NRTraceContextPropagator(TraceContextTextMapPropagator):
46+
LIST_OF_TRACEPARENT_KEYS = ("traceparent", "HTTP_TRACEPARENT")
47+
LIST_OF_TRACESTATE_KEYS = ("tracestate", "HTTP_TRACESTATE")
48+
49+
def _convert_nr_to_otel(self, tracestate):
50+
application_settings = application_instance(activate=False).settings
51+
vendors = W3CTraceState.decode(tracestate)
52+
trusted_account_key = application_settings.trusted_account_key or (
53+
application_settings.serverless_mode.enabled and application_settings.account_id
54+
)
55+
payload = vendors.pop(f"{trusted_account_key}@nr", "")
56+
57+
otel_tracestate = W3CTraceState(NrTraceState.decode(payload, trusted_account_key)).text()
58+
return otel_tracestate
59+
60+
def _convert_otel_to_nr(self, tracestate):
61+
tracestate_dict = W3CTraceState.decode(tracestate)
62+
# Convert sampled, priority, and timestamp data types
63+
tracestate_dict["sa"] = True if tracestate_dict.get("sa").upper() == "TRUE" else False
64+
tracestate_dict["pr"] = float(tracestate_dict.get("pr"))
65+
tracestate_dict["ti"] = int(tracestate_dict.get("ti"))
66+
67+
nr_tracestate = NrTraceState(tracestate_dict).text()
68+
return nr_tracestate
69+
70+
def extract(self, carrier, context=None, getter=None):
71+
# We need to make sure that the carrier goes out
72+
# in OTel format. However, we want to convert this to
73+
# NR to use the `accept_distributed_trace_headers` API
74+
transaction = current_transaction()
75+
tracestate_key = None
76+
tracestate_headers = None
77+
for key in self.LIST_OF_TRACESTATE_KEYS:
78+
if key in carrier:
79+
tracestate_key = key
80+
tracestate_headers = carrier[tracestate_key]
81+
break
82+
# If we are passing into New Relic, traceparent and/or tracestate's keys also need to be NR compatible.
83+
if tracestate_headers:
84+
# Check to see if in NR or OTel format
85+
if "@nr=" in tracestate_headers:
86+
# NR format
87+
# Reformatting DT keys in case they are in the HTTP_* format:
88+
nr_headers = carrier.copy()
89+
for header_type in ("traceparent", "tracestate", "newrelic"):
90+
if (header_type not in nr_headers) and (f"HTTP_{header_type.upper()}" in nr_headers):
91+
nr_headers[header_type] = nr_headers.pop(f"HTTP_{header_type.upper()}")
92+
transaction.accept_distributed_trace_headers(nr_headers)
93+
# Convert NR format to OTel format for OTel extract function
94+
tracestate = self._convert_nr_to_otel(tracestate_headers)
95+
carrier[tracestate_key] = tracestate
96+
else:
97+
# OTel format
98+
if transaction:
99+
# Convert to NR format to use the
100+
# `accept_distributed_trace_headers` API
101+
nr_tracestate = self._convert_otel_to_nr(tracestate_headers)
102+
nr_headers = {key: value for key, value in carrier.items()}
103+
nr_headers.pop("HTTP_TRACESTATE", None)
104+
nr_headers["tracestate"] = nr_tracestate
105+
for header_type in ("traceparent", "newrelic"):
106+
if header_type not in nr_headers:
107+
nr_headers[header_type] = nr_headers.pop(f"HTTP_{header_type.upper()}", None)
108+
transaction.accept_distributed_trace_headers(nr_headers)
109+
elif ("traceparent" in carrier) and transaction:
110+
transaction.accept_distributed_trace_headers(carrier)
111+
112+
return super().extract(carrier=carrier, context=context, getter=getter)
113+
114+
115+
def inject(self, carrier, context=None, setter=None):
116+
transaction = current_transaction()
117+
# Only insert headers if we have not done so already this transaction
118+
# Distributed Trace State will have the following states:
119+
# 0 if not set
120+
# 1 if already accepted
121+
# 2 if inserted but not accepted
122+
123+
if transaction and not transaction._distributed_trace_state:
124+
try:
125+
nr_headers = [(key, value) for key, value in carrier.items()]
126+
transaction.insert_distributed_trace_headers(nr_headers)
127+
# Convert back, now with new headers
128+
carrier.update(dict(nr_headers))
129+
carrier["tracestate"] = self._convert_nr_to_otel(carrier["tracestate"])
130+
131+
except AttributeError:
132+
# Already in list form.
133+
transaction.insert_distributed_trace_headers(carrier)
134+
135+
# If it came in list form, we likely want to keep it in that format.
136+
# Convert to dict to modify NR format of tracestate to Otel's format
137+
# and then convert back to the list of tuples.
138+
otel_headers = dict(carrier)
139+
otel_headers["tracestate"] = self._convert_nr_to_otel(otel_headers["tracestate"])
140+
141+
# This is done instead of assigning the result of a list
142+
# comprehension to preserve the ID of the carrier in
143+
# order to allow propagation.
144+
for header in otel_headers.items():
145+
if header not in carrier:
146+
carrier.append(header)
147+
148+
elif not transaction:
149+
# Convert carrier's tracestate to Otel format if not already
150+
# This assumes that carrier is a dict but tracestate is in NR format.
151+
if ("tracestate" in carrier) and ("@nr=" in carrier["tracestate"]):
152+
# Needs to be converted to OTel before running original function
153+
carrier["tracestate"] = self._convert_nr_to_otel(carrier["tracestate"])
154+
return super().inject(carrier=carrier, context=context, setter=setter)
155+
156+
157+
# Context and Context Propagator Setup
158+
otel_context_propagator = CompositePropagator(
159+
propagators=[
160+
NRTraceContextPropagator(),
161+
W3CBaggagePropagator(),
162+
]
163+
)
164+
set_global_textmap(otel_context_propagator)
165+
36166
# ----------------------------------------------
37167
# Custom OTel Spans and Traces
38168
# ----------------------------------------------
@@ -168,7 +298,17 @@ def get_span_context(self):
168298
if not getattr(self, "nr_trace", False):
169299
return otel_api_trace.INVALID_SPAN_CONTEXT
170300

171-
otel_tracestate_headers = None
301+
if self.nr_transaction.settings.distributed_tracing.enabled:
302+
nr_tracestate_headers = (
303+
self.nr_transaction._create_distributed_trace_data()
304+
)
305+
306+
nr_tracestate_headers["sa"] = self._sampled()
307+
otel_tracestate_headers = [
308+
(key, str(value)) for key, value in nr_tracestate_headers.items()
309+
]
310+
else:
311+
otel_tracestate_headers = None
172312

173313
return otel_api_trace.SpanContext(
174314
trace_id=int(self.nr_transaction.trace_id, 16),
@@ -287,7 +427,17 @@ def start_span(
287427
if parent_span_context is None or not parent_span_context.is_valid:
288428
parent_span_context = None
289429

430+
# If parent_span_context exists, we can create traceparent
431+
# and tracestate headers
432+
_headers = {}
433+
if parent_span_context and self.nr_application.settings.distributed_tracing.enabled:
434+
parent_span_trace_id = parent_span_context.trace_id
435+
parent_span_span_id = parent_span_context.span_id
436+
parent_span_trace_flags = parent_span_context.trace_flags
437+
438+
290439
# If remote_parent, transaction must be created, regardless of kind type
440+
# Make sure we transfer DT headers when we are here, if DT is enabled
291441
if parent_span_context and parent_span_context.is_remote:
292442
if kind in (otel_api_trace.SpanKind.SERVER, otel_api_trace.SpanKind.CLIENT):
293443
# This is a web request
@@ -297,6 +447,11 @@ def start_span(
297447
port = self.attributes.get("net.host.port")
298448
request_method = self.attributes.get("http.method")
299449
request_path = self.attributes.get("http.route")
450+
451+
if not headers:
452+
headers = _headers
453+
update_sampled_flag = True
454+
300455
transaction = WebTransaction(
301456
self.nr_application,
302457
name=name,
@@ -307,7 +462,13 @@ def start_span(
307462
request_path=request_path,
308463
headers=headers,
309464
)
310-
elif kind in (otel_api_trace.SpanKind.PRODUCER, otel_api_trace.SpanKind.INTERNAL):
465+
466+
if update_sampled_flag and parent_span_context:
467+
transaction._sampled = bool(parent_span_trace_flags)
468+
elif kind in (
469+
otel_api_trace.SpanKind.PRODUCER,
470+
otel_api_trace.SpanKind.INTERNAL,
471+
):
311472
transaction = BackgroundTask(self.nr_application, name=name)
312473
elif kind == otel_api_trace.SpanKind.CONSUMER:
313474
transaction = MessageTransaction(
@@ -339,6 +500,10 @@ def start_span(
339500
request_method = self.attributes.get("http.method")
340501
request_path = self.attributes.get("http.route")
341502

503+
if not headers:
504+
headers = _headers
505+
update_GUID_flag = True
506+
342507
transaction = WebTransaction(
343508
self.nr_application,
344509
name=name,
@@ -349,6 +514,11 @@ def start_span(
349514
request_path=request_path,
350515
headers=headers,
351516
)
517+
518+
if update_GUID_flag and parent_span_context:
519+
guid = parent_span_trace_id >> 64
520+
transaction.guid = f"{guid:x}"
521+
352522
transaction.__enter__()
353523
elif kind == otel_api_trace.SpanKind.INTERNAL:
354524
if transaction:
@@ -436,4 +606,4 @@ def get_tracer(
436606
*args,
437607
**kwargs,
438608
):
439-
return Tracer(resource=self._resource, instrumentation_library=instrumenting_module_name, *args, **kwargs)
609+
return Tracer(resource=self._resource, instrumentation_library=instrumenting_module_name, *args, **kwargs)

newrelic/api/transaction.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1427,6 +1427,18 @@ def accept_distributed_trace_headers(self, headers, transport_type="HTTP"):
14271427
data.update(tracestate_data)
14281428
else:
14291429
self._record_supportability("Supportability/TraceContext/TraceState/InvalidNrEntry")
1430+
elif not payload and (tracestate == self.tracestate):
1431+
self._record_supportability("Supportability/TraceContext/TraceState/NoNrEntry")
1432+
self._record_supportability("Supportability/TraceContext/TraceState/OtelEntry")
1433+
try:
1434+
vendors["sa"] = True if vendors.get("sa").lower() == "true" else False
1435+
vendors["pr"] = float(vendors.get("pr"))
1436+
vendors["ti"] = int(vendors.get("ti"))
1437+
1438+
self.trusted_parent_span = vendors.pop("id", None)
1439+
data.update(vendors)
1440+
except:
1441+
pass
14301442
else:
14311443
self._record_supportability("Supportability/TraceContext/TraceState/NoNrEntry")
14321444

newrelic/config.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4370,11 +4370,27 @@ def _process_module_builtin_defaults():
43704370

43714371
# Hybrid Agent Hooks
43724372
_process_module_definition(
4373-
"opentelemetry.trace", "newrelic.hooks.hybridagent_opentelemetry", "instrument_trace_api"
4373+
"opentelemetry.context",
4374+
"newrelic.hooks.hybridagent_opentelemetry",
4375+
"instrument_context_api",
43744376
)
43754377

43764378
_process_module_definition(
4377-
"opentelemetry.instrumentation.utils", "newrelic.hooks.hybridagent_opentelemetry", "instrument_utils"
4379+
"opentelemetry.instrumentation.propagators",
4380+
"newrelic.hooks.hybridagent_opentelemetry",
4381+
"instrument_global_propagators_api",
4382+
)
4383+
4384+
_process_module_definition(
4385+
"opentelemetry.trace",
4386+
"newrelic.hooks.hybridagent_opentelemetry",
4387+
"instrument_trace_api",
4388+
)
4389+
4390+
_process_module_definition(
4391+
"opentelemetry.instrumentation.utils",
4392+
"newrelic.hooks.hybridagent_opentelemetry",
4393+
"instrument_utils",
43784394
)
43794395

43804396

newrelic/core/config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1272,7 +1272,9 @@ def default_otlp_host(host):
12721272
_settings.azure_operator.enabled = _environ_as_bool("NEW_RELIC_AZURE_OPERATOR_ENABLED", default=False)
12731273
_settings.package_reporting.enabled = _environ_as_bool("NEW_RELIC_PACKAGE_REPORTING_ENABLED", default=True)
12741274
_settings.ml_insights_events.enabled = _environ_as_bool("NEW_RELIC_ML_INSIGHTS_EVENTS_ENABLED", default=False)
1275-
_settings.otel_bridge.enabled = _environ_as_bool("NEW_RELIC_OTEL_BRIDGE_ENABLED", default=False)
1275+
_settings.otel_bridge.enabled = _environ_as_bool(
1276+
"NEW_RELIC_OTEL_BRIDGE_ENABLED", default=False
1277+
)
12761278

12771279

12781280
def global_settings():

0 commit comments

Comments
 (0)