Skip to content

Commit 5577ffd

Browse files
lrafeeimergify[bot]hmstepanek
authored
Hybrid agent context management (#1589)
* Context propagation/DT enabling * Add accept/extract DT tests * Propagation extract/accept tests & remote tracestate parsing * Remove extra spaces * Explanation for update flags * Modify carrier and sampling logic * Change test name * Apply suggestions from code review Co-authored-by: Hannah Stepanek <hstepanek@newrelic.com> * Apply reviewer suggestions * Default parent_span_trace_id to None * Apply suggestions from code review Co-authored-by: Hannah Stepanek <hstepanek@newrelic.com> * Reviewer suggestions & cleanup * [MegaLinter] Apply linters fixes * Apply review suggestions (for real this time) --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: Hannah Stepanek <hstepanek@newrelic.com>
1 parent 378c2c6 commit 5577ffd

File tree

8 files changed

+1126
-91
lines changed

8 files changed

+1126
-91
lines changed

newrelic/api/opentelemetry.py

Lines changed: 92 additions & 46 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.baggage.propagation import W3CBaggagePropagator
21+
from opentelemetry.propagate import set_global_textmap
22+
from opentelemetry.propagators.composite import CompositePropagator
23+
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
2024

2125
from newrelic.api.application import application_instance
2226
from newrelic.api.background_task import BackgroundTask
@@ -33,21 +37,68 @@
3337
_logger = logging.getLogger(__name__)
3438

3539

40+
class NRTraceContextPropagator(TraceContextTextMapPropagator):
41+
HEADER_KEYS = ("traceparent", "tracestate", "newrelic")
42+
43+
def extract(self, carrier, context=None, getter=None):
44+
transaction = current_transaction()
45+
# If we are passing into New Relic, traceparent
46+
# and/or tracestate's keys also need to be NR compatible.
47+
48+
if transaction:
49+
nr_headers = {
50+
header_key: getter.get(carrier, header_key)[0]
51+
for header_key in self.HEADER_KEYS
52+
if getter.get(carrier, header_key)
53+
}
54+
transaction.accept_distributed_trace_headers(nr_headers)
55+
return super().extract(carrier=carrier, context=context, getter=getter)
56+
57+
def inject(self, carrier, context=None, setter=None):
58+
transaction = current_transaction()
59+
# Only insert headers if we have not done so already this transaction
60+
# New Relic's Distributed Trace State will have the following states:
61+
# 0 (00) if not set:
62+
# Transaction has not inserted any outbound headers nor has
63+
# it accepted any inbound headers (yet).
64+
# 1 (01) if already accepted:
65+
# Transaction has accepted inbound headers and is able to
66+
# insert outbound headers to the next app if needed.
67+
# 2 (10) if inserted but not accepted:
68+
# Transaction has inserted outbound headers already.
69+
# Do not insert outbound headers multiple times. This is
70+
# a fundamental difference in OTel vs NR behavior: if
71+
# headers are inserted by OTel multiple times, it will
72+
# propagate the last set of data that was inserted. NR
73+
# will not allow more than one header insertion per
74+
# transaction.
75+
# 3 (11) if accepted, then inserted:
76+
# Transaction has accepted inbound headers and has inserted
77+
# outbound headers.
78+
79+
if not transaction:
80+
return super().inject(carrier=carrier, context=context, setter=setter)
81+
82+
if transaction._distributed_trace_state < 2:
83+
nr_headers = []
84+
transaction.insert_distributed_trace_headers(nr_headers)
85+
for key, value in nr_headers:
86+
setter.set(carrier, key, value)
87+
# Do NOT call super().inject() since we have already
88+
# inserted the headers here. It will not cause harm,
89+
# but it is redundant logic.
90+
91+
# If distributed_trace_state == 2 or 3, do not inject headers.
92+
93+
94+
# Context and Context Propagator Setup
95+
otel_context_propagator = CompositePropagator(propagators=[NRTraceContextPropagator(), W3CBaggagePropagator()])
96+
set_global_textmap(otel_context_propagator)
97+
3698
# ----------------------------------------------
3799
# Custom OTel Spans and Traces
38100
# ----------------------------------------------
39101

40-
# TracerProvider: we can think of this as the agent instance. Only one can exist
41-
# SpanProcessor: we can think of this as an application. In NR, we can have multiple applications
42-
# though right now, we can only do SpanProcessor and SynchronousMultiSpanProcessor
43-
# Tracer: we can think of this as the transaction.
44-
# Span: we can think of this as the trace.
45-
# Links functionality has now been enabled but not implemented yet. Links are relationships
46-
# between spans, but lateral in hierarchy. In NR we only have parent-child relationships.
47-
# We may want to preserve this information with a custom attribute. We can also add this
48-
# as a new attribute in a trace, but it will still not be seen in the UI other than a trace
49-
# attribute.
50-
51102

52103
class Span(otel_api_trace.Span):
53104
def __init__(
@@ -73,11 +124,6 @@ def __init__(
73124
self.nr_trace = None
74125
self.instrumenting_module = instrumenting_module
75126

76-
# Do not create a New Relic trace if parent
77-
# is a remote span and it is not sampled
78-
if self._remote() and not self._sampled():
79-
return
80-
81127
self.nr_parent = None
82128
current_nr_trace = current_trace()
83129
if (
@@ -100,7 +146,7 @@ def __init__(
100146
_logger.error(
101147
"OpenTelemetry span (%s) and NR trace (%s) do not match nor correspond to a remote span. Open Telemetry span will not be reported to New Relic. Please report this problem to New Relic.",
102148
self.otel_parent,
103-
current_nr_trace,
149+
current_nr_trace, # NR parent trace
104150
)
105151
return
106152

@@ -140,26 +186,6 @@ def __init__(
140186

141187
self.nr_trace.__enter__()
142188

143-
def _sampled(self):
144-
# Uses NR to determine if the trace is sampled
145-
#
146-
# transaction.sampled can be `None`, `True`, `False`.
147-
# If `None`, this has not been computed by NR which
148-
# can also mean the following:
149-
# 1. There was not a context passed in that explicitly has sampling disabled.
150-
# This flag would be found in the traceparent or traceparent and tracespan headers.
151-
# 2. Transaction was not created where DT headers are accepted during __init__
152-
# Therefore, we will treat a value of `None` as `True` for now.
153-
#
154-
# The primary reason for this behavior is because Otel expects to
155-
# only be able to record information like events and attributes
156-
# when `is_recording()` == `True`
157-
158-
if self.otel_parent:
159-
return bool(self.otel_parent.trace_flags)
160-
else:
161-
return bool(self.nr_transaction and (self.nr_transaction.sampled or (self.nr_transaction.sampled is None)))
162-
163189
def _remote(self):
164190
# Remote span denotes if propagated from a remote parent
165191
return bool(self.otel_parent and self.otel_parent.is_remote)
@@ -168,14 +194,12 @@ def get_span_context(self):
168194
if not getattr(self, "nr_trace", False):
169195
return otel_api_trace.INVALID_SPAN_CONTEXT
170196

171-
otel_tracestate_headers = None
172-
173197
return otel_api_trace.SpanContext(
174198
trace_id=int(self.nr_transaction.trace_id, 16),
175199
span_id=int(self.nr_trace.guid, 16),
176200
is_remote=self._remote(),
177-
trace_flags=otel_api_trace.TraceFlags(0x01 if self._sampled() else 0x00),
178-
trace_state=otel_api_trace.TraceState(otel_tracestate_headers),
201+
trace_flags=otel_api_trace.TraceFlags(0x01),
202+
trace_state=otel_api_trace.TraceState(),
179203
)
180204

181205
def set_attribute(self, key, value):
@@ -193,8 +217,7 @@ def _set_attributes_in_nr(self, otel_attributes=None):
193217

194218
def add_event(self, name, attributes=None, timestamp=None):
195219
# TODO: Not implemented yet.
196-
# We can implement this as a log event
197-
raise NotImplementedError("TODO: We can implement this as a log event.")
220+
raise NotImplementedError("Events are not implemented yet.")
198221

199222
def add_link(self, context=None, attributes=None):
200223
# TODO: Not implemented yet.
@@ -208,7 +231,14 @@ def update_name(self, name):
208231
self.nr_trace.name = self._name
209232

210233
def is_recording(self):
211-
return self._sampled() and not (getattr(self.nr_trace, None), "end_time", None)
234+
# If the trace has an end time set then it is done recording. Otherwise,
235+
# if it does not have an end time set and the transaction's priority
236+
# has not been set yet or it is set to something other than 0 then it
237+
# is also still recording.
238+
if getattr(self.nr_trace, "end_time", None):
239+
return False
240+
241+
return getattr(self.nr_transaction, "priority", 1) > 0
212242

213243
def set_status(self, status, description=None):
214244
# TODO: not implemented yet
@@ -277,6 +307,10 @@ def start_span(
277307
self.nr_application = application_instance()
278308
self.attributes = attributes or {}
279309

310+
if not self.nr_application.active:
311+
# Force application registration if not already active
312+
self.nr_application.activate()
313+
280314
if not self.nr_application.settings.otel_bridge.enabled:
281315
return otel_api_trace.INVALID_SPAN
282316

@@ -286,7 +320,12 @@ def start_span(
286320
if parent_span_context is None or not parent_span_context.is_valid:
287321
parent_span_context = None
288322

323+
parent_span_trace_id = None
324+
if parent_span_context and self.nr_application.settings.distributed_tracing.enabled:
325+
parent_span_trace_id = parent_span_context.trace_id
326+
289327
# If remote_parent, transaction must be created, regardless of kind type
328+
# Make sure we transfer DT headers when we are here, if DT is enabled
290329
if parent_span_context and parent_span_context.is_remote:
291330
if kind in (otel_api_trace.SpanKind.SERVER, otel_api_trace.SpanKind.CLIENT):
292331
# This is a web request
@@ -296,6 +335,7 @@ def start_span(
296335
port = self.attributes.get("net.host.port")
297336
request_method = self.attributes.get("http.method")
298337
request_path = self.attributes.get("http.route")
338+
299339
transaction = WebTransaction(
300340
self.nr_application,
301341
name=name,
@@ -306,6 +346,7 @@ def start_span(
306346
request_path=request_path,
307347
headers=headers,
308348
)
349+
309350
elif kind in (otel_api_trace.SpanKind.PRODUCER, otel_api_trace.SpanKind.INTERNAL):
310351
transaction = BackgroundTask(self.nr_application, name=name)
311352
elif kind == otel_api_trace.SpanKind.CONSUMER:
@@ -315,7 +356,7 @@ def start_span(
315356
destination_name=name,
316357
application=self.nr_application,
317358
transport_type=self.instrumentation_library,
318-
headers=headers,
359+
headers=None,
319360
)
320361

321362
transaction.__enter__()
@@ -348,6 +389,11 @@ def start_span(
348389
request_path=request_path,
349390
headers=headers,
350391
)
392+
393+
transaction._trace_id = (
394+
f"{parent_span_trace_id:x}" if parent_span_trace_id else transaction.trace_id
395+
)
396+
351397
transaction.__enter__()
352398
elif kind == otel_api_trace.SpanKind.INTERNAL:
353399
if transaction:
@@ -372,7 +418,7 @@ def start_span(
372418
destination_name=name,
373419
application=self.nr_application,
374420
transport_type=self.instrumentation_library,
375-
headers=headers,
421+
headers=None,
376422
)
377423
transaction.__enter__()
378424
elif kind == otel_api_trace.SpanKind.PRODUCER:

newrelic/config.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4428,6 +4428,16 @@ def _process_module_builtin_defaults():
44284428
)
44294429

44304430
# Hybrid Agent Hooks
4431+
_process_module_definition(
4432+
"opentelemetry.context", "newrelic.hooks.hybridagent_opentelemetry", "instrument_context_api"
4433+
)
4434+
4435+
_process_module_definition(
4436+
"opentelemetry.instrumentation.propagators",
4437+
"newrelic.hooks.hybridagent_opentelemetry",
4438+
"instrument_global_propagators_api",
4439+
)
4440+
44314441
_process_module_definition(
44324442
"opentelemetry.trace", "newrelic.hooks.hybridagent_opentelemetry", "instrument_trace_api"
44334443
)

0 commit comments

Comments
 (0)