mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-27 04:20:09 +00:00
303 lines
9.7 KiB
Python
303 lines
9.7 KiB
Python
"""Centralized error logging for MCP tools.
|
|
|
|
This module provides comprehensive error logging for all MCP tool operations,
|
|
writing errors to ~/.hanzo/mcp/logs/ for debugging and analysis.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import traceback
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Any, Optional
|
|
|
|
|
|
class MCPErrorLogger:
|
|
"""Centralized error logger for MCP tools."""
|
|
|
|
def __init__(self, log_dir: Optional[Path] = None):
|
|
"""Initialize the error logger.
|
|
|
|
Args:
|
|
log_dir: Directory for log files (default: ~/.hanzo/mcp/logs/)
|
|
"""
|
|
if log_dir is None:
|
|
log_dir = Path.home() / ".hanzo" / "mcp" / "logs"
|
|
|
|
self.log_dir = log_dir
|
|
self.log_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Create daily log file
|
|
today = datetime.now().strftime("%Y-%m-%d")
|
|
self.log_file = self.log_dir / f"mcp-errors-{today}.log"
|
|
|
|
# Also create a general errors file
|
|
self.general_log_file = self.log_dir / "errors.log"
|
|
|
|
# Set up Python logging
|
|
self._setup_logging()
|
|
|
|
def _setup_logging(self):
|
|
"""Set up Python logging infrastructure."""
|
|
# Create logger
|
|
self.logger = logging.getLogger("hanzo_mcp.errors")
|
|
self.logger.setLevel(logging.DEBUG)
|
|
|
|
# Prevent duplicate handlers
|
|
if self.logger.handlers:
|
|
return
|
|
|
|
# File handler for daily logs
|
|
file_handler = logging.FileHandler(self.log_file)
|
|
file_handler.setLevel(logging.ERROR)
|
|
|
|
# File handler for general log
|
|
general_handler = logging.FileHandler(self.general_log_file)
|
|
general_handler.setLevel(logging.ERROR)
|
|
|
|
# Formatter
|
|
formatter = logging.Formatter(
|
|
"%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
datefmt="%Y-%m-%d %H:%M:%S",
|
|
)
|
|
file_handler.setFormatter(formatter)
|
|
general_handler.setFormatter(formatter)
|
|
|
|
# Add handlers
|
|
self.logger.addHandler(file_handler)
|
|
self.logger.addHandler(general_handler)
|
|
|
|
def log_tool_error(
|
|
self,
|
|
tool_name: str,
|
|
error: Exception,
|
|
params: Optional[dict[str, Any]] = None,
|
|
context: Optional[str] = None,
|
|
):
|
|
"""Log a tool execution error.
|
|
|
|
Args:
|
|
tool_name: Name of the tool that errored
|
|
error: The exception that was raised
|
|
params: Tool parameters (will be sanitized)
|
|
context: Additional context about the error
|
|
"""
|
|
error_data = {
|
|
"timestamp": datetime.now().isoformat(),
|
|
"tool_name": tool_name,
|
|
"error_type": type(error).__name__,
|
|
"error_message": str(error),
|
|
"traceback": traceback.format_exc(),
|
|
"params": self._sanitize_params(params) if params else None,
|
|
"context": context,
|
|
}
|
|
|
|
# Write to JSON log for structured parsing
|
|
json_log_file = (
|
|
self.log_dir / f"tool-errors-{datetime.now().strftime('%Y-%m-%d')}.jsonl"
|
|
)
|
|
try:
|
|
with open(json_log_file, "a") as f:
|
|
json.dump(error_data, f)
|
|
f.write("\n")
|
|
except Exception as e:
|
|
# If JSON logging fails, at least log that
|
|
self.logger.error(f"Failed to write JSON log: {e}")
|
|
|
|
# Also log to standard logger
|
|
self.logger.error(
|
|
f"Tool '{tool_name}' error: {type(error).__name__}: {str(error)}",
|
|
extra={"tool": tool_name, "params": error_data.get("params")},
|
|
)
|
|
|
|
# Write detailed error to tool-specific file
|
|
tool_log_file = self.log_dir / f"{tool_name}-errors.log"
|
|
try:
|
|
with open(tool_log_file, "a") as f:
|
|
f.write(f"\n{'=' * 80}\n")
|
|
f.write(f"ERROR at {error_data['timestamp']}\n")
|
|
f.write(f"{'=' * 80}\n")
|
|
f.write(f"Tool: {tool_name}\n")
|
|
f.write(f"Error Type: {error_data['error_type']}\n")
|
|
f.write(f"Error Message: {error_data['error_message']}\n")
|
|
if context:
|
|
f.write(f"Context: {context}\n")
|
|
if params:
|
|
f.write(
|
|
f"\nParameters:\n{json.dumps(error_data['params'], indent=2)}\n"
|
|
)
|
|
f.write(f"\nTraceback:\n{error_data['traceback']}\n")
|
|
except Exception as e:
|
|
self.logger.error(f"Failed to write tool-specific log: {e}")
|
|
|
|
def log_call_signature_error(
|
|
self,
|
|
tool_name: str,
|
|
expected_signature: str,
|
|
actual_call: str,
|
|
error: Exception,
|
|
):
|
|
"""Log an error related to incorrect tool call signature.
|
|
|
|
Args:
|
|
tool_name: Name of the tool
|
|
expected_signature: Expected function signature
|
|
actual_call: How the tool was actually called
|
|
error: The exception that was raised
|
|
"""
|
|
error_data = {
|
|
"timestamp": datetime.now().isoformat(),
|
|
"tool_name": tool_name,
|
|
"error_type": "CallSignatureError",
|
|
"expected_signature": expected_signature,
|
|
"actual_call": actual_call,
|
|
"error_message": str(error),
|
|
"traceback": traceback.format_exc(),
|
|
}
|
|
|
|
# Write to signature errors log
|
|
sig_log_file = self.log_dir / "signature-errors.log"
|
|
try:
|
|
with open(sig_log_file, "a") as f:
|
|
f.write(f"\n{'=' * 80}\n")
|
|
f.write(f"CALL SIGNATURE ERROR at {error_data['timestamp']}\n")
|
|
f.write(f"{'=' * 80}\n")
|
|
f.write(f"Tool: {tool_name}\n")
|
|
f.write(f"Expected: {expected_signature}\n")
|
|
f.write(f"Actual: {actual_call}\n")
|
|
f.write(f"Error: {error_data['error_message']}\n")
|
|
f.write(f"\nTraceback:\n{error_data['traceback']}\n")
|
|
except Exception as e:
|
|
self.logger.error(f"Failed to write signature error log: {e}")
|
|
|
|
# Also log as regular tool error
|
|
self.log_tool_error(
|
|
tool_name,
|
|
error,
|
|
context=f"Call signature mismatch - Expected: {expected_signature}, Got: {actual_call}",
|
|
)
|
|
|
|
def _sanitize_params(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
"""Sanitize parameters to remove sensitive data.
|
|
|
|
Args:
|
|
params: Parameters to sanitize
|
|
|
|
Returns:
|
|
Sanitized parameters
|
|
"""
|
|
sanitized = {}
|
|
sensitive_keys = {
|
|
"password",
|
|
"token",
|
|
"key",
|
|
"secret",
|
|
"api_key",
|
|
"auth",
|
|
"credential",
|
|
"private",
|
|
"ssh_key",
|
|
"passphrase",
|
|
}
|
|
|
|
for key, value in params.items():
|
|
# Check if key contains sensitive terms
|
|
if any(sensitive in key.lower() for sensitive in sensitive_keys):
|
|
sanitized[key] = "[REDACTED]"
|
|
# Recursively sanitize nested dicts
|
|
elif isinstance(value, dict):
|
|
sanitized[key] = self._sanitize_params(value)
|
|
# Convert non-serializable types to strings
|
|
elif not isinstance(value, (str, int, float, bool, list, dict, type(None))):
|
|
sanitized[key] = str(value)
|
|
else:
|
|
sanitized[key] = value
|
|
|
|
return sanitized
|
|
|
|
def get_recent_errors(
|
|
self, tool_name: Optional[str] = None, limit: int = 10
|
|
) -> list[dict]:
|
|
"""Get recent errors from the JSON log.
|
|
|
|
Args:
|
|
tool_name: Filter by tool name (optional)
|
|
limit: Maximum number of errors to return
|
|
|
|
Returns:
|
|
List of error dictionaries
|
|
"""
|
|
today = datetime.now().strftime("%Y-%m-%d")
|
|
json_log_file = self.log_dir / f"tool-errors-{today}.jsonl"
|
|
|
|
if not json_log_file.exists():
|
|
return []
|
|
|
|
errors = []
|
|
try:
|
|
with open(json_log_file, "r") as f:
|
|
for line in f:
|
|
try:
|
|
error = json.loads(line)
|
|
if tool_name is None or error.get("tool_name") == tool_name:
|
|
errors.append(error)
|
|
except json.JSONDecodeError:
|
|
continue
|
|
except Exception as e:
|
|
self.logger.error(f"Failed to read error log: {e}")
|
|
|
|
# Return most recent errors
|
|
return errors[-limit:]
|
|
|
|
|
|
# Global error logger instance
|
|
_global_error_logger: Optional[MCPErrorLogger] = None
|
|
|
|
|
|
def get_error_logger() -> MCPErrorLogger:
|
|
"""Get the global error logger instance.
|
|
|
|
Returns:
|
|
Global error logger
|
|
"""
|
|
global _global_error_logger
|
|
if _global_error_logger is None:
|
|
_global_error_logger = MCPErrorLogger()
|
|
return _global_error_logger
|
|
|
|
|
|
def log_tool_error(
|
|
tool_name: str,
|
|
error: Exception,
|
|
params: Optional[dict[str, Any]] = None,
|
|
context: Optional[str] = None,
|
|
):
|
|
"""Convenience function to log a tool error using the global logger.
|
|
|
|
Args:
|
|
tool_name: Name of the tool that errored
|
|
error: The exception that was raised
|
|
params: Tool parameters
|
|
context: Additional context
|
|
"""
|
|
logger = get_error_logger()
|
|
logger.log_tool_error(tool_name, error, params, context)
|
|
|
|
|
|
def log_call_signature_error(
|
|
tool_name: str,
|
|
expected_signature: str,
|
|
actual_call: str,
|
|
error: Exception,
|
|
):
|
|
"""Convenience function to log a call signature error.
|
|
|
|
Args:
|
|
tool_name: Name of the tool
|
|
expected_signature: Expected function signature
|
|
actual_call: How the tool was actually called
|
|
error: The exception that was raised
|
|
"""
|
|
logger = get_error_logger()
|
|
logger.log_call_signature_error(tool_name, expected_signature, actual_call, error)
|