Skip to content

Commit 7b1d866

Browse files
sdk: Add support for lazy events and links (#474)
The computation of attributes is expensive in some conditions, even in some cases those attributes are not used at all. This commit fixes the add_lazy_event method by providing a true solution that delays the attributes calculation until attributes are accessed. It also provides a new LazyLink class. Events are also moved to SDK, as they are not a required object to satisfy the API specification. Co-authored-by: Leighton Chen <[email protected]>
1 parent 5fbf999 commit 7b1d866

File tree

7 files changed

+169
-52
lines changed

7 files changed

+169
-52
lines changed

Diff for: ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def test_translate_to_jaeger(self):
151151
}
152152

153153
event_timestamp = base_time + 50 * 10 ** 6
154-
event = trace_api.Event(
154+
event = trace.Event(
155155
name="event0",
156156
timestamp=event_timestamp,
157157
attributes=event_attributes,

Diff for: ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def test_translate_to_collector(self):
108108
"key_float": 0.3,
109109
}
110110
event_timestamp = base_time + 50 * 10 ** 6
111-
event = trace_api.Event(
111+
event = trace.Event(
112112
name="event0",
113113
timestamp=event_timestamp,
114114
attributes=event_attributes,

Diff for: ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def test_export(self):
134134
}
135135

136136
event_timestamp = base_time + 50 * 10 ** 6
137-
event = trace_api.Event(
137+
event = trace.Event(
138138
name="event0",
139139
timestamp=event_timestamp,
140140
attributes=event_attributes,

Diff for: opentelemetry-api/src/opentelemetry/trace/__init__.py

+48-25
Original file line numberDiff line numberDiff line change
@@ -87,48 +87,59 @@
8787
ParentSpan = typing.Optional[typing.Union["Span", "SpanContext"]]
8888

8989

90-
class Link:
91-
"""A link to a `Span`."""
92-
93-
def __init__(
94-
self, context: "SpanContext", attributes: types.Attributes = None
95-
) -> None:
90+
class LinkBase(abc.ABC):
91+
def __init__(self, context: "SpanContext") -> None:
9692
self._context = context
97-
if attributes is None:
98-
self._attributes = {} # type: types.Attributes
99-
else:
100-
self._attributes = attributes
10193

10294
@property
10395
def context(self) -> "SpanContext":
10496
return self._context
10597

10698
@property
99+
@abc.abstractmethod
107100
def attributes(self) -> types.Attributes:
108-
return self._attributes
101+
pass
109102

110103

111-
class Event:
112-
"""A text annotation with a set of attributes."""
104+
class Link(LinkBase):
105+
"""A link to a `Span`.
106+
107+
Args:
108+
context: `SpanContext` of the `Span` to link to.
109+
attributes: Link's attributes.
110+
"""
113111

114112
def __init__(
115-
self, name: str, attributes: types.Attributes, timestamp: int
113+
self, context: "SpanContext", attributes: types.Attributes = None,
116114
) -> None:
117-
self._name = name
115+
super().__init__(context)
118116
self._attributes = attributes
119-
self._timestamp = timestamp
120-
121-
@property
122-
def name(self) -> str:
123-
return self._name
124117

125118
@property
126119
def attributes(self) -> types.Attributes:
127120
return self._attributes
128121

122+
123+
class LazyLink(LinkBase):
124+
"""A lazy link to a `Span`.
125+
126+
Args:
127+
context: `SpanContext` of the `Span` to link to.
128+
link_formatter: Callable object that returns the attributes of the
129+
Link.
130+
"""
131+
132+
def __init__(
133+
self,
134+
context: "SpanContext",
135+
link_formatter: types.AttributesFormatter,
136+
) -> None:
137+
super().__init__(context)
138+
self._link_formatter = link_formatter
139+
129140
@property
130-
def timestamp(self) -> int:
131-
return self._timestamp
141+
def attributes(self) -> types.Attributes:
142+
return self._link_formatter()
132143

133144

134145
class SpanKind(enum.Enum):
@@ -206,10 +217,17 @@ def add_event(
206217
"""
207218

208219
@abc.abstractmethod
209-
def add_lazy_event(self, event: Event) -> None:
220+
def add_lazy_event(
221+
self,
222+
name: str,
223+
event_formatter: types.AttributesFormatter,
224+
timestamp: typing.Optional[int] = None,
225+
) -> None:
210226
"""Adds an `Event`.
211227
212-
Adds an `Event` that has previously been created.
228+
Adds a single `Event` with the name, an event formatter that calculates
229+
the attributes lazily and, optionally, a timestamp. Implementations
230+
should generate a timestamp if the `timestamp` argument is omitted.
213231
"""
214232

215233
@abc.abstractmethod
@@ -395,7 +413,12 @@ def add_event(
395413
) -> None:
396414
pass
397415

398-
def add_lazy_event(self, event: Event) -> None:
416+
def add_lazy_event(
417+
self,
418+
name: str,
419+
event_formatter: types.AttributesFormatter,
420+
timestamp: typing.Optional[int] = None,
421+
) -> None:
399422
pass
400423

401424
def update_name(self, name: str) -> None:

Diff for: opentelemetry-api/src/opentelemetry/util/types.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414

1515

16-
from typing import Dict, Optional, Sequence, Union
16+
from typing import Callable, Dict, Optional, Sequence, Union
1717

1818
AttributeValue = Union[
1919
str,
@@ -26,3 +26,4 @@
2626
Sequence[float],
2727
]
2828
Attributes = Optional[Dict[str, AttributeValue]]
29+
AttributesFormatter = Callable[[], Optional[Dict[str, AttributeValue]]]

Diff for: opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py

+104-16
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515

16+
import abc
1617
import atexit
1718
import logging
1819
import random
@@ -114,6 +115,77 @@ def shutdown(self) -> None:
114115
sp.shutdown()
115116

116117

118+
class EventBase(abc.ABC):
119+
def __init__(self, name: str, timestamp: Optional[int] = None) -> None:
120+
self._name = name
121+
if timestamp is None:
122+
self._timestamp = time_ns()
123+
else:
124+
self._timestamp = timestamp
125+
126+
@property
127+
def name(self) -> str:
128+
return self._name
129+
130+
@property
131+
def timestamp(self) -> int:
132+
return self._timestamp
133+
134+
@property
135+
@abc.abstractmethod
136+
def attributes(self) -> types.Attributes:
137+
pass
138+
139+
140+
class Event(EventBase):
141+
"""A text annotation with a set of attributes.
142+
143+
Args:
144+
name: Name of the event.
145+
attributes: Attributes of the event.
146+
timestamp: Timestamp of the event. If `None` it will filled
147+
automatically.
148+
"""
149+
150+
def __init__(
151+
self,
152+
name: str,
153+
attributes: types.Attributes = None,
154+
timestamp: Optional[int] = None,
155+
) -> None:
156+
super().__init__(name, timestamp)
157+
self._attributes = attributes
158+
159+
@property
160+
def attributes(self) -> types.Attributes:
161+
return self._attributes
162+
163+
164+
class LazyEvent(EventBase):
165+
"""A text annotation with a set of attributes.
166+
167+
Args:
168+
name: Name of the event.
169+
event_formatter: Callable object that returns the attributes of the
170+
event.
171+
timestamp: Timestamp of the event. If `None` it will filled
172+
automatically.
173+
"""
174+
175+
def __init__(
176+
self,
177+
name: str,
178+
event_formatter: types.AttributesFormatter,
179+
timestamp: Optional[int] = None,
180+
) -> None:
181+
super().__init__(name, timestamp)
182+
self._event_formatter = event_formatter
183+
184+
@property
185+
def attributes(self) -> types.Attributes:
186+
return self._event_formatter()
187+
188+
117189
class Span(trace_api.Span):
118190
"""See `opentelemetry.trace.Span`.
119191
@@ -149,7 +221,7 @@ def __init__(
149221
trace_config: None = None, # TODO
150222
resource: None = None,
151223
attributes: types.Attributes = None, # TODO
152-
events: Sequence[trace_api.Event] = None, # TODO
224+
events: Sequence[Event] = None, # TODO
153225
links: Sequence[trace_api.Link] = (),
154226
kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL,
155227
span_processor: SpanProcessor = SpanProcessor(),
@@ -266,21 +338,7 @@ def _check_attribute_value_sequence(sequence: Sequence) -> Optional[str]:
266338
return "different type"
267339
return None
268340

269-
def add_event(
270-
self,
271-
name: str,
272-
attributes: types.Attributes = None,
273-
timestamp: Optional[int] = None,
274-
) -> None:
275-
self.add_lazy_event(
276-
trace_api.Event(
277-
name,
278-
Span._empty_attributes if attributes is None else attributes,
279-
time_ns() if timestamp is None else timestamp,
280-
)
281-
)
282-
283-
def add_lazy_event(self, event: trace_api.Event) -> None:
341+
def _add_event(self, event: EventBase) -> None:
284342
with self._lock:
285343
if not self.is_recording_events():
286344
return
@@ -293,6 +351,36 @@ def add_lazy_event(self, event: trace_api.Event) -> None:
293351
return
294352
self.events.append(event)
295353

354+
def add_event(
355+
self,
356+
name: str,
357+
attributes: types.Attributes = None,
358+
timestamp: Optional[int] = None,
359+
) -> None:
360+
if attributes is None:
361+
attributes = Span._empty_attributes
362+
self._add_event(
363+
Event(
364+
name=name,
365+
attributes=attributes,
366+
timestamp=time_ns() if timestamp is None else timestamp,
367+
)
368+
)
369+
370+
def add_lazy_event(
371+
self,
372+
name: str,
373+
event_formatter: types.AttributesFormatter,
374+
timestamp: Optional[int] = None,
375+
) -> None:
376+
self._add_event(
377+
LazyEvent(
378+
name=name,
379+
event_formatter=event_formatter,
380+
timestamp=time_ns() if timestamp is None else timestamp,
381+
)
382+
)
383+
296384
def start(self, start_time: Optional[int] = None) -> None:
297385
with self._lock:
298386
if not self.is_recording_events():

Diff for: opentelemetry-sdk/tests/trace/test_trace.py

+12-7
Original file line numberDiff line numberDiff line change
@@ -547,10 +547,11 @@ def test_events(self):
547547
now = time_ns()
548548
root.add_event("event2", {"name": "birthday"}, now)
549549

550+
def event_formatter():
551+
return {"name": "hello"}
552+
550553
# lazy event
551-
root.add_lazy_event(
552-
trace_api.Event("event3", {"name": "hello"}, now)
553-
)
554+
root.add_lazy_event("event3", event_formatter, now)
554555

555556
self.assertEqual(len(root.events), 4)
556557

@@ -584,11 +585,15 @@ def test_links(self):
584585
span_id=trace.generate_span_id(),
585586
is_remote=False,
586587
)
587-
links = [
588+
589+
def get_link_attributes():
590+
return {"component": "http"}
591+
592+
links = (
588593
trace_api.Link(other_context1),
589594
trace_api.Link(other_context2, {"name": "neighbor"}),
590-
trace_api.Link(other_context3, {"component": "http"}),
591-
]
595+
trace_api.LazyLink(other_context3, get_link_attributes),
596+
)
592597
with self.tracer.start_as_current_span("root", links=links) as root:
593598

594599
self.assertEqual(len(root.links), 3)
@@ -598,7 +603,7 @@ def test_links(self):
598603
self.assertEqual(
599604
root.links[0].context.span_id, other_context1.span_id
600605
)
601-
self.assertEqual(root.links[0].attributes, {})
606+
self.assertEqual(root.links[0].attributes, None)
602607
self.assertEqual(
603608
root.links[1].context.trace_id, other_context2.trace_id
604609
)

0 commit comments

Comments
 (0)