diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index dc57358183d..e6a7a55649d 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -70,6 +70,47 @@ ParentSpan = typing.Optional[typing.Union["Span", "SpanContext"]] +class Link: + """A link to a `Span`.""" + + def __init__( + self, context: "SpanContext", attributes: types.Attributes = None + ) -> None: + self._context = context + self._attributes = attributes + + @property + def context(self) -> "SpanContext": + return self._context + + @property + def attributes(self) -> types.Attributes: + return self._attributes + + +class Event: + """A text annotation with a set of attributes.""" + + def __init__( + self, name: str, timestamp: int, attributes: types.Attributes = None + ) -> None: + self._name = name + self._attributes = attributes + self._timestamp = timestamp + + @property + def name(self) -> str: + return self._name + + @property + def attributes(self) -> types.Attributes: + return self._attributes + + @property + def timestamp(self) -> int: + return self._timestamp + + class Span: """A span represents a single operation within a trace.""" @@ -102,34 +143,44 @@ def get_context(self) -> "SpanContext": A :class:`.SpanContext` with a copy of this span's immutable state. """ - def set_attribute( - self: "Span", key: str, value: types.AttributeValue - ) -> None: + def set_attribute(self, key: str, value: types.AttributeValue) -> None: """Sets an Attribute. Sets a single Attribute with the key and value passed as arguments. """ def add_event( - self: "Span", name: str, attributes: types.Attributes = None + self, name: str, attributes: types.Attributes = None ) -> None: - """Adds an Event. + """Adds an `Event`. - Adds a single Event with the name and, optionally, attributes passed + Adds a single `Event` with the name and, optionally, attributes passed as arguments. """ + def add_lazy_event(self, event: Event) -> None: + """Adds an `Event`. + + Adds an `Event` that has previously been created. + """ + def add_link( - self: "Span", + self, link_target_context: "SpanContext", attributes: types.Attributes = None, ) -> None: - """Adds a Link to another span. + """Adds a `Link` to another span. - Adds a single Link from this Span to another Span identified by the + Adds a single `Link` from this Span to another Span identified by the `SpanContext` passed as argument. """ + def add_lazy_link(self, link: "Link") -> None: + """Adds a `Link` to another span. + + Adds a `Link` that has previously been created. + """ + def update_name(self, name: str) -> None: """Updates the `Span` name. diff --git a/opentelemetry-api/src/opentelemetry/types.py b/opentelemetry-api/src/opentelemetry/types.py index ce5682ee0af..28fab893890 100644 --- a/opentelemetry-api/src/opentelemetry/types.py +++ b/opentelemetry-api/src/opentelemetry/types.py @@ -16,4 +16,4 @@ import typing AttributeValue = typing.Union[str, bool, float] -Attributes = typing.Dict[str, AttributeValue] +Attributes = typing.Optional[typing.Dict[str, AttributeValue]] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 21acb394945..a6f70589b3f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -16,7 +16,7 @@ import random import threading import typing -from collections import OrderedDict, deque, namedtuple +from collections import OrderedDict, deque from contextlib import contextmanager from opentelemetry import trace as trace_api @@ -140,11 +140,6 @@ def from_map(cls, maxlen, mapping): return bounded_dict -Event = namedtuple("Event", ("name", "attributes")) - -Link = namedtuple("Link", ("context", "attributes")) - - class SpanProcessor: """Interface which allows hooks for SDK's `Span`s start and end method invocations. @@ -233,7 +228,7 @@ class Span(trace_api.Span): empty_links = BoundedList(MAX_NUM_LINKS) def __init__( - self: "Span", + self, name: str, context: "trace_api.SpanContext", parent: trace_api.ParentSpan = None, @@ -241,8 +236,8 @@ def __init__( trace_config=None, # TODO resource=None, # TODO attributes: types.Attributes = None, # TODO - events: typing.Sequence[Event] = None, # TODO - links: typing.Sequence[Link] = None, # TODO + events: typing.Sequence[trace_api.Event] = None, # TODO + links: typing.Sequence[trace_api.Link] = None, # TODO span_processor: SpanProcessor = SpanProcessor(), ) -> None: @@ -283,32 +278,36 @@ def __repr__(self): def get_context(self): return self.context - def set_attribute( - self: "Span", key: str, value: types.AttributeValue - ) -> None: + def set_attribute(self, key: str, value: types.AttributeValue) -> None: if self.attributes is Span.empty_attributes: self.attributes = BoundedDict(MAX_NUM_ATTRIBUTES) self.attributes[key] = value def add_event( - self: "Span", name: str, attributes: types.Attributes = None + self, name: str, attributes: types.Attributes = None ) -> None: - if self.events is Span.empty_events: - self.events = BoundedList(MAX_NUM_EVENTS) if attributes is None: attributes = Span.empty_attributes - self.events.append(Event(name, attributes)) + self.add_lazy_event(trace_api.Event(name, util.time_ns(), attributes)) + + def add_lazy_event(self, event: trace_api.Event) -> None: + if self.events is Span.empty_events: + self.events = BoundedList(MAX_NUM_EVENTS) + self.events.append(event) def add_link( - self: "Span", + self, link_target_context: "trace_api.SpanContext", attributes: types.Attributes = None, ) -> None: - if self.links is Span.empty_links: - self.links = BoundedList(MAX_NUM_LINKS) if attributes is None: attributes = Span.empty_attributes - self.links.append(Link(link_target_context, attributes)) + self.add_lazy_link(trace_api.Link(link_target_context, attributes)) + + def add_lazy_link(self, link: "trace_api.Link") -> None: + if self.links is Span.empty_links: + self.links = BoundedList(MAX_NUM_LINKS) + self.links.append(link) def start(self): if self.start_time is None: diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index c0a0e65008d..a1182823b59 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -17,6 +17,7 @@ from opentelemetry import trace as trace_api from opentelemetry.sdk import trace +from opentelemetry.sdk import util class TestTracer(unittest.TestCase): @@ -126,10 +127,15 @@ def test_span_members(self): trace_id=trace.generate_trace_id(), span_id=trace.generate_span_id(), ) + other_context3 = trace_api.SpanContext( + trace_id=trace.generate_trace_id(), + span_id=trace.generate_span_id(), + ) self.assertIsNone(tracer.get_current_span()) with tracer.start_span("root") as root: + # attributes root.set_attribute("component", "http") root.set_attribute("http.method", "GET") root.set_attribute( @@ -144,18 +150,6 @@ def test_span_members(self): root.set_attribute("attr-key", "attr-value1") root.set_attribute("attr-key", "attr-value2") - root.add_event("event0") - root.add_event("event1", {"name": "birthday"}) - - root.add_link(other_context1) - root.add_link(other_context2, {"name": "neighbor"}) - - root.update_name("toor") - self.assertEqual(root.name, "toor") - - # The public API does not expose getters. - # Checks by accessing the span members directly - self.assertEqual(len(root.attributes), 7) self.assertEqual(root.attributes["component"], "http") self.assertEqual(root.attributes["http.method"], "GET") @@ -168,16 +162,34 @@ def test_span_members(self): self.assertEqual(root.attributes["misc.pi"], 3.14) self.assertEqual(root.attributes["attr-key"], "attr-value2") - self.assertEqual(len(root.events), 2) - self.assertEqual( - root.events[0], trace.Event(name="event0", attributes={}) + # events + root.add_event("event0") + root.add_event("event1", {"name": "birthday"}) + now = util.time_ns() + root.add_lazy_event( + trace_api.Event("event2", now, {"name": "hello"}) ) - self.assertEqual( - root.events[1], - trace.Event(name="event1", attributes={"name": "birthday"}), + + self.assertEqual(len(root.events), 3) + + self.assertEqual(root.events[0].name, "event0") + self.assertEqual(root.events[0].attributes, {}) + + self.assertEqual(root.events[1].name, "event1") + self.assertEqual(root.events[1].attributes, {"name": "birthday"}) + + self.assertEqual(root.events[2].name, "event2") + self.assertEqual(root.events[2].attributes, {"name": "hello"}) + self.assertEqual(root.events[2].timestamp, now) + + # links + root.add_link(other_context1) + root.add_link(other_context2, {"name": "neighbor"}) + root.add_lazy_link( + trace_api.Link(other_context3, {"component": "http"}) ) - self.assertEqual(len(root.links), 2) + self.assertEqual(len(root.links), 3) self.assertEqual( root.links[0].context.trace_id, other_context1.trace_id ) @@ -192,6 +204,14 @@ def test_span_members(self): root.links[1].context.span_id, other_context2.span_id ) self.assertEqual(root.links[1].attributes, {"name": "neighbor"}) + self.assertEqual( + root.links[2].context.span_id, other_context3.span_id + ) + self.assertEqual(root.links[2].attributes, {"component": "http"}) + + # name + root.update_name("toor") + self.assertEqual(root.name, "toor") class TestSpan(unittest.TestCase):