Spaces:
Sleeping
Sleeping
Deployment via uv
Browse files- lib/events.py +23 -2
- main.py +17 -5
- mcp_server/mcp_server.py +15 -0
- requirements.txt +4 -1
- utils/dapr_utils.py +65 -0
- utils/logging_config.py +44 -0
- utils/monitoring.py +57 -0
lib/events.py
CHANGED
|
@@ -2,6 +2,7 @@ from confluent_kafka import Producer
|
|
| 2 |
import json
|
| 3 |
import logging
|
| 4 |
import os
|
|
|
|
| 5 |
|
| 6 |
logger = logging.getLogger(__name__)
|
| 7 |
|
|
@@ -14,7 +15,7 @@ def delivery_report(err, msg):
|
|
| 14 |
|
| 15 |
def publish_task_event(event_type: str, task_data: dict) -> bool:
|
| 16 |
"""
|
| 17 |
-
Publish a task event to Kafka.
|
| 18 |
|
| 19 |
Args:
|
| 20 |
event_type: Type of event (e.g., 'task_created', 'task_completed', 'task_updated')
|
|
@@ -23,6 +24,26 @@ def publish_task_event(event_type: str, task_data: dict) -> bool:
|
|
| 23 |
Returns:
|
| 24 |
bool: True if event published successfully, False otherwise
|
| 25 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
try:
|
| 27 |
# Get Kafka configuration from environment variables
|
| 28 |
bootstrap_servers = os.getenv('KAFKA_BOOTSTRAP_SERVERS', 'localhost:9092')
|
|
@@ -60,7 +81,7 @@ def publish_task_event(event_type: str, task_data: dict) -> bool:
|
|
| 60 |
# callbacks to be triggered
|
| 61 |
producer.flush()
|
| 62 |
|
| 63 |
-
logger.info(f"Published {event_type} event for task: {task_data.get('id', 'unknown')}")
|
| 64 |
return True
|
| 65 |
|
| 66 |
except Exception as e:
|
|
|
|
| 2 |
import json
|
| 3 |
import logging
|
| 4 |
import os
|
| 5 |
+
from utils.dapr_utils import dapr_http_fallback
|
| 6 |
|
| 7 |
logger = logging.getLogger(__name__)
|
| 8 |
|
|
|
|
| 15 |
|
| 16 |
def publish_task_event(event_type: str, task_data: dict) -> bool:
|
| 17 |
"""
|
| 18 |
+
Publish a task event to Dapr pub/sub, with Kafka fallback if Dapr not available.
|
| 19 |
|
| 20 |
Args:
|
| 21 |
event_type: Type of event (e.g., 'task_created', 'task_completed', 'task_updated')
|
|
|
|
| 24 |
Returns:
|
| 25 |
bool: True if event published successfully, False otherwise
|
| 26 |
"""
|
| 27 |
+
# First, try to use Dapr sidecar if available
|
| 28 |
+
dapr_response = dapr_http_fallback(
|
| 29 |
+
endpoint="/v1.0/publish/task-pubsub/task-events",
|
| 30 |
+
method="POST",
|
| 31 |
+
data={
|
| 32 |
+
"event_type": event_type,
|
| 33 |
+
"task_data": task_data,
|
| 34 |
+
"timestamp": task_data.get('updated_at', task_data.get('created_at'))
|
| 35 |
+
}
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
if dapr_response is not None:
|
| 39 |
+
# Dapr succeeded
|
| 40 |
+
logger.info(f"Published {event_type} event via Dapr for task: {task_data.get('id', 'unknown')}")
|
| 41 |
+
return True
|
| 42 |
+
else:
|
| 43 |
+
# Dapr not available, fall back to Kafka
|
| 44 |
+
logger.info("Dapr sidecar not available, falling back to Kafka")
|
| 45 |
+
|
| 46 |
+
# Fallback to Kafka
|
| 47 |
try:
|
| 48 |
# Get Kafka configuration from environment variables
|
| 49 |
bootstrap_servers = os.getenv('KAFKA_BOOTSTRAP_SERVERS', 'localhost:9092')
|
|
|
|
| 81 |
# callbacks to be triggered
|
| 82 |
producer.flush()
|
| 83 |
|
| 84 |
+
logger.info(f"Published {event_type} event via Kafka for task: {task_data.get('id', 'unknown')}")
|
| 85 |
return True
|
| 86 |
|
| 87 |
except Exception as e:
|
main.py
CHANGED
|
@@ -1,27 +1,33 @@
|
|
| 1 |
import os
|
| 2 |
-
import logging
|
| 3 |
from fastapi import FastAPI
|
| 4 |
from fastapi.middleware.cors import CORSMiddleware
|
| 5 |
from routes import tasks, chat, chatkit, notifications
|
| 6 |
from mcp_server.mcp_server import mcp
|
| 7 |
from database import create_db_and_tables
|
| 8 |
from dotenv import load_dotenv
|
| 9 |
-
import asyncio
|
| 10 |
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
| 11 |
from apscheduler.triggers.interval import IntervalTrigger
|
| 12 |
from reminder_service import reminder_service
|
| 13 |
from recurring_service import recurring_task_service
|
| 14 |
from lib.consumer import run_consumer_in_thread
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
# Configure logging
|
| 17 |
-
|
| 18 |
-
logger =
|
| 19 |
|
| 20 |
# Load environment variables
|
| 21 |
load_dotenv()
|
| 22 |
|
|
|
|
| 23 |
app = FastAPI(title="Todo API on Hugging Face")
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
# Initialize scheduler
|
| 26 |
scheduler = AsyncIOScheduler()
|
| 27 |
|
|
@@ -72,8 +78,14 @@ def startup():
|
|
| 72 |
# Start the Kafka consumer in a background thread
|
| 73 |
run_consumer_in_thread()
|
| 74 |
logger.info("Kafka consumer started in background thread")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
except Exception as e:
|
| 76 |
-
logger.error(f"Error
|
| 77 |
raise
|
| 78 |
|
| 79 |
@app.on_event("shutdown")
|
|
|
|
| 1 |
import os
|
|
|
|
| 2 |
from fastapi import FastAPI
|
| 3 |
from fastapi.middleware.cors import CORSMiddleware
|
| 4 |
from routes import tasks, chat, chatkit, notifications
|
| 5 |
from mcp_server.mcp_server import mcp
|
| 6 |
from database import create_db_and_tables
|
| 7 |
from dotenv import load_dotenv
|
|
|
|
| 8 |
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
| 9 |
from apscheduler.triggers.interval import IntervalTrigger
|
| 10 |
from reminder_service import reminder_service
|
| 11 |
from recurring_service import recurring_task_service
|
| 12 |
from lib.consumer import run_consumer_in_thread
|
| 13 |
+
from utils.logging_config import setup_logging, get_logger
|
| 14 |
+
from utils.monitoring import start_monitoring_server
|
| 15 |
+
from prometheus_client import make_asgi_app
|
| 16 |
|
| 17 |
# Configure logging
|
| 18 |
+
setup_logging()
|
| 19 |
+
logger = get_logger(__name__)
|
| 20 |
|
| 21 |
# Load environment variables
|
| 22 |
load_dotenv()
|
| 23 |
|
| 24 |
+
# Create FastAPI app with monitoring
|
| 25 |
app = FastAPI(title="Todo API on Hugging Face")
|
| 26 |
|
| 27 |
+
# Add Prometheus metrics endpoint
|
| 28 |
+
metrics_app = make_asgi_app()
|
| 29 |
+
app.mount("/metrics", metrics_app)
|
| 30 |
+
|
| 31 |
# Initialize scheduler
|
| 32 |
scheduler = AsyncIOScheduler()
|
| 33 |
|
|
|
|
| 78 |
# Start the Kafka consumer in a background thread
|
| 79 |
run_consumer_in_thread()
|
| 80 |
logger.info("Kafka consumer started in background thread")
|
| 81 |
+
|
| 82 |
+
# Start monitoring server in a background thread
|
| 83 |
+
import threading
|
| 84 |
+
monitoring_thread = threading.Thread(target=start_monitoring_server, args=(8001,), daemon=True)
|
| 85 |
+
monitoring_thread.start()
|
| 86 |
+
logger.info("Monitoring server started on port 8001")
|
| 87 |
except Exception as e:
|
| 88 |
+
logger.error(f"Error during startup: {e}")
|
| 89 |
raise
|
| 90 |
|
| 91 |
@app.on_event("shutdown")
|
mcp_server/mcp_server.py
CHANGED
|
@@ -15,11 +15,13 @@ from schemas.input_output_validation import (
|
|
| 15 |
validate_output_format
|
| 16 |
)
|
| 17 |
from lib.events import publish_task_event
|
|
|
|
| 18 |
|
| 19 |
# Initialize the official FastMCP server
|
| 20 |
mcp = FastMCP("Focus Task Manager")
|
| 21 |
|
| 22 |
@mcp.tool()
|
|
|
|
| 23 |
def add_task(user_id: str, title: str, description: Optional[str] = None, priority: Optional[str] = "medium", tags: Optional[List[str]] = None, due_date: Optional[str] = None, is_recurring: Optional[bool] = None, recurrence_pattern: Optional[str] = None) -> str:
|
| 24 |
"""
|
| 25 |
Create a new task in the database.
|
|
@@ -85,8 +87,14 @@ def add_task(user_id: str, title: str, description: Optional[str] = None, priori
|
|
| 85 |
|
| 86 |
publish_task_event("task_created", task_data)
|
| 87 |
|
|
|
|
|
|
|
|
|
|
| 88 |
result = f"Success: Created task '{new_task.title}' with ID {new_task.id}"
|
| 89 |
return validate_output_format(result, "add_task")
|
|
|
|
|
|
|
|
|
|
| 90 |
finally:
|
| 91 |
session.close()
|
| 92 |
next(session_gen, None)
|
|
@@ -124,6 +132,7 @@ def list_tasks(user_id: str, status: str = "all") -> str:
|
|
| 124 |
next(session_gen, None)
|
| 125 |
|
| 126 |
@mcp.tool()
|
|
|
|
| 127 |
def complete_task(user_id: str, task_id: int) -> str:
|
| 128 |
"""
|
| 129 |
Mark a specific task as completed.
|
|
@@ -167,9 +176,15 @@ def complete_task(user_id: str, task_id: int) -> str:
|
|
| 167 |
|
| 168 |
publish_task_event("task_completed", task_data)
|
| 169 |
|
|
|
|
|
|
|
|
|
|
| 170 |
result = f"Success: Task {validated_inputs.task_id} marked as completed."
|
| 171 |
|
| 172 |
return validate_output_format(result, "complete_task")
|
|
|
|
|
|
|
|
|
|
| 173 |
finally:
|
| 174 |
session.close()
|
| 175 |
next(session_gen, None)
|
|
|
|
| 15 |
validate_output_format
|
| 16 |
)
|
| 17 |
from lib.events import publish_task_event
|
| 18 |
+
from utils.monitoring import TASK_CREATED_COUNTER, TASK_COMPLETED_COUNTER, TASK_ERRORS_COUNTER, monitor_task_event
|
| 19 |
|
| 20 |
# Initialize the official FastMCP server
|
| 21 |
mcp = FastMCP("Focus Task Manager")
|
| 22 |
|
| 23 |
@mcp.tool()
|
| 24 |
+
@monitor_task_event("task_created")
|
| 25 |
def add_task(user_id: str, title: str, description: Optional[str] = None, priority: Optional[str] = "medium", tags: Optional[List[str]] = None, due_date: Optional[str] = None, is_recurring: Optional[bool] = None, recurrence_pattern: Optional[str] = None) -> str:
|
| 26 |
"""
|
| 27 |
Create a new task in the database.
|
|
|
|
| 87 |
|
| 88 |
publish_task_event("task_created", task_data)
|
| 89 |
|
| 90 |
+
# Increment counter for created tasks
|
| 91 |
+
TASK_CREATED_COUNTER.inc()
|
| 92 |
+
|
| 93 |
result = f"Success: Created task '{new_task.title}' with ID {new_task.id}"
|
| 94 |
return validate_output_format(result, "add_task")
|
| 95 |
+
except Exception as e:
|
| 96 |
+
TASK_ERRORS_COUNTER.inc()
|
| 97 |
+
raise
|
| 98 |
finally:
|
| 99 |
session.close()
|
| 100 |
next(session_gen, None)
|
|
|
|
| 132 |
next(session_gen, None)
|
| 133 |
|
| 134 |
@mcp.tool()
|
| 135 |
+
@monitor_task_event("task_completed")
|
| 136 |
def complete_task(user_id: str, task_id: int) -> str:
|
| 137 |
"""
|
| 138 |
Mark a specific task as completed.
|
|
|
|
| 176 |
|
| 177 |
publish_task_event("task_completed", task_data)
|
| 178 |
|
| 179 |
+
# Increment counter for completed tasks
|
| 180 |
+
TASK_COMPLETED_COUNTER.inc()
|
| 181 |
+
|
| 182 |
result = f"Success: Task {validated_inputs.task_id} marked as completed."
|
| 183 |
|
| 184 |
return validate_output_format(result, "complete_task")
|
| 185 |
+
except Exception as e:
|
| 186 |
+
TASK_ERRORS_COUNTER.inc()
|
| 187 |
+
raise
|
| 188 |
finally:
|
| 189 |
session.close()
|
| 190 |
next(session_gen, None)
|
requirements.txt
CHANGED
|
@@ -14,4 +14,7 @@ openai-agents
|
|
| 14 |
python-dateutil
|
| 15 |
APScheduler
|
| 16 |
pywebpush
|
| 17 |
-
confluent-kafka
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
python-dateutil
|
| 15 |
APScheduler
|
| 16 |
pywebpush
|
| 17 |
+
confluent-kafka
|
| 18 |
+
dapr
|
| 19 |
+
dapr-ext-fastapi
|
| 20 |
+
python-json-logger
|
utils/dapr_utils.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Dapr utility functions for cloud-ready applications.
|
| 3 |
+
Provides graceful fallback when Dapr sidecar is not available.
|
| 4 |
+
"""
|
| 5 |
+
import os
|
| 6 |
+
import logging
|
| 7 |
+
import requests
|
| 8 |
+
import json
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
def dapr_http_fallback(endpoint: str, method: str = "POST", data=None, headers=None):
|
| 13 |
+
"""
|
| 14 |
+
Generic Dapr HTTP fallback function.
|
| 15 |
+
Tries to use Dapr sidecar, falls back gracefully if not available.
|
| 16 |
+
|
| 17 |
+
Args:
|
| 18 |
+
endpoint: Dapr endpoint (e.g., "/v1.0/publish/pubsub_name/topic_name")
|
| 19 |
+
method: HTTP method (GET, POST, PUT, DELETE)
|
| 20 |
+
data: Data to send in request body
|
| 21 |
+
headers: Additional headers to send
|
| 22 |
+
|
| 23 |
+
Returns:
|
| 24 |
+
Response from Dapr or fallback, or None if both fail
|
| 25 |
+
"""
|
| 26 |
+
dapr_port = os.getenv("DAPR_HTTP_PORT", "3500")
|
| 27 |
+
dapr_url = f"http://localhost:{dapr_port}{endpoint}"
|
| 28 |
+
|
| 29 |
+
# Check if Dapr sidecar is available
|
| 30 |
+
try:
|
| 31 |
+
dapr_health_url = f"http://localhost:{dapr_port}/v1.0/healthz"
|
| 32 |
+
health_response = requests.get(dapr_health_url, timeout=2)
|
| 33 |
+
|
| 34 |
+
if health_response.status_code == 200:
|
| 35 |
+
# Dapr sidecar is available, use it
|
| 36 |
+
req_headers = headers or {}
|
| 37 |
+
req_headers["Content-Type"] = "application/json"
|
| 38 |
+
|
| 39 |
+
if method.upper() == "POST":
|
| 40 |
+
response = requests.post(dapr_url, json=data, headers=req_headers, timeout=10)
|
| 41 |
+
elif method.upper() == "GET":
|
| 42 |
+
response = requests.get(dapr_url, headers=req_headers, timeout=10)
|
| 43 |
+
elif method.upper() == "PUT":
|
| 44 |
+
response = requests.put(dapr_url, json=data, headers=req_headers, timeout=10)
|
| 45 |
+
elif method.upper() == "DELETE":
|
| 46 |
+
response = requests.delete(dapr_url, headers=req_headers, timeout=10)
|
| 47 |
+
else:
|
| 48 |
+
logger.error(f"Unsupported HTTP method: {method}")
|
| 49 |
+
return None
|
| 50 |
+
|
| 51 |
+
if response.status_code in [200, 204]:
|
| 52 |
+
logger.debug(f"Dapr call successful: {method} {endpoint}")
|
| 53 |
+
return response
|
| 54 |
+
else:
|
| 55 |
+
logger.warning(f"Dapr call failed with status {response.status_code}, endpoint: {endpoint}")
|
| 56 |
+
return None
|
| 57 |
+
else:
|
| 58 |
+
logger.warning(f"Dapr sidecar not available, skipping Dapr call to {endpoint}")
|
| 59 |
+
return None
|
| 60 |
+
except requests.exceptions.RequestException:
|
| 61 |
+
logger.warning(f"Dapr sidecar not available, skipping Dapr call to {endpoint}")
|
| 62 |
+
return None
|
| 63 |
+
except Exception as e:
|
| 64 |
+
logger.warning(f"Dapr unavailable for {endpoint}: {str(e)}")
|
| 65 |
+
return None
|
utils/logging_config.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import sys
|
| 3 |
+
from pythonjsonlogger import jsonlogger
|
| 4 |
+
import os
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
def setup_logging():
|
| 8 |
+
"""Setup structured logging for the application"""
|
| 9 |
+
|
| 10 |
+
# Get log level from environment, default to INFO
|
| 11 |
+
log_level = os.getenv('LOG_LEVEL', 'INFO').upper()
|
| 12 |
+
|
| 13 |
+
# Create a custom JSON formatter
|
| 14 |
+
json_formatter = jsonlogger.JsonFormatter(
|
| 15 |
+
'%(asctime)s %(name)s %(levelname)s %(filename)s %(lineno)d %(message)s',
|
| 16 |
+
datefmt='%Y-%m-%dT%H:%M:%S'
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
# Setup root logger
|
| 20 |
+
root_logger = logging.getLogger()
|
| 21 |
+
root_logger.setLevel(getattr(logging, log_level))
|
| 22 |
+
|
| 23 |
+
# Clear any existing handlers
|
| 24 |
+
root_logger.handlers.clear()
|
| 25 |
+
|
| 26 |
+
# Create handler for stdout
|
| 27 |
+
handler = logging.StreamHandler(sys.stdout)
|
| 28 |
+
handler.setFormatter(json_formatter)
|
| 29 |
+
root_logger.addHandler(handler)
|
| 30 |
+
|
| 31 |
+
# Also setup specific loggers for different components
|
| 32 |
+
logging.getLogger('uvicorn').setLevel(getattr(logging, log_level))
|
| 33 |
+
logging.getLogger('uvicorn.access').setLevel(getattr(logging, log_level))
|
| 34 |
+
logging.getLogger('uvicorn.error').setLevel(getattr(logging, log_level))
|
| 35 |
+
logging.getLogger('fastapi').setLevel(getattr(logging, log_level))
|
| 36 |
+
logging.getLogger('sqlalchemy').setLevel(logging.WARNING) # Reduce SQLAlchemy noise
|
| 37 |
+
logging.getLogger('confluent_kafka').setLevel(getattr(logging, log_level))
|
| 38 |
+
|
| 39 |
+
def get_logger(name: str) -> logging.Logger:
|
| 40 |
+
"""Get a logger instance with the specified name"""
|
| 41 |
+
return logging.getLogger(name)
|
| 42 |
+
|
| 43 |
+
# Initialize logging when module is imported
|
| 44 |
+
setup_logging()
|
utils/monitoring.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from prometheus_client import Counter, Histogram, Gauge, start_http_server
|
| 2 |
+
import time
|
| 3 |
+
import logging
|
| 4 |
+
from functools import wraps
|
| 5 |
+
|
| 6 |
+
# Create loggers
|
| 7 |
+
logger = logging.getLogger(__name__)
|
| 8 |
+
|
| 9 |
+
# Define metrics
|
| 10 |
+
REQUEST_COUNT = Counter('http_requests_total', 'Total HTTP requests', ['method', 'endpoint', 'status_code'])
|
| 11 |
+
REQUEST_DURATION = Histogram('http_request_duration_seconds', 'Duration of HTTP requests in seconds', ['method', 'endpoint'])
|
| 12 |
+
ACTIVE_TASKS = Gauge('active_tasks_count', 'Number of active tasks')
|
| 13 |
+
TASK_EVENTS_PROCESSED = Counter('task_events_processed_total', 'Total task events processed', ['event_type'])
|
| 14 |
+
|
| 15 |
+
class MonitoringMiddleware:
|
| 16 |
+
"""Custom middleware to collect metrics for FastAPI application"""
|
| 17 |
+
|
| 18 |
+
def __init__(self):
|
| 19 |
+
self.logger = logging.getLogger(self.__class__.__name__)
|
| 20 |
+
|
| 21 |
+
def record_request(self, method: str, endpoint: str, status_code: int, duration: float):
|
| 22 |
+
"""Record request metrics"""
|
| 23 |
+
REQUEST_COUNT.labels(method=method, endpoint=endpoint, status_code=status_code).inc()
|
| 24 |
+
REQUEST_DURATION.labels(method=method, endpoint=endpoint).observe(duration)
|
| 25 |
+
|
| 26 |
+
self.logger.info(f"Request metrics recorded: {method} {endpoint} {status_code} {duration}s")
|
| 27 |
+
|
| 28 |
+
def monitor_task_event(event_type: str):
|
| 29 |
+
"""Decorator to monitor task event processing"""
|
| 30 |
+
def decorator(func):
|
| 31 |
+
@wraps(func)
|
| 32 |
+
def wrapper(*args, **kwargs):
|
| 33 |
+
start_time = time.time()
|
| 34 |
+
try:
|
| 35 |
+
result = func(*args, **kwargs)
|
| 36 |
+
TASK_EVENTS_PROCESSED.labels(event_type=event_type).inc()
|
| 37 |
+
duration = time.time() - start_time
|
| 38 |
+
logger.info(f"Task event {event_type} processed in {duration:.2f}s")
|
| 39 |
+
return result
|
| 40 |
+
except Exception as e:
|
| 41 |
+
logger.error(f"Error processing task event {event_type}: {str(e)}")
|
| 42 |
+
raise
|
| 43 |
+
return wrapper
|
| 44 |
+
return decorator
|
| 45 |
+
|
| 46 |
+
def start_monitoring_server(port: int = 8001):
|
| 47 |
+
"""Start the Prometheus metrics server"""
|
| 48 |
+
try:
|
| 49 |
+
start_http_server(port)
|
| 50 |
+
logger.info(f"Monitoring server started on port {port}")
|
| 51 |
+
except Exception as e:
|
| 52 |
+
logger.error(f"Failed to start monitoring server: {str(e)}")
|
| 53 |
+
|
| 54 |
+
# Predefined metrics for common operations
|
| 55 |
+
TASK_CREATED_COUNTER = Counter('tasks_created_total', 'Total tasks created')
|
| 56 |
+
TASK_COMPLETED_COUNTER = Counter('tasks_completed_total', 'Total tasks completed')
|
| 57 |
+
TASK_ERRORS_COUNTER = Counter('task_errors_total', 'Total task-related errors')
|