import os os.environ["NUMEXPR_MAX_THREADS"] = "12" from fastapi import FastAPI, HTTPException, Depends, Body, status, Response from fastapi.middleware.cors import CORSMiddleware from contextlib import asynccontextmanager import logging from logging.handlers import RotatingFileHandler from datetime import timedelta import pathlib from modules.shared.configuration import APP_CONFIG 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) # 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' ] 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 emojis and other Unicode characters that might cause encoding issues import re # Remove emojis and other Unicode symbols record.msg = re.sub(r'[^\x00-\x7F]+', '[EMOJI]', record.msg) 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): # Get log file path and ensure it's absolute logFile = APP_CONFIG.get("APP_LOGGING_LOG_FILE", "app.log") if not os.path.isabs(logFile): # If relative path, make it relative to the gateway directory gatewayDir = os.path.dirname(os.path.abspath(__file__)) logFile = os.path.join(gatewayDir, logFile) # Ensure log directory exists logDir = os.path.dirname(logFile) if logDir: os.makedirs(logDir, exist_ok=True) rotationSize = int(APP_CONFIG.get("APP_LOGGING_ROTATION_SIZE", 10485760)) # Default: 10MB backupCount = int(APP_CONFIG.get("APP_LOGGING_BACKUP_COUNT", 5)) fileHandler = RotatingFileHandler( logFile, maxBytes=rotationSize, backupCount=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 file: {logFile if APP_CONFIG.get('APP_LOGGING_FILE_ENABLED', True) else '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): # Startup logic logger.info("Application is starting up") # Initialize root interface to ensure database is properly set up from modules.interfaces.interfaceAppObjects import getRootInterface getRootInterface() yield # Shutdown logic 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 ) # 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 ) # 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.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.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.routeJira import router as jiraRouter app.include_router(jiraRouter)