Skip to content

Commit 3d1d57c

Browse files
committed
Add span decorator
1 parent f93c52d commit 3d1d57c

File tree

5 files changed

+235
-13
lines changed

5 files changed

+235
-13
lines changed
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from .config import BaseTracingConfig
2+
from .decorator import span
23

3-
__all__ = ("BaseTracingConfig",)
4+
__all__ = ("span", "BaseTracingConfig")
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import asyncio
2+
from functools import wraps
3+
from typing import Callable, Mapping, ParamSpec, Sequence, TypeVar, cast, overload
4+
5+
from opentelemetry import trace
6+
from opentelemetry.trace import Tracer
7+
8+
__all__ = ("span",)
9+
10+
11+
F_Spec = ParamSpec("F_Spec")
12+
F_Return = TypeVar("F_Return")
13+
Func = Callable[F_Spec, F_Return]
14+
Attributes = Mapping[str, str | bool | int | float | Sequence[str] | Sequence[bool] | Sequence[int] | Sequence[float]]
15+
16+
17+
def _span_wrapper(
18+
span_name: str | None,
19+
attributes: Attributes | None,
20+
tracer: Tracer | None,
21+
) -> Callable[[Func], Func]:
22+
def span_decorator(func: Func) -> Func:
23+
name: str = span_name or func.__name__
24+
tracer_ = tracer or trace.get_tracer(__name__)
25+
26+
@wraps(func)
27+
def sync_span_wrapper(*args: F_Spec.args, **kwargs: F_Spec.kwargs) -> F_Return: # type: ignore[type-var]
28+
with tracer_.start_as_current_span(name=name, attributes=attributes):
29+
result = func(*args, **kwargs)
30+
return cast(F_Return, result)
31+
32+
@wraps(func)
33+
async def async_span_wrapper(*args: F_Spec.args, **kwargs: F_Spec.kwargs) -> F_Return:
34+
with tracer_.start_as_current_span(name=name, attributes=attributes):
35+
result = await func(*args, **kwargs)
36+
return cast(F_Return, result)
37+
38+
if asyncio.iscoroutinefunction(func):
39+
return async_span_wrapper
40+
return sync_span_wrapper
41+
42+
return span_decorator
43+
44+
45+
@overload
46+
def span(
47+
call: Func,
48+
*,
49+
span_name: None = None,
50+
attributes: None = None,
51+
tracer: None = None,
52+
) -> Func: ...
53+
54+
55+
@overload
56+
def span(
57+
call: None = None,
58+
*,
59+
span_name: str | None = None,
60+
attributes: Attributes | None = None,
61+
tracer: Tracer | None = None,
62+
) -> Callable[[Func], Func]: ...
63+
64+
65+
def span(
66+
call: Func | None = None,
67+
*,
68+
span_name: str | None = None,
69+
attributes: Attributes | None = None,
70+
tracer: Tracer | None = None,
71+
) -> Callable[[Func], Func] | Func:
72+
wrap_decorator = _span_wrapper(span_name, attributes, tracer)
73+
if call is None:
74+
return wrap_decorator
75+
else:
76+
return wrap_decorator(call)

tests/integration/factory.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,12 @@ def build_fastapi_tracing_config() -> tuple[FastAPITraceConfig, InMemorySpanExpo
5454
"service.name": "fastapi",
5555
},
5656
)
57-
tracer = TracerProvider(resource=resource)
58-
trace.set_tracer_provider(tracer)
57+
tracer_provider = TracerProvider(resource=resource)
58+
trace.set_tracer_provider(tracer_provider)
5959
exporter = InMemorySpanExporter()
60-
tracer.add_span_processor(SimpleSpanProcessor(exporter))
60+
tracer_provider.add_span_processor(SimpleSpanProcessor(exporter))
6161

62-
return FastAPITraceConfig(tracer_provider=tracer), exporter
62+
return FastAPITraceConfig(tracer_provider=tracer_provider), exporter
6363

6464

6565
def build_starlette_tracing_config() -> tuple[StarletteTraceConfig, InMemorySpanExporter]:
@@ -68,12 +68,12 @@ def build_starlette_tracing_config() -> tuple[StarletteTraceConfig, InMemorySpan
6868
"service.name": "starlette",
6969
},
7070
)
71-
tracer = TracerProvider(resource=resource)
72-
trace.set_tracer_provider(tracer)
71+
tracer_provider = TracerProvider(resource=resource)
72+
trace.set_tracer_provider(tracer_provider)
7373
exporter = InMemorySpanExporter()
74-
tracer.add_span_processor(SimpleSpanProcessor(exporter))
74+
tracer_provider.add_span_processor(SimpleSpanProcessor(exporter))
7575

76-
return StarletteTraceConfig(tracer_provider=tracer), exporter
76+
return StarletteTraceConfig(tracer_provider=tracer_provider), exporter
7777

7878

