Skip to content

Commit c638501

Browse files
authored
add public API for span links (#1562)
* add public API for span links * add link support to OTel bridge * update changelog
1 parent c11f5d2 commit c638501

File tree

7 files changed

+112
-32
lines changed

7 files changed

+112
-32
lines changed

CHANGELOG.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ endif::[]
3838
3939
* Add instrumentation for https://github.com/aio-libs/aiobotocore[`aiobotocore`] {pull}1520[#1520]
4040
* Add instrumentation for https://kafka-python.readthedocs.io/en/master/[`kafka-python`] {pull}1555[#1555]
41+
* Add API for span links, and implement span link support for OpenTelemetry bridge {pull}1562[#1562]
4142
4243
[float]
4344
===== Bug fixes

docs/api.asciidoc

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ client.begin_transaction('processors')
138138
----
139139

140140
* `transaction_type`: (*required*) A string describing the type of the transaction, e.g. `'request'` or `'celery'`.
141-
* `trace_parent`: (*optional*) A TraceParent object. See <<traceparent-api, TraceParent generation>>.
141+
* `trace_parent`: (*optional*) A `TraceParent` object. See <<traceparent-api, TraceParent generation>>.
142+
* `links`: (*optional*) A list of `TraceParent` objects to which this transaction is causally linked.
142143

143144
[float]
144145
[[client-api-end-transaction]]
@@ -164,10 +165,10 @@ they have to be set beforehand by calling <<api-set-transaction-name, `elasticap
164165

165166
[float]
166167
[[traceparent-api]]
167-
==== TraceParent
168+
==== `TraceParent`
168169

169-
Transactions can be started with a TraceParent object. This creates a
170-
transaction that is a child of the TraceParent, which is essential for
170+
Transactions can be started with a `TraceParent` object. This creates a
171+
transaction that is a child of the `TraceParent`, which is essential for
171172
distributed tracing.
172173

173174
[float]
@@ -176,7 +177,7 @@ distributed tracing.
176177

177178
[small]#Added in v5.6.0.#
178179

179-
Create a TraceParent object from the string representation generated by
180+
Create a `TraceParent` object from the string representation generated by
180181
`TraceParent.to_string()`:
181182

182183
[source,python]
@@ -185,7 +186,7 @@ parent = elasticapm.trace_parent_from_string('00-03d67dcdd62b7c0f7a675424347eee3
185186
client.begin_transaction('processors', trace_parent=parent)
186187
----
187188

188-
* `traceparent_string`: (*required*) A string representation of a TraceParent object.
189+
* `traceparent_string`: (*required*) A string representation of a `TraceParent` object.
189190

190191

191192
[float]
@@ -194,7 +195,7 @@ client.begin_transaction('processors', trace_parent=parent)
194195

195196
[small]#Added in v5.6.0.#
196197

197-
Create a TraceParent object from HTTP headers (usually generated by another
198+
Create a `TraceParent` object from HTTP headers (usually generated by another
198199
Elastic APM agent):
199200

200201
[source,python]
@@ -211,7 +212,7 @@ client.begin_transaction('processors', trace_parent=parent)
211212

212213
[small]#Added in v5.10.0.#
213214

214-
Return the string representation of the current transaction TraceParent object:
215+
Return the string representation of the current transaction `TraceParent` object:
215216

216217
[source,python]
217218
----
@@ -469,12 +470,14 @@ def coffee_maker(strength):
469470
----
470471

471472
* `name`: The name of the span
472-
* `span_type`: The type of the span, usually in a dot-separated hierarchy of `type`, `subtype`, and `action`, e.g. `db.mysql.query`. Alternatively, type, subtype and action can be provided as three separate arguments, see `span_subtype` and `span_action`.
473-
* `skip_frames`: The number of stack frames to skip when collecting stack traces. Defaults to `0`.
474-
* `leaf`: if `True`, all spans nested bellow this span will be ignored. Defaults to `False`.
475-
* `labels`: a dictionary of labels. Keys must be strings, values can be strings, booleans, or numerical (`int`, `float`, `decimal.Decimal`). Defaults to `None`.
476-
* `span_subtype`: subtype of the span, e.g. name of the database. Defaults to `None`.
477-
* `span_action`: action of the span, e.g. `query`. Defaults to `None`
473+
* `span_type`: (*optional*) The type of the span, usually in a dot-separated hierarchy of `type`, `subtype`, and `action`, e.g. `db.mysql.query`. Alternatively, type, subtype and action can be provided as three separate arguments, see `span_subtype` and `span_action`.
474+
* `skip_frames`: (*optional*) The number of stack frames to skip when collecting stack traces. Defaults to `0`.
475+
* `leaf`: (*optional*) if `True`, all spans nested bellow this span will be ignored. Defaults to `False`.
476+
* `labels`: (*optional*) a dictionary of labels. Keys must be strings, values can be strings, booleans, or numerical (`int`, `float`, `decimal.Decimal`). Defaults to `None`.
477+
* `span_subtype`: (*optional*) subtype of the span, e.g. name of the database. Defaults to `None`.
478+
* `span_action`: (*optional*) action of the span, e.g. `query`. Defaults to `None`
479+
* `links`: (*optional*) A list of `TraceParent` objects to which this span is causally linked.
480+
478481

479482
[float]
480483
[[api-async-capture-span]]

elasticapm/base.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,15 @@
4444
import warnings
4545
from copy import deepcopy
4646
from datetime import timedelta
47-
from typing import Optional, Tuple
47+
from typing import Optional, Sequence, Tuple
4848

4949
import elasticapm
5050
from elasticapm.conf import Config, VersionedConfig, constants
5151
from elasticapm.conf.constants import ERROR
5252
from elasticapm.metrics.base_metrics import MetricsRegistry
5353
from elasticapm.traces import Tracer, execution_context
5454
from elasticapm.utils import cgroup, cloud, compat, is_master_process, stacks, varmap
55+
from elasticapm.utils.disttracing import TraceParent
5556
from elasticapm.utils.encoding import enforce_label_format, keyword_field, shorten, transform
5657
from elasticapm.utils.logging import get_logger
5758
from elasticapm.utils.module_import import import_string
@@ -288,7 +289,14 @@ def queue(self, event_type, data, flush=False):
288289
flush = False
289290
self._transport.queue(event_type, data, flush)
290291

291-
def begin_transaction(self, transaction_type, trace_parent=None, start=None, auto_activate=True):
292+
def begin_transaction(
293+
self,
294+
transaction_type: str,
295+
trace_parent: Optional[TraceParent] = None,
296+
start: Optional[float] = None,
297+
auto_activate: bool = True,
298+
links: Optional[Sequence[TraceParent]] = None,
299+
):
292300
"""
293301
Register the start of a transaction on the client
294302
@@ -300,7 +308,7 @@ def begin_transaction(self, transaction_type, trace_parent=None, start=None, aut
300308
"""
301309
if self.config.is_recording:
302310
return self.tracer.begin_transaction(
303-
transaction_type, trace_parent=trace_parent, start=start, auto_activate=auto_activate
311+
transaction_type, trace_parent=trace_parent, start=start, auto_activate=auto_activate, links=links
304312
)
305313

306314
def end_transaction(self, name=None, result="", duration=None):

elasticapm/contrib/opentelemetry/trace.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636

3737
from opentelemetry import trace as trace_api
3838
from opentelemetry.sdk import trace as oteltrace
39-
from opentelemetry.trace import Context, SpanKind
39+
from opentelemetry.trace import Context, Link, SpanKind
4040
from opentelemetry.trace.propagation import _SPAN_KEY
4141
from opentelemetry.trace.status import Status, StatusCode
4242
from opentelemetry.util import types
@@ -76,7 +76,7 @@ def start_span(
7676
context: Optional[Context] = None,
7777
kind: SpanKind = SpanKind.INTERNAL,
7878
attributes: types.Attributes = None,
79-
links: Optional[Sequence[Any]] = None,
79+
links: Optional[Sequence[Link]] = None,
8080
start_time: Optional[int] = None,
8181
record_exception: bool = True,
8282
set_status_on_exception: bool = True,
@@ -116,8 +116,6 @@ def start_span(
116116
Returns:
117117
The newly-created span.
118118
"""
119-
if links:
120-
logger.warning("The opentelemetry bridge does not support links at this time.")
121119
if not record_exception:
122120
logger.warning("record_exception was set to False, but exceptions will still be recorded for this span.")
123121

@@ -130,14 +128,15 @@ def start_span(
130128
current_transaction = execution_context.get_transaction()
131129
client = self.client
132130

131+
elastic_links = tuple(get_traceparent(link.context) for link in links) if links else None
133132
if traceparent and current_transaction:
134133
logger.warning(
135134
"Remote context included when a transaction was already active. "
136135
"Ignoring remote context and creating a Span instead."
137136
)
138137
elif traceparent:
139138
elastic_span = client.begin_transaction(
140-
"otel", traceparent=traceparent, start=start_time, auto_activate=False
139+
"otel", trace_parent=traceparent, start=start_time, auto_activate=False, links=elastic_links
141140
)
142141
span = Span(
143142
name=name,
@@ -147,7 +146,7 @@ def start_span(
147146
)
148147
span.set_attributes(attributes)
149148
elif not current_transaction:
150-
elastic_span = client.begin_transaction("otel", start=start_time, auto_activate=False)
149+
elastic_span = client.begin_transaction("otel", start=start_time, auto_activate=False, links=elastic_links)
151150
span = Span(
152151
name=name,
153152
elastic_span=elastic_span,
@@ -156,7 +155,9 @@ def start_span(
156155
)
157156
span.set_attributes(attributes)
158157
else:
159-
elastic_span = current_transaction.begin_span(name, "otel", start=start_time, auto_activate=False)
158+
elastic_span = current_transaction.begin_span(
159+
name, "otel", start=start_time, auto_activate=False, links=elastic_links
160+
)
160161
span = Span(
161162
name=name,
162163
elastic_span=elastic_span,
@@ -176,7 +177,7 @@ def start_as_current_span(
176177
context: Optional[Context] = None,
177178
kind: SpanKind = SpanKind.INTERNAL,
178179
attributes: types.Attributes = None,
179-
links: Optional[Sequence[Any]] = None,
180+
links: Optional[Sequence[Link]] = None,
180181
start_time: Optional[int] = None,
181182
record_exception: bool = True,
182183
set_status_on_exception: bool = True,

elasticapm/traces.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from collections import defaultdict
3939
from datetime import timedelta
4040
from types import TracebackType
41-
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
41+
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, Union
4242

4343
import elasticapm
4444
from elasticapm.conf import constants
@@ -91,7 +91,7 @@ def duration(self) -> timedelta:
9191

9292

9393
class BaseSpan(object):
94-
def __init__(self, labels=None, start=None):
94+
def __init__(self, labels=None, start=None, links: Optional[Sequence[TraceParent]] = None):
9595
self._child_durations = ChildDuration(self)
9696
self.labels = {}
9797
self.outcome: Optional[str] = None
@@ -100,7 +100,10 @@ def __init__(self, labels=None, start=None):
100100
self.start_time: float = time_to_perf_counter(start) if start is not None else _time_func()
101101
self.ended_time: Optional[float] = None
102102
self.duration: Optional[timedelta] = None
103-
self.links: List[Dict[str, str]] = []
103+
self.links: Optional[List[Dict[str, str]]] = None
104+
if links:
105+
for trace_parent in links:
106+
self.add_link(trace_parent)
104107
if labels:
105108
self.label(**labels)
106109

@@ -149,6 +152,8 @@ def add_link(self, trace_parent: TraceParent) -> None:
149152
"""
150153
Causally link this span/transaction to another span/transaction
151154
"""
155+
if self.links is None:
156+
self.links = []
152157
self.links.append({"trace_id": trace_parent.trace_id, "span_id": trace_parent.span_id})
153158

154159
def set_success(self):
@@ -175,6 +180,7 @@ def __init__(
175180
is_sampled: bool = True,
176181
start: Optional[float] = None,
177182
sample_rate: Optional[float] = None,
183+
links: Optional[Sequence[TraceParent]] = None,
178184
):
179185
"""
180186
tracer
@@ -193,6 +199,8 @@ def __init__(
193199
Sample rate which was used to decide whether to sample this transaction.
194200
This is reported to the APM server so that unsampled transactions can
195201
be extrapolated.
202+
links:
203+
A list of traceparents to link this transaction causally
196204
"""
197205
self.id = self.get_dist_tracing_id()
198206
if not trace_parent:
@@ -233,7 +241,10 @@ def __init__(
233241
)
234242
except (LookupError, AttributeError):
235243
self._breakdown = None
236-
super(Transaction, self).__init__(start=start)
244+
super().__init__(start=start)
245+
if links:
246+
for trace_parent in links:
247+
self.add_link(trace_parent)
237248

238249
def end(self, skip_frames: int = 0, duration: Optional[timedelta] = None):
239250
super().end(skip_frames, duration)
@@ -271,6 +282,7 @@ def _begin_span(
271282
sync=None,
272283
start=None,
273284
auto_activate=True,
285+
links: Optional[Sequence[TraceParent]] = None,
274286
):
275287
parent_span = execution_context.get_span()
276288
tracer = self.tracer
@@ -293,6 +305,7 @@ def _begin_span(
293305
span_action=span_action,
294306
sync=sync,
295307
start=start,
308+
links=links,
296309
)
297310
span.frames = tracer.frames_collector_func()
298311
self._span_counter += 1
@@ -312,6 +325,7 @@ def begin_span(
312325
sync=None,
313326
start=None,
314327
auto_activate=True,
328+
links: Optional[Sequence[TraceParent]] = None,
315329
):
316330
"""
317331
Begin a new span
@@ -325,6 +339,7 @@ def begin_span(
325339
:param sync: indicate if the span is synchronous or not. In most cases, `None` should be used
326340
:param start: timestamp, mostly useful for testing
327341
:param auto_activate: whether to set this span in execution_context
342+
:param links: an optional list of traceparents to link this span with
328343
:return: the Span object
329344
"""
330345
return self._begin_span(
@@ -339,6 +354,7 @@ def begin_span(
339354
sync=sync,
340355
start=start,
341356
auto_activate=auto_activate,
357+
links=links,
342358
)
343359

344360
def end_span(self, skip_frames: int = 0, duration: Optional[float] = None, outcome: str = "unknown"):
@@ -503,6 +519,7 @@ def __init__(
503519
span_action: Optional[str] = None,
504520
sync: Optional[bool] = None,
505521
start: Optional[int] = None,
522+
links: Optional[Sequence[TraceParent]] = None,
506523
):
507524
"""
508525
Create a new Span
@@ -538,7 +555,7 @@ def __init__(
538555
self.dist_tracing_propagated = False
539556
self.composite: Dict[str, Any] = {}
540557
self._cancelled: bool = False
541-
super(Span, self).__init__(labels=labels, start=start)
558+
super().__init__(labels=labels, start=start, links=links)
542559
self.timestamp = transaction.timestamp + (self.start_time - transaction.start_time)
543560
if self.transaction._breakdown:
544561
p = self.parent if self.parent else self.transaction
@@ -870,14 +887,22 @@ def span_stack_trace_min_duration(self) -> timedelta:
870887
else:
871888
return self.config.span_frames_min_duration
872889

873-
def begin_transaction(self, transaction_type, trace_parent=None, start=None, auto_activate=True):
890+
def begin_transaction(
891+
self,
892+
transaction_type: str,
893+
trace_parent: Optional[TraceParent] = None,
894+
start: Optional[float] = None,
895+
auto_activate: bool = True,
896+
links: Optional[Sequence[TraceParent]] = None,
897+
):
874898
"""
875899
Start a new transactions and bind it in a thread-local variable
876900
877901
:param transaction_type: type of the transaction, e.g. "request"
878902
:param trace_parent: an optional TraceParent object
879903
:param start: override the start timestamp, mostly useful for testing
880904
:param auto_activate: whether to set this transaction in execution_context
905+
:param list of traceparents to causally link this transaction to
881906
882907
:returns the Transaction object
883908
"""
@@ -900,6 +925,7 @@ def begin_transaction(self, transaction_type, trace_parent=None, start=None, aut
900925
is_sampled=is_sampled,
901926
start=start,
902927
sample_rate=sample_rate,
928+
links=links,
903929
)
904930
if trace_parent is None:
905931
transaction.trace_parent.add_tracestate(constants.TRACESTATE.SAMPLE_RATE, sample_rate)
@@ -949,6 +975,7 @@ class capture_span(object):
949975
"duration",
950976
"start",
951977
"sync",
978+
"links",
952979
)
953980

954981
def __init__(
@@ -964,6 +991,7 @@ def __init__(
964991
start: Optional[int] = None,
965992
duration: Optional[Union[float, timedelta]] = None,
966993
sync: Optional[bool] = None,
994+
links: Optional[Sequence[TraceParent]] = None,
967995
):
968996
self.name = name
969997
if span_subtype is None and "." in span_type:
@@ -985,6 +1013,7 @@ def __init__(
9851013
duration = timedelta(seconds=duration)
9861014
self.duration = duration
9871015
self.sync = sync
1016+
self.links = links
9881017

9891018
def __call__(self, func: Callable) -> Callable:
9901019
self.name = self.name or get_name_from_func(func)
@@ -1017,6 +1046,7 @@ def handle_enter(self, sync: bool) -> Optional[SpanType]:
10171046
span_action=self.action,
10181047
start=self.start,
10191048
sync=sync,
1049+
links=self.links,
10201050
)
10211051
return None
10221052

0 commit comments

Comments
 (0)