From d1f6f53e3bfd6bb84546b258fdc73f16bfbd8b34 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 7 Jul 2025 08:48:59 +0200 Subject: [PATCH 1/7] Make app field optional in Flask, Starlette and FastAPI instrumentations --- logfire/_internal/integrations/fastapi.py | 80 +++++++++++---------- logfire/_internal/integrations/flask.py | 30 +++++--- logfire/_internal/integrations/starlette.py | 36 ++++++---- logfire/_internal/main.py | 6 +- tests/otel_integrations/test_fastapi.py | 19 +++++ tests/otel_integrations/test_starlette.py | 18 +++++ 6 files changed, 128 insertions(+), 61 deletions(-) diff --git a/logfire/_internal/integrations/fastapi.py b/logfire/_internal/integrations/fastapi.py index ac465b6bf..8d9fdb461 100644 --- a/logfire/_internal/integrations/fastapi.py +++ b/logfire/_internal/integrations/fastapi.py @@ -48,7 +48,7 @@ def find_mounted_apps(app: FastAPI) -> list[FastAPI]: def instrument_fastapi( logfire_instance: Logfire, - app: FastAPI, + app: FastAPI | None = None, *, capture_headers: bool = False, request_attributes_mapper: Callable[ @@ -78,39 +78,53 @@ def instrument_fastapi( 'meter_provider': logfire_instance.config.get_meter_provider(), **opentelemetry_kwargs, } - FastAPIInstrumentor.instrument_app( - app, - excluded_urls=excluded_urls, - server_request_hook=_server_request_hook(opentelemetry_kwargs.pop('server_request_hook', None)), - **opentelemetry_kwargs, - ) + if app is None: + FastAPIInstrumentor().instrument( + excluded_urls=excluded_urls, + server_request_hook=_server_request_hook(opentelemetry_kwargs.pop('server_request_hook', None)), + **opentelemetry_kwargs, + ) - registry = patch_fastapi() - if app in registry: # pragma: no cover - raise ValueError('This app has already been instrumented.') + @contextmanager + def uninstrument_context(): + yield + FastAPIInstrumentor().uninstrument() + + return uninstrument_context() + else: + FastAPIInstrumentor.instrument_app( + app, + excluded_urls=excluded_urls, + server_request_hook=_server_request_hook(opentelemetry_kwargs.pop('server_request_hook', None)), + **opentelemetry_kwargs, + ) - mounted_apps = find_mounted_apps(app) - mounted_apps.append(app) + registry = patch_fastapi() + if app in registry: # pragma: no cover + raise ValueError('This app has already been instrumented.') - for _app in mounted_apps: - registry[_app] = FastAPIInstrumentation( - logfire_instance, - request_attributes_mapper or _default_request_attributes_mapper, - ) + mounted_apps = find_mounted_apps(app) + mounted_apps.append(app) - @contextmanager - def uninstrument_context(): - # The user isn't required (or even expected) to use this context manager, - # which is why the instrumenting and patching has already happened before this point. - # It exists mostly for tests, and just in case users want it. - try: - yield - finally: - for _app in mounted_apps: - del registry[_app] - FastAPIInstrumentor.uninstrument_app(_app) + for _app in mounted_apps: + registry[_app] = FastAPIInstrumentation( + logfire_instance, + request_attributes_mapper or _default_request_attributes_mapper, + ) - return uninstrument_context() + @contextmanager + def uninstrument_context(): + # The user isn't required (or even expected) to use this context manager, + # which is why the instrumenting and patching has already happened before this point. + # It exists mostly for tests, and just in case users want it. + try: + yield + finally: + for _app in mounted_apps: + del registry[_app] + FastAPIInstrumentor.uninstrument_app(_app) + + return uninstrument_context() @lru_cache # only patch once @@ -151,13 +165,7 @@ class FastAPIInstrumentation: def __init__( self, logfire_instance: Logfire, - request_attributes_mapper: Callable[ - [ - Request | WebSocket, - dict[str, Any], - ], - dict[str, Any] | None, - ], + request_attributes_mapper: Callable[[Request | WebSocket, dict[str, Any]], dict[str, Any] | None], ): self.logfire_instance = logfire_instance.with_settings(custom_scope_suffix='fastapi') self.request_attributes_mapper = request_attributes_mapper diff --git a/logfire/_internal/integrations/flask.py b/logfire/_internal/integrations/flask.py index 55582b135..13e6bf605 100644 --- a/logfire/_internal/integrations/flask.py +++ b/logfire/_internal/integrations/flask.py @@ -20,7 +20,7 @@ def instrument_flask( - app: Flask, + app: Flask | None = None, *, capture_headers: bool, enable_commenter: bool, @@ -41,12 +41,22 @@ def instrument_flask( warn_at_user_stacklevel('exclude_urls is deprecated; use excluded_urls instead', DeprecationWarning) excluded_urls = excluded_urls or kwargs.pop('exclude_urls', None) - FlaskInstrumentor().instrument_app( # type: ignore[reportUnknownMemberType] - app, - enable_commenter=enable_commenter, - commenter_options=commenter_options, - excluded_urls=excluded_urls, - request_hook=request_hook, - response_hook=response_hook, - **kwargs, - ) + if app is None: + FlaskInstrumentor().instrument( + enable_commenter=enable_commenter, + commenter_options=commenter_options, + excluded_urls=excluded_urls, + request_hook=request_hook, + response_hook=response_hook, + **kwargs, + ) + else: + FlaskInstrumentor().instrument_app( # type: ignore[reportUnknownMemberType] + app, + enable_commenter=enable_commenter, + commenter_options=commenter_options, + excluded_urls=excluded_urls, + request_hook=request_hook, + response_hook=response_hook, + **kwargs, + ) diff --git a/logfire/_internal/integrations/starlette.py b/logfire/_internal/integrations/starlette.py index 5c7bcccfd..eb6dc29f8 100644 --- a/logfire/_internal/integrations/starlette.py +++ b/logfire/_internal/integrations/starlette.py @@ -21,7 +21,7 @@ def instrument_starlette( logfire_instance: Logfire, - app: Starlette, + app: Starlette | None = None, *, record_send_receive: bool = False, capture_headers: bool = False, @@ -35,14 +35,26 @@ def instrument_starlette( See the `Logfire.instrument_starlette` method for details. """ maybe_capture_server_headers(capture_headers) - StarletteInstrumentor().instrument_app( - app, - server_request_hook=server_request_hook, - client_request_hook=client_request_hook, - client_response_hook=client_response_hook, - **{ # type: ignore - 'tracer_provider': tweak_asgi_spans_tracer_provider(logfire_instance, record_send_receive), - 'meter_provider': logfire_instance.config.get_meter_provider(), - **kwargs, - }, - ) + if app is None: + StarletteInstrumentor().instrument( + server_request_hook=server_request_hook, + client_request_hook=client_request_hook, + client_response_hook=client_response_hook, + **{ + 'tracer_provider': tweak_asgi_spans_tracer_provider(logfire_instance, record_send_receive), + 'meter_provider': logfire_instance.config.get_meter_provider(), + **kwargs, + }, + ) + else: + StarletteInstrumentor().instrument_app( + app, + server_request_hook=server_request_hook, + client_request_hook=client_request_hook, + client_response_hook=client_response_hook, + **{ # type: ignore + 'tracer_provider': tweak_asgi_spans_tracer_provider(logfire_instance, record_send_receive), + 'meter_provider': logfire_instance.config.get_meter_provider(), + **kwargs, + }, + ) diff --git a/logfire/_internal/main.py b/logfire/_internal/main.py index b90c60e42..e04369b23 100644 --- a/logfire/_internal/main.py +++ b/logfire/_internal/main.py @@ -1022,7 +1022,7 @@ def instrument_pydantic_ai( def instrument_fastapi( self, - app: FastAPI, + app: FastAPI | None = None, *, capture_headers: bool = False, request_attributes_mapper: Callable[ @@ -1529,7 +1529,7 @@ def instrument_psycopg( def instrument_flask( self, - app: Flask, + app: Flask | None = None, *, capture_headers: bool = False, enable_commenter: bool = True, @@ -1576,7 +1576,7 @@ def instrument_flask( def instrument_starlette( self, - app: Starlette, + app: Starlette | None = None, *, capture_headers: bool = False, record_send_receive: bool = False, diff --git a/tests/otel_integrations/test_fastapi.py b/tests/otel_integrations/test_fastapi.py index f0254e3c9..8c2dc9f0c 100644 --- a/tests/otel_integrations/test_fastapi.py +++ b/tests/otel_integrations/test_fastapi.py @@ -13,6 +13,7 @@ from fastapi.security import SecurityScopes from fastapi.staticfiles import StaticFiles from inline_snapshot import snapshot +from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor from opentelemetry.propagate import inject from starlette.requests import Request from starlette.responses import PlainTextResponse @@ -2204,3 +2205,21 @@ def test_sampled_out(client: TestClient, exporter: TestExporter, config_kwargs: make_request_hook_spans(record_send_receive=False) assert exporter.exported_spans_as_dict() == [] + + +def test_instrumentation_no_app(exporter: TestExporter) -> None: + logfire.instrument_fastapi() + app = FastAPI() + + @app.get('/') + async def homepage(request: Request): + return PlainTextResponse('Hello, world!') + + try: + print(dir(app)) + client = TestClient(app) + response = client.get('/') + assert response.text == 'Hello, world!' + assert exporter.exported_spans_as_dict() == snapshot([]) + finally: + FastAPIInstrumentor().uninstrument() diff --git a/tests/otel_integrations/test_starlette.py b/tests/otel_integrations/test_starlette.py index 70c32df37..389c03542 100644 --- a/tests/otel_integrations/test_starlette.py +++ b/tests/otel_integrations/test_starlette.py @@ -9,6 +9,8 @@ from inline_snapshot import snapshot from opentelemetry.instrumentation.starlette import StarletteInstrumentor from starlette.applications import Starlette +from starlette.requests import Request +from starlette.responses import PlainTextResponse from starlette.routing import Route, WebSocketRoute from starlette.testclient import TestClient from starlette.websockets import WebSocket @@ -245,3 +247,19 @@ def test_missing_opentelemetry_dependency() -> None: You can install this with: pip install 'logfire[starlette]'\ """) + + +def test_instrumentation_no_app(exporter: TestExporter) -> None: + async def homepage(request: Request): + return PlainTextResponse('Hello, world!') + + app = Starlette(routes=[Route('/', homepage)]) + + try: + logfire.instrument_starlette(capture_headers=True, record_send_receive=True) + client = TestClient(app) + response = client.get('/') + assert response.text == 'Hello, world!' + assert exporter.exported_spans_as_dict() == snapshot([]) + finally: + StarletteInstrumentor().uninstrument() From 4f029e3d680b3fc460c3ed1c997e20df1629b65b Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Wed, 16 Jul 2025 17:38:57 +0200 Subject: [PATCH 2/7] Instrument Flask, Starlette and FastAPI in logfire run --- logfire/_internal/cli/run.py | 9 ++++++++- logfire/_internal/integrations/flask.py | 20 +++++++++++++++----- logfire/_internal/main.py | 23 ++++++----------------- tests/otel_integrations/test_fastapi.py | 19 ------------------- tests/otel_integrations/test_starlette.py | 18 ------------------ 5 files changed, 29 insertions(+), 60 deletions(-) diff --git a/logfire/_internal/cli/run.py b/logfire/_internal/cli/run.py index f62f61e2d..8c4103c21 100644 --- a/logfire/_internal/cli/run.py +++ b/logfire/_internal/cli/run.py @@ -164,6 +164,8 @@ def instrument_packages(installed_otel_packages: set[str], instrument_pkg_map: d try: instrument_package(import_name) except Exception: + raise + breakpoint() continue instrumented.append(base_pkg) return instrumented @@ -171,7 +173,12 @@ def instrument_packages(installed_otel_packages: set[str], instrument_pkg_map: d def instrument_package(import_name: str): instrument_attr = f'instrument_{import_name}' - getattr(logfire, instrument_attr)() + + if import_name in ('starlette', 'fastapi', 'flask'): + module = importlib.import_module(f'logfire._internal.integrations.{import_name}') + getattr(module, instrument_attr)(logfire.DEFAULT_LOGFIRE_INSTANCE) + else: + getattr(logfire, instrument_attr)() def find_recommended_instrumentations_to_install( diff --git a/logfire/_internal/integrations/flask.py b/logfire/_internal/integrations/flask.py index 13e6bf605..b17f372b9 100644 --- a/logfire/_internal/integrations/flask.py +++ b/logfire/_internal/integrations/flask.py @@ -15,16 +15,18 @@ " pip install 'logfire[flask]'" ) +from logfire import Logfire from logfire._internal.utils import maybe_capture_server_headers from logfire.integrations.flask import CommenterOptions, RequestHook, ResponseHook def instrument_flask( + logfire_instance: Logfire, app: Flask | None = None, *, - capture_headers: bool, - enable_commenter: bool, - commenter_options: CommenterOptions | None, + capture_headers: bool = False, + enable_commenter: bool = False, + commenter_options: CommenterOptions | None = None, excluded_urls: str | None = None, request_hook: RequestHook | None = None, response_hook: ResponseHook | None = None, @@ -48,7 +50,11 @@ def instrument_flask( excluded_urls=excluded_urls, request_hook=request_hook, response_hook=response_hook, - **kwargs, + **{ + 'tracer_provider': logfire_instance.config.get_tracer_provider(), + 'meter_provider': logfire_instance.config.get_meter_provider(), + **kwargs, + }, ) else: FlaskInstrumentor().instrument_app( # type: ignore[reportUnknownMemberType] @@ -58,5 +64,9 @@ def instrument_flask( excluded_urls=excluded_urls, request_hook=request_hook, response_hook=response_hook, - **kwargs, + **{ + 'tracer_provider': logfire_instance.config.get_tracer_provider(), + 'meter_provider': logfire_instance.config.get_meter_provider(), + **kwargs, + }, ) diff --git a/logfire/_internal/main.py b/logfire/_internal/main.py index 48c254252..526e64ec0 100644 --- a/logfire/_internal/main.py +++ b/logfire/_internal/main.py @@ -11,15 +11,7 @@ from enum import Enum from functools import cached_property from time import time -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Literal, - TypeVar, - Union, - overload, -) +from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, Union, overload import opentelemetry.context as context_api import opentelemetry.trace as trace_api @@ -1022,7 +1014,7 @@ def instrument_pydantic_ai( def instrument_fastapi( self, - app: FastAPI | None = None, + app: FastAPI, *, capture_headers: bool = False, request_attributes_mapper: Callable[ @@ -1541,7 +1533,7 @@ def instrument_psycopg( def instrument_flask( self, - app: Flask | None = None, + app: Flask, *, capture_headers: bool = False, enable_commenter: bool = True, @@ -1572,6 +1564,7 @@ def instrument_flask( self._warn_if_not_initialized_for_instrumentation() return instrument_flask( + self, app, capture_headers=capture_headers, enable_commenter=enable_commenter, @@ -1579,16 +1572,12 @@ def instrument_flask( excluded_urls=excluded_urls, request_hook=request_hook, response_hook=response_hook, - **{ - 'tracer_provider': self._config.get_tracer_provider(), - 'meter_provider': self._config.get_meter_provider(), - **kwargs, - }, + **kwargs, ) def instrument_starlette( self, - app: Starlette | None = None, + app: Starlette, *, capture_headers: bool = False, record_send_receive: bool = False, diff --git a/tests/otel_integrations/test_fastapi.py b/tests/otel_integrations/test_fastapi.py index 8c2dc9f0c..f0254e3c9 100644 --- a/tests/otel_integrations/test_fastapi.py +++ b/tests/otel_integrations/test_fastapi.py @@ -13,7 +13,6 @@ from fastapi.security import SecurityScopes from fastapi.staticfiles import StaticFiles from inline_snapshot import snapshot -from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor from opentelemetry.propagate import inject from starlette.requests import Request from starlette.responses import PlainTextResponse @@ -2205,21 +2204,3 @@ def test_sampled_out(client: TestClient, exporter: TestExporter, config_kwargs: make_request_hook_spans(record_send_receive=False) assert exporter.exported_spans_as_dict() == [] - - -def test_instrumentation_no_app(exporter: TestExporter) -> None: - logfire.instrument_fastapi() - app = FastAPI() - - @app.get('/') - async def homepage(request: Request): - return PlainTextResponse('Hello, world!') - - try: - print(dir(app)) - client = TestClient(app) - response = client.get('/') - assert response.text == 'Hello, world!' - assert exporter.exported_spans_as_dict() == snapshot([]) - finally: - FastAPIInstrumentor().uninstrument() diff --git a/tests/otel_integrations/test_starlette.py b/tests/otel_integrations/test_starlette.py index 389c03542..70c32df37 100644 --- a/tests/otel_integrations/test_starlette.py +++ b/tests/otel_integrations/test_starlette.py @@ -9,8 +9,6 @@ from inline_snapshot import snapshot from opentelemetry.instrumentation.starlette import StarletteInstrumentor from starlette.applications import Starlette -from starlette.requests import Request -from starlette.responses import PlainTextResponse from starlette.routing import Route, WebSocketRoute from starlette.testclient import TestClient from starlette.websockets import WebSocket @@ -247,19 +245,3 @@ def test_missing_opentelemetry_dependency() -> None: You can install this with: pip install 'logfire[starlette]'\ """) - - -def test_instrumentation_no_app(exporter: TestExporter) -> None: - async def homepage(request: Request): - return PlainTextResponse('Hello, world!') - - app = Starlette(routes=[Route('/', homepage)]) - - try: - logfire.instrument_starlette(capture_headers=True, record_send_receive=True) - client = TestClient(app) - response = client.get('/') - assert response.text == 'Hello, world!' - assert exporter.exported_spans_as_dict() == snapshot([]) - finally: - StarletteInstrumentor().uninstrument() From a84cdd375ac471f4c13a0e0659cb5cac92106d2b Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Wed, 16 Jul 2025 17:41:56 +0200 Subject: [PATCH 3/7] Update logfire/_internal/cli/run.py --- logfire/_internal/cli/run.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/logfire/_internal/cli/run.py b/logfire/_internal/cli/run.py index 8c4103c21..2add09428 100644 --- a/logfire/_internal/cli/run.py +++ b/logfire/_internal/cli/run.py @@ -164,8 +164,6 @@ def instrument_packages(installed_otel_packages: set[str], instrument_pkg_map: d try: instrument_package(import_name) except Exception: - raise - breakpoint() continue instrumented.append(base_pkg) return instrumented From 4852de27962888cdced1bcdfa76af302a04d355a Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 17 Jul 2025 11:08:24 +0200 Subject: [PATCH 4/7] add test --- tests/test_cli.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/test_cli.py b/tests/test_cli.py index 41457b474..332a3a86e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -31,6 +31,7 @@ ) from logfire._internal.config import LogfireCredentials, sanitize_project_name from logfire.exceptions import LogfireConfigError +from logfire.testing import TestExporter from tests.import_used_for_tests import run_script_test @@ -1534,6 +1535,38 @@ async def test_instrument_packages_aiohttp_client() -> None: AioHttpClientInstrumentor().uninstrument() +def test_instrument_web_frameworks(exporter: TestExporter) -> None: + try: + instrument_packages( + { + 'opentelemetry-instrumentation-starlette', + 'opentelemetry-instrumentation-fastapi', + 'opentelemetry-instrumentation-flask', + }, + { + 'opentelemetry-instrumentation-starlette': 'starlette', + 'opentelemetry-instrumentation-fastapi': 'fastapi', + 'opentelemetry-instrumentation-flask': 'flask', + }, + ) + + from fastapi import FastAPI + from flask import Flask + from starlette.applications import Starlette + + assert getattr(Starlette(), '_is_instrumented_by_opentelemetry', False) is True + assert getattr(FastAPI(), '_is_instrumented_by_opentelemetry', False) is True + assert getattr(Flask(__name__), '_is_instrumented_by_opentelemetry', False) is True + finally: + from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor + from opentelemetry.instrumentation.flask import FlaskInstrumentor + from opentelemetry.instrumentation.starlette import StarletteInstrumentor + + StarletteInstrumentor().uninstrument() + FastAPIInstrumentor().uninstrument() + FlaskInstrumentor().uninstrument() + + def test_split_args_action() -> None: parser = argparse.ArgumentParser() parser.add_argument('--foo', action=SplitArgs) From e695d01d70a30c2ac73b8094860a9af0c0570087 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 21 Jul 2025 10:10:44 +0200 Subject: [PATCH 5/7] apply comments --- logfire/_internal/cli/run.py | 2 ++ logfire/_internal/integrations/fastapi.py | 16 +++------- logfire/_internal/integrations/flask.py | 38 ++++++++--------------- 3 files changed, 20 insertions(+), 36 deletions(-) diff --git a/logfire/_internal/cli/run.py b/logfire/_internal/cli/run.py index 2add09428..8a49ef638 100644 --- a/logfire/_internal/cli/run.py +++ b/logfire/_internal/cli/run.py @@ -172,6 +172,8 @@ def instrument_packages(installed_otel_packages: set[str], instrument_pkg_map: d def instrument_package(import_name: str): instrument_attr = f'instrument_{import_name}' + # On those packages, the public `logfire.instrument_` function needs to receive the app object. + # But the internal function doesn't need it. if import_name in ('starlette', 'fastapi', 'flask'): module = importlib.import_module(f'logfire._internal.integrations.{import_name}') getattr(module, instrument_attr)(logfire.DEFAULT_LOGFIRE_INSTANCE) diff --git a/logfire/_internal/integrations/fastapi.py b/logfire/_internal/integrations/fastapi.py index d4836c00f..f232e9ba4 100644 --- a/logfire/_internal/integrations/fastapi.py +++ b/logfire/_internal/integrations/fastapi.py @@ -77,16 +77,15 @@ def instrument_fastapi( maybe_capture_server_headers(capture_headers) opentelemetry_kwargs = { + 'excluded_urls': excluded_urls, + 'server_request_hook': _server_request_hook(opentelemetry_kwargs.pop('server_request_hook', None)), 'tracer_provider': tweak_asgi_spans_tracer_provider(logfire_instance, record_send_receive), 'meter_provider': logfire_instance.config.get_meter_provider(), **opentelemetry_kwargs, } + if app is None: - FastAPIInstrumentor().instrument( - excluded_urls=excluded_urls, - server_request_hook=_server_request_hook(opentelemetry_kwargs.pop('server_request_hook', None)), - **opentelemetry_kwargs, - ) + FastAPIInstrumentor().instrument(**opentelemetry_kwargs) @contextmanager def uninstrument_context(): @@ -95,12 +94,7 @@ def uninstrument_context(): return uninstrument_context() else: - FastAPIInstrumentor.instrument_app( - app, - excluded_urls=excluded_urls, - server_request_hook=_server_request_hook(opentelemetry_kwargs.pop('server_request_hook', None)), - **opentelemetry_kwargs, - ) + FastAPIInstrumentor.instrument_app(app, **opentelemetry_kwargs) registry = patch_fastapi() if app in registry: # pragma: no cover diff --git a/logfire/_internal/integrations/flask.py b/logfire/_internal/integrations/flask.py index b17f372b9..0b2b8b994 100644 --- a/logfire/_internal/integrations/flask.py +++ b/logfire/_internal/integrations/flask.py @@ -43,30 +43,18 @@ def instrument_flask( warn_at_user_stacklevel('exclude_urls is deprecated; use excluded_urls instead', DeprecationWarning) excluded_urls = excluded_urls or kwargs.pop('exclude_urls', None) + opentelemetry_kwargs = { + 'enable_commenter': enable_commenter, + 'commenter_options': commenter_options, + 'excluded_urls': excluded_urls, + 'request_hook': request_hook, + 'response_hook': response_hook, + 'tracer_provider': logfire_instance.config.get_tracer_provider(), + 'meter_provider': logfire_instance.config.get_meter_provider(), + **kwargs, + } + if app is None: - FlaskInstrumentor().instrument( - enable_commenter=enable_commenter, - commenter_options=commenter_options, - excluded_urls=excluded_urls, - request_hook=request_hook, - response_hook=response_hook, - **{ - 'tracer_provider': logfire_instance.config.get_tracer_provider(), - 'meter_provider': logfire_instance.config.get_meter_provider(), - **kwargs, - }, - ) + FlaskInstrumentor().instrument(**opentelemetry_kwargs) else: - FlaskInstrumentor().instrument_app( # type: ignore[reportUnknownMemberType] - app, - enable_commenter=enable_commenter, - commenter_options=commenter_options, - excluded_urls=excluded_urls, - request_hook=request_hook, - response_hook=response_hook, - **{ - 'tracer_provider': logfire_instance.config.get_tracer_provider(), - 'meter_provider': logfire_instance.config.get_meter_provider(), - **kwargs, - }, - ) + FlaskInstrumentor().instrument_app(app, **opentelemetry_kwargs) # type: ignore[reportUnknownMemberType] From 8e22479829490b61d58ebc419e8204a09982e18f Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 21 Jul 2025 11:15:14 +0200 Subject: [PATCH 6/7] add pragma --- logfire/_internal/integrations/fastapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logfire/_internal/integrations/fastapi.py b/logfire/_internal/integrations/fastapi.py index f232e9ba4..d05c64d13 100644 --- a/logfire/_internal/integrations/fastapi.py +++ b/logfire/_internal/integrations/fastapi.py @@ -88,7 +88,7 @@ def instrument_fastapi( FastAPIInstrumentor().instrument(**opentelemetry_kwargs) @contextmanager - def uninstrument_context(): + def uninstrument_context(): # pragma: no cover yield FastAPIInstrumentor().uninstrument() From df85e15f16c5b5187e5ac071ac3a3a0fc3b82294 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 18 Aug 2025 13:27:56 +0100 Subject: [PATCH 7/7] add notes --- logfire/_internal/integrations/fastapi.py | 3 +++ logfire/_internal/integrations/flask.py | 3 +++ logfire/_internal/integrations/starlette.py | 3 +++ 3 files changed, 9 insertions(+) diff --git a/logfire/_internal/integrations/fastapi.py b/logfire/_internal/integrations/fastapi.py index c776872e4..eb2997f47 100644 --- a/logfire/_internal/integrations/fastapi.py +++ b/logfire/_internal/integrations/fastapi.py @@ -50,6 +50,9 @@ def find_mounted_apps(app: FastAPI) -> list[FastAPI]: def instrument_fastapi( logfire_instance: Logfire, + # Note that `Logfire.instrument_fastapi()` requires this argument. It's only omitted when called via the + # `logfire run` CLI. This is because `FastAPIInstrumentor.instrument() has to be called before + # `from fastapi import FastAPI` which is easy to get wrong. app: FastAPI | None = None, *, capture_headers: bool = False, diff --git a/logfire/_internal/integrations/flask.py b/logfire/_internal/integrations/flask.py index 0b2b8b994..5a10a1320 100644 --- a/logfire/_internal/integrations/flask.py +++ b/logfire/_internal/integrations/flask.py @@ -22,6 +22,9 @@ def instrument_flask( logfire_instance: Logfire, + # Note that `Logfire.instrument_flask()` requires this argument. It's only omitted when called via the + # `logfire run` CLI. This is because `FlaskInstrumentor.instrument_app()` has to be called before + # `from flask import Flask` which is easy to get wrong. app: Flask | None = None, *, capture_headers: bool = False, diff --git a/logfire/_internal/integrations/starlette.py b/logfire/_internal/integrations/starlette.py index eb6dc29f8..d20d74d76 100644 --- a/logfire/_internal/integrations/starlette.py +++ b/logfire/_internal/integrations/starlette.py @@ -21,6 +21,9 @@ def instrument_starlette( logfire_instance: Logfire, + # Note that `Logfire.instrument_starlette()` requires this argument. It's only omitted when called via the + # `logfire run` CLI. This is because `StarletteInstrumentor.instrument()` has to be called before + # `from starlette import Starlette` which is easy to get wrong. app: Starlette | None = None, *, record_send_receive: bool = False,