diff --git a/config.example.yaml b/config.example.yaml index 8e3ad91..84ca611 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -11,16 +11,10 @@ openviking: # OpenViking HTTP server. The api_key must match server.root_api_key in ov.conf. url: "http://127.0.0.1:1933" api_key: "" - timeout: 30 - verify_ssl: true everos: # EverOS EverCore HTTP server. url: "http://127.0.0.1:1995" - api_key: "" - timeout: 180 - verify_ssl: true - health_path: "/health" storage: sqlite_path: "./memory_system_api.sqlite3" diff --git a/memory_system_api/config.py b/memory_system_api/config.py index 8976863..c20367a 100644 --- a/memory_system_api/config.py +++ b/memory_system_api/config.py @@ -36,6 +36,7 @@ class StorageConfig(BaseModel): class LoggingConfig(BaseModel): level: str = "INFO" + format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" class Config(BaseModel): diff --git a/memory_system_api/server.py b/memory_system_api/server.py index eadd6c8..f390da3 100644 --- a/memory_system_api/server.py +++ b/memory_system_api/server.py @@ -1,13 +1,17 @@ """Standalone FastAPI server for Memory System API.""" from __future__ import annotations -from fastapi import FastAPI +import logging + +from fastapi import FastAPI, Request, Response from fastapi.middleware.cors import CORSMiddleware from .api import router from .config import Config, load_config, set_config +request_logger = logging.getLogger("memory_system_api.requests") + app = FastAPI(title="Memory System API", version="0.1.0") app.add_middleware( CORSMiddleware, @@ -16,6 +20,42 @@ app.add_middleware( allow_methods=["*"], allow_headers=["*"], ) + + +@app.middleware("http") +async def log_request_and_response(request: Request, call_next): + request_body = await request.body() + request_logger.info( + "request %s %s body=%s", + request.method, + _path_with_query(request), + _body_for_log(request_body), + ) + + async def receive(): + return {"type": "http.request", "body": request_body, "more_body": False} + + response = await call_next(Request(request.scope, receive)) + response_body = b"" + async for chunk in response.body_iterator: + response_body += chunk + + request_logger.info( + "response %s %s status=%s body=%s", + request.method, + _path_with_query(request), + response.status_code, + _body_for_log(response_body), + ) + return Response( + content=response_body, + status_code=response.status_code, + headers=dict(response.headers), + media_type=response.media_type, + background=response.background, + ) + + app.include_router(router) @@ -25,6 +65,17 @@ def create_app(config: Config | None = None) -> FastAPI: return app +def _path_with_query(request: Request) -> str: + query = request.url.query + return f"{request.url.path}?{query}" if query else request.url.path + + +def _body_for_log(body: bytes) -> str: + if not body: + return "" + return body.decode("utf-8", errors="replace") + + def main() -> None: import argparse import uvicorn @@ -41,6 +92,7 @@ def main() -> None: if args.port: config.server.port = args.port set_config(config) + logging.basicConfig(level=config.logging.level.upper(), format=config.logging.format) uvicorn.run(app, host=config.server.host, port=config.server.port, log_level=config.logging.level.lower()) diff --git a/tests/test_memory_system_server.py b/tests/test_memory_system_server.py index 1de2ace..1af88ff 100644 --- a/tests/test_memory_system_server.py +++ b/tests/test_memory_system_server.py @@ -1,3 +1,8 @@ +import logging + +from fastapi.testclient import TestClient + + def test_memory_system_server_exposes_routes(): from memory_system_api.server import app @@ -60,3 +65,29 @@ def test_memory_system_messages_does_not_require_account_key_header(): route = next(route for route in app.routes if getattr(route, "path", "") == "/memory-system/messages") assert all(getattr(dependency.call, "__name__", "") != "account_key_header" for dependency in route.dependant.dependencies) + + +def test_memory_system_logs_request_and_response_bodies(caplog): + from memory_system_api.api import get_service + from memory_system_api.server import app + + class FakeService: + async def create_user(self, user_id: str): + return {"status": "success", "account": {"user_id": user_id}} + + app.dependency_overrides[get_service] = lambda: FakeService() + + try: + with caplog.at_level(logging.INFO, logger="memory_system_api.requests"): + response = TestClient(app).post("/memory-system/users", json={"user_id": "userA"}) + finally: + app.dependency_overrides.clear() + + assert response.status_code == 200 + assert response.json() == {"status": "success", "account": {"user_id": "userA"}} + assert any("request POST /memory-system/users body={\"user_id\":\"userA\"}" in record.message for record in caplog.records) + assert any( + "response POST /memory-system/users status=200 body={\"status\":\"success\",\"account\":{\"user_id\":\"userA\"}}" + in record.message + for record in caplog.records + )