Skip to content

[DO NOT MERGE] Add experimental span type to Tracer API and SDK - #5819

Open
lmolkova wants to merge 11 commits into
mainfrom
lmolkova/span-type-api-sdk
Open

lmolkova wants to merge 11 commits into
mainfrom
lmolkova/span-type-api-sdk

Conversation

@lmolkova

@lmolkova lmolkova commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Adds experimental span type support using separate creation methods, preserving existing third-party Tracer implementations.

Implements spec#5378, following OTEP 5233.

Warning

The spec change is not merged yet. Reviews and approvals are requested as a prerequisite for the spec change.

API

with tracer._start_as_current_span_with_options(
    "GET",
    type_="http.client.request",
    kind=SpanKind.CLIENT,
):
    ...
  • Adds _start_span_with_options and _start_as_current_span_with_options. Existing positional arguments and exception-handling defaults match start_span and start_as_current_span.
  • type_ is a top-level, keyword-only argument. A separate keyword-only options: _SpanOptions | None = None provides an extension point for future inputs.
  • _SpanOptions is currently an empty, frozen, keyword-only dataclass. Future fields must have defaults; implementations ignore unsupported fields.
  • Both methods have concrete fallback implementations that discard type_ and options and delegate to the existing methods. Existing Tracer subclasses need no changes.
  • ProxyTracer forwards both methods with explicit signatures.
  • The methods and options class remain experimental. The methods are intended to become start_span_with_options and start_as_current_span_with_options once stable.

This follows the separate-methods-with-options approach discussed in #5877, with span type kept as a top-level argument.

SDK

Validates span type against the spec syntax. Invalid values are logged and dropped while span creation continues. The type is exposed as experimental ReadableSpan._type.

Compatibility

Adding an argument to the existing start_span or start_as_current_span contract would break third-party overrides when callers pass it, and can cause incompatible-override typing errors. New concrete methods preserve the existing contracts and provide a fallback for implementations that do not support span type.

Validation

  • Updated API compatibility tests for unchanged third-party-style tracers and proxy forwarding.
  • SDK coverage includes validation, export, decorators, exception handling, empty/reused options, and ignoring future options.
  • All 24 focused compatibility and span-type tests passed after the final API adjustment.
  • Pre-commit checks passed for changed files.
  • Benchmark cases were updated for the new method names. Earlier timing results have been removed because benchmarks have not been rerun for this API shape.

Follow-ups

Span type as a sampler input, OTLP serialization, and declarative configuration are outside this PR. OTLP serialization waits on opentelemetry-proto#882.

Stacked follow-ups: #5820, #5821.

@opentelemetry-pr-dashboard

opentelemetry-pr-dashboard Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Pull request dashboard status

Waiting on the author · refreshed 2026-11-05 21:05 UTC

Respond to 2 review items (e.g. link a commit, explain why not, ask a follow-up):

  • Inline threads: 1, 2
Status above doesn't look right?
  • Just replied or pushed? Anything around or after the refresh time above may not be picked up yet — give it a few minutes.
  • Should this be with reviewers? Comment /dashboard route:reviewers to route it to them.
  • Anything wrong — including the routing? Report it with what you expected; it helps us improve the dashboard.

Copilot AI left a comment

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.

🟡 Changes recommended

Non-string span types can raise exceptions or bypass validation instead of being logged and dropped.

1 open issue
What changed in this PR

Adds experimental typed-span creation to the tracing API and SDK while preserving existing tracer signatures. Sampler inputs and OTLP serialization remain outside this PR.

Changes:

  • Adds keyword-only typed-span methods with legacy fallbacks and proxy forwarding.
  • Adds SDK span-type validation and storage on ReadableSpan.
  • Adds compatibility tests and SDK coverage for typed-span behavior.
File Description
opentelemetry-sdk/​tests/​trace/​test_trace.py Tests validation, export, decorators, and span lifecycle behavior.
opentelemetry-sdk/​src/​opentelemetry/​sdk/​trace/​__init__.py Implements typed-span creation, validation, and readable-span storage.
opentelemetry-api/​tests/​trace/​test_tracer_compat.py Tests legacy tracer compatibility and proxy delegation.
opentelemetry-api/​src/​opentelemetry/​trace/​__init__.py Adds experimental API methods, fallbacks, and proxy forwarding.
.changelog/​5819.added Documents the experimental tracing additions.

Review effort: Balanced

attributes: types.Attributes = None,
links: _Links = None,
start_time: int | None = None,
**kwargs: Any,

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.

We should benchmark this, some thoughts:

  1. I think you can use Never for kwargs type to make it just a placeholder, and not make typechecker be overly permissive. If that doesn't work you can use an empty Unpack[EmptyTypedDict].
  2. I think **kwargs forces cpython to not use vectorcall calling protocol, and kwargs forces a dict allocation which escapes to heap.

That said, we did use this pattern for user-implementable SDK components in metrics like Exporter

