T10: Monitoring, Performance, Documentation & Environment Config — 38 tests, ruff clean, docs OK
- Extended health endpoint: DB+Redis+Storage+Worker checks with degraded status - Prometheus metrics endpoint: admin-only, text/plain format - Metrics: http_requests_total, db_pool_connections, arq_jobs_total - Structured JSON logging (structlog): timestamp, level, method, path, status, duration_ms, tenant_id - Performance: page_size max 100 enforced (422), streaming CSV export (StreamingResponse) - Scripts: seed_perf_data.py, check_indexes.py - Docs: admin-guide.md (Deploy, Backup, Restore, Env-Vars, Troubleshooting), api-overview.md - README updated: prod setup, API section, env profiles, admin-guide link - .env.example: added SECRET_KEY, STORAGE_PATH, SMTP_* vars - 38 new tests, full regression 564/564 pass (0 failures) - Ruff: all checks passed
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Seed performance test data — creates N contacts in the test database.
|
||||
|
||||
Usage:
|
||||
python scripts/seed_perf_data.py --count 200000
|
||||
python scripts/seed_perf_data.py --count 5000 --tenant-id <uuid>
|
||||
|
||||
Requires DATABASE_URL environment variable or .env file.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
|
||||
# Add project root to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
|
||||
from app.config import get_settings
|
||||
from app.models.contact import Contact
|
||||
from app.models.tenant import Tenant
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
async def seed_contacts(count: int, tenant_id_str: str | None = None) -> None:
|
||||
"""Seed `count` contacts into the database for performance testing."""
|
||||
settings = get_settings()
|
||||
engine = create_async_engine(
|
||||
settings.database_url,
|
||||
pool_size=settings.db_pool_size,
|
||||
max_overflow=settings.db_max_overflow,
|
||||
)
|
||||
session_factory = async_sessionmaker(bind=engine, expire_on_commit=False)
|
||||
|
||||
async with session_factory() as session:
|
||||
# Get or create a tenant
|
||||
if tenant_id_str:
|
||||
tenant_id = uuid.UUID(tenant_id_str)
|
||||
result = await session.execute(select(Tenant).where(Tenant.id == tenant_id))
|
||||
tenant = result.scalar_one_or_none()
|
||||
if not tenant:
|
||||
print(f"ERROR: Tenant {tenant_id_str} not found")
|
||||
return
|
||||
else:
|
||||
# Get first tenant or create one
|
||||
result = await session.execute(select(Tenant).limit(1))
|
||||
tenant = result.scalar_one_or_none()
|
||||
if not tenant:
|
||||
tenant = Tenant(name="Perf Test Tenant", slug="perf-test")
|
||||
session.add(tenant)
|
||||
await session.flush()
|
||||
|
||||
# Get or create a user for this tenant
|
||||
result = await session.execute(
|
||||
select(User).where(User.tenant_id == tenant.id).limit(1)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
if not user:
|
||||
from app.core.auth import hash_password
|
||||
|
||||
user = User(
|
||||
tenant_id=tenant.id,
|
||||
email="perf@test.local",
|
||||
name="Perf Test User",
|
||||
password_hash=hash_password("TestPass123!"),
|
||||
role="admin",
|
||||
is_active=True,
|
||||
preferences={},
|
||||
)
|
||||
session.add(user)
|
||||
await session.flush()
|
||||
|
||||
tenant_id = tenant.id
|
||||
user_id = user.id
|
||||
|
||||
# Seed contacts in batches
|
||||
batch_size = 1000
|
||||
total_seeded = 0
|
||||
first_names = ["Hans", "Maria", "Peter", "Anna", "Klaus", "Ursula", "Werner", "Brigitte", "Jürgen", "Helga"]
|
||||
last_names = ["Müller", "Schmidt", "Schneider", "Fischer", "Weber", "Meyer", "Wagner", "Becker", "Hoffmann", "Schäfer"]
|
||||
|
||||
while total_seeded < count:
|
||||
batch = []
|
||||
batch_count = min(batch_size, count - total_seeded)
|
||||
for i in range(batch_count):
|
||||
idx = total_seeded + i
|
||||
fn = first_names[idx % len(first_names)]
|
||||
ln = last_names[idx % len(last_names)]
|
||||
batch.append(Contact(
|
||||
tenant_id=tenant_id,
|
||||
first_name=f"{fn}-{idx}",
|
||||
last_name=f"{ln}-{idx}",
|
||||
email=f"{fn.lower()}.{ln.lower()}-{idx}@example.com",
|
||||
phone=f"+49-555-{idx:06d}",
|
||||
created_by=user_id,
|
||||
updated_by=user_id,
|
||||
))
|
||||
session.add_all(batch)
|
||||
await session.commit()
|
||||
total_seeded += batch_count
|
||||
if total_seeded % 10000 == 0 or total_seeded == count:
|
||||
print(f"Seeded {total_seeded}/{count} contacts...")
|
||||
|
||||
await engine.dispose()
|
||||
print(f"Done: {total_seeded} contacts seeded.")
|
||||
|
||||
|
||||
def main():
|
||||
"""Parse arguments and run the seeding script."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Seed performance test data into the LeoCRM database."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--count",
|
||||
type=int,
|
||||
default=200000,
|
||||
help="Number of contacts to create (default: 200000)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tenant-id",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Tenant UUID to seed data into (default: first available tenant)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"Starting seed: {args.count} contacts")
|
||||
asyncio.run(seed_contacts(args.count, args.tenant_id))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user