1717from contextlib import contextmanager
1818
1919from 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
2125from newrelic .api .application import application_instance
2226from newrelic .api .background_task import BackgroundTask
2832from newrelic .api .time_trace import current_trace , notice_error
2933from newrelic .api .transaction import Sentinel , current_transaction
3034from newrelic .api .web_transaction import WebTransaction
35+
36+ from newrelic .common .encoding_utils import (
37+ W3CTraceState ,
38+ NrTraceState ,
39+ )
3140from 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 )
0 commit comments