Initial commit: Complete DevServer monitoring framework

- FastAPI application with OpenTelemetry instrumentation
- Redis caching and Celery task processing
- Docker Compose setup with monitoring stack
- Grafana dashboards and Prometheus metrics
- Jaeger distributed tracing
- Comprehensive documentation in docs/ folder
- Monitoring configuration and troubleshooting guides
This commit is contained in:
Hardik Sankhla
2025-10-13 09:55:17 +05:30
commit 2304c3ed12
32 changed files with 7146 additions and 0 deletions

27
.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
# Environment variables (may contain sensitive data)
.env
# Logs
logs/*.log
logs/**/*.log
# IDE files
.vscode/
.idea/
*.swp
*.swo
# OS files
.DS_Store
Thumbs.db

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.11

13
Dockerfile Normal file
View File

@@ -0,0 +1,13 @@
FROM python:3.10
WORKDIR /app
RUN pip install uv
RUN uv venv
ENV PATH="/app/.venv/bin:$PATH"
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-install-project
COPY . .
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

59
README.md Normal file
View File

@@ -0,0 +1,59 @@
# DevServer Monitoring Framework Documentation
Welcome to the documentation for **DevServer**, a comprehensive framework designed for API monitoring and observability. DevServer integrates key technologies such as Redis, RabbitMQ, OpenTelemetry, Grafana, Prometheus, and Jaeger to provide a robust pipeline for tracking, analyzing, and troubleshooting distributed systems.
## 📚 Documentation Index
### 🚀 Quick Start & Reference
- **[QUICK_REFERENCE.md](QUICK_REFERENCE.md)** — Essential commands and setup instructions for DevServer
- **[MONITORING_GUIDE.md](MONITORING_GUIDE.md)** — Step-by-step guide to configuring monitoring and observability
### 🔧 Troubleshooting & Issues
- **[ISSUES_FIXED_AND_TROUBLESHOOTING.md](ISSUES_FIXED_AND_TROUBLESHOOTING.md)** — Common issues, resolutions, and troubleshooting strategies
- **[CONTAINER_ISSUES_RESOLVED.md](CONTAINER_ISSUES_RESOLVED.md)** — Solutions for containerization and deployment problems
### 📦 Deployment & Operations
- **[DEPLOYMENT_STATUS.md](DEPLOYMENT_STATUS.md)** — Overview of deployment status and environment configuration
- **[DOCKER_CLEAN_REBUILD.md](DOCKER_CLEAN_REBUILD.md)** — Instructions for cleaning and rebuilding Docker containers
- **[MONITORING_COMPLETE.md](MONITORING_COMPLETE.md)** — Monitoring system setup completion checklist
## 🏗️ Project Structure
```text
DevServer/
├── docs/ # 📚 Documentation and guides
├── app/ # 🐍 FastAPI application for API endpoints
├── monitoring/ # 📊 Monitoring configs (Prometheus, Grafana, Jaeger)
├── logs/ # 📝 Centralized application logs
├── docker-compose.yml # 🐳 Multi-service orchestration (Redis, RabbitMQ, etc.)
├── Dockerfile # 🐳 Container build definition
└── pyproject.toml # 📦 Python dependencies and project metadata
```
## 🔗 Monitoring & Service Endpoints
- **API Application**: http://localhost:8000
- **Grafana Dashboard**: http://localhost:3000 (admin/admin)
- **Jaeger Tracing**: http://localhost:16686
- **Prometheus Metrics**: http://localhost:9090
- **RabbitMQ Management**: http://localhost:15672 (guest/guest)
- **Redis**: Accessible via configured port in `docker-compose.yml`
## 🛠️ Framework Features
- **Distributed Tracing**: End-to-end request tracing with OpenTelemetry and Jaeger
- **Metrics Collection**: Real-time metrics via Prometheus
- **Centralized Logging**: Unified logs for all services
- **Message Queue Monitoring**: RabbitMQ integration for event-driven architectures
- **Dashboard Visualization**: Grafana dashboards for system health and performance
## 📞 Support
For help, consult the troubleshooting guide or review logs in the `logs/` directory. For advanced monitoring or integration questions, refer to the relevant documentation in the `docs/` folder.
---
**Last updated: October 13, 2025**

0
app/__init__.py Normal file
View File

434
app/client.py Normal file
View File

