diff --git a/config.py b/config.py new file mode 100644 index 0000000..7bb3682 --- /dev/null +++ b/config.py @@ -0,0 +1,26 @@ +"""Application configuration.""" +import os +from pathlib import Path + +# Project root (parent of ``kernel_ai`` package) +PROJECT_ROOT = Path(__file__).resolve().parent.parent + + +class Config: + """Flask config loaded via app.config.from_object(Config).""" + + DEBUG = os.getenv("FLASK_DEBUG", "False").lower() == "true" + ENV = os.getenv("FLASK_ENV", "production" if not DEBUG else "development") + STATIC_FOLDER = "static" + TEMPLATES_FOLDER = "templates" + API_PREFIX = "/api" + SEND_FILE_MAX_AGE_DEFAULT = 0 if not DEBUG else 31536000 + PROJECT_ROOT = PROJECT_ROOT + LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO") + LOG_FORMAT = os.getenv("LOG_FORMAT", "json") + LOG_SERVICE_NAME = os.getenv("LOG_SERVICE_NAME", "kernel-ai-backend") + SENTRY_DSN = os.getenv("SENTRY_DSN", "").strip() + SENTRY_ENVIRONMENT = os.getenv("SENTRY_ENVIRONMENT", ENV) + SENTRY_RELEASE = os.getenv("SENTRY_RELEASE", "").strip() or None + SENTRY_SEND_DEFAULT_PII = os.getenv("SENTRY_SEND_DEFAULT_PII", "false").lower() == "true" + SENTRY_TRACES_SAMPLE_RATE = float(os.getenv("SENTRY_TRACES_SAMPLE_RATE", "0.0")) diff --git a/sentry_setup.py b/sentry_setup.py new file mode 100644 index 0000000..9fef07c --- /dev/null +++ b/sentry_setup.py @@ -0,0 +1,36 @@ +"""Sentry initialization helpers for Flask app.""" + +from __future__ import annotations + +import logging + +logger = logging.getLogger(__name__) + + +def init_sentry(app) -> bool: + """Initialize Sentry from app config. + + Returns True when initialization succeeds and False otherwise. + """ + dsn = str(app.config.get("SENTRY_DSN", "") or "").strip() + if not dsn: + logger.info("Sentry disabled: SENTRY_DSN is empty") + return False + + try: + import sentry_sdk + from sentry_sdk.integrations.flask import FlaskIntegration + except Exception as exc: # pragma: no cover - import failure is deployment issue + logger.warning("Sentry SDK not available: %s", exc) + return False + + sentry_sdk.init( + dsn=dsn, + integrations=[FlaskIntegration()], + environment=app.config.get("SENTRY_ENVIRONMENT"), + release=app.config.get("SENTRY_RELEASE"), + send_default_pii=bool(app.config.get("SENTRY_SEND_DEFAULT_PII", False)), + traces_sample_rate=float(app.config.get("SENTRY_TRACES_SAMPLE_RATE", 0.0)), + ) + logger.info("Sentry initialized") + return True diff --git a/webapp.py b/webapp.py new file mode 100644 index 0000000..0070543 --- /dev/null +++ b/webapp.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +""" +Linux Kernel Visualization Backend +Thin Flask bootstrap module. +""" + +import os +import logging +from flask import Flask, jsonify + +from kernel_ai.config import Config +from kernel_ai.hooks import register_hooks +from kernel_ai.http.common import build_error_payload +from kernel_ai.http.register import register_http_routes +from kernel_ai.logging_helpers import log_event +from kernel_ai.logging_setup import configure_logging +from kernel_ai.prometheus_setup import init_prometheus +from kernel_ai.sentry_setup import init_sentry +from kernel_ai.state import attach_state_container + +logger = logging.getLogger(__name__) + +def _ensure_prometheus_mpdir(): + # Gunicorn -w N: set PROMETHEUS_MULTIPROC_DIR before workers import this module. + if os.environ.get("PROMETHEUS_MULTIPROC_DIR", "").strip(): + _prom_mpdir = os.environ["PROMETHEUS_MULTIPROC_DIR"].strip() + os.makedirs(_prom_mpdir, exist_ok=True) + + +def _register_error_handlers(app): + @app.errorhandler(404) + def not_found(error): + return jsonify(build_error_payload("Not found", "not_found")), 404 + + @app.errorhandler(500) + def internal_error(error): + return jsonify(build_error_payload("Internal server error", "internal_error")), 500 + + +def create_app(): + """Application factory that builds and configures a Flask instance.""" + _ensure_prometheus_mpdir() + root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + app = Flask( + __name__, + static_folder=os.path.join(root, "static"), + template_folder=os.path.join(root, "templates"), + ) + app.config.from_object(Config) + configure_logging(app) + sentry_enabled = init_sentry(app) + log_event( + logger, + "INFO", + "app_startup", + event_dataset="kernel_ai.app", + component="webapp", + operation="create_app", + event_data={ + "service.environment": app.config.get("ENV", "production"), + "debug": bool(app.config.get("DEBUG", False)), + "api_prefix": app.config.get("API_PREFIX", "/api"), + "prometheus_multiproc_enabled": bool( + os.environ.get("PROMETHEUS_MULTIPROC_DIR", "").strip() + ), + "log_level": app.config.get("LOG_LEVEL"), + "log_format": app.config.get("LOG_FORMAT"), + "sentry_enabled": sentry_enabled, + }, + ) + attach_state_container(app) + init_prometheus(app) + register_hooks(app) + register_http_routes(app) + _register_error_handlers(app) + return app + + +# Keep module-level app for Gunicorn and backward-compatible imports. +app = create_app()