| """ |
| MongoDB Connection & Collections (Motor async driver) |
| |
| Collections: |
| - firewall_logs: Per-request classification results |
| - api_keys: API key management |
| - daily_stats: Pre-aggregated daily statistics |
| """ |
|
|
| import logging |
| from typing import Optional |
| from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase |
|
|
| logger = logging.getLogger("llm_firewall.db.mongo") |
|
|
| _client: Optional[AsyncIOMotorClient] = None |
| _db: Optional[AsyncIOMotorDatabase] = None |
|
|
|
|
| async def connect(uri: str, db_name: str = "llm_firewall") -> AsyncIOMotorDatabase: |
| """Connect to MongoDB and return the database instance.""" |
| global _client, _db |
| try: |
| _client = AsyncIOMotorClient(uri, serverSelectionTimeoutMS=5000) |
| |
| await _client.admin.command("ping") |
| _db = _client[db_name] |
| |
| |
| await _create_indexes(_db) |
| |
| logger.info(f"Connected to MongoDB: {db_name}") |
| return _db |
| except Exception as e: |
| logger.error(f"MongoDB connection failed: {e}") |
| raise |
|
|
|
|
| async def _create_indexes(db: AsyncIOMotorDatabase) -> None: |
| """Create necessary indexes for performance.""" |
| |
| users = db.users |
| await users.create_index("email", unique=True) |
|
|
| |
| logs = db.firewall_logs |
| await logs.create_index("request_id", unique=True) |
| await logs.create_index("timestamp") |
| await logs.create_index("api_key_id") |
| await logs.create_index("user_id") |
| await logs.create_index("session_id") |
| await logs.create_index("client_ip") |
| await logs.create_index("safe") |
| await logs.create_index("attack_type") |
| await logs.create_index("provider") |
| await logs.create_index([("timestamp", -1), ("safe", 1)]) |
|
|
| |
| keys = db.api_keys |
| await keys.create_index("key_hash", unique=True) |
| await keys.create_index("is_active") |
| await keys.create_index("user_id") |
|
|
| |
| stats = db.daily_stats |
| await stats.create_index([("api_key_id", 1), ("date", 1)], unique=True) |
|
|
| logger.info("MongoDB indexes created") |
|
|
|
|
| def get_db() -> AsyncIOMotorDatabase: |
| """Get the current database instance.""" |
| if _db is None: |
| raise RuntimeError("MongoDB not connected. Call connect() first.") |
| return _db |
|
|
|
|
| def get_users_collection(): |
| return get_db().users |
|
|
|
|
| def get_logs_collection(): |
| return get_db().firewall_logs |
|
|
|
|
| def get_keys_collection(): |
| return get_db().api_keys |
|
|
|
|
| def get_stats_collection(): |
| return get_db().daily_stats |
|
|
|
|
| async def disconnect() -> None: |
| """Close the MongoDB connection.""" |
| global _client, _db |
| if _client: |
| _client.close() |
| _client = None |
| _db = None |
| logger.info("MongoDB disconnected") |
|
|
|
|
| async def is_connected() -> bool: |
| """Check if MongoDB is reachable.""" |
| if _client is None: |
| return False |
| try: |
| await _client.admin.command("ping") |
| return True |
| except Exception: |
| return False |
|
|