Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/5819.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-api`, `opentelemetry-sdk`: Add experimental span creation methods with `_SpanOptions`, preserving existing tracer implementations; the SDK validates span type and exposes it on `ReadableSpan._type`
171 changes: 170 additions & 1 deletion opentelemetry-api/src/opentelemetry/trace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@
import os
from abc import ABC, abstractmethod
from collections.abc import Iterator, Sequence
from dataclasses import dataclass
from enum import Enum
from logging import getLogger
from typing import cast
from typing import Any, cast

from typing_extensions import deprecated

Expand Down Expand Up @@ -148,6 +149,15 @@ def dropped_attributes(self) -> int:
_Links = Sequence[Link] | None


@dataclass(frozen=True, kw_only=True)
class _SpanOptions:
"""Experimental options for span creation.

New options must have defaults. Implementations ignore fields they do not
support, allowing the options to grow without changing method signatures.
"""


class SpanKind(Enum):
"""Specifies additional details on how this span relates to its parent span.

Expand Down Expand Up @@ -400,6 +410,107 @@ def function(): ...
The newly-created span.
"""

# Experimental. Becomes public as start_span_with_options once span type is stable.
def _start_span_with_options(
self,
name: str,
context: Context | None = None,
kind: SpanKind = SpanKind.INTERNAL,
attributes: types.Attributes = None,
links: _Links = None,
start_time: int | None = None,
record_exception: bool = True,
set_status_on_exception: bool = True,
*,
Comment on lines +417 to +424

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since it's a new method altogether, can we move the * to be right after name to force other params to be passed by name? It makes it possible to reorder params or introduce deprecated overloads.

type_: str | None = None,
options: _SpanOptions | None = None,
) -> "Span":
"""Experimental: create a span with extensible options.

Args:
type_: Identifies the class of spans, for example ``"http.client.request"``.
Immutable after creation. Must be a case-sensitive ASCII string of
at most 255 characters starting with a letter, followed by letters,
digits, ``_``, ``.``, ``-`` or ``/``. Validated by the SDK.
options: Additional span creation options. Unsupported fields are
ignored. Omitting options preserves :meth:`start_span` behavior.

See :meth:`start_span` for the other arguments. The default
implementation calls :meth:`start_span` and ignores ``type_`` and ``options``.
Implementations supporting these options should override this method.

Example::

with tracer._start_span_with_options("GET", type_="http.client.request"):
...
"""
return self.start_span(
name,
**_non_default_start_args(
context, kind, attributes, links, start_time, record_exception, set_status_on_exception
),
)

# Experimental. Becomes public as start_as_current_span_with_options once span type is stable.
@_agnosticcontextmanager
def _start_as_current_span_with_options(
self,
name: str,
context: Context | None = None,
kind: SpanKind = SpanKind.INTERNAL,
attributes: types.Attributes = None,
links: _Links = None,
start_time: int | None = None,
record_exception: bool = True,
set_status_on_exception: bool = True,
end_on_exit: bool = True,
*,
type_: str | None = None,
options: _SpanOptions | None = None,
) -> Iterator["Span"]:
"""Experimental: create a current span with extensible options.

See :meth:`_start_span_with_options` for ``type_`` and ``options``. The default
implementation calls :meth:`start_as_current_span` and ignores
``type_`` and ``options``.
"""
args = _non_default_start_args(
context, kind, attributes, links, start_time, record_exception, set_status_on_exception
)
if not end_on_exit:
args["end_on_exit"] = end_on_exit
with self.start_as_current_span(name, **args) as span:
yield span


def _non_default_start_args(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: we can probably generalize this to:

def _non_default_args(**arguments: tuple[Any, Any]) -> dict[str, Any]:
    return {
        name: value
        for name, (value, default) in arguments.items()
        if value is not default
    }

and forward with something like

return self.start_span(
    name,
    **_non_default_args(
        context=(context, None),
        kind=(kind, SpanKind.INTERNAL),
        attributes=(attributes, None),
        links=(links, None),
        start_time=(start_time, None),
        record_exception=(record_exception, True),
        set_status_on_exception=(set_status_on_exception, True),
    ),
)

that being said, do we even need to do this anymore?

context: Context | None,
kind: SpanKind,
attributes: types.Attributes,
links: _Links,
start_time: int | None,
record_exception: bool,
set_status_on_exception: bool,
) -> dict[str, Any]:
# Forward only what the caller set. Overrides of start_span may not
# accept every parameter.
args: dict[str, Any] = {}
if context is not None:
args["context"] = context
if kind is not SpanKind.INTERNAL:
args["kind"] = kind
if attributes is not None:
args["attributes"] = attributes
if links is not None:
args["links"] = links
if start_time is not None:
args["start_time"] = start_time
if record_exception is not True:
args["record_exception"] = record_exception
if set_status_on_exception is not True:
args["set_status_on_exception"] = set_status_on_exception
return args


class ProxyTracer(Tracer):
# pylint: disable=W0222,signature-differs
Expand Down Expand Up @@ -440,6 +551,64 @@ def start_as_current_span(self, *args, **kwargs) -> Iterator[Span]:
with self._tracer.start_as_current_span(*args, **kwargs) as span: # type: ignore
yield span

def _start_span_with_options(
self,
name: str,
context: Context | None = None,
kind: SpanKind = SpanKind.INTERNAL,
attributes: types.Attributes = None,
links: _Links = None,
start_time: int | None = None,
record_exception: bool = True,
set_status_on_exception: bool = True,
*,
type_: str | None = None,
options: _SpanOptions | None = None,
) -> Span:
return self._tracer._start_span_with_options( # pylint: disable=protected-access
name,
context,
kind,
attributes,
links,
start_time,
record_exception,
set_status_on_exception,
type_=type_,
options=options,
)

@_agnosticcontextmanager
def _start_as_current_span_with_options(
self,
name: str,
context: Context | None = None,
kind: SpanKind = SpanKind.INTERNAL,
attributes: types.Attributes = None,
links: _Links = None,
start_time: int | None = None,
record_exception: bool = True,
set_status_on_exception: bool = True,
end_on_exit: bool = True,
*,
type_: str | None = None,
options: _SpanOptions | None = None,
) -> Iterator[Span]:
with self._tracer._start_as_current_span_with_options( # pylint: disable=protected-access
name,
context,
kind,
attributes,
links,
start_time,
record_exception,
set_status_on_exception,
end_on_exit,
type_=type_,
options=options,
) as span:
yield span


class NoOpTracer(Tracer):
"""The default Tracer, used when no Tracer implementation is available.
Expand Down
Loading
Loading