7979
def build_litestar_tracing_config() -> tuple[LitestarTraceConfig, InMemorySpanExporter]:
@@ -82,9 +82,9 @@ def build_litestar_tracing_config() -> tuple[LitestarTraceConfig, InMemorySpanEx
8282
"service.name": "litestar",
8383
},
8484
)
85-
tracer = TracerProvider(resource=resource)
86-
trace.set_tracer_provider(tracer)
85+
tracer_provider = TracerProvider(resource=resource)
86+
trace.set_tracer_provider(tracer_provider)
8787
exporter = InMemorySpanExporter()
88-
tracer.add_span_processor(SimpleSpanProcessor(exporter))
88+
tracer_provider.add_span_processor(SimpleSpanProcessor(exporter))
8989

90-
return LitestarTraceConfig(tracer_provider=tracer), exporter
90+
return LitestarTraceConfig(tracer_provider=tracer_provider), exporter

tests/unit/tracing/conftest.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import pytest
2+
from opentelemetry.sdk.resources import Resource
3+
from opentelemetry.sdk.trace import TracerProvider
4+
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
5+
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
6+
7+
8+
@pytest.fixture
9+
def tracer_provider() -> TracerProvider:
10+
resource = Resource.create(
11+
attributes={
12+
"service.name": "action",
13+
},
14+
)
15+
return TracerProvider(resource=resource)
16+
17+
18+
@pytest.fixture
19+
def exporter(tracer_provider: TracerProvider) -> InMemorySpanExporter:
20+
exporter = InMemorySpanExporter()
21+
processor = SimpleSpanProcessor(exporter)
22+
tracer_provider.add_span_processor(processor)
23+
return exporter
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import asyncio
2+
import time
3+
4+
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
5+
from opentelemetry.trace import TracerProvider
6+
7+
from asgi_monitor.tracing import span as span_decorator
8+
9+
10+
def test_simple_span_decorator() -> None:
11+
# Arrange
12+
expected = 1
13+
14+
@span_decorator
15+
def action() -> int:
16+
return expected
17+
18+
# Act
19+
result = action()
20+
21+
# Assert
22+
assert result == expected
23+
24+
25+
def test_empty_span_decorator(exporter: InMemorySpanExporter, tracer_provider: TracerProvider) -> None:
26+
# Arrange
27+
@span_decorator(tracer=tracer_provider.get_tracer(__name__))
28+
def action() -> None:
29+
time.sleep(0.01)
30+
31+
# Act
32+
action()
33+
34+
# Assert
35+
span = exporter.get_finished_spans()[0]
36+
assert span.name == action.__name__
37+
assert span.attributes == {}
38+
39+
40+
def test_span_decorator(exporter: InMemorySpanExporter, tracer_provider: TracerProvider) -> None:
41+
# Arrange
42+
@span_decorator(span_name="test", attributes={"test": "test"}, tracer=tracer_provider.get_tracer(__name__))
43+
def action() -> None:
44+
time.sleep(0.01)
45+
46+
# Act
47+
action()
48+
49+
# Assert
50+
span = exporter.get_finished_spans()[0]
51+
assert span.name == "test"
52+
assert span.attributes == {"test": "test"}
53+
54+
55+
async def test_async_simple_span_decorator(exporter: InMemorySpanExporter, tracer_provider: TracerProvider) -> None:
56+
# Arrange
57+
expected = 1
58+
59+
@span_decorator
60+
async def action() -> int:
61+
return expected
62+
63+
# Act
64+
result = await action()
65+
66+
# Assert
67+
assert result == expected
68+
69+
70+
async def test_async_empty_span_decorator(exporter: InMemorySpanExporter, tracer_provider: TracerProvider) -> None:
71+
# Arrange
72+
@span_decorator(tracer=tracer_provider.get_tracer(__name__))
73+
async def action() -> None:
74+
await asyncio.sleep(0.01)
75+
76+
# Act
77+
await action()
78+
79+
# Assert
80+
span = exporter.get_finished_spans()[0]
81+
assert span.name == action.__name__
82+
assert span.attributes == {}
83+
84+
85+
async def test_async_span_decorator(exporter: InMemorySpanExporter, tracer_provider: TracerProvider) -> None:
86+
# Arrange
87+
@span_decorator(span_name="test", attributes={"test": "test"}, tracer=tracer_provider.get_tracer(__name__))
88+
async def action() -> None:
89+
await asyncio.sleep(0.01)
90+
91+
# Act
92+
await action()
93+
94+
# Assert
95+
span = exporter.get_finished_spans()[0]
96+
assert span.name == "test"
97+
assert span.attributes == {"test": "test"}
98+
99+
100+
async def test_nested_spans(exporter: InMemorySpanExporter, tracer_provider: TracerProvider) -> None:
101+
# Arrange
102+
tracer = tracer_provider.get_tracer(__name__)
103+
104+
@span_decorator(tracer=tracer)
105+
async def count() -> int:
106+
return 1
107+
108+
@span_decorator(tracer=tracer)
109+
async def action() -> None:
110+
num = await count()
111+
assert num == 1
112+
113+
# Act
114+
await action()
115+
116+
# Assert
117+
count_sleep, span_action = exporter.get_finished_spans()
118+
assert span_action.name == action.__name__
119+
assert span_action.attributes == {}
120+
assert count_sleep.name == count.__name__
121+
assert count_sleep.attributes == {}
122+
assert count_sleep.parent.span_id == span_action.context.span_id # type: ignore[union-attr]

0 commit comments

Comments
 (0)