""" StarPunk package initialization Creates and configures the Flask application """ import logging from logging.handlers import RotatingFileHandler from pathlib import Path from flask import Flask, g import uuid def configure_logging(app): """ Configure application logging with RotatingFileHandler and structured logging Per ADR-054 and developer Q&A Q3: - Uses RotatingFileHandler (10MB files, keep 10) - Supports correlation IDs for request tracking - Uses Flask's app.logger for all logging Args: app: Flask application instance """ log_level = app.config.get("LOG_LEVEL", "INFO").upper() # Set Flask logger level app.logger.setLevel(getattr(logging, log_level, logging.INFO)) # Configure console handler console_handler = logging.StreamHandler() # Configure file handler with rotation (10MB per file, keep 10 files) log_dir = app.config.get("DATA_PATH", Path("./data")) / "logs" log_dir.mkdir(parents=True, exist_ok=True) log_file = log_dir / "starpunk.log" file_handler = RotatingFileHandler( log_file, maxBytes=10 * 1024 * 1024, # 10MB backupCount=10 ) # Format with correlation ID support if log_level == "DEBUG": formatter = logging.Formatter( "[%(asctime)s] %(levelname)s - %(name)s [%(correlation_id)s]: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) # Warn if DEBUG enabled in production if not app.debug and app.config.get("ENV") != "development": app.logger.warning( "=" * 70 + "\n" + "WARNING: DEBUG logging enabled in production!\n" + "This logs detailed HTTP requests/responses.\n" + "Sensitive data is redacted, but consider using INFO level.\n" + "Set LOG_LEVEL=INFO in production for normal operation.\n" + "=" * 70 ) else: formatter = logging.Formatter( "[%(asctime)s] %(levelname)s [%(correlation_id)s]: %(message)s", datefmt="%Y-%m-%d %H:%M:%S" ) console_handler.setFormatter(formatter) file_handler.setFormatter(formatter) # Remove existing handlers and add our configured handlers app.logger.handlers.clear() app.logger.addHandler(console_handler) app.logger.addHandler(file_handler) # Add filter to inject correlation ID # This filter will be added to ALL loggers to ensure consistency class CorrelationIdFilter(logging.Filter): def filter(self, record): # Get correlation ID from Flask's g object, or use fallback # Handle case where we're outside of request context if not hasattr(record, 'correlation_id'): try: from flask import has_request_context if has_request_context(): record.correlation_id = getattr(g, 'correlation_id', 'no-request') else: record.correlation_id = 'init' except (RuntimeError, AttributeError): record.correlation_id = 'init' return True # Apply filter to Flask's app logger correlation_filter = CorrelationIdFilter() app.logger.addFilter(correlation_filter) # Also apply to the root logger to catch all logging calls root_logger = logging.getLogger() root_logger.addFilter(correlation_filter) def add_correlation_id(): """Generate and store correlation ID for the current request""" if not hasattr(g, 'correlation_id'): g.correlation_id = str(uuid.uuid4()) def create_app(config=None): """ Application factory for StarPunk Args: config: Optional configuration dict to override defaults Returns: Configured Flask application instance """ app = Flask(__name__, static_folder="../static", template_folder="../templates") # Load configuration from starpunk.config import load_config load_config(app, config) # Configure logging configure_logging(app) # Initialize database schema from starpunk.database import init_db, init_pool init_db(app) # Initialize connection pool init_pool(app) # Initialize FTS index if needed from pathlib import Path from starpunk.search import has_fts_table, rebuild_fts_index import sqlite3 db_path = Path(app.config["DATABASE_PATH"]) data_path = Path(app.config["DATA_PATH"]) if has_fts_table(db_path): # Check if index is empty (fresh migration or first run) try: conn = sqlite3.connect(db_path) count = conn.execute("SELECT COUNT(*) FROM notes_fts").fetchone()[0] conn.close() if count == 0: app.logger.info("FTS index is empty, populating from existing notes...") try: rebuild_fts_index(db_path, data_path) app.logger.info("FTS index successfully populated") except Exception as e: app.logger.error(f"Failed to populate FTS index: {e}") except Exception as e: app.logger.debug(f"FTS index check skipped: {e}") # Register blueprints from starpunk.routes import register_routes register_routes(app) # Request middleware - Add correlation ID to each request @app.before_request def before_request(): """Add correlation ID to request context for tracing""" add_correlation_id() # Register centralized error handlers from starpunk.errors import register_error_handlers register_error_handlers(app) # Health check endpoint for containers and monitoring @app.route("/health") def health_check(): """ Health check endpoint for containers and monitoring Per developer Q&A Q10: - Basic mode (/health): Public, no auth, returns 200 OK for load balancers - Detailed mode (/health?detailed=true): Requires auth, checks database/disk Returns: JSON with status and info (varies by mode) Response codes: 200: Application healthy 401: Unauthorized (detailed mode without auth) 500: Application unhealthy Query parameters: detailed: If 'true', perform detailed checks (requires auth) """ from flask import jsonify, request import os import shutil # Check if detailed mode requested detailed = request.args.get('detailed', '').lower() == 'true' if detailed: # Detailed mode requires authentication if not g.get('me'): return jsonify({"error": "Authentication required for detailed health check"}), 401 # Perform comprehensive health checks checks = {} overall_healthy = True # Check database connectivity try: from starpunk.database import get_db db = get_db(app) db.execute("SELECT 1").fetchone() db.close() checks['database'] = {'status': 'healthy', 'message': 'Database accessible'} except Exception as e: checks['database'] = {'status': 'unhealthy', 'error': str(e)} overall_healthy = False # Check filesystem access try: data_path = app.config.get("DATA_PATH", "data") if not os.path.exists(data_path): raise Exception("Data path not accessible") checks['filesystem'] = {'status': 'healthy', 'path': data_path} except Exception as e: checks['filesystem'] = {'status': 'unhealthy', 'error': str(e)} overall_healthy = False # Check disk space try: data_path = app.config.get("DATA_PATH", "data") stat = shutil.disk_usage(data_path) percent_free = (stat.free / stat.total) * 100 checks['disk'] = { 'status': 'healthy' if percent_free > 10 else 'warning', 'total_gb': round(stat.total / (1024**3), 2), 'free_gb': round(stat.free / (1024**3), 2), 'percent_free': round(percent_free, 2) } if percent_free <= 5: overall_healthy = False except Exception as e: checks['disk'] = {'status': 'unhealthy', 'error': str(e)} overall_healthy = False return jsonify({ "status": "healthy" if overall_healthy else "unhealthy", "version": app.config.get("VERSION", __version__), "environment": app.config.get("ENV", "unknown"), "checks": checks }), 200 if overall_healthy else 500 else: # Basic mode - just return 200 OK (for load balancers) # No authentication required, minimal checks return jsonify({ "status": "ok", "version": app.config.get("VERSION", __version__) }), 200 return app # Package version (Semantic Versioning 2.0.0) # See docs/standards/versioning-strategy.md for details __version__ = "1.1.1-rc.2" __version_info__ = (1, 1, 1)