Skip to content

Commit 16a93cf

Browse files
authored
Convert FastAPI arguments log to span, don't set to debug by default (#164)
1 parent 7be01a0 commit 16a93cf

File tree

2 files changed

+263
-167
lines changed

2 files changed

+263
-167
lines changed

logfire/_internal/integrations/fastapi.py

Lines changed: 67 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import inspect
44
from contextlib import contextmanager
55
from functools import lru_cache
6-
from typing import Any, Callable, ContextManager, Iterable, Literal, cast
6+
from typing import Any, Awaitable, Callable, ContextManager, Iterable, cast
77
from weakref import WeakKeyDictionary
88

99
import fastapi.routing
@@ -88,11 +88,11 @@ def patch_fastapi():
8888
registry: WeakKeyDictionary[FastAPI, FastAPIInstrumentation] = WeakKeyDictionary()
8989

9090
async def patched_solve_dependencies(*, request: Request | WebSocket, **kwargs: Any):
91-
result = await original_solve_dependencies(request=request, **kwargs)
91+
original = original_solve_dependencies(request=request, **kwargs)
9292
if instrumentation := registry.get(request.app):
93-
return await instrumentation.solve_dependencies(request, result)
93+
return await instrumentation.solve_dependencies(request, original)
9494
else:
95-
return result # pragma: no cover
95+
return await original # pragma: no cover
9696

9797
# `solve_dependencies` is actually defined in `fastapi.dependencies.utils`,
9898
# but it's imported into `fastapi.routing`, which is where we need to patch it.
@@ -139,60 +139,73 @@ def __init__(
139139
self.excluded_urls_list = parse_excluded_urls(excluded_urls) # pragma: no cover
140140

141141
async def solve_dependencies(
142-
self, request: Request | WebSocket, result: tuple[dict[str, Any], list[Any], Any, Any, Any]
142+
self, request: Request | WebSocket, original: Awaitable[tuple[dict[str, Any], list[Any], Any, Any, Any]]
143143
):
144144
try:
145145
url = cast(str, get_host_port_url_tuple(request.scope)[2])
146-
if self.excluded_urls_list.url_disabled(url):
147-
return result # pragma: no cover
148-
149-
attributes: dict[str, Any] | None = {
150-
# Shallow copy these so that the user can safely modify them, but we don't tell them that.
151-
# We do explicitly tell them that the contents should not be modified.
152-
# Making a deep copy could be very expensive and maybe even impossible.
153-
'values': {
154-
k: v
155-
for k, v in result[0].items()
156-
if not isinstance(v, (Request, WebSocket, BackgroundTasks, SecurityScopes, Response))
157-
},
158-
'errors': result[1].copy(),
159-
}
160-
161-
# Set the current app on `values` so that `patched_run_endpoint_function` can check it.
162-
if isinstance(request, Request): # pragma: no branch
163-
instrumented_values = _InstrumentedValues(result[0])
164-
instrumented_values.request = request
165-
result = (instrumented_values, *result[1:])
166-
167-
attributes = self.request_attributes_mapper(request, attributes)
168-
if not attributes:
169-
# The user can return None to indicate that they don't want to log anything.
170-
# We don't document it, but returning `False`, `{}` etc. seems like it should also work.
171-
return result
172-
173-
# request_attributes_mapper may have removed the errors, so we need .get() here.
174-
level: Literal['error', 'debug'] = 'error' if attributes.get('errors') else 'debug'
175-
176-
# Add a few basic attributes about the request, particularly so that the user can group logs by endpoint.
177-
# Usually this will all be inside a span added by FastAPIInstrumentor with more detailed attributes.
178-
# We only add these attributes after the request_attributes_mapper so that the user
179-
# doesn't rely on what we add here - they can use `request` instead.
180-
if isinstance(request, Request): # pragma: no branch
181-
attributes[SpanAttributes.HTTP_METHOD] = request.method
182-
route: APIRoute | APIWebSocketRoute | None = request.scope.get('route')
183-
if route: # pragma: no branch
184-
attributes.update(
185-
{
186-
SpanAttributes.HTTP_ROUTE: route.path,
187-
'fastapi.route.name': route.name,
188-
}
189-
)
190-
if isinstance(route, APIRoute): # pragma: no branch
191-
attributes['fastapi.route.operation_id'] = route.operation_id
192-
193-
self.logfire_instance.log(level, 'FastAPI arguments', attributes=attributes)
146+
excluded = self.excluded_urls_list.url_disabled(url)
194147
except Exception: # pragma: no cover
195-
self.logfire_instance.exception('Error logging FastAPI arguments')
148+
excluded = False
149+
self.logfire_instance.exception('Error checking if URL is excluded from instrumentation')
150+
151+
if excluded:
152+
return await original # pragma: no cover
153+
154+
with self.logfire_instance.span('FastAPI arguments') as span:
155+
result = await original
156+
157+
try:
158+
attributes: dict[str, Any] | None = {
159+
# Shallow copy these so that the user can safely modify them, but we don't tell them that.
160+
# We do explicitly tell them that the contents should not be modified.
161+
# Making a deep copy could be very expensive and maybe even impossible.
162+
'values': {
163+
k: v
164+
for k, v in result[0].items()
165+
if not isinstance(v, (Request, WebSocket, BackgroundTasks, SecurityScopes, Response))
166+
},
167+
'errors': result[1].copy(),
168+
}
169+
170+
# Set the current app on `values` so that `patched_run_endpoint_function` can check it.
171+
if isinstance(request, Request): # pragma: no branch
172+
instrumented_values = _InstrumentedValues(result[0])
173+
instrumented_values.request = request
174+
result = (instrumented_values, *result[1:])
175+
176+
attributes = self.request_attributes_mapper(request, attributes)
177+
if not attributes:
178+
# The user can return None to indicate that they don't want to log anything.
179+
# We don't document it, but returning `False`, `{}` etc. seems like it should also work.
180+
# We can't drop the span since it's already been created,
181+
# but we can set the level to debug so that it's hidden by default.
182+
span.set_level('debug')
183+
return result
184+
185+
# request_attributes_mapper may have removed the errors, so we need .get() here.
186+
if attributes.get('errors'):
187+
span.set_level('error')
188+
189+
# Add a few basic attributes about the request, particularly so that the user can group logs by endpoint.
190+
# Usually this will all be inside a span added by FastAPIInstrumentor with more detailed attributes.
191+
# We only add these attributes after the request_attributes_mapper so that the user
192+
# doesn't rely on what we add here - they can use `request` instead.
193+
if isinstance(request, Request): # pragma: no branch
194+
attributes[SpanAttributes.HTTP_METHOD] = request.method
195+
route: APIRoute | APIWebSocketRoute | None = request.scope.get('route')
196+
if route: # pragma: no branch
197+
attributes.update(
198+
{
199+
SpanAttributes.HTTP_ROUTE: route.path,
200+
'fastapi.route.name': route.name,
201+
}
202+
)
203+
if isinstance(route, APIRoute): # pragma: no branch
204+
attributes['fastapi.route.operation_id'] = route.operation_id
205+
206+
span.set_attributes(attributes)
207+
except Exception as e: # pragma: no cover
208+
span.record_exception(e)
196209

197210
return result
198211

0 commit comments

Comments
 (0)