✨ Enhance log analysis with advanced filtering and search capabilities
Add comprehensive log analysis features to improve troubleshooting workflows: - Time-based filtering (8pm-6am shifts) with HH:MM format support - Multi-level log filtering (ERROR, WARNING, EMERGENCY, etc.) - Full-text search across log content with case-insensitive matching - Proper line break formatting for readable output - Line count limiting for large log files New REST API endpoints: - /api/logs/errors/{job_id} - Get only error/warning logs - /api/logs/search/{job_id} - Search logs for specific terms - Enhanced /api/logs/job/{job_id} with filtering parameters New MCP tools: - get_error_logs - Streamlined error analysis - search_job_logs - Pattern-based log searching - Enhanced get_job_logs with all filtering options 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
293
mcp_server.py
293
mcp_server.py
@ -9,6 +9,7 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import requests
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from mcp.server import NotificationOptions, Server
|
||||
@ -172,7 +173,7 @@ async def handle_list_tools() -> List[types.Tool]:
|
||||
),
|
||||
types.Tool(
|
||||
name="get_job_logs",
|
||||
description="Get logs for a Nomad job",
|
||||
description="Get logs for a Nomad job with advanced filtering options",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -184,11 +185,103 @@ async def handle_list_tools() -> List[types.Tool]:
|
||||
"type": "string",
|
||||
"description": "Nomad namespace",
|
||||
"default": "development"
|
||||
},
|
||||
"log_type": {
|
||||
"type": "string",
|
||||
"description": "Type of logs: stdout or stderr",
|
||||
"enum": ["stdout", "stderr"],
|
||||
"default": "stderr"
|
||||
},
|
||||
"start_time": {
|
||||
"type": "string",
|
||||
"description": "Start time filter (HH:MM format, e.g., '20:00' for 8 PM)"
|
||||
},
|
||||
"end_time": {
|
||||
"type": "string",
|
||||
"description": "End time filter (HH:MM format, e.g., '06:00' for 6 AM)"
|
||||
},
|
||||
"log_level": {
|
||||
"type": "string",
|
||||
"description": "Filter by log level: ERROR, WARNING, INFO, DEBUG, EMERGENCY, CRITICAL"
|
||||
},
|
||||
"search": {
|
||||
"type": "string",
|
||||
"description": "Search term to filter logs"
|
||||
},
|
||||
"lines": {
|
||||
"type": "integer",
|
||||
"description": "Number of recent log lines to return"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Number of allocations to check",
|
||||
"default": 1
|
||||
}
|
||||
},
|
||||
"required": ["job_id"]
|
||||
}
|
||||
),
|
||||
types.Tool(
|
||||
name="get_error_logs",
|
||||
description="Get only error and warning logs for a Nomad job, useful for troubleshooting",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"job_id": {
|
||||
"type": "string",
|
||||
"description": "ID of the job to get error logs for"
|
||||
},
|
||||
"namespace": {
|
||||
"type": "string",
|
||||
"description": "Nomad namespace",
|
||||
"default": "development"
|
||||
},
|
||||
"start_time": {
|
||||
"type": "string",
|
||||
"description": "Start time filter (HH:MM format, e.g., '20:00' for 8 PM)"
|
||||
},
|
||||
"end_time": {
|
||||
"type": "string",
|
||||
"description": "End time filter (HH:MM format, e.g., '06:00' for 6 AM)"
|
||||
}
|
||||
},
|
||||
"required": ["job_id"]
|
||||
}
|
||||
),
|
||||
types.Tool(
|
||||
name="search_job_logs",
|
||||
description="Search Nomad job logs for specific terms or patterns",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"job_id": {
|
||||
"type": "string",
|
||||
"description": "ID of the job to search logs for"
|
||||
},
|
||||
"search_term": {
|
||||
"type": "string",
|
||||
"description": "Term or pattern to search for in logs"
|
||||
},
|
||||
"namespace": {
|
||||
"type": "string",
|
||||
"description": "Nomad namespace",
|
||||
"default": "development"
|
||||
},
|
||||
"log_type": {
|
||||
"type": "string",
|
||||
"description": "Type of logs: stdout or stderr",
|
||||
"enum": ["stdout", "stderr"],
|
||||
"default": "stderr"
|
||||
},
|
||||
"lines": {
|
||||
"type": "integer",
|
||||
"description": "Number of matching lines to return",
|
||||
"default": 50
|
||||
}
|
||||
},
|
||||
"required": ["job_id", "search_term"]
|
||||
}
|
||||
),
|
||||
types.Tool(
|
||||
name="submit_job_file",
|
||||
description="Submit a Nomad job from HCL or JSON file content",
|
||||
@ -431,56 +524,174 @@ async def handle_call_tool(name: str, arguments: Dict[str, Any]) -> List[types.T
|
||||
text="Error: job_id is required"
|
||||
)]
|
||||
|
||||
# Get allocations
|
||||
allocations = nomad_service.get_allocations(job_id)
|
||||
if not allocations:
|
||||
# Use the enhanced REST API endpoint
|
||||
import requests
|
||||
base_url = os.getenv("BASE_URL", "http://localhost:8000")
|
||||
|
||||
# Build query parameters
|
||||
params = {
|
||||
"namespace": arguments.get("namespace", namespace),
|
||||
"log_type": arguments.get("log_type", "stderr"),
|
||||
"limit": arguments.get("limit", 1),
|
||||
"plain_text": True,
|
||||
"formatted": True
|
||||
}
|
||||
|
||||
# Add optional filters
|
||||
if arguments.get("start_time"):
|
||||
params["start_time"] = arguments["start_time"]
|
||||
if arguments.get("end_time"):
|
||||
params["end_time"] = arguments["end_time"]
|
||||
if arguments.get("log_level"):
|
||||
params["log_level"] = arguments["log_level"]
|
||||
if arguments.get("search"):
|
||||
params["search"] = arguments["search"]
|
||||
if arguments.get("lines"):
|
||||
params["lines"] = arguments["lines"]
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{base_url}/api/logs/job/{job_id}",
|
||||
params=params,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
logs_text = response.text
|
||||
|
||||
result = {
|
||||
"success": True,
|
||||
"job_id": job_id,
|
||||
"namespace": params["namespace"],
|
||||
"message": f"Retrieved logs for job {job_id}",
|
||||
"logs": logs_text,
|
||||
"filters_applied": {k: v for k, v in params.items() if k not in ["namespace", "plain_text", "formatted"]}
|
||||
}
|
||||
else:
|
||||
result = {
|
||||
"success": False,
|
||||
"job_id": job_id,
|
||||
"message": f"Failed to get logs: {response.status_code} - {response.text}",
|
||||
"logs": None
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
result = {
|
||||
"success": False,
|
||||
"job_id": job_id,
|
||||
"message": f"No allocations found for job {job_id}",
|
||||
"message": f"Error getting logs: {str(e)}",
|
||||
"logs": None
|
||||
}
|
||||
else:
|
||||
# Get latest allocation
|
||||
sorted_allocations = sorted(
|
||||
allocations,
|
||||
key=lambda a: a.get("CreateTime", 0),
|
||||
reverse=True
|
||||
)
|
||||
latest_alloc = sorted_allocations[0]
|
||||
alloc_id = latest_alloc.get("ID")
|
||||
|
||||
# Get task name
|
||||
task_name = None
|
||||
if "TaskStates" in latest_alloc:
|
||||
task_states = latest_alloc["TaskStates"]
|
||||
if task_states:
|
||||
task_name = next(iter(task_states.keys()))
|
||||
|
||||
if not task_name:
|
||||
task_name = "app" # Default task name
|
||||
|
||||
# Get logs
|
||||
stdout_logs = nomad_service.get_allocation_logs(alloc_id, task_name, "stdout")
|
||||
stderr_logs = nomad_service.get_allocation_logs(alloc_id, task_name, "stderr")
|
||||
|
||||
result = {
|
||||
"success": True,
|
||||
"job_id": job_id,
|
||||
"allocation_id": alloc_id,
|
||||
"task_name": task_name,
|
||||
"message": f"Retrieved logs for job {job_id}",
|
||||
"logs": {
|
||||
"stdout": stdout_logs,
|
||||
"stderr": stderr_logs
|
||||
}
|
||||
}
|
||||
|
||||
return [types.TextContent(
|
||||
type="text",
|
||||
text=json.dumps(result, indent=2)
|
||||
text=json.dumps(result, indent=2) if not result.get("success") else result["logs"]
|
||||
)]
|
||||
|
||||
elif name == "get_error_logs":
|
||||
job_id = arguments.get("job_id")
|
||||
|
||||
if not job_id:
|
||||
return [types.TextContent(
|
||||
type="text",
|
||||
text="Error: job_id is required"
|
||||
)]
|
||||
|
||||
# Use the error logs endpoint
|
||||
import requests
|
||||
base_url = os.getenv("BASE_URL", "http://localhost:8000")
|
||||
|
||||
params = {
|
||||
"namespace": arguments.get("namespace", namespace),
|
||||
"plain_text": True
|
||||
}
|
||||
|
||||
if arguments.get("start_time"):
|
||||
params["start_time"] = arguments["start_time"]
|
||||
if arguments.get("end_time"):
|
||||
params["end_time"] = arguments["end_time"]
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{base_url}/api/logs/errors/{job_id}",
|
||||
params=params,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
logs_text = response.text
|
||||
|
||||
result = {
|
||||
"success": True,
|
||||
"job_id": job_id,
|
||||
"message": f"Retrieved error logs for job {job_id}",
|
||||
"logs": logs_text
|
||||
}
|
||||
|
||||
return [types.TextContent(
|
||||
type="text",
|
||||
text=logs_text
|
||||
)]
|
||||
else:
|
||||
return [types.TextContent(
|
||||
type="text",
|
||||
text=f"Error: Failed to get error logs: {response.status_code} - {response.text}"
|
||||
)]
|
||||
|
||||
except Exception as e:
|
||||
return [types.TextContent(
|
||||
type="text",
|
||||
text=f"Error getting error logs: {str(e)}"
|
||||
)]
|
||||
|
||||
elif name == "search_job_logs":
|
||||
job_id = arguments.get("job_id")
|
||||
search_term = arguments.get("search_term")
|
||||
|
||||
if not job_id or not search_term:
|
||||
return [types.TextContent(
|
||||
type="text",
|
||||
text="Error: job_id and search_term are required"
|
||||
)]
|
||||
|
||||
# Use the search logs endpoint
|
||||
import requests
|
||||
base_url = os.getenv("BASE_URL", "http://localhost:8000")
|
||||
|
||||
params = {
|
||||
"q": search_term,
|
||||
"namespace": arguments.get("namespace", namespace),
|
||||
"log_type": arguments.get("log_type", "stderr"),
|
||||
"lines": arguments.get("lines", 50),
|
||||
"plain_text": True
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{base_url}/api/logs/search/{job_id}",
|
||||
params=params,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
logs_text = response.text
|
||||
|
||||
return [types.TextContent(
|
||||
type="text",
|
||||
text=logs_text
|
||||
)]
|
||||
else:
|
||||
return [types.TextContent(
|
||||
type="text",
|
||||
text=f"Error: Failed to search logs: {response.status_code} - {response.text}"
|
||||
)]
|
||||
|
||||
except Exception as e:
|
||||
return [types.TextContent(
|
||||
type="text",
|
||||
text=f"Error searching logs: {str(e)}"
|
||||
)]
|
||||
|
||||
elif name == "submit_job_file":
|
||||
file_content = arguments.get("file_content")
|
||||
file_type = arguments.get("file_type", "json")
|
||||
|
Reference in New Issue
Block a user