Initial state

This commit is contained in:
Kalzu Rekku
2025-06-10 22:25:48 +03:00
commit c4fed415dc
6 changed files with 319 additions and 0 deletions

47
Dockerfile Normal file
View File

@ -0,0 +1,47 @@
# Use Debian-based Python 3.13 slim image
FROM python:3.13-slim-bookworm
# Set working directory
WORKDIR /app
# Install system dependencies for rrdtool
RUN apt-get update && apt-get install -y \
rrdtool \
librrd-dev \
build-essential \
python3-dev \
wget \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements first for better Docker layer caching
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
# Remove build dependencies to reduce image size
RUN apt-get purge -y build-essential python3-dev && \
apt-get autoremove -y && \
apt-get clean
# Copy application code
COPY app/ ./app/
# Create directory for RRD data
RUN mkdir -p /app/data
# Expose port
EXPOSE 8000
# Create non-root user for security
RUN useradd --create-home --shell /bin/bash appuser && \
chown -R appuser:appuser /app
USER appuser
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8000/health || exit 1
# Run the application
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

0
app/__init__.py Normal file
View File

203
app/main.py Normal file
View File

@ -0,0 +1,203 @@
# app/main.py
import os
import uuid
import json
import logging
from datetime import datetime
from fastapi import FastAPI, Request, status
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel, Field, validator, constr, conlist
from typing import Dict, List
import uuid as uuid_lib
from collections import deque
from pythonjsonlogger import jsonlogger
# --- Service Configuration ---
# Generate a unique Service UUID on startup, or get it from an environment variable
SERVICE_UUID = os.environ.get("SERVICE_UUID", str(uuid.uuid4()))
# --- Logging Configuration ---
# Get the root logger
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# Use a handler that streams to stdout
logHandler = logging.StreamHandler()
# Create a JSON formatter and add it to the handler
# The format string adds default log attributes to the JSON output
formatter = jsonlogger.JsonFormatter(
'%(asctime)s %(name)s %(levelname)s %(message)s'
)
logHandler.setFormatter(formatter)
# Add the handler to the root logger
# Avoid adding handlers multiple times in a uvicorn environment
if not logger.handlers:
logger.addHandler(logHandler)
class LogBuffer:
def __init__(self, maxlen=1000):
self.buffer = deque(maxlen=maxlen)
def add_log(self, record):
self.buffer.append({
'timestamp': record.get('asctime'),
'level': record.get('levelname'),
'message': record.get('message'),
'extra': {k: v for k, v in record.items()
if k not in ['asctime', 'levelname', 'message', 'name']}
})
def get_logs(self, limit=100):
return list(self.buffer)[-limit:]
# Create global log buffer
log_buffer = LogBuffer()
# Add buffer handler to logger
buffer_handler = BufferHandler()
logger.addHandler(buffer_handler)
# Custom handler to capture logs
class BufferHandler(logging.Handler):
def emit(self, record):
try:
# Format the record as JSON
formatter = jsonlogger.JsonFormatter()
log_entry = json.loads(formatter.format(record))
log_buffer.add_log(log_entry)
except Exception:
pass
# --- FastAPI Application ---
app = FastAPI(
title="Node Monitoring System",
description=f"A distributed monitoring system. Service UUID: {SERVICE_UUID}"
)
# Configure templates for the web interface
templates = Jinja2Templates(directory="app/web/templates")
# --- Data Models (as defined in the project spec) ---
class NodeStatusModel(BaseModel):
uptime_seconds: int
load_avg: conlist(float, min_length=3, max_length=3)
memory_usage_percent: float
class PingModel(BaseModel):
pings: Dict[constr(regex=r'^[0-9a-fA-F-]{36}$'), float]
class StatusUpdate(BaseModel):
node: str = Field(..., description="Node UUID")
timestamp: datetime
status: NodeStatusModel
pings: Dict[str, float]
@validator('node')
def validate_node_uuid(cls, v):
try:
uuid_lib.UUID(v)
return v
except ValueError:
raise ValueError('Invalid UUID format')
@validator('pings')
def validate_ping_uuids(cls, v):
for key in v.keys():
try:
uuid_lib.UUID(key)
except ValueError:
raise ValueError(f'Invalid UUID format in pings: {key}')
return v
# A mock database of known nodes for the auto-discovery demo
# In a real app, this would be managed more dynamically
known_nodes_db = {}
# --- API Endpoints ---
@app.get("/", response_class=HTMLResponse)
async def read_root(request: Request):
"""Serves the main web page which displays the Service UUID."""
logger.info(
"Web root accessed",
extra={'client_ip': request.client.host, 'service_uuid': SERVICE_UUID}
)
return templates.TemplateResponse(
"index.html",
{"request": request, "service_uuid": SERVICE_UUID}
)
# Add the logs endpoint
@app.get("/{service_uuid}/logs")
async def get_logs(service_uuid: str, limit: int = 100):
"""Get recent logs for the service."""
if service_uuid != SERVICE_UUID:
return JSONResponse(
status_code=404,
content={"error": "Service UUID not found"}
)
logs = log_buffer.get_logs(limit)
return {
"service_uuid": service_uuid,
"log_count": len(logs),
"logs": logs
}
@app.put("/{service_uuid}/{node_uuid}/", status_code=status.HTTP_200_OK)
async def update_node_status(
service_uuid: str,
node_uuid: str,
status_update: StatusUpdate,
request: Request
):
"""Receives status updates from a node and returns a list of peers."""
# Log the incoming status update with structured context
logger.info(
"Received node status update",
extra={
'event_type': 'node_status_update',
'client_ip': request.client.host,
'service_uuid': service_uuid,
'node_uuid': node_uuid,
'data': status_update.dict()
}
)
# In a real implementation, you would now update the RRD database here.
# e.g., database.update(node_uuid, status_update)
# For now, we simulate the logic.
if service_uuid != SERVICE_UUID:
logger.warning(
"Node sent status to wrong service UUID",
extra={'client_node_uuid': node_uuid, 'target_uuid': service_uuid}
)
return {"error": "Service UUID mismatch", "peers": []}
# Auto-discovery logic: update our list of known nodes
if node_uuid not in known_nodes_db:
logger.info(f"New node discovered: {node_uuid}")
# A real app would need a strategy to handle node addresses
known_nodes_db[node_uuid] = {"last_seen": datetime.utcnow().isoformat(), "ip": request.client.host}
# Respond with the list of other known peers
peer_list = {uuid: data for uuid, data in known_nodes_db.items() if uuid != node_uuid}
return {"message": "Status received", "peers": peer_list}
@app.get("/health")
async def health_check():
"""Health check endpoint for container orchestration."""
return {"status": "ok"}

