import os os.environ["NUMEXPR_MAX_THREADS"] = "12" from fastapi import FastAPI, HTTPException, Depends, Body, status, Response from fastapi.middleware.cors import CORSMiddleware from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel from fastapi.security import HTTPBearer from contextlib import asynccontextmanager import logging from logging.handlers import RotatingFileHandler from datetime import timedelta, datetime import pathlib from modules.shared.configuration import APP_CONFIG from modules.shared.eventManagement import eventManager class DailyRotatingFileHandler(RotatingFileHandler): """ A rotating file handler that automatically switches to a new file when the date changes. The log file name includes the current date and switches at midnight. """ def __init__( self, log_dir, filename_prefix, max_bytes=10485760, backup_count=5, **kwargs ): self.log_dir = log_dir self.filename_prefix = filename_prefix self.current_date = None self.current_file = None # Initialize with today's file self._update_file_if_needed() # Call parent constructor with current file super().__init__( self.current_file, maxBytes=max_bytes, backupCount=backup_count, **kwargs ) def _update_file_if_needed(self): """Update the log file if the date has changed""" today = datetime.now().strftime("%Y%m%d") if self.current_date != today: self.current_date = today new_file = os.path.join(self.log_dir, f"{self.filename_prefix}_{today}.log") if self.current_file != new_file: self.current_file = new_file return True return False def emit(self, record): """Emit a log record, switching files if date has changed""" # Check if we need to switch to a new file if self._update_file_if_needed(): # Close current file and open new one if self.stream: self.stream.close() self.stream = None # Update the baseFilename for the parent class self.baseFilename = self.current_file # Reopen the stream if not self.delay: self.stream = self._open() # Call parent emit method super().emit(record) def initLogging(): """Initialize logging with configuration from APP_CONFIG""" # Get log level from config (default to INFO if not found) logLevelName = APP_CONFIG.get("APP_LOGGING_LOG_LEVEL", "WARNING") logLevel = getattr(logging, logLevelName) # Get log directory from config logDir = APP_CONFIG.get("APP_LOGGING_LOG_DIR", "./") if not os.path.isabs(logDir): # If relative path, make it relative to the gateway directory gatewayDir = os.path.dirname(os.path.abspath(__file__)) logDir = os.path.join(gatewayDir, logDir) # Ensure log directory exists os.makedirs(logDir, exist_ok=True) # Create formatters - using single line format consoleFormatter = logging.Formatter( fmt="%(asctime)s - %(levelname)s - %(name)s - %(message)s", datefmt=APP_CONFIG.get("APP_LOGGING_DATE_FORMAT", "%Y-%m-%d %H:%M:%S"), ) # File formatter with more detailed error information but still single line fileFormatter = logging.Formatter( fmt="%(asctime)s - %(levelname)s - %(name)s - %(message)s - %(pathname)s:%(lineno)d - %(funcName)s", datefmt=APP_CONFIG.get("APP_LOGGING_DATE_FORMAT", "%Y-%m-%d %H:%M:%S"), ) # Add filter to exclude Chrome DevTools requests class ChromeDevToolsFilter(logging.Filter): def filter(self, record): return not ( isinstance(record.msg, str) and ( ".well-known/appspecific/com.chrome.devtools.json" in record.msg or "Request: /index.html" in record.msg ) ) # Add filter to exclude all httpcore loggers (including sub-loggers) class HttpcoreStarFilter(logging.Filter): def filter(self, record): return not ( record.name == "httpcore" or record.name.startswith("httpcore.") ) # Add filter to exclude HTTP debug messages class HTTPDebugFilter(logging.Filter): def filter(self, record): if isinstance(record.msg, str): # Filter out HTTP debug messages http_debug_patterns = [ "receive_response_body.started", "receive_response_body.complete", "response_closed.started", "_send_single_request", "httpcore.http11", "httpx._client", "HTTP Request", "multipart.multipart", ] return not any(pattern in record.msg for pattern in http_debug_patterns) return True # Add filter to remove emojis from log messages to prevent Unicode encoding errors class EmojiFilter(logging.Filter): def filter(self, record): if isinstance(record.msg, str): # Remove only emojis, preserve other Unicode characters like quotes import re import unicodedata # Remove emoji characters specifically record.msg = "".join( char for char in record.msg if unicodedata.category(char) != "So" or not ( 0x1F600 <= ord(char) <= 0x1F64F or 0x1F300 <= ord(char) <= 0x1F5FF or 0x1F680 <= ord(char) <= 0x1F6FF or 0x1F1E0 <= ord(char) <= 0x1F1FF or 0x2600 <= ord(char) <= 0x26FF or 0x2700 <= ord(char) <= 0x27BF ) ) return True # Configure handlers based on config handlers = [] # Add console handler if enabled if APP_CONFIG.get("APP_LOGGING_CONSOLE_ENABLED", True): consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(consoleFormatter) consoleHandler.addFilter(ChromeDevToolsFilter()) consoleHandler.addFilter(HttpcoreStarFilter()) consoleHandler.addFilter(HTTPDebugFilter()) consoleHandler.addFilter(EmojiFilter()) handlers.append(consoleHandler) # Add file handler if enabled if APP_CONFIG.get("APP_LOGGING_FILE_ENABLED", True): # Create daily application log file with automatic date switching rotationSize = int( APP_CONFIG.get("APP_LOGGING_ROTATION_SIZE", 10485760) ) # Default: 10MB backupCount = int(APP_CONFIG.get("APP_LOGGING_BACKUP_COUNT", 5)) fileHandler = DailyRotatingFileHandler( log_dir=logDir, filename_prefix="log_app", max_bytes=rotationSize, backup_count=backupCount, ) fileHandler.setFormatter(fileFormatter) fileHandler.addFilter(ChromeDevToolsFilter()) fileHandler.addFilter(HttpcoreStarFilter()) fileHandler.addFilter(HTTPDebugFilter()) fileHandler.addFilter(EmojiFilter()) handlers.append(fileHandler) # Configure the root logger logging.basicConfig( level=logLevel, format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", datefmt=APP_CONFIG.get("APP_LOGGING_DATE_FORMAT", "%Y-%m-%d %H:%M:%S"), handlers=handlers, force=True, # Force reconfiguration of the root logger ) # Silence noisy third-party libraries - use the same level as the root logger noisyLoggers = [ "httpx", "httpcore", "urllib3", "asyncio", "fastapi.security.oauth2", "msal", ] for loggerName in noisyLoggers: logging.getLogger(loggerName).setLevel(logging.WARNING) # Log the current logging configuration logger = logging.getLogger(__name__) logger.info(f"Logging initialized with level {logLevelName}") logger.info(f"Log directory: {logDir}") if APP_CONFIG.get("APP_LOGGING_FILE_ENABLED", True): today = datetime.now().strftime("%Y%m%d") appLogFile = os.path.join(logDir, f"log_app_{today}.log") logger.info(f"Application log file: {appLogFile} (auto-switches daily)") else: logger.info("Application log file: disabled") logger.info( f"Console logging: {'enabled' if APP_CONFIG.get('APP_LOGGING_CONSOLE_ENABLED', True) else 'disabled'}" ) # Initialize logging initLogging() logger = logging.getLogger(__name__) instanceLabel = APP_CONFIG.get("APP_ENV_LABEL") # Define lifespan context manager for application startup/shutdown events @asynccontextmanager async def lifespan(app: FastAPI): logger.info("Application is starting up") # Initialize LangGraph checkpointer from modules.features.chatBot.utils.checkpointer import ( initialize_checkpointer, close_checkpointer, ) try: await initialize_checkpointer() logger.info("LangGraph checkpointer initialized successfully") except Exception as e: logger.error(f"Failed to initialize LangGraph checkpointer: {str(e)}") # Continue startup even if checkpointer fails to initialize eventManager.start() yield # Cleanup eventManager.stop() await close_checkpointer() logger.info("Application has been shut down") # START APP app = FastAPI( title="PowerOn | Data Platform API", description=f"Backend API for the Multi-Agent Platform by ValueOn AG ({instanceLabel})", lifespan=lifespan, swagger_ui_init_oauth={ "usePkceWithAuthorizationCodeGrant": True, }, ) # Configure OpenAPI security scheme for Swagger UI # This adds the "Authorize" button to the /docs page security_scheme = HTTPBearer() app.openapi_schema = None # Reset schema to regenerate with security def custom_openapi(): if app.openapi_schema: return app.openapi_schema from fastapi.openapi.utils import get_openapi openapi_schema = get_openapi( title=app.title, version="1.0.0", description=app.description, routes=app.routes, ) # Add security scheme definition openapi_schema["components"]["securitySchemes"] = { "BearerAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT", "description": "Enter your JWT token (obtained from login endpoint or browser cookies)", } } # Apply security globally to all endpoints # Individual endpoints can override this if needed openapi_schema["security"] = [{"BearerAuth": []}] app.openapi_schema = openapi_schema return app.openapi_schema app.openapi = custom_openapi # Parse CORS origins from environment variable def get_allowed_origins(): origins_str = APP_CONFIG.get("APP_ALLOWED_ORIGINS", "http://localhost:8080") # Split by comma and strip whitespace origins = [origin.strip() for origin in origins_str.split(",")] logger.info(f"CORS allowed origins: {origins}") return origins # CORS configuration using environment variables app.add_middleware( CORSMiddleware, allow_origins=get_allowed_origins(), allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["*"], expose_headers=["*"], max_age=86400, # Increased caching for preflight requests ) # CSRF protection middleware from modules.security.csrf import CSRFMiddleware from modules.security.tokenRefreshMiddleware import ( TokenRefreshMiddleware, ProactiveTokenRefreshMiddleware, ) app.add_middleware(CSRFMiddleware) # Token refresh middleware (silent refresh for expired OAuth tokens) app.add_middleware(TokenRefreshMiddleware, enabled=True) # Proactive token refresh middleware (refresh tokens before they expire) app.add_middleware( ProactiveTokenRefreshMiddleware, enabled=True, check_interval_minutes=5 ) # Run triggered features import modules.features.init # Include all routers from modules.routes.routeAdmin import router as generalRouter app.include_router(generalRouter) from modules.routes.routeAttributes import router as attributesRouter app.include_router(attributesRouter) from modules.routes.routeDataMandates import router as mandateRouter app.include_router(mandateRouter) from modules.routes.routeDataUsers import router as userRouter app.include_router(userRouter) from modules.routes.routeDataFiles import router as fileRouter app.include_router(fileRouter) from modules.routes.routeDataNeutralization import router as neutralizationRouter app.include_router(neutralizationRouter) from modules.routes.routeDataPrompts import router as promptRouter app.include_router(promptRouter) from modules.routes.routeDataConnections import router as connectionsRouter app.include_router(connectionsRouter) from modules.routes.routeWorkflows import router as workflowRouter app.include_router(workflowRouter) from modules.routes.routeChatPlayground import router as chatPlaygroundRouter app.include_router(chatPlaygroundRouter) from modules.routes.routeSecurityLocal import router as localRouter app.include_router(localRouter) from modules.routes.routeSecurityMsft import router as msftRouter app.include_router(msftRouter) from modules.routes.routeSecurityGoogle import router as googleRouter app.include_router(googleRouter) from modules.routes.routeVoiceGoogle import router as voiceGoogleRouter app.include_router(voiceGoogleRouter) from modules.routes.routeSecurityAdmin import router as adminSecurityRouter app.include_router(adminSecurityRouter) from modules.routes.routeChatbot import router as chatbotRouter app.include_router(chatbotRouter)