Skip to content
Draft
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
47 changes: 47 additions & 0 deletions docs/examples/span_type/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
Span type
=========

Prototype for the `Span type OTEP
<https://github.com/open-telemetry/opentelemetry-specification/pull/4849>`_:
a span property that identifies the semantic convention definition the span
follows.

* API: ``span_type`` keyword argument on ``Tracer.start_span`` and
``Tracer.start_as_current_span``. Immutable, no setter.
* SDK: ``ReadableSpan.span_type``, plus ``span_type`` passed to
``Sampler.should_sample``.
* OTLP: emitted as the ``otel.span.type`` attribute until the protocol gains a
top-level ``Span.type`` field.

Run
---

.. code-block:: sh

pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-grpc
python span_type.py

A collector on ``localhost:4317`` is optional -- the example prints the encoded
OTLP payload either way.

Live check
----------

``test_span_type.py`` checks the example's span against
``weaver registry live-check`` using the schema v2 registry in ``registry/``,
which defines the ``gen_ai.client.inference`` span with one required and one
recommended attribute. ``weaver.toml`` filters out findings for the default SDK
resource attributes, which the registry deliberately does not define.

.. code-block:: sh

pip install opentelemetry-test-utils pytest # weaver binary also required
pytest test_span_type.py

Live-check resolves the span by its type -- not by its name -- and then checks
it against that definition: the missing recommended ``gen_ai.request.model`` is
an improvement, a missing required attribute or an attribute the registry does
not define fails the check.

Requires a weaver build with span type support, and ``--v2`` -- without it
weaver downconverts the registry to v1, where spans have no type.
2 changes: 2 additions & 0 deletions docs/examples/span_type/registry/manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
description: Tiny registry defining the span the span_type example emits.
schema_url: https://example.com/schemas/0.1.0
40 changes: 40 additions & 0 deletions docs/examples/span_type/registry/registry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Semantic conventions schema v2 registry with a single span definition.
# The span is identified by `type`, which is what the example passes to
# `start_as_current_span(span_type=...)`.
file_format: definition/2

attributes:
- key: otel.span.type
type: string
stability: development
brief: The semantic convention definition this span follows.
note: >
Only needed while span type travels as an attribute. Declared here so
live-check does not report it as an unknown attribute; once span type is a
top-level OTLP field no registry should define it.
examples: ["gen_ai.client.inference"]

- key: gen_ai.operation.name
type: string
stability: development
brief: The name of the GenAI operation being performed.
examples: ["chat"]
- key: gen_ai.request.model
type: string
stability: development
brief: The name of the GenAI model the request is made to.
examples: ["gpt-4o-mini"]

spans:
- type: gen_ai.client.inference
kind: client
stability: development
requirement_level: recommended
brief: Describes a GenAI inference call.
name:
note: "{gen_ai.operation.name} {gen_ai.request.model}"
attributes:
- ref: gen_ai.operation.name
requirement_level: required
- ref: gen_ai.request.model
requirement_level: recommended
51 changes: 51 additions & 0 deletions docs/examples/span_type/span_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0

"""Prototype of the "Span type" OTEP.

Sets a span type at span creation and shows how it reaches the OTLP gRPC
exporter. Until OTLP has a top-level ``Span.type`` field, the exporter emits it
as the ``otel.span.type`` attribute.

Run with a collector on localhost:4317, or without one to just see the encoded
payload.
"""

from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.common._internal.trace_encoder import (
encode_spans,
)
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
OTLPSpanExporter,
)
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor

captured: list = []


class _Capture(SimpleSpanProcessor):
def on_end(self, span):
captured.append(span)
super().on_end(span)


provider = TracerProvider()
provider.add_span_processor(_Capture(OTLPSpanExporter(insecure=True)))
trace.set_tracer_provider(provider)

tracer = trace.get_tracer(__name__)

with tracer.start_as_current_span(
"chat gpt-4o-mini",
kind=trace.SpanKind.CLIENT,
span_type="gen_ai.client.inference",
attributes={"gen_ai.operation.name": "chat"},
) as span:
# span type is immutable: readable from the SDK span, no setter exists
print("span_type on the SDK span:", span.span_type)

provider.force_flush()

print("\nOTLP payload:")
print(encode_spans(captured))
144 changes: 144 additions & 0 deletions docs/examples/span_type/test_span_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0