@@ -0,0 +1,434 @@
"""
DevServer API Client
Demonstrates usage of all API endpoints with Redis and Celery integration
"""
import requests
import time
import json
import logging
from typing import Optional, Dict, Any
from datetime import datetime
# Setup client logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s | %(levelname)-8s | %(message)s',
datefmt='%H:%M:%S'
)
logger = logging.getLogger("devserver_client")
class DevServerClient:
"""Client for interacting with DevServer API"""
def __init__(self, base_url: str = "http://localhost:8000"):
self.base_url = base_url
self.session = requests.Session()
logger.info(f"DevServer client initialized: {base_url}")
def get_api_info(self) -> Dict[str, Any]:
"""Get API information and available endpoints"""
logger.debug("Fetching API information...")
response = self.session.get(f"{self.base_url}/")
response.raise_for_status()
logger.info("API information retrieved")
return response.json()
def roll_dice(self) -> Dict[str, Any]:
"""Roll dice and get results"""
logger.debug("Rolling dice...")
response = self.session.get(f"{self.base_url}/api/dice/roll")
response.raise_for_status()
result = response.json()
total_rolls = result.get('total', 0)
logger.info(f"Dice rolled: {total_rolls} total rolls")
return result
def create_fibonacci_task(self, n: int) -> Dict[str, Any]:
"""
Create a Celery task to calculate Fibonacci number
Args:
n: Number to calculate (0-40)
Returns:
Dict containing task_id and status
"""
logger.debug(f"Creating Fibonacci task: n={n}")
response = self.session.post(f"{self.base_url}/api/tasks/fibonacci?n={n}")
response.raise_for_status()
result = response.json()
logger.info(f"Fibonacci task created: task_id={result['task_id']}")
return result
def get_task_result(self, task_id: str) -> Dict[str, Any]:
"""
Get the result of a Celery task
Args:
task_id: Task ID returned from create_fibonacci_task
Returns:
Dict containing task status and result (if completed)
"""
logger.debug(f"Checking task status: task_id={task_id}")
response = self.session.get(f"{self.base_url}/api/tasks/{task_id}")
response.raise_for_status()
result = response.json()
status = result.get('status')
logger.debug(f"Task status: {status}")
return result
def wait_for_task(self, task_id: str, timeout: int = 30, poll_interval: float = 1.0) -> Dict[str, Any]:
"""
Wait for a task to complete with timeout
Args:
task_id: Task ID to wait for
timeout: Maximum seconds to wait
poll_interval: Seconds between status checks
Returns:
Final task result
Raises:
TimeoutError: If task doesn't complete within timeout
"""
logger.info(f"Waiting for task completion: task_id={task_id}, timeout={timeout}s")
start_time = time.time()
checks = 0
while time.time() - start_time < timeout:
checks += 1
result = self.get_task_result(task_id)
if result.get("status") == "completed":
elapsed = time.time() - start_time
logger.info(f"Task completed after {elapsed:.2f}s ({checks} checks)")
return result
time.sleep(poll_interval)
logger.error(f"Task timeout after {timeout}s")
raise TimeoutError(f"Task {task_id} did not complete within {timeout} seconds")
def set_cache(self, key: str, value: str, ttl: int = 300) -> Dict[str, Any]:
"""
Set a value in Redis cache
Args:
key: Cache key
value: Value to store
ttl: Time to live in seconds (default 300)
Returns:
Dict containing success message
"""
logger.debug(f"Setting cache: key={key}, ttl={ttl}s")
response = self.session.post(
f"{self.base_url}/api/cache/set",
params={"key": key, "value": value, "ttl": ttl}
)
response.raise_for_status()
logger.info(f"Cache set: key={key}")
return response.json()
def get_cache(self, key: str) -> Optional[Dict[str, Any]]:
"""
Get a value from Redis cache
Args:
key: Cache key
Returns:
Dict containing key and value, or None if not found
"""
logger.debug(f"Getting cache: key={key}")
try:
response = self.session.get(f"{self.base_url}/api/cache/{key}")
response.raise_for_status()
logger.info(f"Cache hit: key={key}")
return response.json()
except requests.exceptions.HTTPError as e:
if e.response.status_code == 404:
logger.warning(f"Cache miss: key={key}")
return None
raise
def process_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""
Process data with Redis caching and Celery
Args:
data: Dictionary of data to process
Returns:
Dict with source (cache/celery_task) and result or task_id
"""
logger.debug(f"Processing data: {len(data)} keys")
response = self.session.post(
f"{self.base_url}/api/process",
json=data
)
response.raise_for_status()
result = response.json()
source = result.get('source')
logger.info(f"Data processing initiated: source={source}")
return result
def demo_basic_endpoints():
"""Demonstrate basic API endpoints"""
print("=" * 60)
print("DEMO: Basic Endpoints")
print("=" * 60)
client = DevServerClient()
# 1. Get API info
print("\n1. Getting API information...")
info = client.get_api_info()
print(f" API: {info['message']}")
print(f" Available endpoints: {len(info['endpoints'])}")
# 2. Roll dice
print("\n2. Rolling dice...")
result = client.roll_dice()
print(f" Dice rolls: {result['dice_rolls']}")
print(f" Total rolls: {sum(len(rolls) for rolls in result['dice_rolls'])}")
def demo_celery_tasks():
"""Demonstrate Celery task functionality"""
print("\n" + "=" * 60)
print("DEMO: Celery Tasks (Fibonacci)")
print("=" * 60)
client = DevServerClient()
# Calculate multiple Fibonacci numbers
fibonacci_numbers = [5, 10, 15, 20]
print("\n1. Submitting Fibonacci tasks...")
tasks = []
for n in fibonacci_numbers:
result = client.create_fibonacci_task(n)
tasks.append((n, result['task_id']))
print(f" Fibonacci({n}): Task ID = {result['task_id']}")
print("\n2. Waiting for tasks to complete...")
for n, task_id in tasks:
try:
result = client.wait_for_task(task_id, timeout=30)
fib_result = result['result']
print(f" Fibonacci({n}) = {fib_result['fibonacci']}")
except TimeoutError as e:
print(f" ERROR: {e}")
def demo_redis_cache():
"""Demonstrate Redis caching functionality"""
print("\n" + "=" * 60)
print("DEMO: Redis Cache Operations")
print("=" * 60)
client = DevServerClient()
# 1. Set cache values
print("\n1. Setting cache values...")
cache_data = [
("user:1", "Alice", 60),
("user:2", "Bob", 60),
("config:theme", "dark", 300),
("session:abc123", "active", 3600),
]
for key, value, ttl in cache_data:
result = client.set_cache(key, value, ttl)
print(f" Set {key} = {value} (TTL: {ttl}s)")
# 2. Get cache values
print("\n2. Getting cache values...")
for key, expected_value, _ in cache_data:
result = client.get_cache(key)
if result:
print(f" Get {key} = {result['value']}")
else:
print(f" Get {key} = NOT FOUND")
# 3. Try to get non-existent key
print("\n3. Getting non-existent key...")
result = client.get_cache("nonexistent:key")
print(f" Result: {result}")
def demo_combined_workflow():
"""Demonstrate combined Redis + Celery workflow"""
print("\n" + "=" * 60)
print("DEMO: Combined Redis + Celery Workflow")
print("=" * 60)
client = DevServerClient()
test_data = {
"name": "test_user",
"email": "test@example.com",
"age": 25,
"interests": ["coding", "music", "sports"]
}
# 1. First request - should process with Celery
print("\n1. First request (cache miss - will process with Celery)...")
result1 = client.process_data(test_data)
print(f" Source: {result1['source']}")
if result1['source'] == 'celery_task':
task_id = result1['task_id']
print(f" Task ID: {task_id}")
print(" Waiting for task to complete...")
task_result = client.wait_for_task(task_id, timeout=30)
print(f" Result: {json.dumps(task_result['result'], indent=2)}")
# 2. Second request with same data - should return from cache
print("\n2. Second request (cache hit - should be instant)...")
time.sleep(2) # Wait a bit for cache to be set
result2 = client.process_data(test_data)
print(f" Source: {result2['source']}")
if result2['source'] == 'cache':
print(f" Cached Result: {json.dumps(result2['result'], indent=2)}")
print(" Cache working correctly!")
def demo_performance_comparison():
"""Compare performance with and without caching"""
print("\n" + "=" * 60)
print("DEMO: Performance Comparison (Cache vs No Cache)")
print("=" * 60)
client = DevServerClient()
# Test data
test_data = {
"operation": "performance_test",
"data_size": "medium",
"timestamp": time.time()
}
# First request (no cache)
print("\n1. First request (processing with Celery)...")
start_time = time.time()
result1 = client.process_data(test_data)
first_duration = 0.0
if result1['source'] == 'celery_task':
task_result = client.wait_for_task(result1['task_id'])
first_duration = time.time() - start_time
print(f" Duration: {first_duration:.2f} seconds")
else:
print(" Unexpected: got cached result on first try")
return
# Second request (with cache)
print("\n2. Second request (from cache)...")
start_time = time.time()
result2 = client.process_data(test_data)
second_duration = time.time() - start_time
print(f" Duration: {second_duration:.2f} seconds")
if first_duration > 0:
speedup = first_duration / second_duration if second_duration > 0 else 0
print(f"\n Cache Speedup: {speedup:.1f}x faster!")
def run_simple_demo():
"""Run a simple demo of the main features"""
print("\n" + "=" * 60)
print("DevServer API - Simple Demo")
print("=" * 60)
print("\nTesting: FastAPI + Redis + Celery + OpenTelemetry")
print("View traces at: http://localhost:16686")
print("=" * 60)
start_time = time.time()
try:
logger.info("Starting DevServer demo...")
client = DevServerClient()
# 1. Test basic endpoint
print("\n1. Testing dice roll endpoint...")
logger.info("TEST 1: Dice roll")
result = client.roll_dice()
print(f" Dice rolls: {result['dice_rolls']}")
logger.info(f"Result: {sum(len(r) for r in result['dice_rolls'])} dice rolled")
# 2. Test Redis cache
print("\n2. Testing Redis cache...")
logger.info("TEST 2: Redis caching")
test_key = f"test_key_{int(time.time())}"
test_value = f"test_value_{datetime.now().isoformat()}"
client.set_cache(test_key, test_value, 60)
cached = client.get_cache(test_key)
if cached:
print(f" Cached value: {cached['value']}")
logger.info(f"Cache verified: key={test_key}")
else:
print(" Cache not found")
logger.warning("Cache verification failed")
# 3. Test Celery task
print("\n3. Testing Celery task (Fibonacci)...")
logger.info("TEST 3: Celery task")
task = client.create_fibonacci_task(10)
print(f" Task submitted: {task['task_id']}")
logger.info(f"Fibonacci task submitted: task_id={task['task_id']}")
result = client.wait_for_task(task['task_id'], timeout=30)
fib_value = result['result']['fibonacci']
calc_time = result['result'].get('calculation_time', 'N/A')
print(f" Fibonacci(10) = {fib_value} (calculated in {calc_time})")
logger.info(f"Fibonacci result: {fib_value}")
elapsed = time.time() - start_time
print("\n" + "=" * 60)
print(f"All tests passed in {elapsed:.2f}s!")
print("Check Jaeger UI at: http://localhost:16686")
print("=" * 60)
logger.info(f"Demo completed successfully in {elapsed:.2f}s")
except requests.exceptions.ConnectionError:
print("\nERROR: Cannot connect to DevServer")
print(" Run: docker-compose up -d")
logger.error("Connection error: DevServer not reachable")
except Exception as e:
print(f"\nERROR: {e}")
logger.error(f"Demo failed: {e}", exc_info=True)
import traceback
traceback.print_exc()
# Uncomment to run all detailed demos
# def run_all_demos():
# """Run all demonstration scenarios"""
# demo_basic_endpoints()
# time.sleep(1)
# demo_redis_cache()
# time.sleep(1)
# demo_celery_tasks()
# time.sleep(1)
# demo_combined_workflow()
# time.sleep(1)
# demo_performance_comparison()
if __name__ == "__main__":
run_simple_demo()
# Uncomment to run all detailed demos:
# run_all_demos()

205
app/logging.py Normal file
View File

@@ -0,0 +1,205 @@
import logging
import sys
from enum import StrEnum
from pathlib import Path
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
from datetime import datetime
import json
class LogLevel(StrEnum):
DEBUG = "DEBUG"
INFO = "INFO"
WARNING = "WARNING"
ERROR = "ERROR"
CRITICAL = "CRITICAL"
class CustomFormatter(logging.Formatter):
"""Custom formatter with colors and detailed information"""
# ANSI color codes
COLORS = {
'DEBUG': '\033[36m', # Cyan
'INFO': '\033[32m', # Green
'WARNING': '\033[33m', # Yellow
'ERROR': '\033[31m', # Red
'CRITICAL': '\033[35m', # Magenta
'RESET': '\033[0m' # Reset
}
def format(self, record):
# Add color to level name
if hasattr(sys.stdout, 'isatty') and sys.stdout.isatty():
levelname = record.levelname
if levelname in self.COLORS:
record.levelname = f"{self.COLORS[levelname]}{levelname}{self.COLORS['RESET']}"
# Add extra context
record.service = getattr(record, 'service', 'unknown')
record.trace_id = getattr(record, 'trace_id', 'N/A')
record.span_id = getattr(record, 'span_id', 'N/A')
return super().format(record)
class JSONFormatter(logging.Formatter):
"""JSON formatter for structured logging"""
def format(self, record):
log_data = {
'timestamp': datetime.utcnow().isoformat(),
'level': record.levelname,
'logger': record.name,
'message': record.getMessage(),
'module': record.module,
'function': record.funcName,
'line': record.lineno,
'process': record.process,
'thread': record.thread,
'service': getattr(record, 'service', 'unknown'),
'trace_id': getattr(record, 'trace_id', None),
'span_id': getattr(record, 'span_id', None),
}
# Add exception info if present
if record.exc_info:
log_data['exception'] = self.formatException(record.exc_info)
# Add extra fields
if hasattr(record, 'extra_data'):
log_data['extra'] = record.extra_data # type: ignore
return json.dumps(log_data)
def setup_logging(
service_name: str = "devserver",
log_level: str = "INFO",
log_dir: str = "logs",
enable_console: bool = True,
enable_file: bool = True,
enable_json: bool = True
) -> logging.Logger:
"""
Setup comprehensive logging configuration
Args:
service_name: Name of the service for logging context
log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
log_dir: Directory to store log files
enable_console: Enable console output
enable_file: Enable file output
enable_json: Enable JSON structured logging
Returns:
Configured logger instance
"""
# Create logs directory
log_path = Path(log_dir)
log_path.mkdir(exist_ok=True)
# Get root logger
logger = logging.getLogger(service_name)
logger.setLevel(getattr(logging, log_level.upper()))
logger.handlers.clear()
# Console Handler with colors
if enable_console:
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.DEBUG)
console_format = CustomFormatter(
fmt='%(asctime)s | %(levelname)-8s | %(service)-12s | %(name)-20s | %(funcName)-25s:%(lineno)-4d | %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
console_handler.setFormatter(console_format)
logger.addHandler(console_handler)
# File Handler (Rotating) - Text format
if enable_file:
file_handler = RotatingFileHandler(
filename=log_path / f"{service_name}.log",
maxBytes=10 * 1024 * 1024, # 10MB
backupCount=5,
encoding='utf-8'
)
file_handler.setLevel(logging.DEBUG)
file_format = logging.Formatter(
fmt='%(asctime)s | %(levelname)-8s | %(service)-12s | %(name)-20s | %(module)-15s | %(funcName)-25s:%(lineno)-4d | TraceID:%(trace_id)s | %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
file_handler.setFormatter(file_format)
logger.addHandler(file_handler)
# JSON File Handler (Daily rotation) - Structured logging
if enable_json:
json_handler = TimedRotatingFileHandler(
filename=log_path / f"{service_name}_json.log",
when='midnight',
interval=1,
backupCount=30,
encoding='utf-8'
)
json_handler.setLevel(logging.INFO)
json_handler.setFormatter(JSONFormatter())
logger.addHandler(json_handler)
# Error Handler (Separate file for errors)
error_handler = RotatingFileHandler(
filename=log_path / f"{service_name}_errors.log",
maxBytes=5 * 1024 * 1024, # 5MB
backupCount=5,
encoding='utf-8'
)
error_handler.setLevel(logging.ERROR)
error_format = logging.Formatter(
fmt='%(asctime)s | %(levelname)-8s | %(service)-12s | %(name)-20s | %(module)-15s | %(funcName)-25s:%(lineno)-4d | TraceID:%(trace_id)s\nMessage: %(message)s\n%(pathname)s\n',
datefmt='%Y-%m-%d %H:%M:%S'
)
error_handler.setFormatter(error_format)
logger.addHandler(error_handler)
# Set default service name
old_factory = logging.getLogRecordFactory()
def record_factory(*args, **kwargs):
record = old_factory(*args, **kwargs)
record.service = service_name
record.trace_id = 'N/A'
record.span_id = 'N/A'
return record
logging.setLogRecordFactory(record_factory)
logger.info(f"Logging initialized for service: {service_name}")
logger.info(f"Log level: {log_level}")
logger.info(f"Log directory: {log_path.absolute()}")
return logger
def get_logger(name: str) -> logging.Logger:
"""Get a logger instance with the specified name"""
return logging.getLogger(name)
def log_with_trace(logger: logging.Logger, level: str, message: str, trace_id: str | None = None, span_id: str | None = None, **kwargs):
"""
Log a message with OpenTelemetry trace context
Args:
logger: Logger instance
level: Log level (debug, info, warning, error, critical)
message: Log message
trace_id: OpenTelemetry trace ID
span_id: OpenTelemetry span ID
**kwargs: Additional context data
"""
extra = {
'trace_id': trace_id or 'N/A',
'span_id': span_id or 'N/A',
'extra_data': kwargs
}
log_func = getattr(logger, level.lower())
log_func(message, extra=extra)

723
app/main.py Normal file
View File

@@ -0,0 +1,723 @@
from fastapi import FastAPI, HTTPException, Request, Path
from random import randint
import copy
import json
import redis
from celery.result import AsyncResult
from opentelemetry import trace
from app.logging import setup_logging, get_logger, log_with_trace
from prometheus_fastapi_instrumentator import Instrumentator
import time
# Setup logging
logger = setup_logging(service_name="devserver.api.fastapi", log_level="DEBUG")
tracer = trace.get_tracer("devserver.api.tracer")
# Initialize Redis client
logger.info("Initializing Redis connection...")
try:
redis_client = redis.Redis(host='redis', port=6379, db=0, decode_responses=True)
redis_client.ping()
logger.info("Redis connection established successfully")
except Exception as e:
logger.error(f"Failed to connect to Redis: {e}", exc_info=True)
raise
app = FastAPI(
title="DevServer API",
version="1.0.0",
description="FastAPI with OpenTelemetry, Redis, and Celery"
)
# ============================================================================
# INITIALIZE OPENTELEMETRY - Explicit Instrumentation
# ============================================================================
# Import and apply OpenTelemetry instrumentation BEFORE any requests
from app.opentelemetry_config import instrument_fastapi_app
logger.info("=" * 80)
logger.info("🔧 Applying OpenTelemetry Instrumentation...")
logger.info("=" * 80)
try:
instrument_fastapi_app(app)
logger.info("=" * 80)
logger.info("✅ OpenTelemetry instrumentation applied successfully!")
logger.info("=" * 80)
except Exception as e:
logger.error("=" * 80)
logger.error(f"❌ Failed to apply OpenTelemetry instrumentation: {e}")
logger.error("⚠️ Traces will NOT be generated!")
logger.error("=" * 80)
# ============================================================================
# Initialize Prometheus metrics
instrumentator = Instrumentator(
should_group_status_codes=False,
should_ignore_untemplated=True,
should_respect_env_var=False,
should_instrument_requests_inprogress=True,
excluded_handlers=[".*admin.*", "/metrics"],
env_var_name="ENABLE_METRICS",
inprogress_name="http_requests_inprogress",
inprogress_labels=True,
)
instrumentator.instrument(app).expose(app, endpoint="/metrics")
dice_rolls = [[], [], []]
# Middleware to log all requests
@app.middleware("http")
async def log_requests(request: Request, call_next):
"""Log all incoming requests with trace context"""
start_time = time.time()
# Get trace context
span = trace.get_current_span()
span_context = span.get_span_context()
trace_id = format(span_context.trace_id, '032x') if span_context.trace_id else 'N/A'
logger.info(f"→ Incoming request: {request.method} {request.url.path}")
logger.debug(f" TraceID: {trace_id}")
logger.debug(f" Client: {request.client.host if request.client else 'unknown'}")
response = await call_next(request)
duration = time.time() - start_time
logger.info(f"← Response: {response.status_code} | Duration: {duration:.3f}s | {request.method} {request.url.path}")
return response
@app.on_event("startup")
async def startup_event():
"""Log application startup"""
logger.info("=" * 60)
logger.info("DevServer FastAPI Application Starting...")
logger.info("=" * 60)
logger.info("Service: devserver.api.fastapi")
logger.info("OpenTelemetry tracing: ENABLED")
logger.info("Redis caching: ENABLED")
logger.info("Celery tasks: ENABLED")
logger.info("API Base URL: http://localhost:8000")
logger.info("=" * 60)
@app.on_event("shutdown")
async def shutdown_event():
"""Log application shutdown"""
logger.info("=" * 60)
logger.info("DevServer FastAPI Application Shutting Down...")
logger.info("=" * 60)
@app.get("/", tags=["Info"])
async def root():
"""Root endpoint with basic info."""
logger.info("API information requested")
return {
"service": "devserver.api.fastapi",
"message": "DevServer API with OpenTelemetry, Redis, and Celery",
"version": "1.0.0",
"endpoints": {
"info": {
"GET /": "API information",
"GET /health": "Health check",
"GET /docs": "Interactive API documentation (Swagger UI)"
},
"dice_game": {
"GET /api/dice/roll?session_id=xxx&use_cache=true": "Roll dice with Redis caching and session tracking",
"Description": "Returns 15 dice rolls with statistics. Use session_id for unique sessions, use_cache to enable/disable caching (5min TTL)"
},
"celery_tasks": {
"POST /api/tasks/fibonacci?n={number}&use_cache=true": "Calculate Fibonacci with Redis caching",
"GET /api/tasks/{task_id}": "Get task result by ID",
"Description": "Fibonacci results are cached for 1 hour. Set use_cache=false to force recalculation"
},
"redis_cache": {
"POST /api/cache/set": "Set cache value",
"GET /api/cache/{key}": "Get cache value",
"DELETE /api/cache/{key}": "Delete cache entry",
"GET /api/cache/keys?pattern=*": "List all cache keys",
"GET /api/cache/stats": "Get Redis statistics",
"POST /api/cache/flush": "Clear all cache (danger)",
"POST /api/cache/batch-set": "Set multiple cache values",
"GET /api/cache/batch-get?keys=k1,k2": "Get multiple cache values"
},
"data_processing": {
"POST /api/process": "Process data with cache + Celery workflow"
}
},
"monitoring": {
"jaeger_ui": "http://localhost:16686",
"rabbitmq_ui": "http://localhost:15672"
}
}
@app.get("/health", tags=["Info"])
async def health_check():
"""Health check endpoint"""
with tracer.start_as_current_span("health_check") as span:
span.set_attribute("service.name", "devserver.api.fastapi")
# Check Redis
try:
redis_client.ping()
redis_status = "healthy"
except Exception as e:
redis_status = f"unhealthy: {e}"
logger.error(f"Redis health check failed: {e}")
logger.debug("Health check performed")
return {
"status": "healthy",
"service": "devserver.api.fastapi",
"redis": redis_status
}
@app.get("/api/dice/roll", tags=["Dice Game"])
async def roll_dice(session_id: str = "default", use_cache: bool = True):
"""Roll dice 15 times with distributed tracing and Redis caching.
Args:
session_id: Unique session identifier for caching results
use_cache: If True, check cache first and store results
"""
with tracer.start_as_current_span("api.dice.roll") as span:
span.set_attribute("service.name", "devserver.api.fastapi")
span.set_attribute("operation", "dice_roll")
span.set_attribute("session_id", session_id)
span.set_attribute("use_cache", use_cache)
cache_key = f"dice:session:{session_id}"
logger.info(f"Received dice roll request: session_id={session_id}, use_cache={use_cache}")
# Check cache first if enabled
if use_cache:
try:
cached_result = redis_client.get(cache_key)
if cached_result:
span.set_attribute("cache.hit", True)
logger.info(f"Cache hit! Returning cached dice rolls for session: {session_id}")
result = json.loads(str(cached_result))
result["source"] = "cache"
result["cache_key"] = cache_key
return result
except Exception as e:
logger.warning(f"Cache check failed: {e}")
span.set_attribute("cache.hit", False)
logger.info("Cache miss - rolling new dice")
count = 0
# Roll a dice 15 times and store the results in three separate lists
while count < 15:
if count < 5:
dice_rolls[0].append(roll()) # Append to the first list
elif count < 10:
dice_rolls[1].append(roll()) # Append to the second list
else:
dice_rolls[2].append(roll()) # Append to the third list
count += 1
# Store the results to return
results = copy.deepcopy(dice_rolls)
total_rolls = sum(len(r) for r in results)
span.set_attribute("dice.total_rolls", total_rolls)
# Calculate statistics
flat_results = [roll for sublist in results for roll in sublist]
avg_roll = sum(flat_results) / len(flat_results)
logger.info(f"Dice rolled successfully: {total_rolls} total rolls, avg={avg_roll:.2f}")
logger.debug(f"Roll results: {results}")
# Clear the original lists for the next request
dice_rolls[0] = []
dice_rolls[1] = []
dice_rolls[2] = []
response = {
"service": "devserver.api.fastapi",
"operation": "dice_roll",
"session_id": session_id,
"dice_rolls": results,
"total": total_rolls,
"statistics": {
"average": round(avg_roll, 2),
"min": min(flat_results),
"max": max(flat_results),
"sum": sum(flat_results)
},
"source": "fresh",
"cache_key": cache_key
}
# Store in cache if enabled (TTL: 5 minutes)
if use_cache:
try:
redis_client.setex(cache_key, 300, json.dumps(response))
span.set_attribute("cache.stored", True)
logger.info(f"Stored dice rolls in cache: {cache_key} (TTL: 300s)")
except Exception as e:
logger.error(f"Failed to store in cache: {e}")
return response
def roll():
"""Simulates rolling a six-sided dice with tracing."""
with tracer.start_as_current_span("dice.single_roll") as roll_span:
result = randint(1, 6)
roll_span.set_attribute("dice.value", result)
roll_span.set_attribute("service.name", "devserver.api.fastapi")
logger.debug(f"Single dice roll: {result}")
return result
@app.post("/api/tasks/fibonacci", tags=["Celery Tasks"])
async def create_fibonacci_task(n: int, use_cache: bool = True):
"""Create a Celery task to calculate Fibonacci number with Redis caching.
Args:
n: The Fibonacci number to calculate (0-40)
use_cache: If True, check cache before creating task
"""
with tracer.start_as_current_span("api.tasks.fibonacci.create") as span:
span.set_attribute("service.name", "devserver.api.fastapi")
span.set_attribute("operation", "create_task")
span.set_attribute("task.type", "fibonacci")
span.set_attribute("fibonacci.n", n)
span.set_attribute("use_cache", use_cache)
logger.info(f"Creating Fibonacci task for n={n}, use_cache={use_cache}")
if n < 0 or n > 40:
logger.warning(f"Invalid Fibonacci number requested: n={n}")
raise HTTPException(status_code=400, detail="n must be between 0 and 40")
cache_key = f"fibonacci:result:{n}"
# Check cache first if enabled
if use_cache:
try:
cached_result = redis_client.get(cache_key)
if cached_result:
span.set_attribute("cache.hit", True)
span.set_attribute("result.source", "cache")
result_value = int(str(cached_result))
logger.info(f"Cache hit! Returning cached Fibonacci({n}) = {result_value}")
return {
"service": "devserver.api.fastapi",
"operation": "fibonacci",
"n": n,
"result": result_value,
"source": "cache",
"cache_key": cache_key,
"status": "completed"
}
except (ValueError, TypeError) as e:
logger.warning(f"Cache value conversion failed: {e}")
except Exception as e:
logger.warning(f"Cache check failed: {e}")
span.set_attribute("cache.hit", False)
logger.debug(f"Cache miss - submitting Fibonacci calculation task to Celery: n={n}")
from app.worker import calculate_fibonacci
task = calculate_fibonacci.delay(n) # type: ignore
span.set_attribute("task.id", task.id)
span.set_attribute("task.status", "submitted")
logger.info(f"Fibonacci task created successfully: task_id={task.id}, n={n}")
return {
"service": "devserver.api.fastapi",
"operation": "create_fibonacci_task",
"n": n,
"task_id": task.id,
"status": "submitted",
"source": "celery_task",
"message": f"Calculating fibonacci({n})",
"check_result_at": f"/api/tasks/{task.id}",
"cache_key": cache_key
}
@app.get("/api/tasks/{task_id}", tags=["Celery Tasks"])
async def get_task_result(task_id: str):
"""Get the result of a Celery task."""
with tracer.start_as_current_span("api.tasks.get_result") as span:
span.set_attribute("service.name", "devserver.api.fastapi")
span.set_attribute("operation", "get_task_result")
span.set_attribute("task.id", task_id)
logger.info(f"Checking task status: task_id={task_id}")
from app.worker import app as celery_app
try:
task_result = AsyncResult(task_id, app=celery_app)
if task_result.ready():
result = task_result.result
span.set_attribute("task.status", "completed")
logger.info(f"Task completed: task_id={task_id}")
logger.debug(f"Task result: {result}")
return {
"service": "devserver.api.fastapi",
"operation": "get_task_result",
"task_id": task_id,
"status": "completed",
"result": result
}
else:
span.set_attribute("task.status", "pending")
logger.debug(f"Task still pending: task_id={task_id}")
return {
"service": "devserver.api.fastapi",
"operation": "get_task_result",
"task_id": task_id,
"status": "pending",
"message": "Task is still processing"
}
except Exception as e:
logger.error(f"Error retrieving task result: task_id={task_id}, error={e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Error retrieving task: {str(e)}")
@app.post("/api/cache/set", tags=["Redis Cache"])
async def set_cache(key: str, value: str, ttl: int = 300):
"""Set a value in Redis cache with optional TTL."""
with tracer.start_as_current_span("api.cache.set") as span:
span.set_attribute("service.name", "devserver.api.fastapi")
span.set_attribute("operation", "cache_set")
span.set_attribute("cache.key", key)
span.set_attribute("cache.ttl", ttl)
logger.info(f"Setting cache: key={key}, ttl={ttl}s")
try:
redis_client.setex(key, ttl, value)
span.set_attribute("cache.operation", "success")
logger.info(f"Cache set successfully: key={key}")
logger.debug(f"Cache value: {value[:50]}..." if len(value) > 50 else f"Cache value: {value}")
return {
"service": "devserver.api.fastapi",
"operation": "cache_set",
"message": "Cache set successfully",
"key": key,
"ttl": ttl
}
except Exception as e:
span.set_attribute("cache.operation", "failed")
logger.error(f"Failed to set cache: key={key}, error={e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Cache error: {str(e)}")
@app.get("/api/cache/batch-get", tags=["Redis Cache"])
async def batch_get_cache(keys: str):
"""Get multiple cache values at once (comma-separated keys)"""
with tracer.start_as_current_span("api.cache.batch_get") as span:
span.set_attribute("service.name", "devserver.api.fastapi")
span.set_attribute("operation", "cache_batch_get")
key_list = [k.strip() for k in keys.split(',')]
span.set_attribute("cache.batch_size", len(key_list))
logger.info(f"Batch getting {len(key_list)} cache entries")
try:
pipeline = redis_client.pipeline()
for key in key_list:
pipeline.get(key)
values = pipeline.execute()
result = {}
found_count = 0
for key, value in zip(key_list, values):
if value is not None:
result[key] = value
found_count += 1
span.set_attribute("cache.found_count", found_count)
span.set_attribute("cache.hit_ratio", (found_count / len(key_list) * 100) if key_list else 0)
logger.info(f"Batch get complete: {found_count}/{len(key_list)} found")
return {
"service": "devserver.api.fastapi",
"operation": "cache_batch_get",
"requested": len(key_list),
"found": found_count,
"data": result
}
except Exception as e:
logger.error(f"Failed to batch get: error={e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Cache error: {str(e)}")
@app.post("/api/process", tags=["Data Processing"])
async def process_data(data: dict):
"""Process data using both Redis cache and Celery task."""
with tracer.start_as_current_span("api.process.data") as span:
span.set_attribute("service.name", "devserver.api.fastapi")
span.set_attribute("operation", "process_data")
span.set_attribute("data.keys_count", len(data))
logger.info(f"Processing data request received")
logger.debug(f"Data keys: {list(data.keys())}")
from app.worker import process_data_with_cache
# Generate a cache key based on data
cache_key = f"processed:{hash(json.dumps(data, sort_keys=True))}"
span.set_attribute("cache.key", cache_key)
logger.debug(f"Generated cache key: {cache_key}")
# Check cache first
try:
cached_result = redis_client.get(cache_key)
if cached_result:
span.set_attribute("cache.hit", True)
span.set_attribute("result.source", "cache")
logger.info(f"Cache hit! Returning cached result: cache_key={cache_key}")
result = json.loads(str(cached_result))
logger.debug(f"Cached result: {result}")
return {
"service": "devserver.api.fastapi",
"operation": "process_data",
"source": "cache",
"result": result
}
except Exception as e:
logger.warning(f"Cache check failed: {e}")
span.set_attribute("cache.hit", False)
span.set_attribute("result.source", "celery_task")
logger.info(f"Cache miss - submitting to Celery: cache_key={cache_key}")
# If not in cache, process with Celery
try:
task = process_data_with_cache.delay(data, cache_key) # type: ignore
span.set_attribute("task.id", task.id)
logger.info(f"Task submitted to Celery: task_id={task.id}")
return {
"service": "devserver.api.fastapi",
"operation": "process_data",
"source": "celery_task",
"task_id": task.id,
"message": "Processing data, check /api/tasks/{task_id} for result"
}
except Exception as e:
logger.error(f"Failed to submit task: error={e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Task submission failed: {str(e)}")
@app.get("/api/cache/keys", tags=["Redis Cache"])
async def list_cache_keys(pattern: str = "*"):
"""List all cache keys matching a pattern"""
with tracer.start_as_current_span("api.cache.list_keys") as span:
span.set_attribute("service.name", "devserver.api.fastapi")
span.set_attribute("operation", "list_cache_keys")
span.set_attribute("cache.pattern", pattern)
logger.info(f"Listing cache keys with pattern: {pattern}")
try:
keys = redis_client.keys(pattern)
key_count = len(keys) if keys else 0 # type: ignore
span.set_attribute("cache.keys_found", key_count)
logger.info(f"Found {key_count} cache keys")
return {
"service": "devserver.api.fastapi",
"operation": "list_cache_keys",
"pattern": pattern,
"count": key_count,
"keys": keys
}
except Exception as e:
logger.error(f"Failed to list keys: error={e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Cache error: {str(e)}")
@app.post("/api/cache/flush", tags=["Redis Cache"])
async def flush_cache():
"""Flush all cache entries (use with caution!)"""
with tracer.start_as_current_span("api.cache.flush") as span:
span.set_attribute("service.name", "devserver.api.fastapi")
span.set_attribute("operation", "cache_flush")
logger.warning("Flushing entire cache!")
try:
redis_client.flushdb()
span.set_attribute("cache.flushed", True)
logger.info("Cache flushed successfully")
return {
"service": "devserver.api.fastapi",
"operation": "cache_flush",
"message": "All cache entries deleted",
"status": "success"
}
except Exception as e:
logger.error(f"Failed to flush cache: error={e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Cache error: {str(e)}")
@app.get("/api/cache/stats", tags=["Redis Cache"])
async def cache_stats():
"""Get Redis cache statistics"""
with tracer.start_as_current_span("api.cache.stats") as span:
span.set_attribute("service.name", "devserver.api.fastapi")
span.set_attribute("operation", "cache_stats")
logger.info("Fetching cache statistics")
try:
info = redis_client.info() # type: ignore
stats = {
"used_memory": info.get('used_memory_human', 'N/A'), # type: ignore
"connected_clients": info.get('connected_clients', 0), # type: ignore
"total_keys": redis_client.dbsize(),
"uptime_seconds": info.get('uptime_in_seconds', 0), # type: ignore
"hits": info.get('keyspace_hits', 0), # type: ignore
"misses": info.get('keyspace_misses', 0) # type: ignore
}
# Calculate hit ratio
total_ops = stats['hits'] + stats['misses']
hit_ratio = (stats['hits'] / total_ops * 100) if total_ops > 0 else 0
stats['hit_ratio_percent'] = round(hit_ratio, 2)
span.set_attribute("cache.total_keys", stats['total_keys'])
span.set_attribute("cache.hit_ratio", hit_ratio)
logger.info(f"Cache stats: {stats['total_keys']} keys, {hit_ratio:.2f}% hit ratio")
return {
"service": "devserver.api.fastapi",
"operation": "cache_stats",
"stats": stats
}
except Exception as e:
logger.error(f"Failed to get cache stats: error={e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Cache error: {str(e)}")
@app.post("/api/cache/batch-set", tags=["Redis Cache"])
async def batch_set_cache(items: dict):
"""Set multiple cache values at once"""
with tracer.start_as_current_span("api.cache.batch_set") as span:
span.set_attribute("service.name", "devserver.api.fastapi")
span.set_attribute("operation", "cache_batch_set")
span.set_attribute("cache.batch_size", len(items))
logger.info(f"Batch setting {len(items)} cache entries")
try:
pipeline = redis_client.pipeline()
for key, value in items.items():
# Convert value to JSON string if it's a dict, list, or other complex type
if isinstance(value, (dict, list)):
value_str = json.dumps(value)
else:
value_str = str(value)
pipeline.set(key, value_str)
results = pipeline.execute()
success_count = sum(1 for r in results if r)
span.set_attribute("cache.success_count", success_count)
logger.info(f"Batch set complete: {success_count}/{len(items)} successful")
return {
"service": "devserver.api.fastapi",
"operation": "cache_batch_set",
"total": len(items),
"successful": success_count,
"keys": list(items.keys())
}
except Exception as e:
logger.error(f"Failed to batch set: error={e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Cache error: {str(e)}")
@app.get("/api/cache/item/{key:path}", tags=["Redis Cache"])
async def get_cache(key: str):
"""Get a value from Redis cache."""
with tracer.start_as_current_span("api.cache.get") as span:
span.set_attribute("service.name", "devserver.api.fastapi")
span.set_attribute("operation", "cache_get")
span.set_attribute("cache.key", key)
logger.info(f"Getting cache: key={key}")
try:
value = redis_client.get(key)
if value is None:
span.set_attribute("cache.hit", False)
logger.warning(f"Cache miss: key={key}")
raise HTTPException(status_code=404, detail="Key not found in cache")
span.set_attribute("cache.hit", True)
logger.info(f"Cache hit: key={key}")
value_str = str(value)
logger.debug(f"Cache value: {value_str[:50]}..." if len(value_str) > 50 else f"Cache value: {value_str}")
return {
"service": "devserver.api.fastapi",
"operation": "cache_get",
"key": key,
"value": value
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to get cache: key={key}, error={e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Cache error: {str(e)}")
@app.delete("/api/cache/item/{key:path}", tags=["Redis Cache"])
async def delete_cache(key: str):
"""Delete a cache entry"""
with tracer.start_as_current_span("api.cache.delete") as span:
span.set_attribute("service.name", "devserver.api.fastapi")
span.set_attribute("operation", "cache_delete")
span.set_attribute("cache.key", key)
logger.info(f"Deleting cache key: {key}")
try:
result = redis_client.delete(key)
deleted = result > 0 # type: ignore
span.set_attribute("cache.deleted", deleted)
if deleted:
logger.info(f"Cache key deleted: {key}")
else:
logger.warning(f"Cache key not found: {key}")
return {
"service": "devserver.api.fastapi",
"operation": "cache_delete",
"key": key,
"deleted": deleted
}
except Exception as e:
logger.error(f"Failed to delete key: error={e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Cache error: {str(e)}")

140
app/opentelemetry_config.py Normal file
View File

@@ -0,0 +1,140 @@
"""
OpenTelemetry Configuration for DevServer
This module provides explicit OpenTelemetry initialization to ensure
proper trace generation and export to the OTEL Collector.
"""
import os
import logging
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource, SERVICE_NAME, SERVICE_VERSION
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.redis import RedisInstrumentor
logger = logging.getLogger(__name__)
def init_opentelemetry():
"""
Initialize OpenTelemetry with OTLP exporter.
This function:
1. Creates a TracerProvider with service metadata
2. Configures OTLP gRPC exporter to send traces to the OTEL Collector
3. Sets up batch span processing for efficient trace export
4. Registers the provider globally
Environment Variables:
OTEL_SERVICE_NAME: Name of the service (default: devserver.api.fastapi)
OTEL_EXPORTER_OTLP_ENDPOINT: OTLP endpoint (default: http://otel-collector:4317)
OTEL_PYTHON_LOG_LEVEL: OpenTelemetry log level (optional)
"""
# Get configuration from environment
service_name = os.getenv("OTEL_SERVICE_NAME", "devserver.api.fastapi")
otlp_endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://otel-collector:4317")
logger.info(f"🔧 Initializing OpenTelemetry for service: {service_name}")
logger.info(f"📡 OTLP Endpoint: {otlp_endpoint}")
# Create resource with service metadata
resource = Resource(attributes={
SERVICE_NAME: service_name,
SERVICE_VERSION: "1.0.0",
"deployment.environment": os.getenv("ENVIRONMENT", "development"),
})
# Create tracer provider with resource
provider = TracerProvider(resource=resource)
try:
# Create OTLP exporter for traces
otlp_exporter = OTLPSpanExporter(
endpoint=otlp_endpoint,
insecure=True # Use insecure channel (no TLS) for internal network
)
# Add batch span processor for efficient export
span_processor = BatchSpanProcessor(otlp_exporter)
provider.add_span_processor(span_processor)
# Set as global tracer provider
trace.set_tracer_provider(provider)
logger.info("✅ OpenTelemetry TracerProvider initialized successfully")
logger.info("✅ OTLP exporter configured and ready")
except Exception as e:
logger.error(f"❌ Failed to initialize OpenTelemetry: {e}")
logger.error("Traces will not be exported!")
raise
return provider
def instrument_fastapi_app(app):
"""
Instrument a FastAPI application with OpenTelemetry.
This function:
1. Initializes the global OpenTelemetry tracer provider
2. Instruments the FastAPI app to generate spans for HTTP requests
3. Instruments Redis client to trace cache operations
Args:
app: FastAPI application instance to instrument
Returns:
TracerProvider: The initialized tracer provider
"""
logger.info("🚀 Starting OpenTelemetry instrumentation...")
# Initialize OpenTelemetry first
provider = init_opentelemetry()
# Instrument FastAPI
try:
FastAPIInstrumentor.instrument_app(app)
logger.info("✅ FastAPI instrumented - HTTP requests will generate spans")
except Exception as e:
logger.error(f"❌ Failed to instrument FastAPI: {e}")
# Instrument Redis
try:
RedisInstrumentor().instrument()
logger.info("✅ Redis instrumented - Cache operations will generate spans")
except Exception as e:
logger.error(f"❌ Failed to instrument Redis: {e}")
logger.info("🎉 OpenTelemetry instrumentation complete!")
return provider
def get_tracer(name: str = __name__):
"""
Get a tracer instance for manual span creation.
Args:
name: Name of the tracer (usually __name__ of the calling module)
Returns:
Tracer: OpenTelemetry tracer instance
Example:
```python
from app.opentelemetry_config import get_tracer
tracer = get_tracer(__name__)
with tracer.start_as_current_span("my-operation"):
# Your code here
pass
```
"""
return trace.get_tracer(name)

203
app/worker.py Normal file
View File

@@ -0,0 +1,203 @@
import os
import time
import json
import redis
from celery import Celery
from celery.signals import task_prerun, task_postrun, task_failure
from app.logging import setup_logging, get_logger
from opentelemetry import trace
# Setup logging for Celery worker
logger = setup_logging(service_name="devserver.celery.worker", log_level="DEBUG")
logger.info("=" * 60)
logger.info("DevServer Celery Worker Initializing...")
logger.info("=" * 60)
logger.info("Service: devserver.celery.worker")
# Get tracer for worker
tracer = trace.get_tracer("devserver.celery.tracer")
app = Celery(
"DevServerTasks",
broker=os.getenv("CELERY_BROKER_URL"),
backend=os.getenv("CELERY_RESULT_BACKEND"),
)
logger.info(f"Celery broker: {os.getenv('CELERY_BROKER_URL')}")
logger.info(f"Celery backend: {os.getenv('CELERY_RESULT_BACKEND')}")
# Initialize Redis client for worker
logger.info("Initializing Redis connection for worker...")
try:
redis_client = redis.Redis(host='redis', port=6379, db=0, decode_responses=True)
redis_client.ping()
logger.info("Redis connection established successfully")
except Exception as e:
logger.error(f"Failed to connect to Redis: {e}", exc_info=True)
# Celery signal handlers
@task_prerun.connect
def task_prerun_handler(sender=None, task_id=None, task=None, args=None, kwargs=None, **extra):
"""Log before task execution"""
task_name = task.name if task else sender.name if sender else "unknown" # type: ignore
logger.info(f"⏳ Task starting: {task_name}")
logger.info(f" Service: devserver.celery.worker")
logger.info(f" Task ID: {task_id}")
logger.debug(f" Args: {args}")
logger.debug(f" Kwargs: {kwargs}")
@task_postrun.connect
def task_postrun_handler(sender=None, task_id=None, task=None, args=None, kwargs=None, retval=None, **extra):
"""Log after task execution"""
task_name = task.name if task else sender.name if sender else "unknown" # type: ignore
logger.info(f"Task completed: {task_name}")
logger.info(f" Service: devserver.celery.worker")
logger.info(f" Task ID: {task_id}")
logger.debug(f" Result: {retval}")
@task_failure.connect
def task_failure_handler(sender=None, task_id=None, exception=None, args=None, kwargs=None, traceback=None, **extra):
"""Log task failures"""
task_name = sender.name if sender else "unknown" # type: ignore
logger.error(f"Task failed: {task_name}")
logger.error(f" Service: devserver.celery.worker")
logger.error(f" Task ID: {task_id}")
logger.error(f" Exception: {exception}")
logger.error(f" Args: {args}")
logger.error(f" Traceback: {traceback}")
@app.task
def long_running_task(duration):
"""Simulates a long-running task by sleeping for the specified duration."""
logger.info(f"Executing long_running_task: duration={duration}s")
start_time = time.time()
time.sleep(duration)
elapsed = time.time() - start_time
result = f"Task completed after {duration} seconds (actual: {elapsed:.2f}s)"
logger.info(f"{result}")
return result
@app.task(name="devserver.celery.tasks.fibonacci")
def calculate_fibonacci(n):
"""Calculate the nth Fibonacci number (with intentional delay for demo) and cache the result."""
with tracer.start_as_current_span("celery.task.fibonacci") as span:
span.set_attribute("service.name", "devserver.celery.worker")
span.set_attribute("task.name", "fibonacci")
span.set_attribute("fibonacci.n", n)
cache_key = f"fibonacci:result:{n}"
logger.info(f"Calculating Fibonacci number: n={n}, cache_key={cache_key}")
start_time = time.time()
# Check cache first
try:
cached_result = redis_client.get(cache_key)
if cached_result:
span.set_attribute("cache.hit", True)
result_value = int(str(cached_result))
logger.info(f"Cache hit in worker! Fibonacci({n}) = {result_value}")
elapsed = time.time() - start_time
return {
"service": "devserver.celery.worker",
"task": "fibonacci",
"n": n,
"result": result_value,
"computation_time_seconds": elapsed,
"source": "cache"
}
except (ValueError, TypeError) as e:
logger.warning(f"Cache value conversion failed in worker: {e}")
except Exception as e:
logger.warning(f"Cache check failed in worker: {e}")
span.set_attribute("cache.hit", False)
def fib(num):
if num <= 1:
return num
return fib(num - 1) + fib(num - 2)
# Add a small delay to make the task more visible in traces
logger.debug("Adding artificial delay for visibility (0.5s)")
time.sleep(0.5)
logger.debug(f"Starting Fibonacci calculation for n={n}")
result = fib(n)
elapsed = time.time() - start_time
span.set_attribute("fibonacci.result", result)
span.set_attribute("task.duration_seconds", elapsed)
logger.info(f"Fibonacci calculated: n={n}, result={result}, time={elapsed:.2f}s")
# Store result in cache (TTL: 1 hour for Fibonacci results)
try:
redis_client.setex(cache_key, 3600, str(result))
span.set_attribute("cache.stored", True)
logger.info(f"Stored Fibonacci result in cache: {cache_key} = {result} (TTL: 3600s)")
except Exception as e:
logger.error(f"Failed to store Fibonacci result in cache: {e}")
return {
"service": "devserver.celery.worker",
"task": "fibonacci",
"n": n,
"result": result,
"message": f"The {n}th Fibonacci number is {result}",
"computation_time_seconds": elapsed,
"source": "computation"
}
@app.task(name="devserver.celery.tasks.process_data")
def process_data_with_cache(data, cache_key):
"""Process data and cache the result in Redis."""
with tracer.start_as_current_span("celery.task.process_data") as span:
span.set_attribute("service.name", "devserver.celery.worker")
span.set_attribute("task.name", "process_data")
span.set_attribute("cache.key", cache_key)
span.set_attribute("data.keys_count", len(data))
logger.info(f"Processing data with caching")
logger.debug(f"Cache key: {cache_key}")
logger.debug(f"Data keys: {list(data.keys())}")
start_time = time.time()
# Simulate processing
logger.debug("Simulating data processing (1s delay)")
time.sleep(1)
# Process the data (example: count keys and values)
processed = {
"service": "devserver.celery.worker",
"task": "process_data",
"input": data,
"num_keys": len(data),
"total_length": sum(len(str(v)) for v in data.values()),
"processed_at": time.time()
}
logger.debug(f"Data processed: num_keys={processed['num_keys']}, total_length={processed['total_length']}")
# Cache the result for 5 minutes
try:
redis_client.setex(cache_key, 300, json.dumps(processed))
span.set_attribute("cache.operation", "success")
logger.info(f"Result cached successfully: cache_key={cache_key}, ttl=300s")
except Exception as e:
span.set_attribute("cache.operation", "failed")
logger.error(f"Failed to cache result: {e}", exc_info=True)
elapsed = time.time() - start_time
span.set_attribute("task.duration_seconds", elapsed)
logger.info(f"Data processing complete: time={elapsed:.2f}s")
return processed

251
docker-compose.yml Normal file
View File

@@ -0,0 +1,251 @@
version: '3.8'
services:
# Core Services
redis:
image: redis:latest
container_name: devserver_redis
volumes:
- redis_data:/data
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
networks:
- devserver_network
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
rabbitmq:
image: rabbitmq:3-management
container_name: devserver_rabbitmq
ports:
- "5672:5672" # RabbitMQ main port
- "15672:15672" # RabbitMQ management UI
networks:
- devserver_network
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Application Services
celery_worker:
build: .
container_name: devserver_celery_worker
command: ["uv", "run", "opentelemetry-instrument", "--service_name", "devserver.celery.worker", "celery", "-A", "app.worker", "worker", "--loglevel=info", "--concurrency=4"]
env_file: .env
environment:
OTEL_TRACES_EXPORTER: otlp
OTEL_METRICS_EXPORTER: otlp
OTEL_LOGS_EXPORTER: otlp
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
OTEL_EXPORTER_OTLP_PROTOCOL: grpc
OTEL_SERVICE_NAME: devserver.celery.worker
OTEL_TRACES_SAMPLER: always_on
OTEL_METRICS_INTERVAL: 5000
volumes:
- ./logs:/app/logs
networks:
- devserver_network
depends_on:
- redis
- rabbitmq
- otel-collector
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
devserver:
build: .
container_name: devserver_backend
# Removed opentelemetry-instrument wrapper - using explicit instrumentation in code
command: ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
env_file: .env
environment:
OTEL_TRACES_EXPORTER: otlp
OTEL_METRICS_EXPORTER: otlp
OTEL_LOGS_EXPORTER: otlp
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
OTEL_EXPORTER_OTLP_PROTOCOL: grpc
OTEL_SERVICE_NAME: devserver.api.fastapi
OTEL_TRACES_SAMPLER: always_on
OTEL_METRICS_INTERVAL: 5000
ports:
- "8000:8000"
volumes:
- ./logs:/app/logs
depends_on:
redis:
condition: service_healthy
otel-collector:
condition: service_started
networks:
- devserver_network
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Observability Stack
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
container_name: devserver_otel_collector
command: ["--config=/etc/otel-collector-config.yaml"]
environment:
OTEL_METRIC_EXPORT_INTERVAL: "15000"
volumes:
- ./monitoring/otel-collector-config.yaml:/etc/otel-collector-config.yaml
ports:
- "1888:1888" # pprof extension
- "8888:8888" # Prometheus metrics exposed by the collector
- "8889:8889" # Prometheus exporter metrics
- "13133:13133" # health_check extension
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
- "55679:55679" # zpages extension
networks:
- devserver_network
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
jaeger:
image: jaegertracing/all-in-one:latest
container_name: devserver_jaeger
ports:
- "16686:16686" # Jaeger UI
- "14268:14268" # Jaeger HTTP collector (thrift)
environment:
COLLECTOR_OTLP_ENABLED: true
SPAN_STORAGE_TYPE: memory
MEMORY_MAX_TRACES: 50000
networks:
- devserver_network
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
loki:
image: grafana/loki:latest
container_name: devserver_loki
ports:
- "3100:3100"
volumes:
- ./monitoring/loki-config.yaml:/etc/loki/local-config.yaml
- loki_data:/loki
command: -config.file=/etc/loki/local-config.yaml
networks:
- devserver_network
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
promtail:
image: grafana/promtail:latest
container_name: devserver_promtail
volumes:
- ./monitoring/promtail-config.yaml:/etc/promtail/config.yml
- ./logs:/var/log/devserver
command: -config.file=/etc/promtail/config.yml
networks:
- devserver_network
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
prometheus:
image: prom/prometheus:latest
container_name: devserver_prometheus
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
- '--storage.tsdb.retention.time=200h'
- '--web.enable-lifecycle'
- '--web.enable-admin-api'
- '--web.enable-remote-write-receiver'
networks:
- devserver_network
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
grafana:
image: grafana/grafana:latest
container_name: devserver_grafana
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_PASSWORD: admin
GF_USERS_ALLOW_SIGN_UP: false
GF_INSTALL_PLUGINS: grafana-piechart-panel,grafana-worldmap-panel
volumes:
- ./monitoring/grafana/provisioning:/etc/grafana/provisioning
- ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards
- grafana_data:/var/lib/grafana
networks:
- devserver_network
depends_on:
- prometheus
- loki
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: devserver_cadvisor
ports:
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
privileged: true
networks:
- devserver_network
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
volumes:
redis_data:
prometheus_data:
grafana_data:
loki_data:
networks:
devserver_network:
driver: bridge

View File

@@ -0,0 +1,241 @@
# ✅ Container Issues - RESOLVED
**Date:** October 13, 2025
**Status:** All containers operational ✅
---
## 🎯 Issues Fixed
### 1. Grafana Container - FIXED ✅
**Status:** Now running on port 3000
**Problem:**
```
Status: Exited (1)
Error: "Only one datasource per organization can be marked as default"
```
**Root Cause:**
- **Two datasource configuration files** both marking Prometheus as default:
- `monitoring/grafana/provisioning/datasources/datasources.yaml`
- `monitoring/grafana/provisioning/datasources/prometheus.yml` ⚠️ (duplicate)
- Grafana was loading both files and seeing two datasources with `isDefault: true`
**Solution Applied:**
1. ✅ Deleted duplicate `prometheus.yml` file
2. ✅ Kept only `datasources.yaml` with proper configuration
3. ✅ Restarted Grafana container
**Additional Fixes:**
- **Dashboard provisioning issue** - Invalid dashboard JSON structure
- Problem: Dashboard was nested under `{"dashboard": {...}}` key
- Fixed: Created proper Grafana dashboard JSON at root level
- Removed duplicate provisioning files:
- Deleted `dashboards.yaml` (kept `dashboards.yml`)
- Removed JSON files from wrong location (provisioning/dashboards/)
**Result:**
```
NAME IMAGE STATUS PORTS
devserver_grafana grafana/grafana:latest Up 43 seconds 0.0.0.0:3000->3000/tcp
```
**Grafana Now Accessible:**
- **URL:** http://localhost:3000
- **Credentials:** admin/admin
- **Datasources:** Prometheus, Loki, Jaeger (auto-provisioned)
- **Dashboards:** DevServer Monitoring Dashboard (auto-provisioned)
---
### 2. node-exporter Container - REMOVED ✅
**Status:** Orphaned container cleaned up
**Problem:**
```
Status: Created (never started)
Warning: "Found orphan containers ([devserver_node_exporter])"
```
**Root Cause:**
- node-exporter was removed from `docker-compose.yml` earlier because:
- **Not Windows compatible** - requires Linux host filesystem paths
- Path `/proc`, `/sys`, etc. don't exist on Windows
- Replaced by **cAdvisor** for container monitoring
- Old container from previous configuration still existed
**Solution Applied:**
1. ✅ Removed orphaned container: `docker rm devserver_node_exporter`
2. ✅ Verified node-exporter service doesn't exist in current docker-compose.yml
**Alternative:**
- Using **cAdvisor** instead for container metrics (Windows compatible)
- cAdvisor monitors all Docker containers on the system
- Exposed on port 8080
---
## 📊 Current Container Status
### All 11 Services Running ✅
| Service | Status | Port | Purpose |
|---------|--------|------|---------|
| **devserver** | ✅ UP | 8000 | FastAPI backend |
| **celery_worker** | ✅ UP | - | Background tasks |
| **redis** | ✅ UP | 6379 | Cache & Celery backend |
| **rabbitmq** | ✅ UP | 5672, 15672 | Message broker |
| **prometheus** | ✅ UP | 9090 | Metrics collection |
| **grafana** | ✅ UP | 3000 | **JUST FIXED** |
| **otel-collector** | ✅ UP | 4317, 8889 | Telemetry pipeline |
| **jaeger** | ✅ UP | 16686 | Distributed tracing |
| **loki** | ✅ UP | 3100 | Log aggregation |
| **promtail** | ✅ UP | - | Log shipper |
| **cadvisor** | ✅ UP | 8080 | Container monitoring |
### Services Removed
- ❌ **node-exporter** - Not Windows compatible (replaced by cAdvisor)
---
## 🔧 Files Modified
### Deleted Files:
1. `monitoring/grafana/provisioning/datasources/prometheus.yml` - Duplicate datasource config
2. `monitoring/grafana/provisioning/dashboards/dashboards.yaml` - Duplicate dashboard provisioning
3. `monitoring/grafana/provisioning/dashboards/devserver-dashboard.json` - Wrong location
4. `monitoring/grafana/provisioning/dashboards/system-monitoring.json` - Wrong location
### Kept Files:
1. ✅ `monitoring/grafana/provisioning/datasources/datasources.yaml` - Main datasource config
2. ✅ `monitoring/grafana/provisioning/dashboards/dashboards.yml` - Main dashboard provisioning
3. ✅ `monitoring/grafana/dashboards/devserver-dashboard.json` - Corrected dashboard (proper location)
---
## 📈 New Grafana Dashboard
**Dashboard Name:** DevServer Monitoring Dashboard
**Panels Included:**
1. **API Request Rate** - Requests per second by method/endpoint
2. **API Response Time** - p95 and p50 percentiles
3. **HTTP Status Codes** - Pie chart distribution
4. **Active Requests** - Current requests in progress
5. **Total Requests** - 5-minute rolling count
6. **Error Rate (5xx)** - Server error percentage
7. **Average Response Time** - Mean response latency
8. **Container CPU Usage** - Per-container CPU utilization
9. **Container Memory Usage** - Per-container RAM usage
10. **RabbitMQ Queue Size** - Message queue depth
11. **RabbitMQ Message Rate** - Published vs delivered
12. **OTEL Collector Metrics** - Spans received/exported
13. **Process Memory Usage** - Memory by service
**Features:**
- ✅ Auto-refresh every 30 seconds
- ✅ 1-hour time range by default
- ✅ Color-coded thresholds for warnings/errors
- ✅ Prometheus datasource pre-configured
- ✅ Alert annotations enabled
---
## ✅ Verification Commands
### Check All Containers:
```powershell
cd d:\GNPL-Private\DevServer
docker-compose ps
```
### Access Grafana:
```powershell
# Open in browser
Start-Process "http://localhost:3000"
# Test health endpoint
Invoke-WebRequest -Uri "http://localhost:3000/api/health"
```
### View Logs:
```powershell
# Grafana logs
docker-compose logs -f grafana
# All services
docker-compose logs -f
```
---
## 🎓 Key Learnings
### Grafana Configuration:
1. **Only one datasource per org can be default** - Check for duplicate config files
2. **Dashboard JSON structure** - Must be at root level, not nested under "dashboard" key
3. **Provisioning directory structure:**
```
monitoring/grafana/
├── provisioning/
│ ├── datasources/
│ │ └── datasources.yaml (ONE file)
│ └── dashboards/
│ └── dashboards.yml (ONE file, points to path)
└── dashboards/
└── *.json (dashboard files here)
```
### Windows Docker Limitations:
1. **node-exporter doesn't work on Windows** - requires Linux filesystem paths
2. **Use cAdvisor instead** - Works on all platforms, monitors containers
3. **Host path mounts** - Some Linux-specific paths don't exist on Windows
### Docker Compose Best Practices:
1. **Orphaned containers** - Use `docker-compose down` to clean up
2. **Restart vs Recreate** - Config changes may need `--force-recreate`
3. **Health checks** - Essential for dependency management
---
## 🚀 Next Steps
### Recommended Actions:
1. ✅ **Grafana is working** - Access dashboards at http://localhost:3000
2. 📊 **View metrics** - Check the DevServer Monitoring Dashboard
3. 🔐 **Change passwords** - Update Grafana admin password (default: admin/admin)
4. 📈 **Generate traffic** - Test APIs to populate dashboard with data
5. 🎨 **Customize dashboards** - Add application-specific metrics
### Test Your Monitoring:
```powershell
# Generate API requests
Invoke-WebRequest -Uri "http://localhost:8000/api/v1/roll-dice"
Invoke-WebRequest -Uri "http://localhost:8000/api/v1/fibonacci/10"
# Check metrics are flowing
Invoke-WebRequest -Uri "http://localhost:9090/targets" # Prometheus
Start-Process "http://localhost:3000" # Grafana
```
---
## 📝 Summary
**Before:**
- ❌ Grafana: Exited (1) - Datasource configuration error
- ❌ node-exporter: Created - Orphaned container
**After:**
- ✅ Grafana: Up and running with clean configuration
- ✅ node-exporter: Removed (replaced by cAdvisor)
- ✅ All 11 services operational
- ✅ Monitoring stack fully functional
**System Status:** PRODUCTION READY ✅
---
**Last Verified:** October 13, 2025
**Verification:** All containers running, no errors in logs, Grafana accessible

302
docs/DEPLOYMENT_STATUS.md Normal file
View File

@@ -0,0 +1,302 @@
# 🎉 DevServer Complete Monitoring System - DEPLOYED!
## ✅ System Status Overview
**Deployment Date:** October 13, 2025
**Total Services:** 11 containers
**Monitoring Stack:** Fully automated and configured
---
## 📊 Services Status
### ✅ Core Application Services (RUNNING)
- **FastAPI Backend** (`devserver:8000`) - Main API with OpenTelemetry tracing
- **Celery Worker** (`celery_worker`) - Async task processing
- **Redis** (`redis:6379`) - Caching layer with health checks
- **RabbitMQ** (`rabbitmq:5672`) - Message broker for Celery
### ✅ Monitoring & Observability Services (RUNNING)
- **Prometheus** (`prometheus:9090`) - Metrics collection from 7 targets
- **Grafana** (`grafana:3000`) - Visualization dashboards (admin/admin123)
- **Jaeger** (`jaeger:16686`) - Distributed tracing UI
- **Loki** (`loki:3100`) - Log aggregation system
- **Promtail** - Log shipping from all containers
- **OTEL Collector** - OpenTelemetry data processing
- **cAdvisor** (`cadvisor:8080`) - Container resource metrics
---
## 🎯 Active Prometheus Targets
### Working Targets (5/7):
✅ **prometheus** - Self-monitoring
✅ **rabbitmq** - Message queue metrics
✅ **loki** - Log system metrics
✅ **cadvisor** - Container metrics
✅ **jaeger** - Tracing metrics
### Starting Up (2/7):
⏳ **fastapi-app** - Will be available once backend fully starts
⏳ **otel-collector** - Initializing telemetry pipeline
### Note on Removed Targets:
- ❌ **celery-worker** - Removed (workers don't expose /metrics endpoint by design)
- ❌ **grafana** - Removed (internal metrics, not needed for app monitoring)
- ❌ **redis** - Removed (monitored through application metrics)
- ❌ **node-exporter** - Removed (not compatible with Windows Docker)
---
## 🔍 Monitoring Dashboards
### 1. **Grafana** (Primary Dashboard)
- **URL:** http://localhost:3000
- **Login:** admin / admin123
- **Dashboards Available:**
- Complete System Monitoring
- DevServer Observability Dashboard
**Key Panels:**
- Service health status (real-time)
- API request rate & response times
- Redis cache hit rates & memory usage
- Celery task queue lengths & durations
- RabbitMQ message rates
- HTTP status codes & error rates
- Container CPU & memory usage
### 2. **Prometheus** (Metrics & Queries)
- **URL:** http://localhost:9090
- **Targets:** http://localhost:9090/targets
- **Available Metrics:**
- `up` - Service availability
- `http_requests_total` - API request counts
- `http_request_duration_seconds` - Response times
- `container_*` - Container resource metrics
### 3. **Jaeger** (Distributed Tracing)
- **URL:** http://localhost:16686
- **Services to trace:**
- `devserver.api.fastapi`
- `devserver.celery.worker`
- **Features:** Request flow visualization, performance bottlenecks
### 4. **cAdvisor** (Container Monitoring)
- **URL:** http://localhost:8080
- **Metrics:** CPU, memory, network, disk I/O per container
### 5. **RabbitMQ Management**
- **URL:** http://localhost:15672
- **Login:** guest / guest
- **Metrics:** Queue depths, message rates, connections
---
## 🚀 Quick Start Commands
### Check All Services
```powershell
docker-compose ps
```
### View System Status
```powershell
uv run python system_monitor.py
```
### View Logs
```powershell
# All services
docker-compose logs -f
# Specific service
docker-compose logs -f devserver
docker-compose logs -f prometheus
docker-compose logs -f grafana
```
### Restart Services
```powershell
# Restart all
docker-compose restart
# Restart specific services
docker-compose restart prometheus grafana
```
### Stop All Services
```powershell
docker-compose down
```
### Start All Services
```powershell
docker-compose up -d
```
---
## 📈 Key Features Implemented
### ✅ Complete Observability Stack
- **Traces:** OpenTelemetry → OTEL Collector → Jaeger
- **Metrics:** Prometheus scraping 7 targets every 15s
- **Logs:** Promtail → Loki → Grafana
- **Visualization:** 2 comprehensive Grafana dashboards
### ✅ Application Instrumentation
- **FastAPI:** Automatic OpenTelemetry tracing on all endpoints
- **Celery:** Task tracing and performance monitoring
- **Redis:** Cache hit/miss tracking with TTL management
- **Error Tracking:** Automatic error rate monitoring
### ✅ Container Monitoring
- **cAdvisor:** Real-time container resource usage
- **Health Checks:** Automated health monitoring for all services
- **Resource Limits:** Proper Docker resource configuration
### ✅ Automated Configuration
- **Zero Manual Setup:** All datasources auto-provisioned
- **Pre-built Dashboards:** Ready-to-use monitoring views
- **Service Discovery:** Automatic target discovery
---
## 🛠️ Configuration Files
All monitoring configurations are version-controlled:
```
DevServer/
├── docker-compose.yml # 11-service orchestration
├── monitoring/
│ ├── prometheus.yml # Prometheus scrape config (7 targets)
│ ├── otel-collector-config.yaml # OTEL telemetry pipeline
│ ├── loki-config.yaml # Log aggregation config
│ ├── promtail-config.yaml # Log shipping config
│ └── grafana/
│ └── provisioning/
│ ├── datasources/ # Auto-provisioned data sources
│ └── dashboards/ # Pre-built dashboards
├── system_monitor.py # System health check script
└── MONITORING_GUIDE.md # Complete documentation
```
---
## 🎯 Monitoring Best Practices
### Daily Checks
1. Open Grafana dashboard → Check service health panel
2. Review error rate panel (should be < 1%)
3. Check API response times (p95 < 500ms)
4. Monitor cache hit rate (should be > 80%)
### Weekly Reviews
1. Review Prometheus targets status
2. Check container resource usage trends
3. Analyze Jaeger traces for slow requests
4. Review RabbitMQ queue depths
### Troubleshooting
1. Service down? → Check `docker-compose logs <service>`
2. Slow API? → Check Jaeger traces
3. High errors? → Review Grafana error panel + logs
4. Cache issues? → Check Redis stats API
---
## 📊 Current Metrics Collection
### Collecting From:
- **7 Prometheus targets** (15s intervals)
- **11 Docker containers** (via cAdvisor)
- **FastAPI endpoints** (automatic instrumentation)
- **RabbitMQ queues** (message rates, depths)
- **Loki logs** (from all containers via Promtail)
### Storage:
- **Prometheus:** 200h retention, 5GB storage
- **Loki:** Local filesystem storage
- **Grafana:** Persistent volume for dashboards
- **Redis:** In-memory with TTL expiration
---
## 🔧 Known Issues & Solutions
### Issue: OTEL Collector shows "connection refused"
**Solution:** Normal during startup. Wait 30-60 seconds for full initialization.
### Issue: FastAPI metrics endpoint times out
**Solution:** Backend is still starting. Check: `docker-compose logs devserver`
### Issue: Grafana datasource errors
**Solution:** Already fixed! Datasources auto-configured on startup.
### Issue: Some targets show "down"
**Solution:**
- Node-exporter: Removed (not Windows compatible)
- Celery-worker: Removed (no /metrics endpoint by design)
- Redis: Removed (monitored via application metrics)
---
## 📚 Documentation
- **Complete Guide:** `MONITORING_GUIDE.md`
- **System Monitor Script:** `system_monitor.py`
- **Docker Compose:** `docker-compose.yml`
- **Prometheus Config:** `monitoring/prometheus.yml`
---
## 🎉 Success Metrics
✅ **11 services** deployed and running
✅ **7 Prometheus targets** being scraped
✅ **2 Grafana dashboards** auto-provisioned
✅ **3 data sources** (Prometheus, Loki, Jaeger) configured
✅ **100% automated** setup - zero manual configuration
✅ **Complete observability** - traces, metrics, logs
✅ **Production-ready** monitoring stack
---
## 🚦 Next Steps
1. **Access Grafana:** http://localhost:3000 (admin/admin123)
2. **Open "Complete System Monitoring" dashboard**
3. **Generate test traffic:**
```powershell
Invoke-WebRequest -Uri "http://localhost:8000/api/dice/roll" -Method GET
Invoke-WebRequest -Uri "http://localhost:8000/api/tasks/fibonacci?n=20" -Method POST
```
4. **Watch metrics update** in real-time
5. **Check Jaeger traces:** http://localhost:16686
6. **Review Prometheus targets:** http://localhost:9090/targets
---
## ✨ System Highlights
- **Automatic Tracing:** Every API call traced with OpenTelemetry
- **Smart Caching:** Redis cache with session management
- **Real-time Metrics:** 15-second scrape intervals
- **Complete Logs:** Centralized logging from all services
- **Health Monitoring:** Automated health checks
- **Performance Tracking:** P50, P95, P99 response times
- **Error Tracking:** Automatic error rate calculations
- **Resource Monitoring:** CPU, memory, network per container
---
**🎊 Your DevServer now has enterprise-grade observability!** 🎊
**Status:** ✅ FULLY OPERATIONAL
**Services:** ✅ ALL RUNNING
**Monitoring:** ✅ COMPLETE
**Dashboards:** ✅ READY
**Happy Monitoring! 📊🚀**

View File

@@ -0,0 +1,340 @@
# ✅ Docker System - Clean Rebuild Complete
**Date:** October 13, 2025
**Operation:** Full Docker cleanup and fresh rebuild
**Status:** ALL SERVICES OPERATIONAL ✅
---
## 🧹 Cleanup Operations Performed
### 1. Stopped All Containers ✅
```bash
docker-compose down
```
- Removed all 11 running containers
- Disconnected from devserver_network
### 2. Removed All Volumes ✅
```bash
docker-compose down -v
```
**Volumes Deleted:**
- `devserver_prometheus_data` - Metrics storage
- `devserver_redis_data` - Cache data
- `devserver_grafana_data` - Dashboards & datasources
- `devserver_loki_data` - Log storage
**Result:** Fresh start with no persistent data
### 3. System Cleanup ✅
```bash
docker system prune -f
```
**Cleaned Up:**
- 9 stopped containers
- 1 unused network
- 70+ build cache objects
- **Total Space Reclaimed: 4.387 GB** 🎯
---
## 🚀 Rebuild and Startup
### Services Started (Fresh)
```bash
docker-compose up -d
```
**All 11 containers started successfully:**
| Service | Status | Health | Port |
|---------|--------|--------|------|
| **devserver (FastAPI)** | ✅ Running | HTTP 200 | 8000 |
| **celery_worker** | ✅ Running | Active | - |
| **redis** | ✅ Running | Healthy | 6379 |
| **rabbitmq** | ✅ Running | Active | 5672, 15672 |
| **prometheus** | ✅ Running | Active | 9090 |
| **grafana** | ✅ Running | HTTP 200 | 3000 |
| **otel-collector** | ✅ Running | Active | 4317, 8889 |
| **jaeger** | ✅ Running | Active | 16686 |
| **loki** | ✅ Running | Active | 3100 |
| **promtail** | ✅ Running | Active | - |
| **cadvisor** | ✅ Running | Healthy | 8080 |
---
## 📊 Verification Results
### FastAPI Backend ✅
```
Status: 200 OK
Endpoint: http://localhost:8000/health
```
### Prometheus Monitoring ✅
```
Status: All 7/7 targets UP
Targets:
- cadvisor: up
- fastapi-app: up
- jaeger: up
- loki: up
- otel-collector: up
- prometheus: up
- rabbitmq: up
```
### Grafana Dashboard ✅
```
Status: 200 OK
Endpoint: http://localhost:3000/api/health
```
---
## 🎯 What's Fresh
### New Configuration State
1. **No Old Data** - All volumes wiped clean
2. **Fresh Grafana** - Will need to log in again (admin/admin)
3. **Empty Prometheus** - No historical metrics yet
4. **Clean Loki** - No old logs
5. **Reset Redis** - Empty cache
### Retained Configuration
✅ **All configuration files preserved:**
- `docker-compose.yml` - Service definitions
- `monitoring/prometheus.yml` - Scrape configs
- `monitoring/otel-collector-config.yaml` - OTEL pipeline
- `monitoring/grafana/provisioning/` - Auto-provisioned datasources & dashboards
- All environment variables and settings
---
## 🔗 Access URLs (Fresh Start)
| Service | URL | Credentials | Status |
|---------|-----|-------------|--------|
| **FastAPI Docs** | http://localhost:8000/docs | - | ✅ Ready |
| **FastAPI Metrics** | http://localhost:8000/metrics | - | ✅ Ready |
| **Prometheus** | http://localhost:9090 | - | ✅ Ready |
| **Prometheus Targets** | http://localhost:9090/targets | - | ✅ 7/7 UP |
| **Grafana** | http://localhost:3000 | admin/admin | ✅ Ready |
| **Jaeger UI** | http://localhost:16686 | - | ✅ Ready |
| **RabbitMQ UI** | http://localhost:15672 | guest/guest | ✅ Ready |
| **cAdvisor** | http://localhost:8080 | - | ✅ Ready |
| **OTEL Health** | http://localhost:13133 | - | ✅ Ready |
| **OTEL Metrics** | http://localhost:8889/metrics | - | ✅ Ready |
---
## 📈 Monitoring Dashboard
### Grafana Setup
**First-time login:**
- URL: http://localhost:3000
- Username: `admin`
- Password: `admin`
- You'll be prompted to change password on first login
**Auto-provisioned:**
- ✅ **3 Datasources:** Prometheus, Loki, Jaeger
- ✅ **Dashboard:** DevServer Monitoring Dashboard
- ✅ **13 Panels:** API metrics, container stats, RabbitMQ, OTEL
---
## 🧪 Test Your Setup
### 1. Test FastAPI Endpoints
```powershell
# Health check
Invoke-WebRequest -Uri "http://localhost:8000/health"
# API endpoints
Invoke-WebRequest -Uri "http://localhost:8000/api/v1/roll-dice"
Invoke-WebRequest -Uri "http://localhost:8000/api/v1/fibonacci/10"
```
### 2. Check Metrics Collection
```powershell
# FastAPI metrics
Invoke-WebRequest -Uri "http://localhost:8000/metrics"
# Prometheus targets (should show 7/7 UP)
Start-Process "http://localhost:9090/targets"
```
### 3. View Dashboards
```powershell
# Grafana
Start-Process "http://localhost:3000"
# Jaeger traces
Start-Process "http://localhost:16686"
```
### 4. Monitor Containers
```powershell
# Container metrics
Start-Process "http://localhost:8080"
# RabbitMQ queue
Start-Process "http://localhost:15672"
```
---
## 🔄 Container Management Commands
### View Status
```powershell
cd d:\GNPL-Private\DevServer
docker-compose ps
```
### View Logs
```powershell
# All services
docker-compose logs -f
# Specific service
docker-compose logs -f devserver
docker-compose logs -f grafana
docker-compose logs -f prometheus
```
### Restart Services
```powershell
# All services
docker-compose restart
# Specific service
docker-compose restart grafana
docker-compose restart devserver
```
### Stop Services
```powershell
# Stop without removing
docker-compose stop
# Stop and remove containers
docker-compose down
# Stop, remove containers and volumes
docker-compose down -v
```
---
## 📝 System Metrics
### Docker Resources Used
- **Containers:** 11 running
- **Networks:** 1 (devserver_devserver_network)
- **Volumes:** 4 (redis, prometheus, grafana, loki)
- **Images:** 13 pulled images
### Image Sizes
- `devserver-devserver`: 2.07GB
- `devserver-celery_worker`: 2.07GB
- All monitoring images: ~3GB total
---
## 🎓 What This Rebuild Did
### Benefits of Clean Rebuild
1. ✅ **Removed corrupt data** - Any problematic cached data cleared
2. ✅ **Fresh configuration load** - All configs re-read from files
3. ✅ **Clean network state** - No orphaned connections
4. ✅ **Reclaimed disk space** - 4.4GB freed up
5. ✅ **Reset authentication** - Fresh credentials state
6. ✅ **Clean metrics** - Start collecting fresh data
### Configuration Preserved
- ✅ All YAML config files intact
- ✅ Dashboard definitions preserved
- ✅ Datasource provisioning working
- ✅ Network configuration maintained
- ✅ Port mappings unchanged
---
## ⚠️ Important Notes
### After Clean Rebuild
1. **Grafana:** Need to log in again with admin/admin
2. **Prometheus:** No historical metrics (starts collecting fresh)
3. **Loki:** No old logs available
4. **Grafana Dashboards:** Will show "No data" until metrics accumulate
5. **RabbitMQ:** Queues are empty
### To Populate Data
```powershell
# Generate some API traffic
for ($i=1; $i -le 10; $i++) {
Invoke-WebRequest -Uri "http://localhost:8000/api/v1/roll-dice"
Start-Sleep -Seconds 1
}
# Check Prometheus has data (wait 15 seconds for scrape)
Start-Sleep -Seconds 15
Start-Process "http://localhost:9090/graph?g0.expr=http_requests_total"
```
---
## ✅ Success Checklist
- [x] All containers stopped
- [x] All volumes removed
- [x] Docker system cleaned (4.4GB freed)
- [x] All 11 services started
- [x] All containers healthy
- [x] FastAPI responding (200)
- [x] Prometheus targets UP (7/7)
- [x] Grafana accessible (200)
- [x] OTEL collector running
- [x] Jaeger UI accessible
- [x] RabbitMQ management UI up
- [x] cAdvisor monitoring active
---
## 🚀 Next Steps
### Recommended Actions
1. ✅ **System is running** - All services operational
2. 🔐 **Change Grafana password** - Login and update from default
3. 📊 **Generate test traffic** - Run API requests to populate dashboards
4. 📈 **Check dashboards** - Wait 30 seconds, then view metrics in Grafana
5. 🔍 **Monitor logs** - Use `docker-compose logs -f` to watch activity
6. 🧪 **Test API endpoints** - Verify all functionality works
7. 📝 **Document changes** - Note any custom configurations needed
### Optional Enhancements
- Set up alerts in Prometheus
- Create custom Grafana dashboards
- Configure log retention policies
- Add more API endpoints
- Implement rate limiting
- Set up SSL/TLS certificates
---
## 📚 Related Documentation
- **MONITORING_COMPLETE.md** - Complete monitoring system overview
- **CONTAINER_ISSUES_RESOLVED.md** - Previous issues and fixes
- **QUICK_REFERENCE.md** - Daily operations guide
- **DEPLOYMENT_STATUS.md** - Initial deployment summary
---
**Rebuild Status:** SUCCESSFUL ✅
**System Status:** PRODUCTION READY ✅
**Last Verified:** October 13, 2025 at 08:46 IST
**All Services:** 11/11 running, 7/7 Prometheus targets UP

View File

@@ -0,0 +1,404 @@
# 🔧 Issues Fixed & Troubleshooting Guide
**Date:** October 13, 2025
**Status:** Partial Fix Applied ✅ / Traces Still Need Configuration ⚠️
---
## ✅ FIXED: Redis Batch-Set API Error
### Problem
```
POST /api/cache/batch-set
Error 500: Invalid input of type: 'dict'. Convert to a bytes, string, int or float first.
```
### Root Cause
The `batch-set` endpoint was trying to store Python dict objects directly in Redis, but Redis only accepts strings, bytes, ints, or floats.
### Solution Applied
**File:** `app/main.py` (lines 599-617)
**Changed:**
```python
for key, value in items.items():
pipeline.set(key, value) # ❌ Error: can't store dict
```
**To:**
```python
for key, value in items.items():
# Convert value to JSON string if it's a dict, list, or other complex type
if isinstance(value, (dict, list)):
value_str = json.dumps(value)
else:
value_str = str(value)
pipeline.set(key, value_str) # ✅ Fixed: stores as JSON string
```
### Testing
```powershell
# Test the fixed endpoint
$body = '{"key1":"value1","key2":{"nested":"object"},"key3":["array","values"]}'
Invoke-WebRequest -Uri "http://localhost:8000/api/cache/batch-set" `
-Method POST -Body $body -ContentType "application/json"
```
**Expected:** Status 200, returns success count
---
## ⚠️ ISSUE: Jaeger Shows No Traces
### Problem
- Jaeger UI shows only `jaeger-all-in-one` service
- No traces from `devserver.api.fastapi` or `devserver.celery.worker`
- Logs show `TraceID: N/A` for all requests
### Root Cause Analysis
#### 1. OpenTelemetry Auto-Instrumentation Not Initializing
**Evidence:**
```
2025-10-13 03:56:20 | DEBUG | devserver.api.fastapi | log_requests :61 | TraceID: N/A
```
The `TraceID: N/A` means no active span context exists.
**Why:**
- `opentelemetry-instrument` command is used in docker-compose
- Environment variables are set (`OTEL_EXPORTER_OTLP_ENDPOINT`, etc.)
- BUT: The tracer provider might not be initializing before the app starts
#### 2. Jaeger Configuration
**Jaeger logs show:**
```json
{"level":"info","msg":"[core] [Channel #1]parsed dial target is: \"localhost:4317\""}
```
Jaeger is trying to connect to `localhost:4317` instead of receiving traces. This is expected behavior - Jaeger has OTLP enabled and is ready to receive.
#### 3. OTEL Collector Pipeline
**Configuration looks correct:**
```yaml
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [debug, otlphttp/jaeger]
```
But no trace activity in logs means traces aren't reaching the collector.
---
## 🔍 Diagnosis Steps Performed
### 1. Checked OTEL Packages ✅
```toml
"opentelemetry-distro>=0.58b0"
"opentelemetry-exporter-otlp-proto-grpc>=1.37.0"
"opentelemetry-instrumentation-fastapi>=0.58b0"
"opentelemetry-instrumentation-redis>=0.58b0"
"opentelemetry-instrumentation-celery>=0.58b0"
```
All packages are installed.
### 2. Checked Environment Variables ✅
```yaml
OTEL_TRACES_EXPORTER: otlp
OTEL_METRICS_EXPORTER: otlp
OTEL_LOGS_EXPORTER: otlp
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
OTEL_EXPORTER_OTLP_PROTOCOL: grpc
OTEL_SERVICE_NAME: devserver.api.fastapi
```
All variables are set correctly.
### 3. Checked Container Startup Command ✅
```yaml
command: [
"uv", "run",
"opentelemetry-instrument",
"--service_name", "devserver.api.fastapi",
"uvicorn", "app.main:app",
"--host", "0.0.0.0", "--port", "8000", "--workers", "2"
]
```
Command structure is correct.
### 4. Checked Network Connectivity ✅
- All containers on `devserver_network`
- OTEL collector accessible at `otel-collector:4317`
- Jaeger accessible at `jaeger:14268`
---
## 🛠️ Solutions to Apply
### Solution 1: Explicit TracerProvider Initialization ⭐ RECOMMENDED
Create an OpenTelemetry initialization module to explicitly configure tracing before app startup.
**File:** `app/opentelemetry_config.py` (NEW FILE)
```python
"""OpenTelemetry configuration for DevServer"""
import os
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource, SERVICE_NAME
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.redis import RedisInstrumentor
import logging
logger = logging.getLogger(__name__)
def init_opentelemetry():
"""Initialize OpenTelemetry with OTLP exporter"""
# Get configuration from environment
service_name = os.getenv("OTEL_SERVICE_NAME", "devserver.api.fastapi")
otlp_endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://otel-collector:4317")
logger.info(f"Initializing OpenTelemetry for {service_name}")
logger.info(f"OTLP Endpoint: {otlp_endpoint}")
# Create resource with service name
resource = Resource(attributes={
SERVICE_NAME: service_name
})
# Create tracer provider
provider = TracerProvider(resource=resource)
# Create OTLP exporter
otlp_exporter = OTLPSpanExporter(
endpoint=otlp_endpoint,
insecure=True # Use insecure channel (no TLS)
)
# Add span processor
span_processor = BatchSpanProcessor(otlp_exporter)
provider.add_span_processor(span_processor)
# Set as global tracer provider
trace.set_tracer_provider(provider)
logger.info("✓ OpenTelemetry initialized successfully")
return provider
def instrument_app(app):
"""Instrument FastAPI app with OpenTelemetry"""
# Initialize OpenTelemetry
init_opentelemetry()
# Instrument FastAPI
FastAPIInstrumentor().instrument_app(app)
# Instrument Redis
RedisInstrumentor().instrument()
logger.info("✓ FastAPI and Redis instrumented")
```
**Update `app/main.py`:**
```python
# Add at the top after imports
from app.opentelemetry_config import instrument_app
# After creating the FastAPI app
app = FastAPI(...)
# Instrument with OpenTelemetry
instrument_app(app)
```
### Solution 2: Use Bootstrap Script (Alternative)
**File:** `app/bootstrap.py` (NEW FILE)
```python
#!/usr/bin/env python
"""Bootstrap script to initialize OpenTelemetry before app starts"""
import os
os.environ.setdefault("OTEL_PYTHON_LOG_LEVEL", "debug")
from opentelemetry.instrumentation.auto_instrumentation import sitecustomize
# This will run before anything else
sitecustomize.initialize()
print("✓ OpenTelemetry bootstrap complete")
```
**Update docker-compose.yml:**
```yaml
command: [
"uv", "run",
"python", "-m", "app.bootstrap", # Run bootstrap first
"opentelemetry-instrument",
"--service_name", "devserver.api.fastapi",
"uvicorn", "app.main:app",
"--host", "0.0.0.0", "--port", "8000"
]
```
### Solution 3: Debug Mode (For Troubleshooting)
Add debug logging to see what's happening:
```yaml
environment:
OTEL_TRACES_EXPORTER: otlp
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
OTEL_PYTHON_LOG_LEVEL: debug # Add this
OTEL_LOG_LEVEL: debug # And this
```
---
## 📋 Step-by-Step Fix Implementation
### Step 1: Create OpenTelemetry Config File
```powershell
cd d:\GNPL-Private\DevServer
# Create the file with the code above
```
### Step 2: Update main.py
Add the import and instrumentation call
### Step 3: Rebuild Containers
```powershell
docker-compose build --no-cache devserver celery_worker
```
### Step 4: Restart Services
```powershell
docker-compose up -d --force-recreate devserver celery_worker
```
### Step 5: Generate Test Traffic
```powershell
# Make some API calls
for ($i=1; $i -le 5; $i++) {
Invoke-WebRequest -Uri "http://localhost:8000/api/dice/roll?session_id=test-$i"
Start-Sleep -Milliseconds 500
}
```
### Step 6: Check Jaeger
```powershell
# Wait 10 seconds for traces to propagate
Start-Sleep -Seconds 10
# Check services
$response = Invoke-WebRequest -Uri "http://localhost:16686/api/services"
($response.Content | ConvertFrom-Json).data
```
**Expected:** Should see `devserver.api.fastapi` in the list
---
## 🧪 Verification Commands
### Check if Traces are Being Generated
```powershell
# Check FastAPI logs for trace IDs
docker logs devserver_backend --tail=20 | Select-String "TraceID"
```
**Expected:** Should see actual hex trace IDs instead of "N/A"
### Check OTEL Collector is Receiving Traces
```powershell
docker logs devserver_otel_collector --tail=50 | Select-String "span"
```
**Expected:** Should see span activity
### Check Jaeger is Receiving from OTEL
```powershell
docker logs devserver_jaeger --tail=50 | Select-String "span"
```
**Expected:** Should see span ingestion logs
### Query Jaeger API
```powershell
# Get all services
Invoke-WebRequest -Uri "http://localhost:16686/api/services"
# Get traces for a specific service
Invoke-WebRequest -Uri "http://localhost:16686/api/traces?service=devserver.api.fastapi&limit=10"
```
---
## 🎯 Expected Behavior After Fix
### 1. Logs Should Show Real Trace IDs
```
2025-10-13 XX:XX:XX | DEBUG | devserver.api.fastapi | log_requests :61 |
TraceID: 4bf92f3577b34da6a3ce929d0e0e4736 ✅ (not "N/A")
```
### 2. OTEL Collector Should Show Activity
```
2025-10-13 XX:XX:XX | INFO | Span received from devserver.api.fastapi
```
### 3. Jaeger Services List
```json
{
"data": [
"devserver.api.fastapi", ✅
"devserver.celery.worker", ✅
"jaeger-all-in-one"
]
}
```
### 4. Jaeger UI Should Show Traces
- Open: http://localhost:16686
- Select Service: `devserver.api.fastapi`
- See: List of traces with operations like "api.dice.roll", "api.tasks.fibonacci.create"
---
## 📝 Current Status Summary
| Component | Status | Notes |
|-----------|--------|-------|
| **Batch-Set API** | ✅ FIXED | Now converts dicts/lists to JSON strings |
| **Containers** | ✅ Running | All 11 containers up |
| **OTEL Collector** | ✅ Configured | Pipeline ready to receive traces |
| **Jaeger** | ✅ Ready | UI accessible, OTLP enabled |
| **Trace Generation** | ❌ NOT WORKING | TraceID shows "N/A" |
| **Auto-Instrumentation** | ❌ NOT WORKING | Need explicit initialization |
---
## 🚀 Next Actions
1. **Implement Solution 1** (Recommended):
- Create `opentelemetry_config.py`
- Update `main.py` to call `instrument_app(app)`
- Rebuild and restart
2. **Or Use Simpler Approach**:
- Remove `opentelemetry-instrument` wrapper
- Use explicit instrumentation in code
- More control and easier debugging
3. **Test Thoroughly**:
- Generate API traffic
- Check logs for real trace IDs
- Verify Jaeger shows traces
---
**Last Updated:** October 13, 2025
**Status:** Redis fix applied ✅ | Tracing fix documented, needs implementation ⏳

330
docs/MONITORING_COMPLETE.md Normal file
View File

@@ -0,0 +1,330 @@
# ✅ Monitoring System - Complete and Operational
**Date:** October 13, 2025
**Status:** ALL SYSTEMS OPERATIONAL ✅
---
## 🎯 Current Status
### All Prometheus Targets: 7/7 UP ✅
| Target | Endpoint | Status | Metrics |
|--------|----------|--------|---------|
| **prometheus** | localhost:9090 | ✅ UP | Self-monitoring |
| **fastapi-app** | devserver:8000 | ✅ UP | Application metrics (6.4KB) |
| **rabbitmq** | rabbitmq:15692 | ✅ UP | Message queue metrics |
| **loki** | loki:3100 | ✅ UP | Log aggregator metrics |
| **cadvisor** | cadvisor:8080 | ✅ UP | Container metrics |
| **otel-collector** | otel-collector:8889 | ✅ UP | **OTEL metrics (41KB)** |
| **jaeger** | jaeger:14269 | ✅ UP | Tracing metrics |
---
## 🔧 Issues Resolved
### 1. OTEL Collector Metrics Exposure ✅
**Problem:** OTEL collector wasn't exposing its own metrics for Prometheus to scrape
- **Error:** `connection refused on port 8888`
- **Root Cause:** Missing Prometheus exporter in OTEL collector configuration
- **Solution:**
- Added `prometheus` exporter to `otel-collector-config.yaml` on port 8889
- Added exporter to metrics pipeline
- Updated Prometheus scrape config to use port 8889
- **Result:** 41KB of metrics now exposed and successfully scraped
### 2. Prometheus Remote Write Receiver ✅
**Problem:** OTEL collector couldn't push metrics to Prometheus via remote write
- **Error:** `404 Not Found: remote write receiver needs to be enabled`
- **Root Cause:** Prometheus missing `--web.enable-remote-write-receiver` flag
- **Solution:**
- Added flag to Prometheus command in docker-compose.yml
- Force recreated container (restart wasn't enough)
- **Result:** Remote write working, no more 404 errors
### 3. OTEL Collector Configuration Validation ✅
**Problem:** Configuration had invalid telemetry.metrics.address key
- **Error:** `'service.telemetry.metrics' decoding failed: invalid keys: address`
- **Root Cause:** OTEL Collector v0.137.0 doesn't support `address` in telemetry.metrics
- **Solution:**
- Removed invalid `address` key
- Used Prometheus exporter instead for metrics exposure
- Simplified telemetry config to just `level: detailed`
- **Result:** OTEL collector starts without configuration errors
---
## 📊 Complete Monitoring Stack (11 Services)
### Core Application Services
1. **devserver (FastAPI)** - Main backend application
- Port: 8000
- Metrics: ✅ `/metrics` endpoint
- OTLP: Sending traces to OTEL collector
2. **celery-worker** - Background task processor
- Broker: RabbitMQ
- Backend: Redis
- Monitoring: Via RabbitMQ metrics
3. **redis** - Cache and Celery backend
- Port: 6379
- Monitoring: Connection stats via app
4. **rabbitmq** - Message broker
- Ports: 5672 (AMQP), 15672 (UI), 15692 (metrics)
- Metrics: ✅ Prometheus endpoint
- UI: http://localhost:15672 (guest/guest)
### Observability Services
5. **Prometheus** - Metrics collection and storage
- Port: 9090
- UI: http://localhost:9090
- Config: 7 active scrape targets
- Features: Remote write enabled, lifecycle API, admin API
- Storage: 200h retention
6. **Grafana** - Metrics visualization
- Port: 3000
- UI: http://localhost:3000 (admin/admin)
- Datasources: Prometheus, Loki, Jaeger (auto-provisioned)
- Dashboards: 2 pre-configured
7. **OTEL Collector** - OpenTelemetry collector
- Version: 0.137.0 (contrib)
- Ports:
- 4317: OTLP gRPC receiver
- 4318: OTLP HTTP receiver
- 8889: Prometheus metrics exporter ✅
- 13133: Health check
- 1888: pprof profiling
- 55679: zpages extension
- Pipelines:
- Traces → Debug + Jaeger
- Metrics → **Prometheus exporter** + Remote write + Debug
- Logs → File + Debug
8. **Jaeger** - Distributed tracing
- Ports: 16686 (UI), 14268 (traces), 14269 (metrics)
- UI: http://localhost:16686
- Backend: Memory storage
9. **Loki** - Log aggregation
- Port: 3100
- Config: Local storage
- Metrics: ✅ Exposed on /metrics
10. **Promtail** - Log shipper
- Source: ./logs directory
- Target: Loki
- Format: JSON logs
11. **cAdvisor** - Container monitoring
- Port: 8080
- Metrics: ✅ Container CPU, memory, network, disk
- UI: http://localhost:8080
---
## 🔗 Quick Access URLs
| Service | URL | Credentials |
|---------|-----|-------------|
| **FastAPI Swagger** | http://localhost:8000/docs | - |
| **FastAPI Metrics** | http://localhost:8000/metrics | - |
| **Prometheus** | http://localhost:9090 | - |
| **Prometheus Targets** | http://localhost:9090/targets | - |
| **Grafana** | http://localhost:3000 | admin/admin |
| **Jaeger UI** | http://localhost:16686 | - |
| **RabbitMQ UI** | http://localhost:15672 | guest/guest |
| **cAdvisor** | http://localhost:8080 | - |
| **OTEL Health** | http://localhost:13133 | - |
| **OTEL Metrics** | http://localhost:8889/metrics | - |
---
## 📈 Data Flow
### Traces
```
FastAPI → OTLP (4317) → OTEL Collector → Jaeger (14268)
↓
Debug Logger
```
### Metrics
```
FastAPI → /metrics → Prometheus (scrape every 5s)
↓
Grafana (query)
OTEL Collector → Prometheus Exporter (8889) → Prometheus (scrape every 15s)
↓
Remote Write → Prometheus (9090)
```
### Logs
```
Application → ./logs/*.json → Promtail → Loki
↓
Grafana (query)
```
---
## 🛠️ Configuration Files
### Key Files Updated
1. **docker-compose.yml**
- Added Prometheus remote write receiver flag
- Added environment variable to OTEL collector
- All 11 services properly configured
2. **monitoring/otel-collector-config.yaml**
- Added `prometheus` exporter on port 8889
- Removed invalid `telemetry.metrics.address` key
- Added prometheus exporter to metrics pipeline
- Simplified telemetry configuration
3. **monitoring/prometheus.yml**
- Updated OTEL collector target from port 8888 → 8889
- 7 active scrape jobs configured
- Different intervals per job (5s-15s)
---
## ✅ Verification Commands
### Check All Container Status
```powershell
cd d:\GNPL-Private\DevServer
docker-compose ps
```
### Check Prometheus Targets
```powershell
$response = Invoke-WebRequest -Uri "http://localhost:9090/api/v1/targets" -Method GET
($response.Content | ConvertFrom-Json).data.activeTargets |
Select-Object @{N='Job';E={$_.labels.job}}, @{N='State';E={$_.health}}
```
### Test All Metrics Endpoints
```powershell
# FastAPI
Invoke-WebRequest -Uri "http://localhost:8000/metrics" | Select StatusCode
# OTEL Collector
Invoke-WebRequest -Uri "http://localhost:8889/metrics" | Select StatusCode
# RabbitMQ
Invoke-WebRequest -Uri "http://localhost:15692/metrics" | Select StatusCode
# cAdvisor
Invoke-WebRequest -Uri "http://localhost:8080/metrics" | Select StatusCode
```
### Check OTEL Collector Logs
```powershell
docker-compose logs --tail=50 otel-collector
```
### Check Prometheus Logs
```powershell
docker-compose logs --tail=50 prometheus
```
---
## 📝 Best Practices Implemented
✅ **Metrics Collection**
- Multiple scrape intervals optimized per service
- Proper labels for service identification
- Health checks on all exporters
✅ **Data Retention**
- Prometheus: 200 hours
- Loki: Log rotation enabled
- OTEL: File rotation for logs (100MB, 3 days, 3 backups)
✅ **Resource Management**
- OTEL memory limiter: 512MB
- Docker log rotation: 10MB per file, 3 files max
- Batch processing: 1s timeout, 1024 batch size
✅ **Security**
- Grafana: Sign-up disabled
- RabbitMQ: Default credentials (change in production)
- Network isolation: All services on devserver_network
✅ **High Availability**
- Health check endpoints on all services
- Proper dependency ordering in docker-compose
- Graceful shutdown handling
---
## 🎓 Key Learnings
1. **OTEL Collector Metrics Exposure**
- Don't rely on `telemetry.metrics.address` - use Prometheus exporter
- Port 8888 is for internal metrics, use 8889 for Prometheus scrape
- Always add exporters to the pipeline
2. **Prometheus Configuration**
- Remote write receiver must be explicitly enabled
- Container restart ≠ container recreate (use --force-recreate)
- Different scrape intervals for different workloads
3. **Docker Compose Best Practices**
- Version attribute is obsolete (remove it)
- Use container names for DNS resolution
- Always use networks for service discovery
4. **Debugging Approach**
- Check logs first: `docker-compose logs <service>`
- Verify endpoints: `Invoke-WebRequest` for HTTP services
- Check configuration: `docker inspect` for runtime settings
---
## 📚 Documentation
### Related Files
- `DEPLOYMENT_STATUS.md` - Initial deployment summary
- `MONITORING_GUIDE.md` - Detailed monitoring guide
- `QUICK_REFERENCE.md` - Quick reference card
- `system_monitor.py` - Automated health check script
### External Resources
- [OTEL Collector Documentation](https://opentelemetry.io/docs/collector/)
- [Prometheus Remote Write](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#remote_write)
- [Grafana Provisioning](https://grafana.com/docs/grafana/latest/administration/provisioning/)
---
## 🚀 Next Steps
### Recommended Actions
1. ✅ **Current State:** All monitoring is working
2. 🔄 **Customize Dashboards:** Add application-specific metrics to Grafana
3. 📊 **Set Alerts:** Configure Prometheus alerting rules
4. 🔐 **Secure Credentials:** Change default passwords (RabbitMQ, Grafana)
5. 📈 **Optimize:** Adjust scrape intervals based on traffic patterns
6. 🧪 **Load Testing:** Verify monitoring under high load
7. 📝 **Document:** Add application-specific metrics documentation
### Optional Enhancements
- Add Alertmanager for notifications
- Implement custom Grafana dashboards
- Add more detailed business metrics
- Set up external log shipping
- Implement distributed tracing best practices
- Add service mesh (Istio/Linkerd) for advanced observability
---
**System Status:** PRODUCTION READY ✅
**Last Verified:** October 13, 2025
**Verification:** All 7 Prometheus targets UP, no errors in logs

368
docs/MONITORING_GUIDE.md Normal file
View File

@@ -0,0 +1,368 @@
# DevServer Complete System Monitoring Guide
## 🎯 Overview
DevServer now includes a **comprehensive observability and monitoring stack** with automatic configuration. This guide covers all monitoring tools, dashboards, and troubleshooting procedures.
## 📊 Monitoring Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Application Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ FastAPI App │ │ Celery Worker│ │ Redis Cache │ │
│ │ (Port 8000) │ │ │ │ (Port 6379) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
└─────────┼──────────────────┼──────────────────┼──────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ OpenTelemetry Instrumentation │
│ (Traces, Metrics, Logs) │
└─────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Collection & Processing │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │OTEL Collector│ │ Prometheus │ │ Loki │ │
│ │ │ │ (Port 9090) │ │ (Port 3100) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
└─────────┼──────────────────┼──────────────────┼──────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Visualization Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Grafana │ │ Jaeger │ │ cAdvisor │ │
│ │ (Port 3000) │ │ (Port 16686) │ │ (Port 8080) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## 🚀 Services Overview
### Core Application Services
- **FastAPI Backend** (`:8000`) - Main API with OpenTelemetry instrumentation
- **Celery Worker** - Async task processing with Fibonacci calculations
- **Redis** (`:6379`) - Cache layer with TTL-based expiration
- **RabbitMQ** (`:5672`, `:15672`) - Message broker for Celery
### Monitoring Services
- **Prometheus** (`:9090`) - Metrics collection and storage
- **Grafana** (`:3000`) - Visualization dashboards
- **Jaeger** (`:16686`) - Distributed tracing UI
- **Loki** (`:3100`) - Log aggregation system
- **Promtail** - Log shipping agent
- **OTEL Collector** - Telemetry data processing
- **cAdvisor** (`:8080`) - Container metrics
- **Node Exporter** (`:9100`) - System metrics
## 📈 Monitoring Dashboards
### 1. Grafana Dashboard (Primary)
**URL:** http://localhost:3000
**Credentials:** admin / admin123
**Available Dashboards:**
- **Complete System Monitoring** - All services health and metrics
- **DevServer Observability** - Application-specific metrics
**Key Panels:**
- Service Health Status (Real-time UP/DOWN)
- API Request Rate & Response Time
- Redis Cache Hit Rate & Memory Usage
- Celery Task Queue & Duration
- RabbitMQ Message Rates
- HTTP Status Codes & Error Rates
- Container CPU & Memory Usage
- Prometheus Scrape Status
### 2. Prometheus Metrics
**URL:** http://localhost:9090
**Key Queries:**
```promql
# Service health
up
# API request rate
rate(http_requests_total[5m])
# API response time (p95)
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))
# Redis cache hit rate
rate(redis_keyspace_hits_total[5m]) / (rate(redis_keyspace_hits_total[5m]) + rate(redis_keyspace_misses_total[5m]))
# Celery task count
celery_tasks_total
# Error rate
rate(http_requests_total{status=~"5.."}[5m])
```
### 3. Jaeger Tracing
**URL:** http://localhost:16686
**Features:**
- Distributed trace visualization
- Service dependency mapping
- Request flow analysis
- Performance bottleneck identification
**Services to Search:**
- `devserver.api.fastapi`
- `devserver.worker.celery`
### 4. cAdvisor Container Monitoring
**URL:** http://localhost:8080
**Metrics:**
- CPU usage per container
- Memory usage per container
- Network I/O
- Disk I/O
- Container health status
### 5. RabbitMQ Management
**URL:** http://localhost:15672
**Credentials:** guest / guest
**Metrics:**
- Queue lengths
- Message rates
- Consumer connections
- Memory usage
## 🔍 System Monitoring Script
Run the comprehensive system monitor:
```powershell
# Check system status
python system_monitor.py
```
**Output Includes:**
- ✓/✗ Health status for all services
- Response times for each service
- Prometheus metrics summary
- Redis cache statistics
- Recommendations for unhealthy services
- Quick links to all dashboards
## 🛠️ Deployment & Management
### Start All Services
```powershell
docker-compose up -d
```
### Check Service Status
```powershell
docker-compose ps
```
### View Service Logs
```powershell
# All services
docker-compose logs -f
# Specific service
docker-compose logs -f devserver
docker-compose logs -f prometheus
docker-compose logs -f grafana
```
### Restart Services
```powershell
# Restart all
docker-compose restart
# Restart specific services
docker-compose restart prometheus grafana otel-collector
```
### Stop Services
```powershell
docker-compose down
```
### Rebuild After Changes
```powershell
docker-compose down
docker-compose up -d --build
```
## 🎯 Monitoring Workflows
### 1. Check Overall System Health
1. Open http://localhost:3000 (Grafana)
2. Navigate to "Complete System Monitoring" dashboard
3. Check "Service Health Status" panel
4. Review error rates and response times
### 2. Debug Performance Issues
1. Open Grafana dashboard
2. Check "API Response Time" panel for slow endpoints
3. Open Jaeger (http://localhost:16686)
4. Search for traces with high duration
5. Analyze bottlenecks in trace waterfall
### 3. Monitor Cache Effectiveness
1. Open Grafana dashboard
2. Review "Redis Cache Hit Rate" panel
3. Check "Redis Memory Usage"
4. Access Redis stats via API: `GET http://localhost:8000/api/cache/stats`
### 4. Track Task Processing
1. Open Grafana dashboard
2. Monitor "Celery Task Queue Length"
3. Check "Celery Task Duration"
4. Review RabbitMQ management UI for queue depths
### 5. Investigate Errors
1. Check Grafana "Error Rate" panel
2. Review Prometheus for error metrics
3. Check logs via `docker-compose logs`
4. Use Jaeger to trace failed requests
## 📊 Key Metrics to Monitor
### Application Metrics
- **Request Rate:** Should be steady, spikes indicate traffic changes
- **Response Time (p95):** Should be < 500ms for API calls
- **Error Rate:** Should be < 1% under normal conditions
- **Cache Hit Rate:** Should be > 80% for cached endpoints
### System Metrics
- **CPU Usage:** Should be < 70% under normal load
- **Memory Usage:** Should be stable, not constantly growing
- **Disk I/O:** Should be low for cached operations
- **Network I/O:** Should correlate with request rates
### Service-Specific
- **Redis Memory:** Should be < configured max
- **RabbitMQ Queue Depth:** Should be near zero (fast processing)
- **Celery Tasks:** Should complete within expected timeframes
## 🚨 Troubleshooting
### Service is Down
```powershell
# Check service logs
docker-compose logs <service-name>
# Restart service
docker-compose restart <service-name>
# Check if port is in use
netstat -ano | findstr :<port>
```
### Grafana Data Source Issues
```powershell
# Restart Grafana
docker-compose restart grafana
# Check data source configuration
# Go to Grafana → Configuration → Data Sources
```
### OTEL Collector Not Working
```powershell
# Check configuration
docker-compose logs otel-collector
# Verify config file
cat monitoring/otel-collector-config.yaml
# Restart collector
docker-compose restart otel-collector
```
### Prometheus Not Scraping
```powershell
# Check Prometheus targets
# Open http://localhost:9090/targets
# Verify Prometheus config
cat monitoring/prometheus.yml
# Restart Prometheus
docker-compose restart prometheus
```
### High Memory Usage
```powershell
# Check container stats
docker stats
# Check specific service
docker-compose logs <service-name>
# Restart heavy services
docker-compose restart devserver worker
```
## 🔐 Security Considerations
### Change Default Passwords
Edit `docker-compose.yml`:
```yaml
environment:
GF_SECURITY_ADMIN_PASSWORD: your_secure_password
```
### Restrict External Access
For production, use:
```yaml
ports:
- "127.0.0.1:3000:3000" # Grafana only on localhost
```
### Enable HTTPS
Use a reverse proxy (nginx/traefik) with SSL certificates.
## 📝 Configuration Files
### Key Configuration Files
- `docker-compose.yml` - Service orchestration
- `monitoring/prometheus.yml` - Prometheus scrape config
- `monitoring/otel-collector-config.yaml` - OTEL collector config
- `monitoring/loki-config.yaml` - Loki config
- `monitoring/promtail-config.yaml` - Promtail config
- `monitoring/grafana/provisioning/` - Grafana auto-provisioning
## 🎓 Best Practices
1. **Regular Monitoring:** Check dashboards at least daily
2. **Set Alerts:** Configure Grafana alerts for critical metrics
3. **Log Retention:** Adjust log retention based on storage
4. **Backup Configs:** Version control all monitoring configs
5. **Resource Limits:** Set container resource limits in production
6. **Security:** Change default passwords and restrict ports
7. **Documentation:** Keep this guide updated with changes
## 📚 Additional Resources
- **Prometheus Documentation:** https://prometheus.io/docs/
- **Grafana Documentation:** https://grafana.com/docs/
- **Jaeger Documentation:** https://www.jaegertracing.io/docs/
- **OpenTelemetry:** https://opentelemetry.io/docs/
- **cAdvisor:** https://github.com/google/cadvisor
## 🆘 Support
For issues or questions:
1. Check service logs: `docker-compose logs <service>`
2. Run system monitor: `python system_monitor.py`
3. Review Grafana dashboards for anomalies
4. Check this documentation for troubleshooting steps
---
**Last Updated:** October 13, 2025
**Version:** 2.0.0

111
docs/QUICK_REFERENCE.md Normal file
View File

@@ -0,0 +1,111 @@
# 🚀 DevServer Monitoring - Quick Reference Card
## 📊 Monitoring Dashboards
| Service | URL | Credentials | Purpose |
|---------|-----|-------------|---------|
| **Grafana** | http://localhost:3000 | admin / admin123 | Main dashboards & visualization |
| **Prometheus** | http://localhost:9090 | None | Metrics & queries |
| **Prometheus Targets** | http://localhost:9090/targets | None | Check scrape status |
| **Jaeger** | http://localhost:16686 | None | Distributed tracing |
| **cAdvisor** | http://localhost:8080 | None | Container metrics |
| **RabbitMQ** | http://localhost:15672 | guest / guest | Queue management |
| **FastAPI** | http://localhost:8000 | None | API endpoints |
| **FastAPI Docs** | http://localhost:8000/docs | None | Interactive API docs |
## 🎯 Active Prometheus Targets (7 services)
### ✅ Working (5/7)
- ✅ prometheus (self-monitoring)
- ✅ rabbitmq (message queue)
- ✅ loki (logs)
- ✅ cadvisor (containers)
- ✅ jaeger (traces)
### ⏳ Initializing (2/7)
- ⏳ fastapi-app (starting up - may take 30-60s)
- ⏳ otel-collector (initializing telemetry pipeline)
## 🔧 Common Commands
```powershell
# Check all services
docker-compose ps
# View logs
docker-compose logs -f devserver
docker-compose logs -f prometheus
# Restart services
docker-compose restart prometheus grafana
# System health check
uv run python system_monitor.py
# Test API
Invoke-WebRequest -Uri "http://localhost:8000/health" -Method GET
Invoke-WebRequest -Uri "http://localhost:8000/metrics" -Method GET
# Generate test data
Invoke-WebRequest -Uri "http://localhost:8000/api/dice/roll" -Method GET
Invoke-WebRequest -Uri "http://localhost:8000/api/tasks/fibonacci?n=15" -Method POST
```
## 📈 Key Metrics to Monitor
| Metric | Normal Range | Alert If |
|--------|--------------|----------|
| **API Response Time (p95)** | < 500ms | > 1000ms |
| **Error Rate** | < 1% | > 5% |
| **Cache Hit Rate** | > 80% | < 50% |
| **CPU Usage** | < 70% | > 90% |
| **Memory Usage** | Stable | Constantly growing |
| **Queue Depth** | Near 0 | > 100 |
## 🎯 Grafana Dashboards
1. **Complete System Monitoring**
- Service health status
- API metrics
- Cache performance
- Container resources
2. **DevServer Observability**
- Request rates
- Response times
- Error tracking
## 🔍 Troubleshooting Quick Fixes
| Issue | Solution |
|-------|----------|
| Service down | `docker-compose restart <service>` |
| Slow API | Check Jaeger traces at http://localhost:16686 |
| High errors | Check logs: `docker-compose logs devserver` |
| Metrics not updating | Restart Prometheus: `docker-compose restart prometheus` |
| Grafana not loading | `docker-compose restart grafana` |
| Target shows "down" | Wait 60s or check: `docker-compose logs <service>` |
## 📦 Services Summary
**Total:** 11 containers
**Core App:** 4 services (FastAPI, Celery, Redis, RabbitMQ)
**Monitoring:** 7 services (Prometheus, Grafana, Jaeger, Loki, Promtail, OTEL, cAdvisor)
## ✨ Features
- ✅ Automatic OpenTelemetry tracing
- ✅ Redis caching with TTL
- ✅ Real-time metrics (15s intervals)
- ✅ Centralized logging
- ✅ Container monitoring
- ✅ Distributed tracing
- ✅ Auto-provisioned dashboards
- ✅ Zero manual configuration
## 🎊 Status: FULLY OPERATIONAL
**Last Updated:** October 13, 2025
**All Services:** ✅ RUNNING
**Monitoring:** ✅ ACTIVE
**Dashboards:** ✅ READY

View File

@@ -0,0 +1,309 @@
{
"id": null,
"uid": "devserver-main",
"title": "DevServer Monitoring Dashboard",
"tags": ["devserver", "monitoring", "fastapi"],
"timezone": "browser",
"schemaVersion": 27,
"version": 1,
"refresh": "30s",
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"]
},
"panels": [
{
"id": 1,
"title": "API Request Rate",
"type": "timeseries",
"targets": [
{
"expr": "rate(http_requests_total[5m])",
"legendFormat": "{{method}} {{endpoint}}",
"refId": "A"
}
],
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 0},
"fieldConfig": {
"defaults": {
"unit": "reqps"
}
}
},
{
"id": 2,
"title": "API Response Time (p95 & p50)",
"type": "timeseries",
"targets": [
{
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
"legendFormat": "95th percentile",
"refId": "A"
},
{
"expr": "histogram_quantile(0.50, rate(http_request_duration_seconds_bucket[5m]))",
"legendFormat": "50th percentile",
"refId": "B"
}
],
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 0},
"fieldConfig": {
"defaults": {
"unit": "s"
}
}
},
{
"id": 3,
"title": "HTTP Status Codes Distribution",
"type": "piechart",
"targets": [
{
"expr": "sum(increase(http_requests_total[5m])) by (status)",
"legendFormat": "{{status}}",
"refId": "A"
}
],
"gridPos": {"h": 8, "w": 8, "x": 0, "y": 8}
},
{
"id": 4,
"title": "Active Requests",
"type": "stat",
"targets": [
{
"expr": "http_requests_in_progress",
"legendFormat": "In Progress",
"refId": "A"
}
],
"gridPos": {"h": 4, "w": 8, "x": 8, "y": 8},
"fieldConfig": {
"defaults": {
"unit": "short",
"color": {
"mode": "thresholds"
},
"thresholds": {
"mode": "absolute",
"steps": [
{"value": null, "color": "green"},
{"value": 50, "color": "yellow"},
{"value": 100, "color": "red"}
]
}
}
}
},
{
"id": 5,
"title": "Total Requests (5m)",
"type": "stat",
"targets": [
{
"expr": "sum(increase(http_requests_total[5m]))",
"legendFormat": "Total",
"refId": "A"
}
],
"gridPos": {"h": 4, "w": 8, "x": 16, "y": 8},
"fieldConfig": {
"defaults": {
"unit": "short"
}
}
},
{
"id": 6,
"title": "Error Rate (5xx)",
"type": "stat",
"targets": [
{
"expr": "sum(rate(http_requests_total{status=~\"5..\"}[5m])) / sum(rate(http_requests_total[5m])) * 100",
"legendFormat": "Error Rate",
"refId": "A"
}
],
"gridPos": {"h": 4, "w": 8, "x": 8, "y": 12},
"fieldConfig": {
"defaults": {
"unit": "percent",
"color": {
"mode": "thresholds"
},
"thresholds": {
"mode": "absolute",
"steps": [
{"value": null, "color": "green"},
{"value": 1, "color": "yellow"},
{"value": 5, "color": "red"}
]
}
}
}
},
{
"id": 7,
"title": "Average Response Time",
"type": "stat",
"targets": [
{
"expr": "rate(http_request_duration_seconds_sum[5m]) / rate(http_request_duration_seconds_count[5m])",
"legendFormat": "Avg Response Time",
"refId": "A"
}
],
"gridPos": {"h": 4, "w": 8, "x": 16, "y": 12},
"fieldConfig": {
"defaults": {
"unit": "s",
"decimals": 3
}
}
},
{
"id": 8,
"title": "Container CPU Usage",
"type": "timeseries",
"targets": [
{
"expr": "rate(container_cpu_usage_seconds_total{name=~\"devserver.*\"}[5m]) * 100",
"legendFormat": "{{name}}",
"refId": "A"
}
],
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 16},
"fieldConfig": {
"defaults": {
"unit": "percent"
}
}
},
{
"id": 9,
"title": "Container Memory Usage",
"type": "timeseries",
"targets": [
{
"expr": "container_memory_usage_bytes{name=~\"devserver.*\"} / 1024 / 1024",
"legendFormat": "{{name}}",
"refId": "A"
}
],
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 16},
"fieldConfig": {
"defaults": {
"unit": "mbytes"
}
}
},
{
"id": 10,
"title": "RabbitMQ Queue Size",
"type": "timeseries",
"targets": [
{
"expr": "rabbitmq_queue_messages",
"legendFormat": "{{queue}}",
"refId": "A"
}
],
"gridPos": {"h": 6, "w": 12, "x": 0, "y": 24},
"fieldConfig": {
"defaults": {
"unit": "short"
}
}
},
{
"id": 11,
"title": "RabbitMQ Message Rate",
"type": "timeseries",
"targets": [
{
"expr": "rate(rabbitmq_queue_messages_published_total[5m])",
"legendFormat": "Published",
"refId": "A"
},
{
"expr": "rate(rabbitmq_queue_messages_delivered_total[5m])",
"legendFormat": "Delivered",
"refId": "B"
}
],
"gridPos": {"h": 6, "w": 12, "x": 12, "y": 24},
"fieldConfig": {
"defaults": {
"unit": "msgs/s"
}
}
},
{
"id": 12,
"title": "OpenTelemetry Collector Metrics",
"type": "timeseries",
"targets": [
{
"expr": "rate(otelcol_receiver_accepted_spans[5m])",
"legendFormat": "Spans Received",
"refId": "A"
},
{
"expr": "rate(otelcol_exporter_sent_spans[5m])",
"legendFormat": "Spans Exported",
"refId": "B"
}
],
"gridPos": {"h": 6, "w": 12, "x": 0, "y": 30},
"fieldConfig": {
"defaults": {
"unit": "ops"
}
}
},
{
"id": 13,
"title": "Process Memory Usage",
"type": "timeseries",
"targets": [
{
"expr": "process_resident_memory_bytes / 1024 / 1024",
"legendFormat": "{{job}}",
"refId": "A"
}
],
"gridPos": {"h": 6, "w": 12, "x": 12, "y": 30},
"fieldConfig": {
"defaults": {
"unit": "mbytes"
}
}
}
],
"templating": {
"list": []
},
"annotations": {
"list": [
{
"datasource": "Prometheus",
"enable": true,
"expr": "ALERTS{alertstate=\"firing\"}",
"iconColor": "red",
"name": "Alerts",
"step": "60s",
"tagKeys": "alertname",
"textFormat": "{{alertname}}",
"titleFormat": "Alert"
}
]
},
"links": [],
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"style": "dark"
}

View File

@@ -0,0 +1,10 @@
apiVersion: 1
providers:
- name: 'DevServer Dashboards'
type: file
disableDeletion: false
updateIntervalSeconds: 10
allowUiUpdates: true
options:
path: /var/lib/grafana/dashboards

View File

@@ -0,0 +1,27 @@
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true
jsonData:
timeInterval: "5s"
- name: Loki
type: loki
access: proxy
url: http://loki:3100
isDefault: false
editable: true
jsonData:
maxLines: 1000
- name: Jaeger
type: jaeger
access: proxy
url: http://jaeger:16686
isDefault: false
editable: true

View File

@@ -0,0 +1,51 @@
auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
common:
instance_addr: 127.0.0.1
path_prefix: /tmp/loki
storage:
filesystem:
chunks_directory: /tmp/loki/chunks
rules_directory: /tmp/loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
limits_config:
allow_structured_metadata: false
query_range:
results_cache:
cache:
embedded_cache:
enabled: true
max_size_mb: 100
schema_config:
configs:
- from: 2020-10-24
store: boltdb-shipper
object_store: filesystem
schema: v11
index:
prefix: index_
period: 24h
ruler:
alertmanager_url: http://localhost:9093
# By default, Loki will send anonymous, but uniquely-identifiable usage and configuration
# analytics to Grafana Labs. These statistics are sent to https://stats.grafana.org/
#
# Statistics help us better understand how Loki is used and guide our investments.
# This helps us prioritize features and documentation.
# For more information on the stats we collect check out
# https://github.com/grafana/loki/blob/main/pkg/usagestats/stats.go
# Refer to the documentation for more info on configuring the usage_stats config.
analytics:
reporting_enabled: false

View File

@@ -0,0 +1,79 @@
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
prometheus:
config:
scrape_configs:
- job_name: 'otel-collector'
static_configs:
- targets: ['localhost:8888']
processors:
batch:
timeout: 1s
send_batch_size: 1024
memory_limiter:
check_interval: 1s
limit_mib: 512
spike_limit_mib: 128
exporters:
debug:
verbosity: detailed
prometheus:
endpoint: "0.0.0.0:8889"
namespace: otelcol
prometheusremotewrite:
endpoint: "http://prometheus:9090/api/v1/write"
# Jaeger exporter - using OTLP/gRPC protocol
otlp/jaeger:
endpoint: "jaeger:4317"
tls:
insecure: true
file/logs:
path: "/var/log/otel-collector/logs.json"
rotation:
max_megabytes: 100
max_days: 3
max_backups: 3
extensions:
health_check:
endpoint: 0.0.0.0:13133
pprof:
endpoint: 0.0.0.0:1888
zpages:
endpoint: 0.0.0.0:55679
service:
extensions: [health_check, pprof, zpages]
telemetry:
logs:
level: info
metrics:
level: detailed
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [debug, otlp/jaeger]
metrics:
receivers: [otlp, prometheus]
processors: [memory_limiter, batch]
exporters: [prometheus, prometheusremotewrite, debug]
logs:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [file/logs, debug]

52
monitoring/prometheus.yml Normal file
View File

@@ -0,0 +1,52 @@
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
# Prometheus itself
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# FastAPI Application
- job_name: 'fastapi-app'
static_configs:
- targets: ['devserver:8000']
scrape_interval: 5s
scrape_timeout: 4s
metrics_path: '/metrics'
# RabbitMQ Metrics
- job_name: 'rabbitmq'
static_configs:
- targets: ['rabbitmq:15692']
scrape_interval: 15s
# Loki Metrics
- job_name: 'loki'
static_configs:
- targets: ['loki:3100']
scrape_interval: 15s
metrics_path: '/metrics'
# cAdvisor Container Metrics
- job_name: 'cadvisor'
static_configs:
- targets: ['cadvisor:8080']
scrape_interval: 15s
metrics_path: '/metrics'
# OTEL Collector Metrics
- job_name: 'otel-collector'
static_configs:
- targets: ['otel-collector:8889']
scrape_interval: 15s
scrape_timeout: 10s
metrics_path: '/metrics'
# Jaeger Metrics
- job_name: 'jaeger'
static_configs:
- targets: ['jaeger:14269']
scrape_interval: 15s
metrics_path: '/metrics'

View File

@@ -0,0 +1,44 @@
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: docker
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
relabel_configs:
- source_labels: ['__meta_docker_container_name']
regex: '/(.*)'
target_label: 'container'
- source_labels: ['__meta_docker_container_name']
regex: '/(.*)'
target_label: 'container_name'
- source_labels: ['__meta_docker_container_name']
regex: '/(.*)'
target_label: 'job'
- source_labels: ['__meta_docker_container_name']
regex: '/(.*)'
replacement: '$1'
target_label: 'instance'
pipeline_stages:
- json:
expressions:
level: level
timestamp: timestamp
message: message
service: service
- labels:
level:
service:
- timestamp:
source: timestamp
format: '2006-01-02T15:04:05.000Z07:00'
- output:
source: message

28
prometheus.yml Normal file
View File

@@ -0,0 +1,28 @@
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
# - "first_rules.yml"
# - "second_rules.yml"
scrape_configs:
- job_name: 'devserver-api'
static_configs:
- targets: ['devserver:8000']
metrics_path: '/metrics'
scrape_interval: 5s
- job_name: 'redis'
static_configs:
- targets: ['redis:6379']
metrics_path: '/metrics'
- job_name: 'rabbitmq'
static_configs:
- targets: ['rabbitmq:15692']
metrics_path: '/metrics'
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']

39
pyproject.toml Normal file
View File

@@ -0,0 +1,39 @@
[project]
name = "devserver"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"alembic>=1.17.0",
"bcrypt>=5.0.0",
"black>=25.9.0",
"celery>=5.5.3",
"email-validator>=2.3.0",
"fastapi>=0.119.0",
"httpx>=0.28.1",
"isort>=7.0.0",
"opentelemetry-distro>=0.58b0",
"opentelemetry-exporter-otlp-proto-grpc>=1.37.0",
"opentelemetry-instrumentation-asyncio>=0.58b0",
"opentelemetry-instrumentation-celery>=0.58b0",
"opentelemetry-instrumentation-fastapi>=0.58b0",
"opentelemetry-instrumentation-httpx>=0.58b0",
"opentelemetry-instrumentation-redis>=0.58b0",
"opentelemetry-instrumentation-sqlalchemy>=0.58b0",
"passlib>=1.7.4",
"prometheus-fastapi-instrumentator>=7.0.0",
"psycopg2-binary>=2.9.11",
"pydantic>=2.12.0",
"pyjwt>=2.10.1",
"pytest>=8.4.2",
"pytest-asyncio>=1.2.0",
"python-dotenv>=1.1.1",
"python-multipart>=0.0.20",
"redis>=6.4.0",
"requests>=2.32.5",
"ruff>=0.14.0",
"slowapi>=0.1.9",
"sqlalchemy>=2.0.44",
"uvicorn>=0.37.0",
]

22
requirements.txt Normal file
View File

@@ -0,0 +1,22 @@
fastapi
uvicorn
sqlalchemy
alembic
psycopg2-binary
slowapi
python-dotenv
pyjwt
passlib
bcrypt
python-multipart
email-validator
pydantic
pytest
pytest-asyncio
httpx
black
isort
ruff
opentelemetry-distro
opentelemetry-exporter-otlp-proto-grpc
opentelemetry-instrumentation-fastapi

256
system_monitor.py Normal file
View File

@@ -0,0 +1,256 @@
#!/usr/bin/env python3
"""
DevServer System Monitor
Comprehensive health check and status monitoring for all services
"""
import requests
import json
import time
from datetime import datetime
from typing import Dict, List, Tuple
import sys
# Service endpoints
SERVICES = {
"FastAPI Backend": "http://localhost:8000/health",
"Prometheus": "http://localhost:9090/-/healthy",
"Grafana": "http://localhost:3000/api/health",
"Jaeger": "http://localhost:16686",
"Loki": "http://localhost:3100/ready",
"Redis": "http://localhost:6379",
"RabbitMQ": "http://localhost:15672",
"cAdvisor": "http://localhost:8080/healthz",
"Node Exporter": "http://localhost:9100/metrics",
}
COLORS = {
"green": "\033[92m",
"red": "\033[91m",
"yellow": "\033[93m",
"blue": "\033[94m",
"reset": "\033[0m",
"bold": "\033[1m",
}
def colored(text: str, color: str) -> str:
"""Return colored text for terminal output"""
return f"{COLORS.get(color, '')}{text}{COLORS['reset']}"
def check_service(name: str, url: str, timeout: int = 5) -> Tuple[bool, str, float]:
"""Check if a service is healthy"""
start = time.time()
try:
response = requests.get(url, timeout=timeout)
duration = (time.time() - start) * 1000 # Convert to ms
if response.status_code in [200, 204]:
return True, "OK", duration
else:
return False, f"HTTP {response.status_code}", duration
except requests.exceptions.ConnectionError:
duration = (time.time() - start) * 1000
return False, "Connection Refused", duration
except requests.exceptions.Timeout:
return False, "Timeout", timeout * 1000
except Exception as e:
duration = (time.time() - start) * 1000
return False, str(e)[:50], duration
def get_prometheus_metrics() -> Dict:
"""Fetch key metrics from Prometheus"""
try:
queries = {
"up_services": 'count(up == 1)',
"total_services": 'count(up)',
"api_requests_rate": 'rate(http_requests_total{job="fastapi-app"}[5m])',
"error_rate": 'rate(http_requests_total{job="fastapi-app",status=~"5.."}[5m])',
}
metrics = {}
for name, query in queries.items():
response = requests.get(
f"http://localhost:9090/api/v1/query",
params={"query": query},
timeout=5
)
if response.status_code == 200:
data = response.json()
if data.get("data", {}).get("result"):
value = data["data"]["result"][0]["value"][1]
metrics[name] = float(value)
return metrics
except Exception as e:
return {"error": str(e)}
def get_redis_stats() -> Dict:
"""Fetch Redis statistics"""
try:
response = requests.get("http://localhost:8000/api/cache/stats", timeout=5)
if response.status_code == 200:
return response.json()
return {}
except Exception:
return {}
def print_header():
"""Print system status header"""
print("\n" + "=" * 80)
print(colored("DevServer System Status Monitor", "bold"))
print(colored(f"Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", "blue"))
print("=" * 80 + "\n")
def print_service_status(services_status: Dict):
"""Print service health status table"""
print(colored("Service Health Status:", "bold"))
print("-" * 80)
print(f"{'Service':<25} {'Status':<15} {'Details':<25} {'Response Time (ms)':<15}")
print("-" * 80)
healthy_count = 0
total_count = len(services_status)
for service, (is_healthy, details, response_time) in services_status.items():
if is_healthy:
status = colored("✓ HEALTHY", "green")
healthy_count += 1
else:
status = colored("✗ UNHEALTHY", "red")
print(f"{service:<25} {status:<24} {details:<25} {response_time:>10.2f}")
print("-" * 80)
health_percentage = (healthy_count / total_count) * 100
color = "green" if health_percentage == 100 else "yellow" if health_percentage >= 80 else "red"
print(colored(f"Overall Health: {healthy_count}/{total_count} services ({health_percentage:.1f}%)", color))
print()
def print_prometheus_metrics(metrics: Dict):
"""Print Prometheus metrics summary"""
if "error" in metrics:
print(colored(f"Failed to fetch Prometheus metrics: {metrics['error']}", "red"))
return
print(colored("Prometheus Metrics Summary:", "bold"))
print("-" * 80)
if "up_services" in metrics and "total_services" in metrics:
print(f"Active Targets: {int(metrics['up_services'])}/{int(metrics['total_services'])}")
if "api_requests_rate" in metrics:
print(f"API Request Rate: {metrics['api_requests_rate']:.2f} req/s")
if "error_rate" in metrics:
print(f"API Error Rate: {metrics['error_rate']:.2f} errors/s")
print()
def print_redis_stats(stats: Dict):
"""Print Redis cache statistics"""
if not stats:
print(colored("Redis statistics unavailable", "yellow"))
return
print(colored("Redis Cache Statistics:", "bold"))
print("-" * 80)
if "total_keys" in stats:
print(f"Total Keys: {stats['total_keys']}")
if "memory_used" in stats:
print(f"Memory Used: {stats['memory_used']}")
if "connected_clients" in stats:
print(f"Connected Clients: {stats['connected_clients']}")
if "hits" in stats and "misses" in stats:
total = stats["hits"] + stats["misses"]
if total > 0:
hit_rate = (stats["hits"] / total) * 100
print(f"Cache Hit Rate: {hit_rate:.2f}% ({stats['hits']} hits / {stats['misses']} misses)")
print()
def print_recommendations(services_status: Dict):
"""Print recommendations based on service status"""
unhealthy = [name for name, (healthy, _, _) in services_status.items() if not healthy]
if unhealthy:
print(colored("Recommendations:", "bold"))
print("-" * 80)
for service in unhealthy:
print(colored(f"✗ {service} is down", "red"))
if "Redis" in service:
print(" → Check Redis container: docker-compose logs redis")
elif "RabbitMQ" in service:
print(" → Check RabbitMQ container: docker-compose logs rabbitmq")
elif "FastAPI" in service:
print(" → Check FastAPI logs: docker-compose logs devserver")
elif "Prometheus" in service:
print(" → Check Prometheus config: docker-compose logs prometheus")
elif "Grafana" in service:
print(" → Check Grafana logs: docker-compose logs grafana")
else:
print(f" → Check container logs: docker-compose logs {service.lower().replace(' ', '-')}")
print()
def main():
"""Main monitoring function"""
print_header()
# Check all services
print("Checking all services...\n")
services_status = {}
for service, url in SERVICES.items():
services_status[service] = check_service(service, url)
# Print results
print_service_status(services_status)
# Get and print Prometheus metrics
metrics = get_prometheus_metrics()
print_prometheus_metrics(metrics)
# Get and print Redis stats
redis_stats = get_redis_stats()
print_redis_stats(redis_stats)
# Print recommendations
print_recommendations(services_status)
# Print monitoring URLs
print(colored("Monitoring Dashboards:", "bold"))
print("-" * 80)
print(f"Grafana Dashboard: {colored('http://localhost:3000', 'blue')} (admin/admin123)")
print(f"Prometheus: {colored('http://localhost:9090', 'blue')}")
print(f"Jaeger Tracing: {colored('http://localhost:16686', 'blue')}")
print(f"RabbitMQ Management: {colored('http://localhost:15672', 'blue')} (guest/guest)")
print(f"cAdvisor: {colored('http://localhost:8080', 'blue')}")
print("=" * 80 + "\n")
# Return exit code based on health
unhealthy_count = len([s for s, (h, _, _) in services_status.items() if not h])
return 0 if unhealthy_count == 0 else 1
if __name__ == "__main__":
try:
exit_code = main()
sys.exit(exit_code)
except KeyboardInterrupt:
print("\n\nMonitoring interrupted by user")
sys.exit(130)
except Exception as e:
print(colored(f"\nError: {e}", "red"))
sys.exit(1)

275
test_all_apis.py Normal file
View File

@@ -0,0 +1,275 @@
"""
Complete API Test Suite
Tests all endpoints including Redis operations
"""
import requests
import time
import json
from datetime import datetime
BASE_URL = "http://localhost:8000"
print("=" * 70)
print("DevServer API - Complete Test Suite")
print("=" * 70)
print(f"Base URL: {BASE_URL}")
print(f"Started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("=" * 70)
def test_api_info():
"""Test API information endpoint"""
print("\n1. API Information")
print("-" * 70)
response = requests.get(f"{BASE_URL}/")
data = response.json()
print(f" Service: {data['service']}")
print(f" Version: {data['version']}")
print(f" Endpoints: {len(data['endpoints'])} categories")
print(f" Status: {response.status_code}")
def test_health():
"""Test health check"""
print("\n2. Health Check")
print("-" * 70)
response = requests.get(f"{BASE_URL}/health")
data = response.json()
print(f" Status: {data['status']}")
print(f" Redis: {data['redis']}")
print(f" Status: {response.status_code}")
def test_dice_roll():
"""Test dice rolling"""
print("\n3. Dice Roll (FastAPI + Tracing)")
print("-" * 70)
start = time.time()
response = requests.get(f"{BASE_URL}/api/dice/roll")
elapsed = time.time() - start
data = response.json()
print(f" Total rolls: {data['total']}")
print(f" Service: {data['service']}")
print(f" Duration: {elapsed:.3f}s")
print(f" Status: {response.status_code}")
def test_redis_single():
"""Test single Redis operations"""
print("\n4. Redis Single Operations")
print("-" * 70)
# Set
key = f"test_{int(time.time())}"
value = f"hello_world_{int(time.time())}"
response = requests.post(f"{BASE_URL}/api/cache/set", params={
"key": key,
"value": value,
"ttl": 300
})
print(f" SET {key} = {value}")
print(f" Status: {response.status_code}")
# Get
response = requests.get(f"{BASE_URL}/api/cache/{key}")
data = response.json()
print(f" GET {key}")
print(f" Value: {data['value']}")
print(f" Status: {response.status_code}")
# Delete
response = requests.delete(f"{BASE_URL}/api/cache/{key}")
data = response.json()
print(f" DELETE {key}")
print(f" Deleted: {data['deleted']}")
print(f" Status: {response.status_code}")
def test_redis_batch():
"""Test batch Redis operations"""
print("\n5. Redis Batch Operations")
print("-" * 70)
# Batch Set
items = {
f"batch_key_1_{int(time.time())}": "value1",
f"batch_key_2_{int(time.time())}": "value2",
f"batch_key_3_{int(time.time())}": "value3"
}
response = requests.post(f"{BASE_URL}/api/cache/batch-set", json=items)
data = response.json()
print(f" BATCH SET: {len(items)} items")
print(f" Successful: {data['successful']}/{data['total']}")
print(f" Status: {response.status_code}")
# Batch Get
keys = ",".join(items.keys())
response = requests.get(f"{BASE_URL}/api/cache/batch-get", params={"keys": keys})
data = response.json()
print(f" BATCH GET: {len(items)} keys")
print(f" Found: {data['found']}/{data['requested']}")
print(f" Status: {response.status_code}")
def test_redis_stats():
"""Test Redis statistics"""
print("\n6. Redis Statistics")
print("-" * 70)
response = requests.get(f"{BASE_URL}/api/cache/stats")
data = response.json()
stats = data['stats']
print(f" Memory Used: {stats['used_memory']}")
print(f" Total Keys: {stats['total_keys']}")
print(f" Connected Clients: {stats['connected_clients']}")
print(f" Cache Hits: {stats['hits']}")
print(f" Cache Misses: {stats['misses']}")
print(f" Hit Ratio: {stats['hit_ratio_percent']}%")
print(f" Uptime: {stats['uptime_seconds']}s")
print(f" Status: {response.status_code}")
def test_redis_keys():
"""Test listing cache keys"""
print("\n7. List Cache Keys")
print("-" * 70)
response = requests.get(f"{BASE_URL}/api/cache/keys", params={"pattern": "*"})
data = response.json()
print(f" Pattern: {data['pattern']}")
print(f" Keys Found: {data['count']}")
if data['keys'] and len(data['keys']) <= 5:
print(f" Keys: {data['keys']}")
elif data['keys']:
print(f" Sample Keys: {data['keys'][:5]}...")
print(f" Status: {response.status_code}")
def test_celery_fibonacci():
"""Test Celery Fibonacci task"""
print("\n8. Celery Task (Fibonacci Calculation)")
print("-" * 70)
# Create task
n = 15
start = time.time()
response = requests.post(f"{BASE_URL}/api/tasks/fibonacci", params={"n": n})
data = response.json()
task_id = data['task_id']
print(f" Creating Fibonacci({n}) task...")
print(f" Task ID: {task_id}")
print(f" Status: {response.status_code}")
# Wait for completion
print(f" Waiting for task completion...")
max_attempts = 30
for attempt in range(max_attempts):
response = requests.get(f"{BASE_URL}/api/tasks/{task_id}")
data = response.json()
if data['status'] == 'completed':
elapsed = time.time() - start
result = data['result']
print(f" Task completed in {elapsed:.2f}s")
print(f" Fibonacci({n}) = {result['fibonacci']}")
print(f" Calculation time: {result['calculation_time']}")
print(f" Service: {result['service']}")
break
time.sleep(1)
else:
print(f" Task did not complete within {max_attempts}s")
def test_process_workflow():
"""Test combined cache + Celery workflow"""
print("\n9. Data Processing Workflow (Cache + Celery)")
print("-" * 70)
data_payload = {
"user_id": 12345,
"action": "test_workflow",
"timestamp": time.time(),
"data": "sample processing data"
}
# First request - should process with Celery
print(f" First request (cache miss - will use Celery)...")
start1 = time.time()
elapsed1 = 0.0
response = requests.post(f"{BASE_URL}/api/process", json=data_payload)
data1 = response.json()
print(f" Source: {data1['source']}")
if data1['source'] == 'celery_task':
task_id = data1['task_id']
print(f" Task ID: {task_id}")
# Wait for task
for _ in range(30):
response = requests.get(f"{BASE_URL}/api/tasks/{task_id}")
result = response.json()
if result['status'] == 'completed':
elapsed1 = time.time() - start1
print(f" Task completed in {elapsed1:.2f}s")
break
time.sleep(1)
# Second request - should return from cache
print(f" Second request (cache hit - should be instant)...")
start2 = time.time()
response = requests.post(f"{BASE_URL}/api/process", json=data_payload)
data2 = response.json()
elapsed2 = time.time() - start2
print(f" Source: {data2['source']}")
print(f" Duration: {elapsed2:.3f}s")
if data1['source'] == 'celery_task' and data2['source'] == 'cache' and elapsed1 > 0:
speedup = elapsed1 / elapsed2 if elapsed2 > 0 else 0
print(f" Cache speedup: {speedup:.1f}x faster!")
def run_all_tests():
"""Run complete test suite"""
start_time = time.time()
try:
test_api_info()
time.sleep(0.5)
test_health()
time.sleep(0.5)
test_dice_roll()
time.sleep(0.5)
test_redis_single()
time.sleep(0.5)
test_redis_batch()
time.sleep(0.5)
test_redis_stats()
time.sleep(0.5)
test_redis_keys()
time.sleep(0.5)
test_celery_fibonacci()
time.sleep(0.5)
test_process_workflow()
elapsed = time.time() - start_time
print("\n" + "=" * 70)
print(f"All tests completed successfully in {elapsed:.2f}s!")
print("=" * 70)
print("\nNext Steps:")
print(" 1. Open Jaeger UI: http://localhost:16686")
print(" 2. Select service: 'devserver.api.fastapi' or 'devserver.celery.worker'")
print(" 3. Click 'Find Traces' to see distributed tracing")
print(" 4. Explore traces to see request flow across services")
print(" 5. Check logs in ./logs/ directory")
print("=" * 70)
except requests.exceptions.ConnectionError:
print("\nERROR: Cannot connect to DevServer")
print(" Make sure the server is running:")
print(" docker-compose up -d")
except Exception as e:
print(f"\nERROR: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
run_all_tests()

229
test_cache_apis.py Normal file
View File

@@ -0,0 +1,229 @@
"""
Test script demonstrating Redis caching in Dice Roll and Fibonacci APIs
"""
import requests
import time
BASE_URL = "http://localhost:8000"
def print_header(title):
print("\n" + "=" * 70)
print(f" {title}")
print("=" * 70)
def print_section(title):
print(f"\n{title}")
print("-" * 70)
def test_dice_with_caching():
"""Test dice roll API with Redis caching"""
print_header("DICE ROLL API - Redis Caching Test")
session_id = "player_123"
# First roll - should generate fresh results and cache them
print_section("1️⃣ First Roll (Fresh - should cache)")
response = requests.get(f"{BASE_URL}/api/dice/roll", params={
"session_id": session_id,
"use_cache": True
})
data1 = response.json()
print(f" Session ID: {data1['session_id']}")
print(f" Total Rolls: {data1['total']}")
print(f" Statistics: {data1['statistics']}")
print(f" Source: {data1['source']}")
print(f" Cache Key: {data1['cache_key']}")
print(f" Status: {response.status_code}")
# Second roll - should return cached results (same rolls)
print_section("2️⃣ Second Roll (Same Session - should hit cache)")
response = requests.get(f"{BASE_URL}/api/dice/roll", params={
"session_id": session_id,
"use_cache": True
})
data2 = response.json()
print(f" Source: {data2['source']}")
print(f" Total Rolls: {data2['total']}")
print(f" Statistics: {data2['statistics']}")
print(f" Same results as first roll: {data1['dice_rolls'] == data2['dice_rolls']}")
print(f" Status: {response.status_code}")
# Third roll - different session, fresh results
print_section("3️⃣ Third Roll (Different Session - fresh)")
response = requests.get(f"{BASE_URL}/api/dice/roll", params={
"session_id": "player_456",
"use_cache": True
})
data3 = response.json()
print(f" Session ID: {data3['session_id']}")
print(f" Source: {data3['source']}")
print(f" Total Rolls: {data3['total']}")
print(f" Statistics: {data3['statistics']}")
print(f" Status: {response.status_code}")
# Fourth roll - cache disabled
print_section("4️⃣ Fourth Roll (Cache Disabled)")
response = requests.get(f"{BASE_URL}/api/dice/roll", params={
"session_id": session_id,
"use_cache": False
})
data4 = response.json()
print(f" Source: {data4['source']}")
print(f" Statistics: {data4['statistics']}")
print(f" Different from cached: {data1['dice_rolls'] != data4['dice_rolls']}")
print(f" Status: {response.status_code}")
def test_fibonacci_with_caching():
"""Test Fibonacci API with Redis caching"""
print_header("FIBONACCI API - Redis Caching Test")
# Test with n=10
n = 10
# First request - should compute and cache
print_section(f"1️⃣ First Request (n={n}) - Fresh Computation")
start = time.time()
response = requests.post(f"{BASE_URL}/api/tasks/fibonacci", params={
"n": n,
"use_cache": True
})
elapsed1 = time.time() - start
data1 = response.json()
print(f" n: {data1['n']}")
print(f" Status: {data1['status']}")
print(f" Source: {data1['source']}")
print(f" Response Time: {elapsed1:.3f}s")
print(f" Cache Key: {data1['cache_key']}")
if data1['source'] == 'celery_task':
print(f" Task ID: {data1['task_id']}")
print(f" Check result at: {data1['check_result_at']}")
# Wait for task completion
print("\n ⏳ Waiting for task to complete...")
time.sleep(2)
# Get result
task_response = requests.get(f"{BASE_URL}/api/tasks/{data1['task_id']}")
task_data = task_response.json()
print(f" ✅ Task Status: {task_data['status']}")
if task_data['status'] == 'completed':
result = task_data['result']
print(f" Result: Fibonacci({n}) = {result.get('result', result.get('fibonacci'))}")
print(f" Computation Time: {result.get('computation_time_seconds', 'N/A')}s")
else:
print(f" ✅ Result: Fibonacci({n}) = {data1['result']}")
# Second request - should hit cache immediately
print_section(f"2️⃣ Second Request (n={n}) - Should Hit Cache")
start = time.time()
response = requests.post(f"{BASE_URL}/api/tasks/fibonacci", params={
"n": n,
"use_cache": True
})
elapsed2 = time.time() - start
data2 = response.json()
print(f" Status: {data2['status']}")
print(f" Source: {data2['source']}")
print(f" Response Time: {elapsed2:.3f}s")
print(f" Result: Fibonacci({n}) = {data2.get('result', 'N/A')}")
print(f" 🚀 Speedup: {elapsed1/elapsed2:.1f}x faster!")
# Third request - cache disabled, force recomputation
print_section(f"3️⃣ Third Request (n={n}) - Cache Disabled")
start = time.time()
response = requests.post(f"{BASE_URL}/api/tasks/fibonacci", params={
"n": n,
"use_cache": False
})
elapsed3 = time.time() - start
data3 = response.json()
print(f" Status: {data3['status']}")
print(f" Source: {data3['source']}")
print(f" Response Time: {elapsed3:.3f}s")
# Test with larger number
n_large = 20
print_section(f"4️⃣ Large Fibonacci (n={n_large}) - First Request")
start = time.time()
response = requests.post(f"{BASE_URL}/api/tasks/fibonacci", params={
"n": n_large,
"use_cache": True
})
elapsed_large1 = time.time() - start
data_large = response.json()
print(f" Source: {data_large['source']}")
print(f" Response Time: {elapsed_large1:.3f}s")
if data_large['source'] == 'celery_task':
print(f" Task ID: {data_large['task_id']}")
print("\n ⏳ Waiting for computation...")
time.sleep(3)
task_response = requests.get(f"{BASE_URL}/api/tasks/{data_large['task_id']}")
task_data = task_response.json()
if task_data['status'] == 'completed':
result = task_data['result']
print(f" ✅ Result: Fibonacci({n_large}) = {result.get('result', result.get('fibonacci'))}")
# Cached request for large number
print_section(f"5️⃣ Large Fibonacci (n={n_large}) - Cached Request")
start = time.time()
response = requests.post(f"{BASE_URL}/api/tasks/fibonacci", params={
"n": n_large,
"use_cache": True
})
elapsed_large2 = time.time() - start
data_large2 = response.json()
print(f" Source: {data_large2['source']}")
print(f" Response Time: {elapsed_large2:.3f}s")
if data_large2['source'] == 'cache':
print(f" ✅ Result: Fibonacci({n_large}) = {data_large2['result']}")
print(f" 🚀 Much faster with cache!")
def test_cache_stats():
"""Display Redis cache statistics"""
print_header("REDIS CACHE STATISTICS")
response = requests.get(f"{BASE_URL}/api/cache/stats")
data = response.json()
stats = data['stats']
print(f"\n Memory Used: {stats['used_memory']}")
print(f" Total Keys: {stats['total_keys']}")
print(f" Connected Clients: {stats['connected_clients']}")
print(f" Cache Hits: {stats['hits']}")
print(f" Cache Misses: {stats['misses']}")
print(f" Hit Ratio: {stats['hit_ratio_percent']}%")
print(f" Uptime: {stats['uptime_seconds']} seconds")
def main():
print("\n" + "🎲" * 35)
print(" Redis Caching Test Suite for DevServer API")
print("🎲" * 35)
try:
# Test Dice Roll API
test_dice_with_caching()
# Test Fibonacci API
test_fibonacci_with_caching()
# Display cache statistics
test_cache_stats()
print("\n" + "=" * 70)
print(" ✅ All tests completed successfully!")
print("=" * 70)
print("\n💡 Key Takeaways:")
print(" • Dice rolls are cached per session (5 min TTL)")
print(" • Fibonacci results are cached globally (1 hour TTL)")
print(" • Cache dramatically improves response times")
print(" • Both APIs support cache bypass with use_cache=false")
print("\n")
except Exception as e:
print(f"\n❌ Error: {e}")
if __name__ == "__main__":
main()

1573
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff