|
3 | 3 | import inspect |
4 | 4 | from contextlib import contextmanager |
5 | 5 | 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 |
7 | 7 | from weakref import WeakKeyDictionary |
8 | 8 |
|
9 | 9 | import fastapi.routing |
@@ -88,11 +88,11 @@ def patch_fastapi(): |
88 | 88 | registry: WeakKeyDictionary[FastAPI, FastAPIInstrumentation] = WeakKeyDictionary() |
89 | 89 |
|
90 | 90 | 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) |
92 | 92 | if instrumentation := registry.get(request.app): |
93 | | - return await instrumentation.solve_dependencies(request, result) |
| 93 | + return await instrumentation.solve_dependencies(request, original) |
94 | 94 | else: |
95 | | - return result # pragma: no cover |
| 95 | + return await original # pragma: no cover |
96 | 96 |
|
97 | 97 | # `solve_dependencies` is actually defined in `fastapi.dependencies.utils`, |
98 | 98 | # but it's imported into `fastapi.routing`, which is where we need to patch it. |
@@ -139,60 +139,73 @@ def __init__( |
139 | 139 | self.excluded_urls_list = parse_excluded_urls(excluded_urls) # pragma: no cover |
140 | 140 |
|
141 | 141 | 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]] |
143 | 143 | ): |
144 | 144 | try: |
145 | 145 | 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) |
194 | 147 | 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) |
196 | 209 |
|
197 | 210 | return result |
198 | 211 |
|
|
0 commit comments