Files
tabbyAPI/common/debug_requests.py
2026-05-27 00:33:45 +02:00

116 lines
3.2 KiB
Python

"""Utilities for writing reproducible request debug logs."""
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
import aiofiles
from fastapi import Request
from common.logger import xlogger
from common.tabby_config import config
DEBUG_LOG_DIR = Path("logs/debug")
SENSITIVE_HEADERS = {
"authorization",
"cookie",
"host",
"set-cookie",
"user-agent",
"x-admin-key",
"x-api-key",
}
SENSITIVE_KEYS = {
"admin_key",
"api_key",
"authorization",
"hf_token",
"token",
"x-admin-key",
"x-api-key",
}
def redact_sensitive_values(value: Any) -> Any:
"""Recursively redact auth-like fields from a JSON-compatible object."""
if isinstance(value, dict):
redacted = {}
for key, item in value.items():
if key.lower() in SENSITIVE_KEYS:
redacted[key] = "[REDACTED]"
else:
redacted[key] = redact_sensitive_values(item)
return redacted
if isinstance(value, list):
return [redact_sensitive_values(item) for item in value]
return value
def redact_headers(headers) -> dict:
"""Return request headers with credential-bearing values removed."""
return {
key: "[REDACTED]" if key.lower() in SENSITIVE_HEADERS else value
for key, value in headers.items()
}
async def write_chat_completion_request_log(request: Request, body: dict) -> Path | None:
"""Write a /v1/chat/completions request as a JSON debug artifact."""
timestamp = datetime.now(timezone.utc)
request_id = getattr(request.state, "id", "unknown")
filename = f"{timestamp.strftime('%Y%m%dT%H%M%S.%fZ')}_{request_id}.json"
log_path = DEBUG_LOG_DIR / filename
payload = {
"schema": "tabbyapi.chat_completion_request_log.v1",
"created_at": timestamp.isoformat(),
"request": {
"id": "[REDACTED]",
"method": request.method,
"url": f"[REDACTED]{request.url.path}",
"path": request.url.path,
"query": redact_sensitive_values(dict(request.query_params)),
"headers": redact_headers(request.headers),
"body": redact_sensitive_values(body),
},
}
try:
DEBUG_LOG_DIR.mkdir(parents=True, exist_ok=True)
async with aiofiles.open(log_path, "w", encoding="utf8") as debug_file:
await debug_file.write(json.dumps(payload, indent=2, ensure_ascii=False))
await debug_file.write("\n")
except Exception as exc:
xlogger.error(
"Failed to write chat completion request debug log",
str(exc),
details=f"\n{exc}",
)
return None
xlogger.info(f"Wrote chat completion request debug log: {log_path}")
return log_path
async def log_chat_completion_request(request: Request):
"""FastAPI dependency to log chat completion request bodies when enabled."""
if not config.logging.log_chat_completion_requests:
return
try:
body = await request.json()
except Exception:
body = {"_invalid_json_body": "[omitted: request body was not valid JSON]"}
await write_chat_completion_request_log(request, body)