Skip to content

Commit f3462f0

Browse files
authored
Handle FastAPI update with SolvedDependencies (#415)
1 parent 4e64216 commit f3462f0

File tree

4 files changed

+53
-34
lines changed

4 files changed

+53
-34
lines changed

logfire/_internal/integrations/fastapi.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import dataclasses
34
import inspect
45
from contextlib import contextmanager
56
from functools import lru_cache
@@ -115,7 +116,7 @@ def patch_fastapi():
115116
"""Globally monkeypatch fastapi functions and return a dictionary for recording instrumentation config per app."""
116117
registry: WeakKeyDictionary[FastAPI, FastAPIInstrumentation] = WeakKeyDictionary()
117118

118-
async def patched_solve_dependencies(*, request: Request | WebSocket, **kwargs: Any):
119+
async def patched_solve_dependencies(*, request: Request | WebSocket, **kwargs: Any) -> Any:
119120
original = original_solve_dependencies(request=request, **kwargs)
120121
if instrumentation := registry.get(request.app):
121122
return await instrumentation.solve_dependencies(request, original)
@@ -166,9 +167,7 @@ def __init__(
166167
else:
167168
self.excluded_urls_list = parse_excluded_urls(excluded_urls) # pragma: no cover
168169

169-
async def solve_dependencies(
170-
self, request: Request | WebSocket, original: Awaitable[tuple[dict[str, Any], list[Any], Any, Any, Any]]
171-
):
170+
async def solve_dependencies(self, request: Request | WebSocket, original: Awaitable[Any]) -> Any:
172171
try:
173172
url = cast(str, get_host_port_url_tuple(request.scope)[2])
174173
excluded = self.excluded_urls_list.url_disabled(url)
@@ -180,7 +179,23 @@ async def solve_dependencies(
180179
return await original # pragma: no cover
181180

182181
with self.logfire_instance.span('FastAPI arguments') as span:
183-
result = await original
182+
result: Any = await original
183+
184+
solved_values: dict[str, Any]
185+
solved_errors: list[Any]
186+
187+
if isinstance(result, tuple): # pragma: no cover
188+
solved_values = result[0] # type: ignore
189+
solved_errors = result[1] # type: ignore
190+
191+
def solved_with_new_values(new_values: dict[str, Any]) -> Any:
192+
return new_values, *result[1:]
193+
else:
194+
solved_values = result.values
195+
solved_errors = result.errors
196+
197+
def solved_with_new_values(new_values: dict[str, Any]) -> Any:
198+
return dataclasses.replace(result, values=new_values)
184199

185200
try:
186201
attributes: dict[str, Any] | None = {
@@ -189,17 +204,17 @@ async def solve_dependencies(
189204
# Making a deep copy could be very expensive and maybe even impossible.
190205
'values': {
191206
k: v
192-
for k, v in result[0].items()
207+
for k, v in solved_values.items() # type: ignore
193208
if not isinstance(v, (Request, WebSocket, BackgroundTasks, SecurityScopes, Response))
194209
},
195-
'errors': result[1].copy(),
210+
'errors': solved_errors.copy(), # type: ignore
196211
}
197212

198213
# Set the current app on `values` so that `patched_run_endpoint_function` can check it.
199214
if isinstance(request, Request): # pragma: no branch
200-
instrumented_values = _InstrumentedValues(result[0])
215+
instrumented_values = _InstrumentedValues(solved_values)
201216
instrumented_values.request = request
202-
result = (instrumented_values, *result[1:])
217+
result: Any = solved_with_new_values(instrumented_values)
203218

204219
attributes = self.request_attributes_mapper(request, attributes)
205220
if not attributes:
@@ -208,7 +223,7 @@ async def solve_dependencies(
208223
# We can't drop the span since it's already been created,
209224
# but we can set the level to debug so that it's hidden by default.
210225
span.set_level('debug')
211-
return result
226+
return result # type: ignore
212227

213228
# request_attributes_mapper may have removed the errors, so we need .get() here.
214229
if attributes.get('errors'):
@@ -235,7 +250,7 @@ async def solve_dependencies(
235250
except Exception as e: # pragma: no cover
236251
span.record_exception(e)
237252

238-
return result
253+
return result # type: ignore
239254

240255
async def run_endpoint_function(
241256
self,

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ dev-dependencies = [
142142
"griffe==0.48.0",
143143
"pyarrow>=17.0.0",
144144
"pytest-recording>=0.13.2",
145+
"uvicorn>=0.30.6",
145146
]
146147

147148
[tool.rye.scripts]

requirements-dev.lock

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# features: []
77
# all-features: false
88
# with-sources: false
9+
# generate-hashes: false
910

1011
-e file:.
1112
aiohappyeyeballs==2.4.0
@@ -17,7 +18,7 @@ amqp==5.2.0
1718
# via kombu
1819
annotated-types==0.7.0
1920
# via pydantic
20-
anthropic==0.34.1
21+
anthropic==0.34.2
2122
anyio==4.3.0
2223
# via anthropic
2324
# via httpx
@@ -28,8 +29,6 @@ asgiref==3.8.1
2829
# via opentelemetry-instrumentation-asgi
2930
asttokens==2.4.1
3031
# via inline-snapshot
31-
async-timeout==4.0.3
32-
# via asyncpg
3332
asyncpg==0.29.0
3433
attrs==24.2.0
3534
# via aiohttp
@@ -60,6 +59,7 @@ click==8.1.7
6059
# via inline-snapshot
6160
# via mkdocs
6261
# via mkdocstrings
62+
# via uvicorn
6363
click-didyoumean==0.3.1
6464
# via celery
6565
click-plugins==1.1.1
@@ -81,24 +81,24 @@ distlib==0.3.8
8181
distro==1.9.0
8282
# via anthropic
8383
# via openai
84-
django==5.1
84+
django==5.1.1
8585
dnspython==2.6.1
8686
# via pymongo
8787
docker==7.1.0
8888
# via testcontainers
8989
eval-type-backport==0.2.0
90-
executing==2.0.1
90+
executing==2.1.0
9191
# via inline-snapshot
9292
# via logfire
93-
fastapi==0.112.2
93+
fastapi==0.112.3
9494
filelock==3.15.4
9595
# via huggingface-hub
9696
# via virtualenv
9797
flask==3.0.3
9898
frozenlist==1.4.1
9999
# via aiohttp
100100
# via aiosignal
101-
fsspec==2024.6.1
101+
fsspec==2024.9.0
102102
# via huggingface-hub
103103
ghp-import==2.1.0
104104
# via mkdocs
@@ -108,6 +108,7 @@ griffe==0.48.0
108108
# via mkdocstrings-python
109109
h11==0.14.0
110110
# via httpcore
111+
# via uvicorn
111112
httpcore==1.0.5
112113
# via httpx
113114
httpx==0.27.2
@@ -164,15 +165,15 @@ mkdocs==1.6.1
164165
# via mkdocs-autorefs
165166
# via mkdocs-material
166167
# via mkdocstrings
167-
mkdocs-autorefs==1.1.0
168+
mkdocs-autorefs==1.2.0
168169
# via mkdocstrings
169170
mkdocs-get-deps==0.2.0
170171
# via mkdocs
171172
mkdocs-glightbox==0.4.0
172-
mkdocs-material==9.5.33
173+
mkdocs-material==9.5.34
173174
mkdocs-material-extensions==1.3.1
174175
# via mkdocs-material
175-
mkdocstrings==0.25.2
176+
mkdocstrings==0.26.0
176177
# via mkdocstrings-python
177178
mkdocstrings-python==1.10.7
178179
multidict==6.0.5
@@ -186,7 +187,7 @@ mysql-connector-python==8.4.0
186187
nodeenv==1.9.1
187188
# via pre-commit
188189
# via pyright
189-
numpy==2.1.0
190+
numpy==2.1.1
190191
# via pandas
191192
# via pyarrow
192193
openai==1.43.0
@@ -332,7 +333,7 @@ psycopg-binary==3.2.1
332333
# via psycopg
333334
psycopg2-binary==2.9.9
334335
pyarrow==17.0.0
335-
pydantic @ git+https://github.com/pydantic/pydantic@111eb01ea3808e5283f8951265c35f47855f0faa
336+
pydantic @ git+https://github.com/pydantic/pydantic@447879b44ab8a9871193d6aef1b0846288929495
336337
# via anthropic
337338
# via fastapi
338339
# via openai
@@ -346,12 +347,12 @@ pymdown-extensions==10.9
346347
# via mkdocs-material
347348
# via mkdocstrings
348349
pymongo==4.8.0
349-
pyright==1.1.378
350+
pyright==1.1.379
350351
pytest==8.3.2
351352
# via pytest-django
352353
# via pytest-pretty
353354
# via pytest-recording
354-
pytest-django==4.8.0
355+
pytest-django==4.9.0
355356
pytest-pretty==1.2.0
356357
pytest-recording==0.13.2
357358
python-dateutil==2.9.0.post0
@@ -385,7 +386,7 @@ rich==13.8.0
385386
# via logfire
386387
# via pytest-pretty
387388
ruff==0.6.3
388-
setuptools==74.0.0
389+
setuptools==74.1.2
389390
# via opentelemetry-instrumentation
390391
six==1.16.0
391392
# via asttokens
@@ -395,15 +396,15 @@ sniffio==1.3.1
395396
# via anyio
396397
# via httpx
397398
# via openai
398-
sqlalchemy==2.0.32
399+
sqlalchemy==2.0.34
399400
# via sqlmodel
400-
sqlmodel==0.0.21
401+
sqlmodel==0.0.22
401402
sqlparse==0.5.1
402403
# via django
403-
starlette==0.38.2
404+
starlette==0.38.4
404405
# via fastapi
405406
structlog==24.4.0
406-
testcontainers==4.8.0
407+
testcontainers==4.8.1
407408
tokenizers==0.20.0
408409
# via anthropic
409410
toml==0.10.2
@@ -435,6 +436,7 @@ urllib3==2.2.2
435436
# via docker
436437
# via requests
437438
# via testcontainers
439+
uvicorn==0.30.6
438440
vcrpy==6.0.1
439441
# via pytest-recording
440442
vine==5.1.0
@@ -443,7 +445,7 @@ vine==5.1.0
443445
# via kombu
444446
virtualenv==20.26.3
445447
# via pre-commit
446-
watchdog==5.0.0
448+
watchdog==5.0.2
447449
# via mkdocs
448450
wcwidth==0.2.13
449451
# via prompt-toolkit
@@ -458,7 +460,7 @@ wrapt==1.16.0
458460
# via opentelemetry-instrumentation-sqlalchemy
459461
# via testcontainers
460462
# via vcrpy
461-
yarl==1.9.4
463+
yarl==1.9.11
462464
# via aiohttp
463465
# via vcrpy
464466
zipp==3.20.1

requirements.lock

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# features: []
77
# all-features: false
88
# with-sources: false
9+
# generate-hashes: false
910

1011
-e file:.
1112
certifi==2024.8.30
@@ -16,7 +17,7 @@ deprecated==1.2.14
1617
# via opentelemetry-api
1718
# via opentelemetry-exporter-otlp-proto-http
1819
# via opentelemetry-semantic-conventions
19-
executing==2.0.1
20+
executing==2.1.0
2021
# via logfire
2122
googleapis-common-protos==1.65.0
2223
# via opentelemetry-exporter-otlp-proto-http
@@ -57,7 +58,7 @@ requests==2.32.3
5758
# via opentelemetry-exporter-otlp-proto-http
5859
rich==13.8.0
5960
# via logfire
60-
setuptools==74.0.0
61+
setuptools==74.1.2
6162
# via opentelemetry-instrumentation
6263
typing-extensions==4.12.2
6364
# via logfire

0 commit comments

Comments
 (0)