Skip to content

Commit cf09512

Browse files
committed
create TracingData dataclass for carrying around tracing data
1 parent fa76bef commit cf09512

File tree

5 files changed

+124
-49
lines changed

5 files changed

+124
-49
lines changed

packages/service-library/src/servicelib/fastapi/tracing.py

Lines changed: 54 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,19 @@
66
from fastapi import FastAPI, Request
77
from fastapi_lifespan_manager import State
88
from httpx import AsyncClient, Client
9-
from opentelemetry import trace
109
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
1110
OTLPSpanExporter as OTLPSpanExporterHTTP,
1211
)
1312
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
1413
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
15-
from opentelemetry.sdk.resources import Resource
1614
from opentelemetry.sdk.trace import SpanProcessor, TracerProvider
1715
from opentelemetry.sdk.trace.export import BatchSpanProcessor
18-
from opentelemetry.sdk.trace.sampling import ParentBased, TraceIdRatioBased
1916
from settings_library.tracing import TracingSettings
2017
from starlette.middleware.base import BaseHTTPMiddleware
2118
from yarl import URL
2219

2320
from ..logging_utils import log_context
24-
from ..tracing import get_trace_id_header
21+
from ..tracing import TracingData, get_trace_id_header
2522

2623
_logger = logging.getLogger(__name__)
2724

@@ -79,22 +76,14 @@ def _create_span_processor(tracing_destination: str) -> SpanProcessor:
7976
return BatchSpanProcessor(otlp_exporter)
8077

8178

82-
def _startup(tracing_settings: TracingSettings, service_name: str) -> None:
79+
def _startup(tracing_settings: TracingSettings, tracing_data: TracingData) -> None:
8380
if (
8481
not tracing_settings.TRACING_OPENTELEMETRY_COLLECTOR_ENDPOINT
8582
and not tracing_settings.TRACING_OPENTELEMETRY_COLLECTOR_PORT
8683
):
8784
_logger.warning("Skipping opentelemetry tracing setup")
8885
return
89-
# Set up the tracer provider
90-
resource = Resource(attributes={"service.name": service_name})
91-
sampler = ParentBased(
92-
root=TraceIdRatioBased(tracing_settings.TRACING_SAMPLING_PROBABILITY)
93-
)
94-
trace_provider = TracerProvider(resource=resource, sampler=sampler)
95-
trace.set_tracer_provider(trace_provider)
96-
global_tracer_provider = trace.get_tracer_provider()
97-
assert isinstance(global_tracer_provider, TracerProvider) # nosec
86+
assert isinstance(tracing_data.tracer_provider, TracerProvider) # nosec
9887