"""Checks the span the span_type.py example emits against
``weaver registry live-check``.

The registry in ``registry/`` is a semantic conventions schema v2 registry
defining a single span, ``gen_ai.client.inference``, with one required and one
recommended attribute -- the same type the example passes to
``start_as_current_span(span_type=...)``.

Requires the ``weaver`` binary on PATH:
https://github.com/open-telemetry/weaver/releases
"""

import os
import shutil
import unittest

from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
OTLPSpanExporter,
)
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.test.weaver_live_check import (
LiveCheckError,
WeaverLiveCheck,
)
from opentelemetry.trace import SpanKind

_DIR = os.path.dirname(os.path.abspath(__file__))
_REGISTRY_DIR = os.path.join(_DIR, "registry")

SPAN_TYPE = "gen_ai.client.inference"

# gen_ai.operation.name is required by the span definition,
# gen_ai.request.model is only recommended.
CONFORMANT_ATTRIBUTES = {"gen_ai.operation.name": "chat"}


def _emit_and_collect(attributes: dict) -> dict:
"""Emit the example's span into live-check, return the span sample."""
with WeaverLiveCheck(
registry=_REGISTRY_DIR,
# --v2 is required for the registry to load as schema v2; without it
# weaver downconverts to v1, where spans have no type.
extra_args=["--v2", "--config", os.path.join(_DIR, "weaver.toml")],
) as weaver:
provider = TracerProvider()
provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(endpoint=weaver.otlp_endpoint, insecure=True)
)
)
with provider.get_tracer(__name__).start_as_current_span(
"chat gpt-4o-mini",
kind=SpanKind.CLIENT,
span_type=SPAN_TYPE,
attributes=attributes,
):
pass
provider.force_flush()
# validates the span against the registry and returns the report
# with details
# it'll fail if anny violations are found
report = weaver.end_and_check()

spans = [s["span"] for s in report.get("samples", []) if "span" in s]
assert len(spans) == 1, f"expected one span, got {len(spans)}"
return {"span": spans[0], "report": report}


@unittest.skipUnless(
shutil.which("weaver") is not None,
"weaver binary not found on PATH — install from https://github.com/open-telemetry/weaver/releases",
)
class TestSpanTypeExample(unittest.TestCase):
def test_span_type_reaches_weaver(self):
"""The span type set at creation arrives at live-check.

Until OTLP has a top-level ``Span.type`` field the exporter carries it
as the ``otel.span.type`` attribute, so that is what weaver sees.
"""
result = _emit_and_collect(CONFORMANT_ATTRIBUTES)
attributes = {
a["name"]: a["value"] for a in result["span"]["attributes"]
}
self.assertEqual(attributes.get("otel.span.type"), SPAN_TYPE)

def test_live_check_resolves_span_by_type(self):
"""What span type is for: live-check resolves the span to its
definition and checks it against that definition.

The example omits ``gen_ai.request.model``, which the registry marks
recommended, so live-check advises on it. Nothing about the span name or
its attributes is used to find the definition -- only the type.
"""
result = _emit_and_collect(CONFORMANT_ATTRIBUTES)
advice = result["span"]["live_check_result"]["all_advice"]

# improvement, not a violation, so end_and_check() above did not raise:
# Recommended attribute 'gen_ai.request.model' is not present.
self.assertEqual(len(advice), 1, advice)
self.assertEqual(advice[0]["id"], "recommended_attribute_not_present")
self.assertEqual(
advice[0]["context"]["attribute_key"], "gen_ai.request.model"
)

def test_missing_required_attribute_fails(self):
"""Dropping a required attribute fails the live check.

Only possible because the span resolved to its definition -- without a
span type weaver has nothing to compare the attributes against.
"""
with self.assertRaises(LiveCheckError) as ctx:
_emit_and_collect({})

# Semconv violations found:
# - [required_attribute_not_present] Required attribute
# 'gen_ai.operation.name' is not present. (1 occurrence(s) on span
# 'gen_ai.client.inference')
violations = ctx.exception.report.violations
self.assertEqual(len(violations), 1, violations)
self.assertEqual(violations[0]["id"], "required_attribute_not_present")
self.assertEqual(
violations[0]["context"]["attribute_key"], "gen_ai.operation.name"
)

def test_unknown_attribute_fails(self):
"""An attribute the registry does not define fails the live check."""
with self.assertRaises(LiveCheckError) as ctx:
_emit_and_collect(
{**CONFORMANT_ATTRIBUTES, "gen_ai.made.up": "value"}
)

