""" Intelligent Logging Middleware for AI SEO Tools Provides structured logging, file saving, and monitoring capabilities for all SEO tool operations with performance tracking. """ import json import asyncio import aiofiles from datetime import datetime from functools import wraps from typing import Dict, Any, Callable from pathlib import Path from loguru import logger import os import time # Logging configuration LOG_BASE_DIR = "logs" os.makedirs(LOG_BASE_DIR, exist_ok=True) # Ensure subdirectories exist for subdir in ["seo_tools", "api_calls", "errors", "performance"]: os.makedirs(f"{LOG_BASE_DIR}/{subdir}", exist_ok=True) class PerformanceLogger: """Performance monitoring and logging for SEO operations""" def __init__(self): self.performance_data = {} async def log_performance(self, operation: str, duration: float, metadata: Dict[str, Any] = None): """Log performance metrics for operations""" performance_log = { "operation": operation, "duration_seconds": duration, "timestamp": datetime.utcnow().isoformat(), "metadata": metadata or {} } await save_to_file(f"{LOG_BASE_DIR}/performance/metrics.jsonl", performance_log) # Log performance warnings for slow operations if duration > 30: # More than 30 seconds logger.warning(f"Slow operation detected: {operation} took {duration:.2f} seconds") elif duration > 10: # More than 10 seconds logger.info(f"Operation {operation} took {duration:.2f} seconds") performance_logger = PerformanceLogger() async def save_to_file(filepath: str, data: Dict[str, Any]) -> None: """ Asynchronously save structured data to a JSONL file Args: filepath: Path to the log file data: Dictionary data to save """ try: # Ensure directory exists Path(filepath).parent.mkdir(parents=True, exist_ok=True) # Convert data to JSON string json_line = json.dumps(data, default=str) + "\n" # Write asynchronously async with aiofiles.open(filepath, "a", encoding="utf-8") as file: await file.write(json_line) except Exception as e: logger.error(f"Failed to save log to {filepath}: {e}") def log_api_call(func: Callable) -> Callable: """ Decorator for logging API calls with performance tracking Automatically logs request/response data, timing, and errors for SEO tool endpoints. """ @wraps(func) async def wrapper(*args, **kwargs): start_time = time.time() operation_name = func.__name__ # Extract request data request_data = {} for arg in args: if hasattr(arg, 'dict'): # Pydantic model request_data.update(arg.dict()) # Log API call start call_log = { "operation": operation_name, "timestamp": datetime.utcnow().isoformat(), "request_data": request_data, "status": "started" } logger.info(f"API Call Started: {operation_name}") try: # Execute the function result = await func(*args, **kwargs) execution_time = time.time() - start_time # Log successful completion call_log.update({ "status": "completed", "execution_time": execution_time, "success": getattr(result, 'success', True), "completion_timestamp": datetime.utcnow().isoformat() }) await save_to_file(f"{LOG_BASE_DIR}/api_calls/successful.jsonl", call_log) await performance_logger.log_performance(operation_name, execution_time, request_data) logger.info(f"API Call Completed: {operation_name} in {execution_time:.2f}s") return result except Exception as e: execution_time = time.time() - start_time # Log error error_log = call_log.copy() error_log.update({ "status": "failed", "execution_time": execution_time, "error_type": type(e).__name__, "error_message": str(e), "completion_timestamp": datetime.utcnow().isoformat() }) await save_to_file(f"{LOG_BASE_DIR}/api_calls/failed.jsonl", error_log) logger.error(f"API Call Failed: {operation_name} after {execution_time:.2f}s - {e}") # Re-raise the exception raise return wrapper class SEOToolsLogger: """Centralized logger for SEO tools with intelligent categorization""" @staticmethod async def log_tool_usage(tool_name: str, input_data: Dict[str, Any], output_data: Dict[str, Any], success: bool = True): """Log SEO tool usage with input/output tracking""" usage_log = { "tool": tool_name, "timestamp": datetime.utcnow().isoformat(), "input_data": input_data, "output_data": output_data, "success": success, "input_size": len(str(input_data)), "output_size": len(str(output_data)) } await save_to_file(f"{LOG_BASE_DIR}/seo_tools/usage.jsonl", usage_log) @staticmethod async def log_ai_analysis(tool_name: str, prompt: str, response: str, model_used: str, tokens_used: int = None): """Log AI analysis operations with token tracking""" ai_log = { "tool": tool_name, "timestamp": datetime.utcnow().isoformat(), "model": model_used, "prompt_length": len(prompt), "response_length": len(response), "tokens_used": tokens_used, "prompt_preview": prompt[:200] + "..." if len(prompt) > 200 else prompt, "response_preview": response[:200] + "..." if len(response) > 200 else response } await save_to_file(f"{LOG_BASE_DIR}/seo_tools/ai_analysis.jsonl", ai_log) @staticmethod async def log_external_api_call(api_name: str, endpoint: str, response_code: int, response_time: float, request_data: Dict[str, Any] = None): """Log external API calls (PageSpeed, etc.)""" api_log = { "api": api_name, "endpoint": endpoint, "response_code": response_code, "response_time": response_time, "timestamp": datetime.utcnow().isoformat(), "request_data": request_data or {}, "success": 200 <= response_code < 300 } await save_to_file(f"{LOG_BASE_DIR}/seo_tools/external_apis.jsonl", api_log) @staticmethod async def log_crawling_operation(url: str, pages_crawled: int, errors_found: int, crawl_depth: int, duration: float): """Log web crawling operations""" crawl_log = { "url": url, "pages_crawled": pages_crawled, "errors_found": errors_found, "crawl_depth": crawl_depth, "duration": duration, "timestamp": datetime.utcnow().isoformat(), "pages_per_second": pages_crawled / duration if duration > 0 else 0 } await save_to_file(f"{LOG_BASE_DIR}/seo_tools/crawling.jsonl", crawl_log) class LogAnalyzer: """Analyze logs to provide insights and monitoring""" @staticmethod async def get_performance_summary(hours: int = 24) -> Dict[str, Any]: """Get performance summary for the last N hours""" try: performance_file = f"{LOG_BASE_DIR}/performance/metrics.jsonl" if not os.path.exists(performance_file): return {"error": "No performance data available"} # Read recent performance data cutoff_time = datetime.utcnow().timestamp() - (hours * 3600) operations = [] async with aiofiles.open(performance_file, "r") as file: async for line in file: try: data = json.loads(line.strip()) log_time = datetime.fromisoformat(data["timestamp"]).timestamp() if log_time >= cutoff_time: operations.append(data) except (json.JSONDecodeError, KeyError): continue if not operations: return {"message": f"No operations in the last {hours} hours"} # Calculate statistics durations = [op["duration_seconds"] for op in operations] operation_counts = {} for op in operations: op_name = op["operation"] operation_counts[op_name] = operation_counts.get(op_name, 0) + 1 return { "total_operations": len(operations), "average_duration": sum(durations) / len(durations), "max_duration": max(durations), "min_duration": min(durations), "operations_by_type": operation_counts, "time_period_hours": hours } except Exception as e: logger.error(f"Error analyzing performance logs: {e}") return {"error": str(e)} @staticmethod async def get_error_summary(hours: int = 24) -> Dict[str, Any]: """Get error summary for the last N hours""" try: error_file = f"{LOG_BASE_DIR}/seo_tools/errors.jsonl" if not os.path.exists(error_file): return {"message": "No errors recorded"} cutoff_time = datetime.utcnow().timestamp() - (hours * 3600) errors = [] async with aiofiles.open(error_file, "r") as file: async for line in file: try: data = json.loads(line.strip()) log_time = datetime.fromisoformat(data["timestamp"]).timestamp() if log_time >= cutoff_time: errors.append(data) except (json.JSONDecodeError, KeyError): continue if not errors: return {"message": f"No errors in the last {hours} hours"} # Analyze errors error_types = {} functions_with_errors = {} for error in errors: error_type = error.get("error_type", "Unknown") function = error.get("function", "Unknown") error_types[error_type] = error_types.get(error_type, 0) + 1 functions_with_errors[function] = functions_with_errors.get(function, 0) + 1 return { "total_errors": len(errors), "error_types": error_types, "functions_with_errors": functions_with_errors, "recent_errors": errors[-5:], # Last 5 errors "time_period_hours": hours } except Exception as e: logger.error(f"Error analyzing error logs: {e}") return {"error": str(e)} # Initialize global logger instance seo_logger = SEOToolsLogger() log_analyzer = LogAnalyzer() # Configure loguru for structured logging # Commented out to prevent conflicts with main logging configuration # logger.add( # f"{LOG_BASE_DIR}/application.log", # rotation="1 day", # retention="30 days", # level="INFO", # format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name}:{function}:{line} | {message}", # serialize=True # ) # logger.add( # f"{LOG_BASE_DIR}/errors.log", # rotation="1 day", # retention="30 days", # level="ERROR", # format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name}:{function}:{line} | {message}", # serialize=True # ) logger.info("Logging middleware initialized successfully")