|
7 | 7 |
|
8 | 8 | from __future__ import annotations |
9 | 9 |
|
10 | | -import asyncio |
11 | 10 | import re |
12 | 11 | from abc import ABC, abstractmethod |
13 | | -from datetime import datetime, timezone |
14 | 12 | from time import perf_counter |
15 | 13 | from typing import Any, Awaitable, Callable, MutableMapping, Optional, Set |
16 | 14 | from uuid import uuid4 |
17 | 15 |
|
18 | | -from fideslog.sdk.python.event import AnalyticsEvent |
19 | 16 | from loguru import logger |
20 | 17 | from pyinstrument import Profiler |
21 | 18 | from starlette.requests import Request |
22 | 19 |
|
23 | | -from fides.api.analytics import ( |
24 | | - accessed_through_local_host, |
25 | | - in_docker_container, |
26 | | - send_analytics_event, |
27 | | -) |
28 | 20 | from fides.api.middleware import handle_audit_log_resource |
29 | 21 | from fides.api.request_context import set_request_id |
30 | | -from fides.api.schemas.analytics import Event, ExtraData |
31 | 22 | from fides.api.util.endpoint_utils import API_PREFIX |
32 | | -from fides.api.util.logger import _log_exception |
33 | 23 | from fides.config import CONFIG |
34 | 24 |
|
35 | 25 | # Type aliases for ASGI |
@@ -228,107 +218,6 @@ async def handle_http(self, scope: Scope, receive: Receive, send: Send) -> None: |
228 | 218 | ).info("Request received") |
229 | 219 |
|
230 | 220 |
|
231 | | -class AnalyticsLoggingMiddleware(BaseASGIMiddleware): |
232 | | - """ |
233 | | - Pure ASGI middleware that logs analytics events for each call to Fides endpoints. |
234 | | -
|
235 | | - Only logs for API endpoints (paths starting with API_PREFIX) and skips /health endpoints. |
236 | | - """ |
237 | | - |
238 | | - # Class-level set to hold references to pending tasks, preventing garbage collection |
239 | | - # before completion. Tasks remove themselves from this set when done. |
240 | | - _pending_tasks: set[asyncio.Task] = set() |
241 | | - |
242 | | - def __init__(self, app: ASGIApp, api_prefix: str = API_PREFIX) -> None: |
243 | | - super().__init__(app) |
244 | | - self.api_prefix = api_prefix |
245 | | - |
246 | | - async def handle_http(self, scope: Scope, receive: Receive, send: Send) -> None: |
247 | | - path = self.get_path(scope) |
248 | | - |
249 | | - # Skip non-API endpoints and health endpoints |
250 | | - if not path.startswith(self.api_prefix) or path.endswith("/health"): |
251 | | - await self.app(scope, receive, send) |
252 | | - return |
253 | | - |
254 | | - method = self.get_method(scope) |
255 | | - fides_source: Optional[str] = self.get_header(scope, b"x-fides-source") or None |
256 | | - hostname = self.get_host(scope) |
257 | | - full_url = self.build_url(scope) |
258 | | - |
259 | | - now = datetime.now(tz=timezone.utc) |
260 | | - endpoint = f"{method}: {full_url}" |
261 | | - |
262 | | - # Capture status and detect HTTP errors |
263 | | - captured: dict[str, Any] = {"status": 500, "error_class": None} |
264 | | - |
265 | | - async def send_wrapper(message: Message) -> None: |
266 | | - if message["type"] == "http.response.start": |
267 | | - status_code: int = message.get("status", 500) |
268 | | - captured["status"] = status_code |
269 | | - if status_code >= 400: |
270 | | - captured["error_class"] = "HTTPException" |
271 | | - await send(message) |
272 | | - |
273 | | - try: |
274 | | - await self.app(scope, receive, send_wrapper) |
275 | | - except Exception as e: |
276 | | - captured["status"] = 500 |
277 | | - captured["error_class"] = e.__class__.__name__ |
278 | | - _log_exception(e, CONFIG.dev_mode) |
279 | | - raise |
280 | | - finally: |
281 | | - # Schedule analytics logging as a background task. |
282 | | - # Store task reference to prevent garbage collection before completion. |
283 | | - task = asyncio.create_task( |
284 | | - self._log_analytics( |
285 | | - endpoint, |
286 | | - hostname, |
287 | | - captured["status"], |
288 | | - now, |
289 | | - fides_source, |
290 | | - captured["error_class"], |
291 | | - ) |
292 | | - ) |
293 | | - self._pending_tasks.add(task) |
294 | | - # Task will be passed as argument to discard when it completes |
295 | | - task.add_done_callback(self._pending_tasks.discard) |
296 | | - |
297 | | - async def _log_analytics( |
298 | | - self, |
299 | | - endpoint: str, |
300 | | - hostname: Optional[str], |
301 | | - status_code: int, |
302 | | - event_created_at: datetime, |
303 | | - fides_source: Optional[str], |
304 | | - error_class: Optional[str], |
305 | | - ) -> None: |
306 | | - """Log analytics event if not opted out.""" |
307 | | - if CONFIG.user.analytics_opt_out: |
308 | | - return |
309 | | - |
310 | | - try: |
311 | | - await send_analytics_event( |
312 | | - AnalyticsEvent( |
313 | | - docker=in_docker_container(), |
314 | | - event=Event.endpoint_call.value, |
315 | | - event_created_at=event_created_at, |
316 | | - local_host=accessed_through_local_host(hostname), |
317 | | - endpoint=endpoint, |
318 | | - status_code=status_code, |
319 | | - error=error_class or None, |
320 | | - extra_data=( |
321 | | - {ExtraData.fides_source.value: fides_source} |
322 | | - if fides_source |
323 | | - else None |
324 | | - ), |
325 | | - ) |
326 | | - ) |
327 | | - except Exception: |
328 | | - # Analytics should never break the request |
329 | | - pass |
330 | | - |
331 | | - |
332 | 221 | class AuditLogMiddleware(BaseASGIMiddleware): |
333 | 222 | """ |
334 | 223 | Pure ASGI middleware that logs audit information for non-GET requests. |
|
0 commit comments