# Semconv violations found:
# - [missing_attribute] Attribute 'gen_ai.made.up' does not exist in
# the registry. (1 occurrence(s) on span 'gen_ai.client.inference')
violations = ctx.exception.report.violations
self.assertEqual(len(violations), 1, violations)
self.assertEqual(violations[0]["id"], "missing_attribute")
self.assertEqual(
violations[0]["context"]["attribute_key"], "gen_ai.made.up"
)
4 changes: 4 additions & 0 deletions docs/examples/span_type/weaver.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# The registry only defines the span under test, so drop findings for the
# resource attributes the SDK sets by default.
[[live-check.finding_filters]]
exclude_samples = ["service.*", "telemetry.sdk.*"]
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,20 @@ def _span_flags(parent_span_context: SpanContext | None) -> int:
return flags


# Until OTLP gains a top-level Span.type field, span type travels as an
# attribute in this prototype.
# See OTEP "Span type" - TODO add link
_SPAN_TYPE_ATTRIBUTE_KEY = "otel.span.type"


def _encode_span(sdk_span: ReadableSpan) -> PB2SPan:
span_context = sdk_span.get_span_context()
attributes = sdk_span.attributes
if sdk_span.span_type:
attributes = {
**(attributes or {}),
_SPAN_TYPE_ATTRIBUTE_KEY: sdk_span.span_type,
}
return PB2SPan(
trace_id=_encode_trace_id(span_context.trace_id),
span_id=_encode_span_id(span_context.span_id),
Expand All @@ -112,7 +124,7 @@ def _encode_span(sdk_span: ReadableSpan) -> PB2SPan:
kind=_SPAN_KIND_MAP[sdk_span.kind],
start_time_unix_nano=sdk_span.start_time,
end_time_unix_nano=sdk_span.end_time,
attributes=_encode_attributes(sdk_span.attributes),
attributes=_encode_attributes(attributes),
events=_encode_events(sdk_span.events),
links=_encode_links(sdk_span.links),
status=_encode_status(sdk_span.status),
Expand Down
16 changes: 16 additions & 0 deletions opentelemetry-api/src/opentelemetry/trace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ def start_span(
start_time: int | None = None,
record_exception: bool = True,
set_status_on_exception: bool = True,
*,
span_type: str | None = None,
) -> "Span":
"""Starts a span.

Expand Down Expand Up @@ -322,6 +324,10 @@ def start_span(
be automatically set to ERROR when an uncaught exception is
raised in the span with block. The span status won't be set by
this mechanism if it was previously set manually.
span_type: Identifies the semantic convention definition this span follows
(within schema_url specified for this tracer), for example
``"http.server.request"``. Low-cardinality and immutable after
creation. Not validated by the API.

Returns:
The newly-created span.
Expand All @@ -340,6 +346,8 @@ def start_as_current_span(
record_exception: bool = True,
set_status_on_exception: bool = True,
end_on_exit: bool = True,
*,
span_type: str | None = None,
) -> Iterator["Span"]:
"""Context manager for creating a new span and set it
as the current span in this tracer's context.
Expand Down Expand Up @@ -397,6 +405,9 @@ def function():
this mechanism if it was previously set manually.
end_on_exit: Whether to end the span automatically when leaving the
context manager.
span_type: Identifies the semantic convention definition this span
follows, for example ``"http.server.request"``. Low-cardinality
and immutable after creation. Not validated by the API.

Yields:
The newly-created span.
Expand Down Expand Up @@ -459,6 +470,8 @@ def start_span(
start_time: int | None = None,
record_exception: bool = True,
set_status_on_exception: bool = True,
*,
span_type: str | None = None,
) -> "Span":
current_span = get_current_span(context)
if isinstance(current_span, NonRecordingSpan):
Expand Down Expand Up @@ -488,6 +501,8 @@ def start_as_current_span(
record_exception: bool = True,
set_status_on_exception: bool = True,
end_on_exit: bool = True,
*,
span_type: str | None = None,
) -> Iterator["Span"]:
span = self.start_span(
name=name,
Expand All @@ -498,6 +513,7 @@ def start_as_current_span(
start_time=start_time,
record_exception=record_exception,
set_status_on_exception=set_status_on_exception,
span_type=span_type,
)
with use_span(
span,
Expand Down
Loading
Loading