Skip to content

Commit 68ebc12

Browse files
committed
starlette: add missing configuration params from ASGI Middleware
1 parent a651600 commit 68ebc12

2 files changed

Lines changed: 255 additions & 93 deletions

File tree

instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py

Lines changed: 56 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,8 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
165165

166166
from __future__ import annotations
167167

168-
from typing import TYPE_CHECKING, Any, Collection, cast
168+
import logging
169+
from typing import TYPE_CHECKING, Any, Collection, Literal, cast
169170
from weakref import WeakSet
170171

171172
from starlette import applications
@@ -185,10 +186,10 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
185186
HTTP_ROUTE,
186187
)
187188
from opentelemetry.trace import TracerProvider, get_tracer
188-
from opentelemetry.util.http import get_excluded_urls
189+
from opentelemetry.util.http import get_excluded_urls, parse_excluded_urls
189190

190191
if TYPE_CHECKING:
191-
from typing import TypedDict, Unpack
192+
from typing import TypedDict
192193

193194
class InstrumentKwargs(TypedDict, total=False):
194195
tracer_provider: TracerProvider
@@ -198,7 +199,8 @@ class InstrumentKwargs(TypedDict, total=False):
198199
client_response_hook: ClientResponseHook
199200

200201

201-
_excluded_urls = get_excluded_urls("STARLETTE")
202+
_excluded_urls_from_env = get_excluded_urls("STARLETTE")
203+
_logger = logging.getLogger(__name__)
202204

203205

204206
class StarletteInstrumentor(BaseInstrumentor):
@@ -217,6 +219,11 @@ def instrument_app(
217219
client_response_hook: ClientResponseHook = None,
218220
meter_provider: MeterProvider | None = None,
219221
tracer_provider: TracerProvider | None = None,
222+
excluded_urls: str | None = None,
223+
http_capture_headers_server_request: list[str] | None = None,
224+
http_capture_headers_server_response: list[str] | None = None,
225+
http_capture_headers_sanitize_fields: list[str] | None = None,
226+
exclude_spans: list[Literal["receive", "send"]] | None = None,
220227
):
221228
"""Instrument an uninstrumented Starlette application.
222229
@@ -232,35 +239,56 @@ def instrument_app(
232239
the current globally configured one is used.
233240
tracer_provider: The optional tracer provider to use. If omitted
234241
the current globally configured one is used.
242+
excluded_urls: Optional comma delimited string of regexes to match URLs that should not be traced.
243+
http_capture_headers_server_request: Optional list of HTTP headers to capture from the request.
244+
http_capture_headers_server_response: Optional list of HTTP headers to capture from the response.
245+
http_capture_headers_sanitize_fields: Optional list of HTTP headers to sanitize.
246+
exclude_spans: Optionally exclude HTTP `send` and/or `receive` spans from the trace.
235247
"""
236-
tracer = get_tracer(
237-
__name__,
238-
__version__,
239-
tracer_provider,
240-
schema_url="https://opentelemetry.io/schemas/1.11.0",
241-
)
242-
meter = get_meter(
243-
__name__,
244-
__version__,
245-
meter_provider,
246-
schema_url="https://opentelemetry.io/schemas/1.11.0",
247-
)
248+
if not hasattr(app, "_is_instrumented_by_opentelemetry"):
249+
app._is_instrumented_by_opentelemetry = False
250+
248251
if not getattr(app, "_is_instrumented_by_opentelemetry", False):
252+
if excluded_urls is None:
253+
excluded_urls = _excluded_urls_from_env
254+
else:
255+
excluded_urls = parse_excluded_urls(excluded_urls)
256+
tracer = get_tracer(
257+
__name__,
258+
__version__,
259+
tracer_provider,
260+
schema_url="https://opentelemetry.io/schemas/1.11.0",
261+
)
262+
meter = get_meter(
263+
__name__,
264+
__version__,
265+
meter_provider,
266+
schema_url="https://opentelemetry.io/schemas/1.11.0",
267+
)
268+
249269
app.add_middleware(
250270
OpenTelemetryMiddleware,
251-
excluded_urls=_excluded_urls,
271+
excluded_urls=excluded_urls,
252272
default_span_details=_get_default_span_details,
253273
server_request_hook=server_request_hook,
254274
client_request_hook=client_request_hook,
255275
client_response_hook=client_response_hook,
256276
# Pass in tracer/meter to get __name__and __version__ of starlette instrumentation
257277
tracer=tracer,
258278
meter=meter,
279+
http_capture_headers_server_request=http_capture_headers_server_request,
280+
http_capture_headers_server_response=http_capture_headers_server_response,
281+
http_capture_headers_sanitize_fields=http_capture_headers_sanitize_fields,
282+
exclude_spans=exclude_spans,
259283
)
260284
app._is_instrumented_by_opentelemetry = True
261285

