OpenTelemetry SDK spans captured with InMemorySpanExporter: working versions (tested 2026-10-15)
Record nested spans, links and attributes with the OpenTelemetry SDK, then assert on the finished spans via InMemorySpanExporter and a SimpleSpanProcessor, fully offline under pytest.
passed5 passed · 0 failed2026-10-15 18:09 UTC- Status
- passed
- Last passed
- 2026-10-15 18:09 UTC
- Tests
- 5 / 5
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions3
opentelemetry-sdk@1.45.1opentelemetry-api@1.45.1pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38783276983
The code
Exactly the files that passed. Your agent can pull them with get_recipe("opentelemetry-sdk-inmemory-span") (install), or see them on GitHub.
requirements.txt
opentelemetry-sdk==1.45.1 opentelemetry-api==1.45.1 pytest==9.1.1
spans.py
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
def make_tracer(service_name: str = "recipe"):
# Fresh provider + exporter per call, so tests never touch the global provider.
exporter = InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(exporter))
return provider.get_tracer(service_name), exporter
tests/test_spans.py
from opentelemetry.trace import Link
from spans import make_tracer
def by_name(exporter):
return {span.name: span for span in exporter.get_finished_spans()}
def test_span_exported_with_attributes():
tracer, exporter = make_tracer()
with tracer.start_as_current_span("work") as span:
span.set_attribute("http.method", "GET")
spans = by_name(exporter)
assert spans["work"].attributes["http.method"] == "GET"
def test_unfinished_span_is_not_exported():
tracer, exporter = make_tracer()
span = tracer.start_span("pending")
assert exporter.get_finished_spans() == ()
span.end()
assert len(exporter.get_finished_spans()) == 1
def test_nested_spans_share_trace_id():
tracer, exporter = make_tracer()
with tracer.start_as_current_span("parent"):
with tracer.start_as_current_span("child"):
pass
spans = by_name(exporter)
assert spans["child"].context.trace_id == spans["parent"].context.trace_id
assert spans["child"].parent.span_id == spans["parent"].context.span_id
def test_set_attribute_after_end_is_ignored():
tracer, exporter = make_tracer()
span = tracer.start_span("done")
span.end()
span.set_attribute("late", True)
spans = by_name(exporter)
assert "late" not in spans["done"].attributes
def test_links_are_recorded_without_parenting():
tracer, exporter = make_tracer()
with tracer.start_as_current_span("root") as root:
pass
root_ctx = root.get_span_context()
with tracer.start_as_current_span("linked", links=[Link(root_ctx)]):
pass
spans = by_name(exporter)
assert spans["linked"].parent is None
assert spans["linked"].links[0].context.span_id == root_ctx.span_id
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.