@@ -165,7 +165,8 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
165165
166166from __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
169170from weakref import WeakSet
170171
171172from 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)
187188from 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
190191if 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
204206class 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
302323class _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