View File

View File

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html>
<head>
<title>Node Monitor - Logs</title>
<style>
body { font-family: monospace; margin: 20px; background: #1a1a1a; color: #00ff00; }
.log-entry { margin: 5px 0; padding: 5px; border-left: 3px solid #333; }
.log-info { border-left-color: #0066cc; }
.log-warning { border-left-color: #ff9900; }
.log-error { border-left-color: #cc0000; }
.timestamp { color: #666; }
.level { font-weight: bold; margin-right: 10px; }
.message { color: #fff; }
.extra { color: #888; font-size: 0.9em; margin-top: 3px; }
h1 { color: #00ff00; }
.refresh-info { color: #666; margin: 10px 0; }
</style>
</head>
<body>
<h1>Node Monitor Logs - {{ service_uuid }}</h1>
<div class="refresh-info">Auto-refreshing every 5 seconds...</div>
<div id="logs-container">
{% for log in logs %}
<div class="log-entry log-{{ log.level.lower() }}">
<span class="timestamp">{{ log.timestamp }}</span>
<span class="level">{{ log.level }}</span>
<span class="message">{{ log.message }}</span>
{% if log.extra %}
<div class="extra">{{ log.extra | tojson }}</div>
{% endif %}
</div>
{% endfor %}
</div>
<script>
// Auto-refresh logs every 5 seconds
setInterval(async function() {
try {
const response = await fetch('{{ service_uuid }}/logs/json');
const data = await response.json();
const container = document.getElementById('logs-container');
container.innerHTML = '';
data.logs.forEach(log => {
const div = document.createElement('div');
div.className = `log-entry log-${log.level.toLowerCase()}`;
div.innerHTML = `
<span class="timestamp">${log.timestamp}</span>
<span class="level">${log.level}</span>
<span class="message">${log.message}</span>
${log.extra ? `<div class="extra">${JSON.stringify(log.extra)}</div>` : ''}
`;
container.appendChild(div);
});
} catch (error) {
console.error('Failed to refresh logs:', error);
}
}, 5000);
</script>
</body>
</html>

6
requirements.txt Normal file
View File

@ -0,0 +1,6 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
python-rrdtool==1.4.7
jinja2==3.1.2
python-multipart==0.0.6
python-json-logger==2.0.7