""" 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 Returns: JSON with status and basic info Response codes: 200: Application healthy 500: Application unhealthy Checks: - Database connectivity - File system access - Basic application state """ from flask import jsonify import os try: # Check database connectivity from starpunk.database import get_db db = get_db(app) db.execute("SELECT 1").fetchone() db.close() # Check filesystem access data_path = app.config.get("DATA_PATH", "data") if not os.path.exists(data_path): raise Exception("Data path not accessible") return ( jsonify( { "status": "healthy", "version": app.config.get("VERSION", __version__), "environment": app.config.get("ENV", "unknown"), } ), 200, ) except Exception as e: return jsonify({"status": "unhealthy", "error": str(e)}), 500 return app # Package version (Semantic Versioning 2.0.0) # See docs/standards/versioning-strategy.md for details __version__ = "1.1.1" __version_info__ = (1, 1, 1)