262286
# adding apps to set for uninstrumenting
263287
_InstrumentedStarlette._instrumented_starlette_apps.add(app)
288+
else:
289+
_logger.warning(
290+
"Attempting to instrument Starlette app while already instrumented"
291+
)
264292

265293
@staticmethod
266294
def uninstrument_app(app: applications.Starlette):
@@ -275,64 +303,33 @@ def uninstrument_app(app: applications.Starlette):
275303
def instrumentation_dependencies(self) -> Collection[str]:
276304
return _instruments
277305

278-
def _instrument(self, **kwargs: Unpack[InstrumentKwargs]):
306+
def _instrument(self, **kwargs: Any):
279307
self._original_starlette = applications.Starlette
280-
_InstrumentedStarlette._tracer_provider = kwargs.get("tracer_provider")
281-
_InstrumentedStarlette._server_request_hook = kwargs.get(
282-
"server_request_hook"
283-
)
284-
_InstrumentedStarlette._client_request_hook = kwargs.get(
285-
"client_request_hook"
286-
)
287-
_InstrumentedStarlette._client_response_hook = kwargs.get(
288-
"client_response_hook"
289-
)
290-
_InstrumentedStarlette._meter_provider = kwargs.get("meter_provider")
291-
308+
_InstrumentedStarlette._instrument_kwargs = kwargs
292309
applications.Starlette = _InstrumentedStarlette
293310

294311
def _uninstrument(self, **kwargs: Any):
295312
"""uninstrumenting all created apps by user"""
296-
for instance in _InstrumentedStarlette._instrumented_starlette_apps:
313+
# Create a copy of the set to avoid RuntimeError during iteration
314+
instances_to_uninstrument = list(
315+
_InstrumentedStarlette._instrumented_starlette_apps
316+
)
317+
for instance in instances_to_uninstrument:
297318
self.uninstrument_app(instance)
298319
_InstrumentedStarlette._instrumented_starlette_apps.clear()
299320
applications.Starlette = self._original_starlette
300321

301322

302323
class _InstrumentedStarlette(applications.Starlette):
303-
_tracer_provider: TracerProvider | None = None
304-
_meter_provider: MeterProvider | None = None
305-
_server_request_hook: ServerRequestHook = None
306-
_client_request_hook: ClientRequestHook = None
307-
_client_response_hook: ClientResponseHook = None
324+
_instrument_kwargs: dict[str, Any] = {}
325+
# Track instrumented app instances using weak references to avoid GC leaks
308326
_instrumented_starlette_apps: WeakSet[applications.Starlette] = WeakSet()
309327

310328
def __init__(self, *args: Any, **kwargs: Any):
311329
super().__init__(*args, **kwargs)
312-
tracer = get_tracer(
313-
__name__,
314-
__version__,
315-
_InstrumentedStarlette._tracer_provider,
316-
schema_url="https://opentelemetry.io/schemas/1.11.0",
317-
)
318-
meter = get_meter(
319-
__name__,
320-
__version__,
321-
_InstrumentedStarlette._meter_provider,
322-
schema_url="https://opentelemetry.io/schemas/1.11.0",
323-
)
324-
self.add_middleware(
325-
OpenTelemetryMiddleware,
326-
excluded_urls=_excluded_urls,
327-
default_span_details=_get_default_span_details,
328-
server_request_hook=_InstrumentedStarlette._server_request_hook,
329-
client_request_hook=_InstrumentedStarlette._client_request_hook,
330-
client_response_hook=_InstrumentedStarlette._client_response_hook,
331-
# Pass in tracer/meter to get __name__and __version__ of starlette instrumentation
332-
tracer=tracer,
333-
meter=meter,
330+
StarletteInstrumentor.instrument_app(
331+
self, **_InstrumentedStarlette._instrument_kwargs
334332
)
335-
self._is_instrumented_by_opentelemetry = True
336333
# adding apps to set for uninstrumenting
337334
_InstrumentedStarlette._instrumented_starlette_apps.add(self)
338335

0 commit comments

Comments
 (0)