Files
taylanbakircioglu d7ca50b387 feat: Flowfish - eBPF-powered multi-cluster Kubernetes observability platform
Multi-cluster dependency mapping, real-time network monitoring,
impact analysis, and CI/CD integration capabilities.

Made-with: Cursor
2026-03-29 21:43:20 +03:00

183 lines
6.5 KiB
Python

"""
PostgreSQL database connection and session management
"""
import asyncpg
from sqlalchemy import create_engine, MetaData, text
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.pool import NullPool
import structlog
from config import settings
logger = structlog.get_logger()
# Convert postgres:// to postgresql:// for async
async_database_url = settings.DATABASE_URL.replace('postgresql://', 'postgresql+asyncpg://')
# Async SQLAlchemy engine (SQLAlchemy 2.0)
async_engine = create_async_engine(
async_database_url,
pool_size=settings.DATABASE_POOL_SIZE,
max_overflow=20,
pool_timeout=settings.DATABASE_POOL_TIMEOUT,
pool_recycle=3600,
echo=settings.DEBUG
)
# Sync engine for migrations
sync_engine = create_engine(
settings.DATABASE_URL,
pool_size=5,
echo=settings.DEBUG
)
# Session for sync operations (migrations, etc.)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=sync_engine)
# Async session factory
async_session = AsyncSession(async_engine)
# Base class for models
Base = declarative_base()
metadata = MetaData()
# Simple database operations
class DatabaseService:
@staticmethod
async def execute(query: str, values: dict = None):
"""Execute raw SQL query"""
async with AsyncSession(async_engine) as session:
result = await session.execute(text(query), values or {})
await session.commit()
return result
@staticmethod
async def fetch_one(query: str, values: dict = None):
"""Fetch single row - commits if query modifies data (INSERT/UPDATE/DELETE with RETURNING)"""
async with AsyncSession(async_engine) as session:
result = await session.execute(text(query), values or {})
row = result.fetchone()
# Commit for write operations (INSERT/UPDATE/DELETE with RETURNING)
await session.commit()
if row:
return dict(row._mapping)
return None
@staticmethod
async def fetch_all(query: str, values: dict = None):
"""Fetch all rows - commits if query modifies data"""
async with AsyncSession(async_engine) as session:
result = await session.execute(text(query), values or {})
rows = result.fetchall()
# Commit for write operations
await session.commit()
return [dict(row._mapping) for row in rows]
# Create database instance
database = DatabaseService()
def get_db_session():
"""Get sync database session for migrations/admin tasks"""
db = SessionLocal()
try:
return db
finally:
db.close()
# Connection test function
async def test_connection():
"""Test PostgreSQL connection"""
try:
result = await database.fetch_one("SELECT 1 as test")
logger.info("PostgreSQL connection test successful")
# Run auto-migrations for new tables
await run_auto_migrations()
return True
except Exception as e:
logger.error("PostgreSQL connection test failed", error=str(e))
return False
async def run_auto_migrations():
"""Run auto-migrations to ensure required tables exist"""
try:
# Check and create api_keys table if not exists
await create_api_keys_table_if_not_exists()
logger.info("Auto-migrations completed successfully")
except Exception as e:
import traceback
logger.error("Auto-migration FAILED", error=str(e), traceback=traceback.format_exc())
# Don't raise - let the app continue but log the error prominently
async def create_api_keys_table_if_not_exists():
"""Create api_keys table if it doesn't exist"""
try:
# Check if table exists
check_query = """
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'api_keys'
);
"""
result = await database.fetch_one(check_query)
if result and result.get('exists'):
logger.debug("api_keys table already exists, checking for schema updates")
# Fix key_prefix column size if needed (was VARCHAR(10), should be VARCHAR(12))
try:
await database.execute(
"ALTER TABLE api_keys ALTER COLUMN key_prefix TYPE VARCHAR(12)"
)
logger.info("Updated key_prefix column to VARCHAR(12)")
except Exception:
pass # Column might already be correct size
return
# Create the table (asyncpg requires separate statements)
create_table_query = """
CREATE TABLE IF NOT EXISTS api_keys (
id SERIAL PRIMARY KEY,
key_id VARCHAR(50) UNIQUE NOT NULL,
key_hash VARCHAR(255) NOT NULL,
key_prefix VARCHAR(12) NOT NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
user_id INTEGER NOT NULL REFERENCES users(id),
scopes TEXT[] DEFAULT ARRAY['blast-radius'],
cluster_ids INTEGER[],
is_active BOOLEAN DEFAULT TRUE,
expires_at TIMESTAMP,
last_used_at TIMESTAMP,
last_used_ip VARCHAR(45),
usage_count INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
revoked_at TIMESTAMP,
revoked_by INTEGER REFERENCES users(id),
revoke_reason TEXT,
metadata JSONB DEFAULT '{}'::jsonb
)
"""
await database.execute(create_table_query)
# Create indexes separately (asyncpg cannot execute multiple statements at once)
index_queries = [
"CREATE INDEX IF NOT EXISTS idx_api_keys_key_id ON api_keys(key_id)",
"CREATE INDEX IF NOT EXISTS idx_api_keys_key_hash ON api_keys(key_hash)",
"CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id)",
"CREATE INDEX IF NOT EXISTS idx_api_keys_is_active ON api_keys(is_active)",
]
for index_query in index_queries:
await database.execute(index_query)
logger.info("api_keys table created successfully")
except Exception as e:
import traceback
logger.error("Failed to create api_keys table", error=str(e), traceback=traceback.format_exc())