MuhammedSuhaib commited on
Commit
808e55f
·
verified ·
1 Parent(s): 187a9e5

Deployment via uv

Browse files
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
- logging.basicConfig(level=logging.INFO)
18
- logger = logging.getLogger(__name__)
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 creating database tables: {e}")
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')