trying to make the logs view to have better date format.
This commit is contained in:
42
app/main.py
42
app/main.py
@ -2,7 +2,7 @@ import os
|
|||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone, timedelta
|
||||||
from fastapi import FastAPI, Request, status, Query
|
from fastapi import FastAPI, Request, status, Query
|
||||||
from fastapi.responses import HTMLResponse, JSONResponse
|
from fastapi.responses import HTMLResponse, JSONResponse
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
@ -12,6 +12,7 @@ from typing import Dict, List, Annotated
|
|||||||
import uuid as uuid_lib
|
import uuid as uuid_lib
|
||||||
|
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
from dateutil.parser import isoparse
|
||||||
|
|
||||||
from pythonjsonlogger import jsonlogger
|
from pythonjsonlogger import jsonlogger
|
||||||
import sys
|
import sys
|
||||||
@ -61,7 +62,7 @@ class LogBuffer:
|
|||||||
# Apply level filter
|
# Apply level filter
|
||||||
if level and level.strip():
|
if level and level.strip():
|
||||||
level = level.upper()
|
level = level.upper()
|
||||||
valid_levels = {'INFO', 'WARNING', 'ERROR', 'DEBUG'} # Added DEBUG for completeness
|
valid_levels = {'INFO', 'WARNING', 'ERROR', 'DEBUG'}
|
||||||
if level in valid_levels:
|
if level in valid_levels:
|
||||||
logs = [log for log in logs if log['level'].upper() == level]
|
logs = [log for log in logs if log['level'].upper() == level]
|
||||||
else:
|
else:
|
||||||
@ -69,8 +70,16 @@ class LogBuffer:
|
|||||||
# Apply since filter
|
# Apply since filter
|
||||||
if since:
|
if since:
|
||||||
try:
|
try:
|
||||||
# Handle 'Z' for UTC and ensure timezone awareness for comparison
|
# Use isoparse for robust parsing of ISO 8601 strings
|
||||||
since_dt = datetime.fromisoformat(since.replace('Z', '+00:00')).astimezone(timezone.utc)
|
since_dt = isoparse(since)
|
||||||
|
|
||||||
|
# If the parsed datetime is naive (no timezone info), assume it's UTC
|
||||||
|
if since_dt.tzinfo is None:
|
||||||
|
since_dt = since_dt.replace(tzinfo=timezone.utc)
|
||||||
|
else:
|
||||||
|
# If it has timezone info, convert it to UTC for consistent comparison
|
||||||
|
since_dt = since_dt.astimezone(timezone.utc)
|
||||||
|
|
||||||
logs = [log for log in logs if
|
logs = [log for log in logs if
|
||||||
datetime.fromisoformat(log['timestamp'].replace('Z', '+00:00')).astimezone(timezone.utc) >= since_dt]
|
datetime.fromisoformat(log['timestamp'].replace('Z', '+00:00')).astimezone(timezone.utc) >= since_dt]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -90,6 +99,13 @@ if not logger.handlers:
|
|||||||
logger.addHandler(logHandler)
|
logger.addHandler(logHandler)
|
||||||
logger.addHandler(BufferHandler())
|
logger.addHandler(BufferHandler())
|
||||||
|
|
||||||
|
# Configure Uvicorn's loggers to propagate to the root logger
|
||||||
|
# This ensures Uvicorn's startup and access logs are captured by our BufferHandler
|
||||||
|
logging.getLogger("uvicorn").propagate = True
|
||||||
|
logging.getLogger("uvicorn.access").propagate = True
|
||||||
|
logging.getLogger("uvicorn.error").propagate = True
|
||||||
|
|
||||||
|
|
||||||
# --- FastAPI Application ---
|
# --- FastAPI Application ---
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Node Monitoring System",
|
title="Node Monitoring System",
|
||||||
@ -139,6 +155,7 @@ LOAD_AVG_CRITICAL_THRESHOLD = 3.0
|
|||||||
MEMORY_WARNING_THRESHOLD = 75.0
|
MEMORY_WARNING_THRESHOLD = 75.0
|
||||||
MEMORY_CRITICAL_THRESHOLD = 90.0
|
MEMORY_CRITICAL_THRESHOLD = 90.0
|
||||||
LAST_SEEN_CRITICAL_THRESHOLD_SECONDS = 30
|
LAST_SEEN_CRITICAL_THRESHOLD_SECONDS = 30
|
||||||
|
NODE_INACTIVE_REMOVAL_THRESHOLD_SECONDS = 300 # Remove node from UI after 5 minutes of inactivity
|
||||||
|
|
||||||
def get_node_health(node_data: Dict) -> str:
|
def get_node_health(node_data: Dict) -> str:
|
||||||
last_seen_str = node_data.get("last_seen")
|
last_seen_str = node_data.get("last_seen")
|
||||||
@ -330,13 +347,25 @@ async def update_node_status(
|
|||||||
@app.get("/nodes/status")
|
@app.get("/nodes/status")
|
||||||
async def get_all_nodes_status():
|
async def get_all_nodes_status():
|
||||||
logger.info("Fetching all nodes status for UI.")
|
logger.info("Fetching all nodes status for UI.")
|
||||||
response_nodes = []
|
|
||||||
|
|
||||||
|
# Prune inactive nodes from known_nodes_db before processing
|
||||||
|
current_time_utc = datetime.now(timezone.utc)
|
||||||
|
nodes_to_remove = []
|
||||||
|
for node_uuid, data in known_nodes_db.items():
|
||||||
|
last_seen_dt = datetime.fromisoformat(data["last_seen"]).replace(tzinfo=timezone.utc)
|
||||||
|
if (current_time_utc - last_seen_dt).total_seconds() > NODE_INACTIVE_REMOVAL_THRESHOLD_SECONDS:
|
||||||
|
nodes_to_remove.append(node_uuid)
|
||||||
|
logger.info(f"Removing inactive node {node_uuid} from known_nodes_db.")
|
||||||
|
|
||||||
|
for node_uuid in nodes_to_remove:
|
||||||
|
known_nodes_db.pop(node_uuid)
|
||||||
|
|
||||||
|
response_nodes = []
|
||||||
for node_uuid, data in known_nodes_db.items():
|
for node_uuid, data in known_nodes_db.items():
|
||||||
current_health = get_node_health(data)
|
current_health = get_node_health(data)
|
||||||
|
|
||||||
connections = {}
|
connections = {}
|
||||||
for target_uuid in known_nodes_db:
|
for target_uuid in known_nodes_db: # Only iterate over currently active nodes
|
||||||
if target_uuid != node_uuid:
|
if target_uuid != node_uuid:
|
||||||
ping_data = database.get_ping_data(node_uuid, target_uuid, start_time="-300s")
|
ping_data = database.get_ping_data(node_uuid, target_uuid, start_time="-300s")
|
||||||
latency_ms = None
|
latency_ms = None
|
||||||
@ -363,3 +392,4 @@ async def get_all_nodes_status():
|
|||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
async def health_check():
|
async def health_check():
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
# --- END OF FILE main.py ---
|
||||||
|
@ -25,15 +25,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
console.log('Response status:', response.status);
|
console.log('Response status:', response.status);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
const errorText = await response.text(); // Try to get response body as text
|
||||||
|
console.error('Response text on error:', errorText); // Log it
|
||||||
|
// If the server returns a 404, it might be due to a stale UUID.
|
||||||
|
// Log a more specific message.
|
||||||
|
if (response.status === 404) {
|
||||||
|
console.error(`404 Not Found: Service UUID mismatch. Current page UUID: ${serviceUuid}. Server UUID might have changed. Please hard refresh the page.`);
|
||||||
|
logTableContainer.innerHTML = `<p class="loading-message error-message">Error: Service UUID mismatch. Please hard refresh your browser tab (Ctrl+F5 or Cmd+Shift+R).</p>`;
|
||||||
|
} else {
|
||||||
|
// Log the status code to help debug
|
||||||
|
console.error(`HTTP error! status: ${response.status} - ${errorText}`);
|
||||||
|
logTableContainer.innerHTML = `<p class="loading-message error-message">Error loading logs. HTTP Status: ${response.status}. Please check server connection or hard refresh.</p>`;
|
||||||
|
}
|
||||||
|
return; // Stop further processing if error
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('Received logs:', data.logs.length);
|
console.log('Received logs:', data.logs.length);
|
||||||
renderLogTable(data.logs);
|
renderLogTable(data.logs);
|
||||||
logCountSpan.textContent = data.log_count;
|
logCountSpan.textContent = data.log_count;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching logs:", error);
|
console.error("Error fetching logs (JSON parsing or other):", error);
|
||||||
logTableContainer.innerHTML = '<p class="loading-message">Error loading logs. Please check server connection.</p>';
|
logTableContainer.innerHTML = '<p class="loading-message error-message">Error loading logs. Failed to parse response or other network issue. See console for details.</p>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,9 +150,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
applyFiltersButton.addEventListener('click', () => {
|
applyFiltersButton.addEventListener('click', () => {
|
||||||
const selectedRadio = document.querySelector('input[name="log-level"]:checked');
|
const selectedRadio = document.querySelector('input[name="log-level"]:checked');
|
||||||
currentLevel = selectedRadio ? selectedRadio.value : '';
|
currentLevel = selectedRadio ? selectedRadio.value : '';
|
||||||
const sinceValue = sinceFilter.value;
|
// Send the datetime-local value directly. It's already in YYYY-MM-DDTHH:mm (24-hour) format.
|
||||||
// Convert local datetime input to ISO string for backend, handling potential timezone issues
|
// The backend will interpret this as UTC.
|
||||||
currentSince = sinceValue ? new Date(sinceValue).toISOString().replace(/\.\d{3}Z$/, 'Z') : ''; // Ensure 'Z' for UTC
|
currentSince = sinceFilter.value;
|
||||||
console.log('Applying filters:', { level: currentLevel, since: currentSince });
|
console.log('Applying filters:', { level: currentLevel, since: currentSince });
|
||||||
fetchLogs();
|
fetchLogs();
|
||||||
});
|
});
|
||||||
|
@ -19,8 +19,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderNodeGrid(nodes) {
|
function renderNodeGrid(nodes) {
|
||||||
nodeGridContainer.innerHTML = ''; // Clear existing content
|
nodeGridContainer.innerHTML = '';
|
||||||
nodeCountSpan.textContent = nodes.length; // Update total node count
|
nodeCountSpan.textContent = nodes.length;
|
||||||
|
|
||||||
if (nodes.length === 0) {
|
if (nodes.length === 0) {
|
||||||
nodeGridContainer.innerHTML = '<p class="loading-message">No nodes reporting yet. Start a client!</p>';
|
nodeGridContainer.innerHTML = '<p class="loading-message">No nodes reporting yet. Start a client!</p>';
|
||||||
@ -132,3 +132,4 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
fetchNodeData();
|
fetchNodeData();
|
||||||
setInterval(fetchNodeData, POLLING_INTERVAL_MS);
|
setInterval(fetchNodeData, POLLING_INTERVAL_MS);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -40,6 +40,8 @@ body {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
width: 80vw;
|
width: 80vw;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
@ -63,13 +65,17 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#node-grid-container, #log-table-container {
|
#node-grid-container, #log-table-container {
|
||||||
width: 80vw;
|
width: 95vw;
|
||||||
max-width: 1200px;
|
max-width: 1600px;
|
||||||
min-width: 400px;
|
min-width: 400px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background-color: var(--nord3);
|
background-color: var(--nord3);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connection-grid {
|
.connection-grid {
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<h1>Node Monitoring System</h1>
|
<h1>Node Monitoring System</h1>
|
||||||
<p>Total Nodes: <span id="node-count">0</span></p>
|
<p>Total Nodes: <span id="node-count">0</span></p>
|
||||||
<p>Service UUID: <code>{{ service_uuid }}</code></p> <!-- Always display service UUID -->
|
<p>Service UUID: <code>{{ service_uuid }}</code></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="node-grid-container">
|
<div id="node-grid-container">
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<!-- Removed: <meta http-equiv="refresh" content="5"> -->
|
|
||||||
<title>Node Monitor Logs - {{ service_uuid }}</title>
|
<title>Node Monitor Logs - {{ service_uuid }}</title>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', path='/style.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', path='/style.css') }}">
|
||||||
</head>
|
</head>
|
||||||
@ -19,8 +18,8 @@
|
|||||||
<label><input type="radio" name="log-level" value="WARNING"> WARNING</label>
|
<label><input type="radio" name="log-level" value="WARNING"> WARNING</label>
|
||||||
<label><input type="radio" name="log-level" value="ERROR"> ERROR</label>
|
<label><input type="radio" name="log-level" value="ERROR"> ERROR</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<label for="since-filter">Since (ISO):</label>
|
<label for="since-filter">Since (UTC ISO 8601):</label>
|
||||||
<input type="datetime-local" id="since-filter" placeholder="2025-06-11T13:32:00">
|
<input type="datetime-local" id="since-filter" placeholder="e.g., 2025-06-11T13:32 (UTC)">
|
||||||
<button id="apply-filters">Apply</button>
|
<button id="apply-filters">Apply</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -64,3 +63,4 @@
|
|||||||
<script src="{{ url_for('static', path='/logs.js') }}"></script>
|
<script src="{{ url_for('static', path='/logs.js') }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
@ -4,3 +4,5 @@ rrdtool==0.1.16
|
|||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
python-multipart==0.0.6
|
python-multipart==0.0.6
|
||||||
python-json-logger==2.0.7
|
python-json-logger==2.0.7
|
||||||
|
python-dateutil==2.9.0.post0
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user