attstore / main.py
dn-scribe's picture
feat: move to production gunicorn server
d0edcb4
Raw History Blame Contribute Delete
3.94 kB
import logging
import os
import threading
import time
from sqlalchemy import inspect
from sqlalchemy.engine.url import make_url
from app import app, db
from config import PORT
from werkzeug.serving import WSGIRequestHandler
from logging_utils import cleanup_old_logs
# --- Alembic migration runner ---
from alembic import command
from alembic.config import Config
# On startup, run Alembic migrations to ensure DB schema is up to date.
# To generate a migration after changing models:
# 1. Run: alembic -c migrations/alembic.ini revision --autogenerate -m "Describe your change". To upstream: alembic -c migrations/alembic.ini upgrade head
# 2. Review the migration file in migrations/versions/
# 3. (Optional) Edit for correctness
# 4. Migrations will be applied automatically on next startup
def run_migrations():
alembic_cfg = Config("migrations/alembic.ini")
with app.app_context():
command.upgrade(alembic_cfg, "head")
def ensure_sqlite_database():
"""Bootstrap SQLite DB files before Alembic runs."""
db_uri = app.config.get("SQLALCHEMY_DATABASE_URI", "")
if not db_uri.startswith("sqlite"):
return
url = make_url(db_uri)
database = url.database
if not database:
return
if os.path.isabs(database):
db_path = database
else:
db_path = os.path.join(app.instance_path, database)
os.makedirs(os.path.dirname(db_path), exist_ok=True)
bootstrap_reason = None
if not os.path.exists(db_path):
bootstrap_reason = "file missing"
else:
with app.app_context():
inspector = inspect(db.engine)
tables = inspector.get_table_names()
if 'users' not in tables:
bootstrap_reason = "schema missing"
if not bootstrap_reason:
logging.info(f"SQLite database already initialized at {db_path}")
return
logging.info(f"Bootstrapping SQLite database at {db_path} ({bootstrap_reason})")
with app.app_context():
db.create_all()
logging.info("SQLite database initialization complete")
def schedule_log_cleanup():
"""
Background thread that runs log cleanup daily.
Cleans up logs older than LOG_RETENTION_DAYS (default: 10 days).
"""
def cleanup_thread():
while True:
try:
# Wait 24 hours between cleanups
time.sleep(24 * 60 * 60)
with app.app_context():
deleted = cleanup_old_logs()
if deleted > 0:
logging.info(f"Scheduled cleanup: removed {deleted} old log entries")
except Exception as e:
logging.error(f"Error in scheduled log cleanup: {e}")
# Start cleanup thread as daemon so it exits when main thread exits
thread = threading.Thread(target=cleanup_thread, daemon=True)
thread.start()
logging.info("Log cleanup scheduler started (runs daily)")
# Filter out specific paths from Werkzeug logs
class FilteredWSGIRequestHandler(WSGIRequestHandler):
def log(self, type, message, *args):
# Skip logging for Chrome DevTools discovery requests
if '.well-known/appspecific/com.chrome.devtools.json' in message:
return
return super().log(type, message, *args)
if __name__ == '__main__':
from config import create_default_admin_user
ensure_sqlite_database()
run_migrations() # Always ensure DB schema is up to date before serving
with app.app_context():
create_default_admin_user(app)
# Run initial cleanup on startup
cleanup_old_logs()
# Start background log cleanup scheduler
schedule_log_cleanup()
# Use filtered request handler
debug_mode = os.environ.get('FLASK_DEBUG', 'false').lower() in ('true', '1', 'yes')
app.run(host='0.0.0.0', port=PORT, debug=debug_mode, request_handler=FilteredWSGIRequestHandler)