9988
opentelemetry_collector_endpoint: str = (
10089
f"{tracing_settings.TRACING_OPENTELEMETRY_COLLECTOR_ENDPOINT}"
@@ -106,11 +95,11 @@ def _startup(tracing_settings: TracingSettings, service_name: str) -> None:
10695

10796
_logger.info(
10897
"Trying to connect service %s to opentelemetry tracing collector at %s.",
109-
service_name,
98+
tracing_data.service_name,
11099
tracing_destination,
111100
)
112101
# Add the span processor to the tracer provider
113-
global_tracer_provider.add_span_processor(
102+
tracing_data.tracer_provider.add_span_processor(
114103
_create_span_processor(tracing_destination)
115104
)
116105

@@ -120,75 +109,95 @@ def _startup(tracing_settings: TracingSettings, service_name: str) -> None:
120109
logging.INFO,
121110
msg="Attempting to add asyncpg opentelemetry autoinstrumentation...",
122111
):
123-
AiopgInstrumentor().instrument()
112+
AiopgInstrumentor().instrument(tracer_provider=tracing_data.tracer_provider)
124113
if HAS_AIOPIKA_INSTRUMENTOR:
125114
with log_context(
126115
_logger,
127116
logging.INFO,
128117
msg="Attempting to add aio_pika opentelemetry autoinstrumentation...",
129118
):
130-
AioPikaInstrumentor().instrument()
119+
AioPikaInstrumentor().instrument(
120+
tracer_provider=tracing_data.tracer_provider
121+
)
131122
if HAS_ASYNCPG:
132123
with log_context(
133124
_logger,
134125
logging.INFO,
135126
msg="Attempting to add asyncpg opentelemetry autoinstrumentation...",
136127
):
137-
AsyncPGInstrumentor().instrument()
128+
AsyncPGInstrumentor().instrument(
129+
tracer_provider=tracing_data.tracer_provider
130+
)
138131
if HAS_REDIS:
139132
with log_context(
140133
_logger,
141134
logging.INFO,
142135
msg="Attempting to add redis opentelemetry autoinstrumentation...",
143136
):
144-
RedisInstrumentor().instrument()
137+
RedisInstrumentor().instrument(tracer_provider=tracing_data.tracer_provider)
145138
if HAS_BOTOCORE:
146139
with log_context(
147140
_logger,
148141
logging.INFO,
149142
msg="Attempting to add botocore opentelemetry autoinstrumentation...",
150143
):
151-
BotocoreInstrumentor().instrument()
144+
BotocoreInstrumentor().instrument(
145+
tracer_provider=tracing_data.tracer_provider
146+
)
152147
if HAS_REQUESTS:
153148
with log_context(
154149
_logger,
155150
logging.INFO,
156151
msg="Attempting to add requests opentelemetry autoinstrumentation...",
157152
):
158-
RequestsInstrumentor().instrument()
153+
RequestsInstrumentor().instrument(
154+
tracer_provider=tracing_data.tracer_provider
155+
)
159156

160157

161-
def _shutdown() -> None:
158+
def _shutdown(tracing_data: TracingData) -> None:
162159
"""Uninstruments all opentelemetry instrumentors that were instrumented."""
163-
FastAPIInstrumentor().uninstrument()
160+
FastAPIInstrumentor().uninstrument(tracer_provider=tracing_data.tracer_provider)
164161
if HAS_AIOPG:
165162
try:
166-
AiopgInstrumentor().uninstrument()
163+
AiopgInstrumentor().uninstrument(
164+
tracer_provider=tracing_data.tracer_provider
165+
)
167166
except Exception: # pylint:disable=broad-exception-caught
168167
_logger.exception("Failed to uninstrument AiopgInstrumentor")
169168
if HAS_AIOPIKA_INSTRUMENTOR:
170169
try:
171-
AioPikaInstrumentor().uninstrument()
170+
AioPikaInstrumentor().uninstrument(
171+
tracer_provider=tracing_data.tracer_provider
172+
)
172173
except Exception: # pylint:disable=broad-exception-caught
173174
_logger.exception("Failed to uninstrument AioPikaInstrumentor")
174175
if HAS_ASYNCPG:
175176
try:
176-
AsyncPGInstrumentor().uninstrument()
177+
AsyncPGInstrumentor().uninstrument(
178+
tracer_provider=tracing_data.tracer_provider
179+
)
177180
except Exception: # pylint:disable=broad-exception-caught
178181
_logger.exception("Failed to uninstrument AsyncPGInstrumentor")
179182
if HAS_REDIS:
180183
try:
181-
RedisInstrumentor().uninstrument()
184+
RedisInstrumentor().uninstrument(
185+
tracer_provider=tracing_data.tracer_provider
186+
)
182187
except Exception: # pylint:disable=broad-exception-caught
183188
_logger.exception("Failed to uninstrument RedisInstrumentor")
184189
if HAS_BOTOCORE:
185190
try:
186-
BotocoreInstrumentor().uninstrument()
191+
BotocoreInstrumentor().uninstrument(
192+
tracer_provider=tracing_data.tracer_provider
193+
)
187194
except Exception: # pylint:disable=broad-exception-caught
188195
_logger.exception("Failed to uninstrument BotocoreInstrumentor")
189196
if HAS_REQUESTS:
190197
try:
191-
RequestsInstrumentor().uninstrument()
198+
RequestsInstrumentor().uninstrument(
199+
tracer_provider=tracing_data.tracer_provider
200+
)
192201
except Exception: # pylint:disable=broad-exception-caught
193202
_logger.exception("Failed to uninstrument RequestsInstrumentor")
194203

@@ -209,19 +218,24 @@ def setup_tracing(
209218
app: FastAPI, tracing_settings: TracingSettings, service_name: str
210219
) -> None:
211220
# NOTE: This does not instrument the app itself. Call setup_fastapi_app_tracing to do that.
212-
_startup(tracing_settings=tracing_settings, service_name=service_name)
221+
assert getattr(app.state, "tracing_data", None) is None
222+
tracing_data = TracingData.create(
223+
tracing_settings=tracing_settings, service_name=service_name
224+
)
225+
app.state.tracing_data = tracing_data
226+
_startup(tracing_settings=tracing_settings, tracing_data=get_tracing_data(app))
213227

214228
def _on_shutdown() -> None:
215-
_shutdown()
229+
_shutdown(tracing_data=get_tracing_data(app))
216230

217231
app.add_event_handler("shutdown", _on_shutdown)
218232

219233

220234
def get_tracing_instrumentation_lifespan(
221-
tracing_settings: TracingSettings, service_name: str
235+
tracing_settings: TracingSettings, tracing_data: TracingData
222236
):
223237
# NOTE: This lifespan does not instrument the app itself. Call setup_fastapi_app_tracing to do that.
224-
_startup(tracing_settings=tracing_settings, service_name=service_name)
238+
_startup(tracing_settings=tracing_settings, tracing_data=tracing_data)
225239

226240
async def tracing_instrumentation_lifespan(
227241
app: FastAPI,
@@ -230,7 +244,7 @@ async def tracing_instrumentation_lifespan(
230244

231245
yield {}
232246

233-
_shutdown()
247+
_shutdown(tracing_data=tracing_data)
234248

235249
return tracing_instrumentation_lifespan
236250

@@ -243,3 +257,8 @@ async def dispatch(self, request: Request, call_next):
243257
if trace_id_header:
244258
response.headers.update(trace_id_header)
245259
return response
260+
261+
262+
def get_tracing_data(app: FastAPI) -> TracingData:
263+
assert hasattr(app.state, "tracing_data"), "Tracing not setup for this app" # nosec
264+
return app.state.tracing_data

packages/service-library/src/servicelib/tracing.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
from collections.abc import Callable, Coroutine
22
from contextlib import contextmanager
33
from contextvars import Token
4+
from dataclasses import dataclass
45
from functools import wraps
5-
from typing import Any, Final, TypeAlias
6+
from typing import Any, Final, Self, TypeAlias
67

78
import pyinstrument
89
import pyinstrument.renderers
910
from opentelemetry import context as otcontext
1011
from opentelemetry import trace
1112
from opentelemetry.instrumentation.logging import LoggingInstrumentor
13+
from opentelemetry.sdk.resources import Resource
14+
from opentelemetry.sdk.trace import TracerProvider
15+
from opentelemetry.sdk.trace.sampling import ParentBased, TraceIdRatioBased
1216
from settings_library.tracing import TracingSettings
1317

1418
TracingContext: TypeAlias = otcontext.Context | None
@@ -93,3 +97,21 @@ async def wrapper(*args: Any, **kwargs: Any) -> Any:
9397
)
9498

9599
return wrapper
100+
101+
102+
@dataclass
103+
class TracingData:
104+
service_name: str
105+
tracer_provider: TracerProvider
106+
107+
@classmethod
108+
def create(cls, tracing_settings: TracingSettings, service_name: str) -> Self:
109+
resource = Resource(attributes={"service.name": service_name})
110+
sampler = ParentBased(
111+
root=TraceIdRatioBased(tracing_settings.TRACING_SAMPLING_PROBABILITY)
112+
)
113+
trace_provider = TracerProvider(resource=resource, sampler=sampler)
114+
return cls(
115+
service_name=service_name,
116+
tracer_provider=trace_provider,
117+
)

packages/service-library/tests/fastapi/test_tracing.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from servicelib.tracing import (
2525
_OSPARC_TRACE_ID_HEADER,
2626
_PROFILE_ATTRIBUTE_NAME,
27+
TracingData,
2728
with_profiled_span,
2829
)
2930
from settings_library.tracing import TracingSettings
@@ -83,13 +84,16 @@ async def test_valid_tracing_settings(
8384
tracing_settings_in: Callable[[], dict[str, Any]],
8485
):
8586
tracing_settings = TracingSettings()
87+
tracing_data = TracingData.create(
88+
tracing_settings=tracing_settings, service_name="Mock-Openetlemetry-Pytest"
89+
)
8690
async for _ in get_tracing_instrumentation_lifespan(
8791
tracing_settings=tracing_settings,
88-
service_name="Mock-Openetlemetry-Pytest",
92+
tracing_data=tracing_data,
8993
)(app=mocked_app):
9094
async for _ in get_tracing_instrumentation_lifespan(
9195
tracing_settings=tracing_settings,
92-
service_name="Mock-Openetlemetry-Pytest",
96+
tracing_data=tracing_data,
9397
)(app=mocked_app):
9498
pass
9599

@@ -120,9 +124,12 @@ async def test_invalid_tracing_settings(
120124
app = mocked_app
121125
with pytest.raises((BaseException, ValidationError, TypeError)): # noqa: PT012
122126
tracing_settings = TracingSettings()
127+
tracing_data = TracingData.create(
128+
tracing_settings=tracing_settings, service_name="Mock-Openetlemetry-Pytest"
129+
)
123130
async for _ in get_tracing_instrumentation_lifespan(
124131
tracing_settings=tracing_settings,
125-
service_name="Mock-Openetlemetry-Pytest",
132+
tracing_data=tracing_data,
126133
)(app=app):
127134
pass
128135

@@ -176,14 +183,17 @@ async def test_tracing_setup_package_detection(
176183
package_name = manage_package
177184
importlib.import_module(package_name)
178185
tracing_settings = TracingSettings()
186+
tracing_data = TracingData.create(
187+
tracing_settings=tracing_settings, service_name="Mock-Openetlemetry-Pytest"
188+
)
179189
async for _ in get_tracing_instrumentation_lifespan(
180190
tracing_settings=tracing_settings,
181-
service_name="Mock-Openetlemetry-Pytest",
191+
tracing_data=tracing_data,
182192
)(app=mocked_app):
183193
# idempotency check
184194
async for _ in get_tracing_instrumentation_lifespan(
185195
tracing_settings=tracing_settings,
186-
service_name="Mock-Openetlemetry-Pytest",
196+
tracing_data=tracing_data,
187197
)(app=mocked_app):
188198
pass
189199

@@ -210,6 +220,9 @@ async def test_trace_id_in_response_header(
210220
server_response: PlainTextResponse | HTTPException,
211221
) -> None:
212222
tracing_settings = TracingSettings()
223+
tracing_data = TracingData.create(
224+
tracing_settings=tracing_settings, service_name="Mock-Openetlemetry-Pytest"
225+
)
213226

214227
handler_data = dict()
215228

@@ -226,7 +239,7 @@ async def handler(handler_data: dict):
226239

227240
async for _ in get_tracing_instrumentation_lifespan(
228241
tracing_settings=tracing_settings,
229-
service_name="Mock-OpenTelemetry-Pytest",
242+
tracing_data=tracing_data,
230243
)(app=mocked_app):
231244
initialize_fastapi_app_tracing(mocked_app, add_response_trace_id_header=True)
232245
client = TestClient(mocked_app)
@@ -259,6 +272,9 @@ async def test_with_profile_span(
259272
server_response: PlainTextResponse | HTTPException,
260273
):
261274
tracing_settings = TracingSettings()
275+
tracing_data = TracingData.create(
276+
tracing_settings=tracing_settings, service_name="Mock-Openetlemetry-Pytest"
277+
)
262278

263279
handler_data = dict()
264280

@@ -276,7 +292,7 @@ async def handler(handler_data: dict):
276292

277293
async for _ in get_tracing_instrumentation_lifespan(
278294
tracing_settings=tracing_settings,
279-
service_name="Mock-OpenTelemetry-Pytest",
295+
tracing_data=tracing_data,
280296
)(app=mocked_app):
281297
initialize_fastapi_app_tracing(mocked_app, add_response_trace_id_header=True)
282298
client = TestClient(mocked_app)
@@ -315,6 +331,9 @@ async def test_tracing_sampling_probability_effective(
315331
tolerance_probability = 0.5
316332

317333
tracing_settings = TracingSettings()
334+
tracing_data = TracingData.create(
335+
tracing_settings=tracing_settings, service_name="Mock-Openetlemetry-Pytest"
336+
)
318337

319338
async def handler():
320339
return PlainTextResponse("ok")
@@ -323,7 +342,7 @@ async def handler():
323342

324343
async for _ in get_tracing_instrumentation_lifespan(
325344
tracing_settings=tracing_settings,
326-
service_name="Mock-OpenTelemetry-Pytest",
345+
tracing_data=tracing_data,
327346
)(app=mocked_app):
328347
initialize_fastapi_app_tracing(mocked_app, add_response_trace_id_header=True)
329348
client = TestClient(mocked_app)

0 commit comments

Comments
 (0)