@lmolkova lmolkova Oct 26, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

2bdf5cf

Benchmarked, added to test_benchmark_trace.py.

  • Typed as Unpack[_SpanOptions] now. Type checkers catch typos, SDK warns on unknown keys.
  • SDK skips kwargs when empty.

AI-based research, confirmed by benchmarks:

  • **kwargs doesn't affect vectorcall. Every Python function gets _PyFunction_Vectorcall (funcobject.c), and the kwargs dict is built later, inside frame setup (ceval.c).
  • Specialization is the same too. **kwargs and keyword-only params both rule out the exact-args fast path (specialize.c), and these methods are keyword-only anyway. Keyword calls specialize to CALL_KW_PY either way (specialize.c). I checked with dis(adaptive=True) on 3.12-3.14.
  • The dict comes from the freelist (dictobject.c), and an empty one shares the static empty keys table (dictobject.c). No extra allocations in tracemalloc, 0 GC collections over 1M calls.

The remaining delta is span type validation (median, py3.14):

untyped typed, no type typed, with type
validation on 6.04 µs 6.06 µs 6.25 µs
validation off 6.12 µs 6.06 µs 6.06 µs

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Peak allocated per span (tracemalloc), start + end:

untyped typed, no type typed, with type typed, type + option
start span 3661 B 3661 B 3661 B 3661 B

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.

  • Typed as Unpack[_SpanOptions] now. Type checkers catch typos, SDK warns on unknown keys.

The catch is mypy doesn't force implementors to add **kwds. This passes in mypy (pyright handles it as expected tho)

from typing import TypedDict, Unpack
from abc import ABC, abstractmethod

class Options(TypedDict):
  extra: str

class Example(ABC):
  @abstractmethod
  def foo(self, *, a: str, **kwags: Unpack[Options]): ...

class Implements(Example):
  # mypy DOESNT force **kwargs in the implementations
  def foo(self, *, a: str, extra: str): ...

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.

Looks like this comment is out of date and we moved to a Options dataclass

- Type typed-span kwargs as Unpack[_SpanOptions] so type checkers catch typos.
- SDK logs a warning for unknown span options.
- SDK skips reading kwargs when none are passed and reads them once otherwise.
- Validate span type with a compiled regex instead of a per-character check.
- Add typed span and span type validation benchmarks with allocation tracking.

Assisted-by: Antigravity
@lmolkova
lmolkova force-pushed the lmolkova/span-type-api-sdk branch from 561a6b4 to e36ab49 Compare October 26, 2026 22:40

Copilot AI left a comment

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.

🔵 Needs a closer look

The new proxy overrides discard the typed API’s keyword-only and span-option checks for callers using ProxyTracer directly.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Preserve typed proxy override signatures and argument checks

opentelemetry-api/​src/​opentelemetry/​trace/​__init__.py:547

Both new proxy overrides use untyped *args/**kwargs, so callers typed as ProxyTracer lose the keyword-only and _SpanOptions checks and can pass arguments that fail at runtime. Mirror the base signatures for _start_typed_span and _start_as_current_typed_span, and forward their arguments explicitly. This also removes the new blanket typing suppressions prohibited by AGENTS.md:62.

1 resolved since last review

Review effort: Balanced

@lmolkova
lmolkova marked this pull request as ready for review October 26, 2026 23:18
@lmolkova
lmolkova requested a review from a team as a code owner October 26, 2026 23:18
"""

# Experimental. Becomes public as start_typed_span once span type is stable.
def _start_typed_span(

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.

We should probably add a TODO/tracking issue to eventually move this into the standard start_span function (e.g. once we do a 2.x release)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

2.x API ? I can create a tracking issue, but even the idea of v2 API would be highly controversial.

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.

Maybe we can reference #5877 and repurpose it to be about generally making ABCs evolvable. There were some ideas floated there to avoid needing separate methods.

Comment thread .changelog/5819.added Resolved
Typed span methods now take the same (positional) arguments as start_span and start_as_current_span, with type_ as a trailing keyword-only argument. record_exception and set_status_on_exception are regular arguments again; _SpanOptions is reserved for future options.
@lmolkova lmolkova changed the title Add experimental span type to Tracer API and SDK [DO NOT MERGE] Add experimental span type to Tracer API and SDK Oct 29, 2026
Keep span type as a top-level keyword argument and preserve fallback behavior for existing tracer implementations.

Assisted-by: ChatGPT
Comment on lines +417 to +424
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,
*,

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.

attributes: types.Attributes = None,
links: _Links = None,
start_time: int | None = None,
**kwargs: Any,

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.

Looks like this comment is out of date and we moved to a Options dataclass

@herin049 herin049 left a comment

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.

Thanks! A one nit, but design-wise looks good. We'll still want to update this once @aabmass's compatibility changes are added so we can just add type_ directly to existing tracer methods.

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?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

5 participants