Compare commits

..

9 Commits

Author SHA1 Message Date
a15de4f72b update CI
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2026-03-16 14:01:24 -03:00
8c6ec2e683 commit to trigger pipeline after updating gitea tokens
Some checks failed
ci/woodpecker/manual/build Pipeline failed
2026-03-16 13:50:49 -03:00
6dc3c01637 pipeline update fix 2026-03-16 13:42:21 -03:00
0f60556e81 update docs 2026-03-16 13:35:53 -03:00
buenosairesam
91f95d55a5 Move edge HTML to templates, add jinja2
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-26 21:00:50 -03:00
buenosairesam
3106bc835e Update edge: match full stack dashboard style, fix metric names
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-26 20:58:46 -03:00
buenosairesam
a013e0116f Fix deploy.sh: use sh instead of bash, ensure executable
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-26 20:56:27 -03:00
buenosairesam
8ecd702b63 Fix pipeline: run deploy directly instead of SSH
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-01-26 20:55:24 -03:00
buenosairesam
2da4b30019 Fix edge to handle nested metrics format from gateway forwarding 2026-01-26 20:36:34 -03:00
18 changed files with 962 additions and 660 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,5 @@
def
.env
ctrl/.env
#

View File

@@ -15,14 +15,11 @@ steps:
- ${CI_COMMIT_SHA:0:7}
dockerfile: ctrl/edge/Dockerfile
context: ctrl/edge
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- name: deploy
image: docker:24-cli
commands:
- cd /repo/ctrl/edge
- ./deploy.sh
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /home/mariano/sysmonstm:/repo
- docker pull registry.mcrn.ar/sysmonstm/edge:latest
- docker stop sysmonstm-edge || true
- docker rm sysmonstm-edge || true
- docker run -d --name sysmonstm-edge --restart unless-stopped --network gateway -p 8080:8080 registry.mcrn.ar/sysmonstm/edge:latest

3
ctrl/.env.example Normal file
View File

@@ -0,0 +1,3 @@
EDGE_URL=wss://sysmonstm.mcrn.ar/ws
EDGE_API_KEY=your-api-key-here
MACHINE_ID=your-hostname

46
ctrl/collector.sh Executable file
View File

@@ -0,0 +1,46 @@
#!/bin/bash
# Run sysmonstm collector only (for secondary machines)
# Usage: ./ctrl/collector.sh [aggregator-ip] [--remote]
#
# Examples:
# ./ctrl/collector.sh 192.168.1.33 # Build locally (default)
# ./ctrl/collector.sh 192.168.1.33 --remote # Use registry image
cd "$(dirname "$0")/.."
AGGREGATOR_IP=${1:-192.168.1.33}
MACHINE_ID=$(hostname)
USE_REMOTE=false
if [ "$2" = "--remote" ]; then
USE_REMOTE=true
fi
# Check if aggregator is reachable
echo "Checking connection to aggregator at $AGGREGATOR_IP:50051..."
if ! nc -z -w 3 "$AGGREGATOR_IP" 50051 2>/dev/null; then
echo ""
echo "ERROR: Cannot connect to aggregator at $AGGREGATOR_IP:50051"
echo ""
echo "Make sure the full stack is running on the main machine first:"
echo " cd ~/wdir/sms && ./ctrl/run.sh"
echo ""
exit 1
fi
echo "Aggregator reachable."
if [ "$USE_REMOTE" = true ]; then
IMAGE="registry.mcrn.ar/sysmonstm/collector:latest"
echo "Using remote image: $IMAGE"
else
IMAGE="sysmonstm-collector:local"
echo "Building local image..."
docker build -t $IMAGE -f services/collector/Dockerfile .
fi
echo "Starting collector for $MACHINE_ID -> $AGGREGATOR_IP:50051"
docker run --rm --name sysmonstm-collector --network host \
-e AGGREGATOR_URL=${AGGREGATOR_IP}:50051 \
-e MACHINE_ID=${MACHINE_ID} \
$IMAGE

View File

@@ -2,9 +2,10 @@ FROM python:3.11-slim
WORKDIR /app
RUN pip install --no-cache-dir fastapi uvicorn[standard] websockets
RUN pip install --no-cache-dir fastapi uvicorn[standard] websockets jinja2
COPY edge.py .
COPY templates/ templates/
ENV API_KEY=""
ENV LOG_LEVEL=INFO

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/sh
# Deploy sysmonstm edge service
# Called by Woodpecker or manually

View File

@@ -4,10 +4,12 @@ import asyncio
import json
import logging
import os
from datetime import datetime
from pathlib import Path
from fastapi import FastAPI, Query, WebSocket, WebSocketDisconnect
from fastapi.requests import Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
# Configuration
API_KEY = os.environ.get("API_KEY", "")
@@ -23,162 +25,19 @@ log = logging.getLogger("gateway")
app = FastAPI(title="sysmonstm")
# Templates
templates_path = Path(__file__).parent / "templates"
templates = Jinja2Templates(directory=str(templates_path))
# Store connected websockets
connections: list[WebSocket] = []
# Store latest metrics from collectors
machines: dict = {}
HTML = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>sysmonstm</title>
<style>
:root {
--bg: #1a1a2e;
--bg2: #16213e;
--text: #eee;
--accent: #e94560;
--success: #4ade80;
--muted: #666;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
padding: 2rem;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid var(--accent);
}
h1 { font-size: 1.5rem; }
.status {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--accent);
}
.dot.ok { background: var(--success); }
.machines {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1rem;
}
.machine {
background: var(--bg2);
border-radius: 8px;
padding: 1rem;
}
.machine h3 { margin-bottom: 0.5rem; }
.metric {
display: flex;
justify-content: space-between;
padding: 0.25rem 0;
border-bottom: 1px solid #333;
}
.empty {
text-align: center;
color: var(--muted);
padding: 4rem;
}
.empty p { margin-top: 1rem; }
</style>
</head>
<body>
<header>
<h1>sysmonstm</h1>
<div class="status">
<span class="dot" id="ws-status"></span>
<span id="status-text">connecting...</span>
</div>
</header>
<main>
<div id="machines" class="machines">
<div class="empty">
<h2>No collectors connected</h2>
<p>Start a collector to see metrics</p>
</div>
</div>
</main>
<script>
const machinesEl = document.getElementById('machines');
const statusDot = document.getElementById('ws-status');
const statusText = document.getElementById('status-text');
let machines = {};
function connect() {
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${protocol}//${location.host}/ws`);
ws.onopen = () => {
statusDot.classList.add('ok');
statusText.textContent = 'connected';
};
ws.onclose = () => {
statusDot.classList.remove('ok');
statusText.textContent = 'disconnected';
setTimeout(connect, 2000);
};
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
if (data.type === 'metrics') {
machines[data.machine_id] = data;
render();
}
};
}
function render() {
const ids = Object.keys(machines).sort();
if (ids.length === 0) {
machinesEl.innerHTML = '<div class="empty"><h2>No collectors connected</h2><p>Start a collector to see metrics</p></div>';
return;
}
machinesEl.innerHTML = ids.map(id => {
const m = machines[id];
const ts = m.timestamp ? new Date(m.timestamp * 1000).toLocaleTimeString() : '-';
return `
<div class="machine">
<h3>${id}</h3>
<div class="metric"><span>CPU</span><span>${m.cpu?.toFixed(1) || '-'}%</span></div>
<div class="metric"><span>Memory</span><span>${m.memory?.toFixed(1) || '-'}%</span></div>
<div class="metric"><span>Disk</span><span>${m.disk?.toFixed(1) || '-'}%</span></div>
<div class="metric"><span>Load (1m)</span><span>${m.load_1m?.toFixed(2) || '-'}</span></div>
<div class="metric"><span>Processes</span><span>${m.processes || '-'}</span></div>
<div class="metric"><span>Updated</span><span>${ts}</span></div>
</div>
`;
}).join('');
}
connect();
</script>
</body>
</html>
"""
@app.get("/", response_class=HTMLResponse)
async def index():
return HTML
async def index(request: Request):
return templates.TemplateResponse("dashboard.html", {"request": request})
@app.get("/health")
@@ -194,7 +53,6 @@ async def get_machines():
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket, key: str = Query(default="")):
# API key validation for collectors (browsers don't need key)
# Check if this looks like a collector (will send metrics) or browser (will receive)
# We validate key only when metrics are received, allowing browsers to connect freely
await websocket.accept()
@@ -222,16 +80,37 @@ async def websocket_endpoint(websocket: WebSocket, key: str = Query(default=""))
await websocket.close(code=4001, reason="Invalid API key")
return
# Handle both formats:
# 1. Direct: {"type": "metrics", "machine_id": "...", "cpu": ...}
# 2. Nested (from gateway): {"type": "metrics", "data": {...}, "timestamp": "..."}
if "data" in data and isinstance(data["data"], dict):
# Nested format from gateway forwarding
payload = data["data"]
machine_id = payload.get("machine_id", "unknown")
# Extract metrics from nested structure
metrics = payload.get("metrics", {})
metric_data = {
"type": "metrics",
"machine_id": machine_id,
"hostname": payload.get("hostname", ""),
"timestamp": data.get("timestamp"),
}
# Flatten metrics for dashboard display
for key_name, value in metrics.items():
metric_data[key_name.lower()] = value
machines[machine_id] = metric_data
log.debug(f"Metrics (forwarded) from {machine_id}")
else:
# Direct format from collector
machine_id = data.get("machine_id", "unknown")
machines[machine_id] = data
log.debug(f"Metrics from {machine_id}: cpu={data.get('cpu')}%")
# Broadcast to all connected clients
broadcast_data = machines[machine_id]
for conn in connections:
try:
await conn.send_json(
{"type": "metrics", "machine_id": machine_id, **data}
)
await conn.send_json(broadcast_data)
except Exception:
pass

View File

@@ -0,0 +1,349 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>System Monitor Dashboard</title>
<style>
:root {
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--bg-card: #0f3460;
--text-primary: #eee;
--text-secondary: #a0a0a0;
--accent: #e94560;
--success: #4ade80;
--warning: #fbbf24;
--danger: #ef4444;
--border: #2a2a4a;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, -apple-system, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
}
header {
background: var(--bg-secondary);
padding: 1rem 2rem;
border-bottom: 2px solid var(--accent);
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 { font-size: 1.5rem; }
.status {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--danger);
}
.status-dot.connected { background: var(--success); }
main {
padding: 1.5rem;
max-width: 1600px;
margin: 0 auto;
}
.machines-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 1.5rem;
}
.machine-card {
background: var(--bg-secondary);
border-radius: 8px;
padding: 1.25rem;
border: 1px solid var(--border);
}
.machine-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--border);
}
.machine-name {
font-weight: 600;
color: var(--accent);
}
.machine-id {
font-size: 0.75rem;
color: var(--text-secondary);
}
.machine-status {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
background: var(--success);
color: #000;
}
.machine-status.warning { background: var(--warning); }
.machine-status.critical { background: var(--danger); color: #fff; }
.metrics-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
.metric {
background: var(--bg-card);
padding: 0.75rem;
border-radius: 6px;
}
.metric-label {
font-size: 0.75rem;
color: var(--text-secondary);
margin-bottom: 0.25rem;
}
.metric-value {
font-size: 1.5rem;
font-weight: 600;
}
.metric-bar {
height: 4px;
background: var(--border);
border-radius: 2px;
margin-top: 0.5rem;
overflow: hidden;
}
.metric-bar-fill {
height: 100%;
background: var(--success);
transition: width 0.3s ease;
}
.metric-bar-fill.warning { background: var(--warning); }
.metric-bar-fill.critical { background: var(--danger); }
.last-seen {
font-size: 0.75rem;
color: var(--text-secondary);
margin-top: 1rem;
text-align: right;
}
.no-machines {
text-align: center;
padding: 3rem;
color: var(--text-secondary);
}
.no-machines h2 {
color: var(--text-primary);
margin-bottom: 0.5rem;
}
@media (max-width: 600px) {
.machines-grid { grid-template-columns: 1fr; }
.metrics-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<header>
<h1>System Monitor</h1>
<div class="status">
<span class="status-dot" id="status-dot"></span>
<span id="status-text">Connecting...</span>
</div>
</header>
<main>
<div class="machines-grid" id="machines-grid">
<div class="no-machines">
<h2>No machines connected</h2>
<p>Waiting for collectors to send metrics...</p>
</div>
</div>
</main>
<script>
const machinesGrid = document.getElementById('machines-grid');
const statusDot = document.getElementById('status-dot');
const statusText = document.getElementById('status-text');
const machines = new Map();
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
function formatRate(bytesPerSec) {
return formatBytes(bytesPerSec) + '/s';
}
function getBarClass(value, warning = 80, critical = 95) {
if (value >= critical) return 'critical';
if (value >= warning) return 'warning';
return '';
}
function getStatusClass(m) {
const cpu = m.cpu_percent || 0;
const mem = m.memory_percent || 0;
const disk = m.disk_percent || 0;
if (cpu > 95 || mem > 95 || disk > 90) return 'critical';
if (cpu > 80 || mem > 85 || disk > 80) return 'warning';
return '';
}
function timeSince(timestamp) {
if (!timestamp) return '-';
const date = typeof timestamp === 'string' ? new Date(timestamp) : new Date(timestamp);
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
if (seconds < 5) return 'just now';
if (seconds < 60) return seconds + 's ago';
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return minutes + 'm ago';
return Math.floor(minutes / 60) + 'h ago';
}
function renderMachine(data) {
const m = data;
const statusClass = getStatusClass(m);
return `
<div class="machine-card" data-machine="${data.machine_id}">
<div class="machine-header">
<div>
<div class="machine-name">${data.hostname || data.machine_id}</div>
<div class="machine-id">${data.machine_id}</div>
</div>
<span class="machine-status ${statusClass}">${statusClass || 'healthy'}</span>
</div>
<div class="metrics-grid">
<div class="metric">
<div class="metric-label">CPU</div>
<div class="metric-value">${(m.cpu_percent || 0).toFixed(1)}%</div>
<div class="metric-bar">
<div class="metric-bar-fill ${getBarClass(m.cpu_percent || 0)}"
style="width: ${m.cpu_percent || 0}%"></div>
</div>
</div>
<div class="metric">
<div class="metric-label">Memory</div>
<div class="metric-value">${(m.memory_percent || 0).toFixed(1)}%</div>
<div class="metric-bar">
<div class="metric-bar-fill ${getBarClass(m.memory_percent || 0, 85, 95)}"
style="width: ${m.memory_percent || 0}%"></div>
</div>
</div>
<div class="metric">
<div class="metric-label">Disk</div>
<div class="metric-value">${(m.disk_percent || 0).toFixed(1)}%</div>
<div class="metric-bar">
<div class="metric-bar-fill ${getBarClass(m.disk_percent || 0, 80, 90)}"
style="width: ${m.disk_percent || 0}%"></div>
</div>
</div>
<div class="metric">
<div class="metric-label">Load (1m)</div>
<div class="metric-value">${(m.load_avg_1m || 0).toFixed(2)}</div>
</div>
<div class="metric">
<div class="metric-label">Network In</div>
<div class="metric-value">${formatRate(m.network_recv_bytes_sec || 0)}</div>
</div>
<div class="metric">
<div class="metric-label">Network Out</div>
<div class="metric-value">${formatRate(m.network_sent_bytes_sec || 0)}</div>
</div>
</div>
<div class="last-seen">Last seen: ${timeSince(m.timestamp)}</div>
</div>
`;
}
function updateUI() {
if (machines.size === 0) {
machinesGrid.innerHTML = `
<div class="no-machines">
<h2>No machines connected</h2>
<p>Waiting for collectors to send metrics...</p>
</div>
`;
return;
}
machinesGrid.innerHTML = Array.from(machines.values())
.map(renderMachine)
.join('');
}
function connect() {
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${protocol}//${location.host}/ws`);
ws.onopen = () => {
statusDot.classList.add('connected');
statusText.textContent = 'Connected';
};
ws.onclose = () => {
statusDot.classList.remove('connected');
statusText.textContent = 'Disconnected - Reconnecting...';
setTimeout(connect, 3000);
};
ws.onerror = () => {
statusDot.classList.remove('connected');
statusText.textContent = 'Connection error';
};
ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.type === 'metrics' || msg.type === 'initial') {
machines.set(msg.machine_id, msg);
updateUI();
}
} catch (e) {
console.error('Failed to parse message:', e);
}
};
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send('ping');
}
}, 30000);
}
setInterval(updateUI, 5000);
connect();
</script>
</body>
</html>

23
ctrl/run.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/bin/bash
# Run sysmonstm full stack locally with edge forwarding
# Usage: ./ctrl/run.sh [--remote]
#
# Examples:
# ./ctrl/run.sh # Build locally (default)
# ./ctrl/run.sh --remote # Use registry images
cd "$(dirname "$0")/.."
# Load env from ctrl/.env
set -a
source ctrl/.env
set +a
if [ "$1" = "--remote" ]; then
echo "Using remote images from registry"
docker compose pull
docker compose up "${@:2}"
else
echo "Building locally..."
docker compose up --build "$@"
fi

View File

@@ -24,9 +24,19 @@ digraph SystemOverview {
machines [label="Monitored\nMachines", fillcolor="#FFF3E0", shape=box3d];
}
// Edge (AWS)
subgraph cluster_edge {
label="AWS (sysmonstm.mcrn.ar)";
style=filled;
color="#F3E5F5";
fillcolor="#F3E5F5";
edge_relay [label="Edge\n(WebSocket Relay)", fillcolor="#E1BEE7"];
}
// Core Services
subgraph cluster_services {
label="Application Services";
label="Local Stack";
style=filled;
color="#E8F5E9";
fillcolor="#E8F5E9";
@@ -59,7 +69,10 @@ digraph SystemOverview {
}
// Connections
browser -> gateway [label="WebSocket\nREST", color="#1976D2"];
browser -> edge_relay [label="WebSocket", color="#1976D2"];
edge_relay -> gateway [label="WebSocket\nForward", color="#1976D2", dir=back];
browser -> gateway [label="WebSocket\n(local dev)", color="#1976D2", style=dashed];
gateway -> aggregator [label="gRPC", color="#388E3C"];
gateway -> redis [label="State\nQuery", style=dashed];
gateway -> timescale [label="Historical\nQuery", style=dashed];
@@ -73,6 +86,4 @@ digraph SystemOverview {
events -> alerts [label="Subscribe", color="#7B1FA2"];
events -> gateway [label="Subscribe", color="#7B1FA2"];
alerts -> timescale [label="Store\nAlerts", style=dashed];
}

View File

@@ -1,193 +1,212 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.1 (0)
<!-- Generated by graphviz version 14.1.2 (0)
-->
<!-- Title: SystemOverview Pages: 1 -->
<svg width="444pt" height="508pt"
viewBox="0.00 0.00 444.00 508.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 503.78)">
<svg width="577pt" height="618pt"
viewBox="0.00 0.00 577.00 618.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 614.03)">
<title>SystemOverview</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-503.78 440,-503.78 440,4 -4,4"/>
<text xml:space="preserve" text-anchor="middle" x="218" y="-480.58" font-family="Helvetica,sans-Serif" font-size="16.00">System Monitoring Platform &#45; Architecture Overview</text>
<polygon fill="white" stroke="none" points="-4,4 -4,-614.03 573,-614.03 573,4 -4,4"/>
<text xml:space="preserve" text-anchor="middle" x="284.5" y="-590.83" font-family="Helvetica,sans-Serif" font-size="16.00">System Monitoring Platform &#45; Architecture Overview</text>
<g id="clust1" class="cluster">
<title>cluster_external</title>
<polygon fill="none" stroke="gray" stroke-dasharray="5,2" points="45.5,-374.2 45.5,-453.7 235.5,-453.7 235.5,-374.2 45.5,-374.2"/>
<text xml:space="preserve" text-anchor="middle" x="140.5" y="-434.5" font-family="Helvetica,sans-Serif" font-size="16.00">External</text>
<polygon fill="none" stroke="gray" stroke-dasharray="5,2" points="208,-495.03 208,-574.53 398,-574.53 398,-495.03 208,-495.03"/>
<text xml:space="preserve" text-anchor="middle" x="303" y="-555.33" font-family="Helvetica,sans-Serif" font-size="16.00">External</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_services</title>
<polygon fill="#e8f5e9" stroke="#e8f5e9" points="101.5,-143.12 101.5,-320.12 363.5,-320.12 363.5,-143.12 101.5,-143.12"/>
<text xml:space="preserve" text-anchor="middle" x="232.5" y="-300.93" font-family="Helvetica,sans-Serif" font-size="16.00">Application Services</text>
<title>cluster_edge</title>
<polygon fill="#f3e5f5" stroke="#f3e5f5" points="8,-374.2 8,-453.7 238,-453.7 238,-374.2 8,-374.2"/>
<text xml:space="preserve" text-anchor="middle" x="123" y="-434.5" font-family="Helvetica,sans-Serif" font-size="16.00">AWS (sysmonstm.mcrn.ar)</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_data</title>
<polygon fill="#fff8e1" stroke="#fff8e1" points="22.5,-8 22.5,-99.62 260.5,-99.62 260.5,-8 22.5,-8"/>
<text xml:space="preserve" text-anchor="middle" x="141.5" y="-80.42" font-family="Helvetica,sans-Serif" font-size="16.00">Data Layer</text>
<title>cluster_services</title>
<polygon fill="#e8f5e9" stroke="#e8f5e9" points="227,-143.12 227,-320.12 489,-320.12 489,-143.12 227,-143.12"/>
<text xml:space="preserve" text-anchor="middle" x="358" y="-300.93" font-family="Helvetica,sans-Serif" font-size="16.00">Local Stack</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_data</title>
<polygon fill="#fff8e1" stroke="#fff8e1" points="162,-8 162,-99.62 400,-99.62 400,-8 162,-8"/>
<text xml:space="preserve" text-anchor="middle" x="281" y="-80.42" font-family="Helvetica,sans-Serif" font-size="16.00">Data Layer</text>
</g>
<g id="clust5" class="cluster">
<title>cluster_events</title>
<polygon fill="#f3e5f5" stroke="#f3e5f5" points="243.5,-363.62 243.5,-464.28 413.5,-464.28 413.5,-363.62 243.5,-363.62"/>
<text xml:space="preserve" text-anchor="middle" x="328.5" y="-445.08" font-family="Helvetica,sans-Serif" font-size="16.00">Event Stream</text>
<polygon fill="#f3e5f5" stroke="#f3e5f5" points="391,-363.62 391,-464.28 561,-464.28 561,-363.62 391,-363.62"/>
<text xml:space="preserve" text-anchor="middle" x="476" y="-445.08" font-family="Helvetica,sans-Serif" font-size="16.00">Event Stream</text>
</g>
<!-- browser -->
<g id="node1" class="node">
<title>browser</title>
<path fill="#e3f2fd" stroke="black" d="M125.62,-418.2C125.62,-418.2 65.38,-418.2 65.38,-418.2 59.38,-418.2 53.38,-412.2 53.38,-406.2 53.38,-406.2 53.38,-394.2 53.38,-394.2 53.38,-388.2 59.38,-382.2 65.38,-382.2 65.38,-382.2 125.62,-382.2 125.62,-382.2 131.62,-382.2 137.62,-388.2 137.62,-394.2 137.62,-394.2 137.62,-406.2 137.62,-406.2 137.62,-412.2 131.62,-418.2 125.62,-418.2"/>
<text xml:space="preserve" text-anchor="middle" x="95.5" y="-403.25" font-family="Helvetica,sans-Serif" font-size="11.00">Browser</text>
<text xml:space="preserve" text-anchor="middle" x="95.5" y="-389.75" font-family="Helvetica,sans-Serif" font-size="11.00">(Dashboard)</text>
<path fill="#e3f2fd" stroke="black" d="M288.12,-539.03C288.12,-539.03 227.88,-539.03 227.88,-539.03 221.88,-539.03 215.88,-533.03 215.88,-527.03 215.88,-527.03 215.88,-515.03 215.88,-515.03 215.88,-509.03 221.88,-503.03 227.88,-503.03 227.88,-503.03 288.12,-503.03 288.12,-503.03 294.12,-503.03 300.12,-509.03 300.12,-515.03 300.12,-515.03 300.12,-527.03 300.12,-527.03 300.12,-533.03 294.12,-539.03 288.12,-539.03"/>
<text xml:space="preserve" text-anchor="middle" x="258" y="-524.08" font-family="Helvetica,sans-Serif" font-size="11.00">Browser</text>
<text xml:space="preserve" text-anchor="middle" x="258" y="-510.58" font-family="Helvetica,sans-Serif" font-size="11.00">(Dashboard)</text>
</g>
<!-- edge_relay -->
<g id="node3" class="node">
<title>edge_relay</title>
<path fill="#e1bee7" stroke="black" d="M217.12,-418.2C217.12,-418.2 120.88,-418.2 120.88,-418.2 114.88,-418.2 108.88,-412.2 108.88,-406.2 108.88,-406.2 108.88,-394.2 108.88,-394.2 108.88,-388.2 114.88,-382.2 120.88,-382.2 120.88,-382.2 217.12,-382.2 217.12,-382.2 223.12,-382.2 229.12,-388.2 229.12,-394.2 229.12,-394.2 229.12,-406.2 229.12,-406.2 229.12,-412.2 223.12,-418.2 217.12,-418.2"/>
<text xml:space="preserve" text-anchor="middle" x="169" y="-403.25" font-family="Helvetica,sans-Serif" font-size="11.00">Edge</text>
<text xml:space="preserve" text-anchor="middle" x="169" y="-389.75" font-family="Helvetica,sans-Serif" font-size="11.00">(WebSocket Relay)</text>
</g>
<!-- browser&#45;&gt;edge_relay -->
<g id="edge1" class="edge">
<title>browser&#45;&gt;edge_relay</title>
<path fill="none" stroke="#1976d2" d="M218.56,-502.6C210.86,-497.79 203.43,-491.95 197.75,-485.03 184.82,-469.28 177.57,-447.39 173.59,-429.93"/>
<polygon fill="#1976d2" stroke="#1976d2" points="177.03,-429.28 171.62,-420.16 170.17,-430.66 177.03,-429.28"/>
<text xml:space="preserve" text-anchor="middle" x="224.38" y="-475.53" font-family="Helvetica,sans-Serif" font-size="10.00">WebSocket</text>
</g>
<!-- gateway -->
<g id="node3" class="node">
<g id="node4" class="node">
<title>gateway</title>
<path fill="#c8e6c9" stroke="black" d="M161.88,-284.62C161.88,-284.62 121.12,-284.62 121.12,-284.62 115.12,-284.62 109.12,-278.62 109.12,-272.62 109.12,-272.62 109.12,-260.62 109.12,-260.62 109.12,-254.62 115.12,-248.62 121.12,-248.62 121.12,-248.62 161.88,-248.62 161.88,-248.62 167.88,-248.62 173.88,-254.62 173.88,-260.62 173.88,-260.62 173.88,-272.62 173.88,-272.62 173.88,-278.62 167.88,-284.62 161.88,-284.62"/>
<text xml:space="preserve" text-anchor="middle" x="141.5" y="-269.68" font-family="Helvetica,sans-Serif" font-size="11.00">Gateway</text>
<text xml:space="preserve" text-anchor="middle" x="141.5" y="-256.18" font-family="Helvetica,sans-Serif" font-size="11.00">(FastAPI)</text>
<path fill="#c8e6c9" stroke="black" d="M287.38,-284.62C287.38,-284.62 246.62,-284.62 246.62,-284.62 240.62,-284.62 234.62,-278.62 234.62,-272.62 234.62,-272.62 234.62,-260.62 234.62,-260.62 234.62,-254.62 240.62,-248.62 246.62,-248.62 246.62,-248.62 287.38,-248.62 287.38,-248.62 293.38,-248.62 299.38,-254.62 299.38,-260.62 299.38,-260.62 299.38,-272.62 299.38,-272.62 299.38,-278.62 293.38,-284.62 287.38,-284.62"/>
<text xml:space="preserve" text-anchor="middle" x="267" y="-269.68" font-family="Helvetica,sans-Serif" font-size="11.00">Gateway</text>
<text xml:space="preserve" text-anchor="middle" x="267" y="-256.18" font-family="Helvetica,sans-Serif" font-size="11.00">(FastAPI)</text>
</g>
<!-- browser&#45;&gt;gateway -->
<g id="edge1" class="edge">
<g id="edge3" class="edge">
<title>browser&#45;&gt;gateway</title>
<path fill="none" stroke="#1976d2" d="M92.73,-381.75C91.08,-367.05 90.32,-345.66 96.25,-328.12 100.5,-315.57 108.45,-303.5 116.51,-293.49"/>
<polygon fill="#1976d2" stroke="#1976d2" points="119.02,-295.94 122.86,-286.06 113.7,-291.39 119.02,-295.94"/>
<text xml:space="preserve" text-anchor="middle" x="122.88" y="-344.12" font-family="Helvetica,sans-Serif" font-size="10.00">WebSocket</text>
<text xml:space="preserve" text-anchor="middle" x="122.88" y="-331.38" font-family="Helvetica,sans-Serif" font-size="10.00">REST</text>
<path fill="none" stroke="#1976d2" stroke-dasharray="5,2" d="M258.62,-502.68C260.14,-459.9 264.1,-349.03 265.98,-296.3"/>
<polygon fill="#1976d2" stroke="#1976d2" points="269.46,-296.73 266.32,-286.61 262.47,-296.48 269.46,-296.73"/>
<text xml:space="preserve" text-anchor="middle" x="290.17" y="-403.45" font-family="Helvetica,sans-Serif" font-size="10.00">WebSocket</text>
<text xml:space="preserve" text-anchor="middle" x="290.17" y="-390.7" font-family="Helvetica,sans-Serif" font-size="10.00">(local dev)</text>
</g>
<!-- machines -->
<g id="node2" class="node">
<title>machines</title>
<polygon fill="#fff3e0" stroke="black" points="227.25,-418.2 159.75,-418.2 155.75,-414.2 155.75,-382.2 223.25,-382.2 227.25,-386.2 227.25,-418.2"/>
<polyline fill="none" stroke="black" points="223.25,-414.2 155.75,-414.2"/>
<polyline fill="none" stroke="black" points="223.25,-414.2 223.25,-382.2"/>
<polyline fill="none" stroke="black" points="223.25,-414.2 227.25,-418.2"/>
<text xml:space="preserve" text-anchor="middle" x="191.5" y="-403.25" font-family="Helvetica,sans-Serif" font-size="11.00">Monitored</text>
<text xml:space="preserve" text-anchor="middle" x="191.5" y="-389.75" font-family="Helvetica,sans-Serif" font-size="11.00">Machines</text>
<polygon fill="#fff3e0" stroke="black" points="389.75,-539.03 322.25,-539.03 318.25,-535.03 318.25,-503.03 385.75,-503.03 389.75,-507.03 389.75,-539.03"/>
<polyline fill="none" stroke="black" points="385.75,-535.03 318.25,-535.03"/>
<polyline fill="none" stroke="black" points="385.75,-535.03 385.75,-503.03"/>
<polyline fill="none" stroke="black" points="385.75,-535.03 389.75,-539.03"/>
<text xml:space="preserve" text-anchor="middle" x="354" y="-524.08" font-family="Helvetica,sans-Serif" font-size="11.00">Monitored</text>
<text xml:space="preserve" text-anchor="middle" x="354" y="-510.58" font-family="Helvetica,sans-Serif" font-size="11.00">Machines</text>
</g>
<!-- collector -->
<g id="node6" class="node">
<g id="node7" class="node">
<title>collector</title>
<path fill="#dcedc8" stroke="black" d="M343.88,-284.62C343.88,-284.62 279.12,-284.62 279.12,-284.62 273.12,-284.62 267.12,-278.62 267.12,-272.62 267.12,-272.62 267.12,-260.62 267.12,-260.62 267.12,-254.62 273.12,-248.62 279.12,-248.62 279.12,-248.62 343.88,-248.62 343.88,-248.62 349.88,-248.62 355.88,-254.62 355.88,-260.62 355.88,-260.62 355.88,-272.62 355.88,-272.62 355.88,-278.62 349.88,-284.62 343.88,-284.62"/>
<text xml:space="preserve" text-anchor="middle" x="311.5" y="-269.68" font-family="Helvetica,sans-Serif" font-size="11.00">Collector</text>
<text xml:space="preserve" text-anchor="middle" x="311.5" y="-256.18" font-family="Helvetica,sans-Serif" font-size="11.00">(gRPC Client)</text>
<path fill="#dcedc8" stroke="black" d="M394.38,-284.62C394.38,-284.62 329.62,-284.62 329.62,-284.62 323.62,-284.62 317.62,-278.62 317.62,-272.62 317.62,-272.62 317.62,-260.62 317.62,-260.62 317.62,-254.62 323.62,-248.62 329.62,-248.62 329.62,-248.62 394.38,-248.62 394.38,-248.62 400.38,-248.62 406.38,-254.62 406.38,-260.62 406.38,-260.62 406.38,-272.62 406.38,-272.62 406.38,-278.62 400.38,-284.62 394.38,-284.62"/>
<text xml:space="preserve" text-anchor="middle" x="362" y="-269.68" font-family="Helvetica,sans-Serif" font-size="11.00">Collector</text>
<text xml:space="preserve" text-anchor="middle" x="362" y="-256.18" font-family="Helvetica,sans-Serif" font-size="11.00">(gRPC Client)</text>
</g>
<!-- machines&#45;&gt;collector -->
<g id="edge5" class="edge">
<g id="edge7" class="edge">
<title>machines&#45;&gt;collector</title>
<path fill="none" stroke="#f57c00" stroke-dasharray="1,5" d="M210.81,-381.83C219.12,-375.21 229.26,-368.17 239.5,-363.62 260.21,-354.43 273.06,-369.22 289.5,-353.62 304.98,-338.94 310.15,-314.98 311.64,-296.08"/>
<polygon fill="#f57c00" stroke="#f57c00" points="315.12,-296.47 312.08,-286.32 308.13,-296.15 315.12,-296.47"/>
<text xml:space="preserve" text-anchor="middle" x="318.1" y="-337.75" font-family="Helvetica,sans-Serif" font-size="10.00">psutil</text>
<path fill="none" stroke="#f57c00" stroke-dasharray="1,5" d="M354.55,-502.68C355.91,-459.9 359.42,-349.03 361.09,-296.3"/>
<polygon fill="#f57c00" stroke="#f57c00" points="364.58,-296.71 361.4,-286.61 357.58,-296.49 364.58,-296.71"/>
<text xml:space="preserve" text-anchor="middle" x="372.43" y="-397.08" font-family="Helvetica,sans-Serif" font-size="10.00">psutil</text>
</g>
<!-- edge_relay&#45;&gt;gateway -->
<g id="edge2" class="edge">
<title>edge_relay&#45;&gt;gateway</title>
<path fill="none" stroke="#1976d2" d="M177.03,-370.92C181.74,-357.23 188.58,-341 197.75,-328.12 209.78,-311.22 227.45,-295.88 241.93,-284.88"/>
<polygon fill="#1976d2" stroke="#1976d2" points="173.75,-369.67 174.03,-380.26 180.42,-371.81 173.75,-369.67"/>
<text xml:space="preserve" text-anchor="middle" x="224.38" y="-344.12" font-family="Helvetica,sans-Serif" font-size="10.00">WebSocket</text>
<text xml:space="preserve" text-anchor="middle" x="224.38" y="-331.38" font-family="Helvetica,sans-Serif" font-size="10.00">Forward</text>
</g>
<!-- aggregator -->
<g id="node4" class="node">
<g id="node5" class="node">
<title>aggregator</title>
<path fill="#c8e6c9" stroke="black" d="M343.12,-187.12C343.12,-187.12 273.88,-187.12 273.88,-187.12 267.88,-187.12 261.88,-181.12 261.88,-175.12 261.88,-175.12 261.88,-163.12 261.88,-163.12 261.88,-157.12 267.88,-151.12 273.88,-151.12 273.88,-151.12 343.12,-151.12 343.12,-151.12 349.12,-151.12 355.12,-157.12 355.12,-163.12 355.12,-163.12 355.12,-175.12 355.12,-175.12 355.12,-181.12 349.12,-187.12 343.12,-187.12"/>
<text xml:space="preserve" text-anchor="middle" x="308.5" y="-172.18" font-family="Helvetica,sans-Serif" font-size="11.00">Aggregator</text>
<text xml:space="preserve" text-anchor="middle" x="308.5" y="-158.68" font-family="Helvetica,sans-Serif" font-size="11.00">(gRPC Server)</text>
<path fill="#c8e6c9" stroke="black" d="M412.62,-187.12C412.62,-187.12 343.38,-187.12 343.38,-187.12 337.38,-187.12 331.38,-181.12 331.38,-175.12 331.38,-175.12 331.38,-163.12 331.38,-163.12 331.38,-157.12 337.38,-151.12 343.38,-151.12 343.38,-151.12 412.62,-151.12 412.62,-151.12 418.62,-151.12 424.62,-157.12 424.62,-163.12 424.62,-163.12 424.62,-175.12 424.62,-175.12 424.62,-181.12 418.62,-187.12 412.62,-187.12"/>
<text xml:space="preserve" text-anchor="middle" x="378" y="-172.18" font-family="Helvetica,sans-Serif" font-size="11.00">Aggregator</text>
<text xml:space="preserve" text-anchor="middle" x="378" y="-158.68" font-family="Helvetica,sans-Serif" font-size="11.00">(gRPC Server)</text>
</g>
<!-- gateway&#45;&gt;aggregator -->
<g id="edge2" class="edge">
<g id="edge4" class="edge">
<title>gateway&#45;&gt;aggregator</title>
<path fill="none" stroke="#388e3c" d="M171.74,-248.33C198.77,-232.88 238.56,-210.12 268.26,-193.13"/>
<polygon fill="#388e3c" stroke="#388e3c" points="269.66,-196.37 276.6,-188.36 266.19,-190.29 269.66,-196.37"/>
<text xml:space="preserve" text-anchor="middle" x="257.62" y="-214.75" font-family="Helvetica,sans-Serif" font-size="10.00">gRPC</text>
<path fill="none" stroke="#388e3c" d="M287.1,-248.33C304.44,-233.41 329.68,-211.69 349.17,-194.93"/>
<polygon fill="#388e3c" stroke="#388e3c" points="351.23,-197.78 356.53,-188.6 346.66,-192.47 351.23,-197.78"/>
<text xml:space="preserve" text-anchor="middle" x="348.46" y="-214.75" font-family="Helvetica,sans-Serif" font-size="10.00">gRPC</text>
</g>
<!-- redis -->
<g id="node7" class="node">
<g id="node8" class="node">
<title>redis</title>
<path fill="#ffecb3" stroke="black" d="M146,-59.75C146,-62.16 120.23,-64.12 88.5,-64.12 56.77,-64.12 31,-62.16 31,-59.75 31,-59.75 31,-20.38 31,-20.38 31,-17.96 56.77,-16 88.5,-16 120.23,-16 146,-17.96 146,-20.38 146,-20.38 146,-59.75 146,-59.75"/>
<path fill="none" stroke="black" d="M146,-59.75C146,-57.34 120.23,-55.38 88.5,-55.38 56.77,-55.38 31,-57.34 31,-59.75"/>
<text xml:space="preserve" text-anchor="middle" x="88.5" y="-43.11" font-family="Helvetica,sans-Serif" font-size="11.00">Redis</text>
<text xml:space="preserve" text-anchor="middle" x="88.5" y="-29.61" font-family="Helvetica,sans-Serif" font-size="11.00">(Pub/Sub + State)</text>
<path fill="#ffecb3" stroke="black" d="M285.5,-59.75C285.5,-62.16 259.73,-64.12 228,-64.12 196.27,-64.12 170.5,-62.16 170.5,-59.75 170.5,-59.75 170.5,-20.38 170.5,-20.38 170.5,-17.96 196.27,-16 228,-16 259.73,-16 285.5,-17.96 285.5,-20.38 285.5,-20.38 285.5,-59.75 285.5,-59.75"/>
<path fill="none" stroke="black" d="M285.5,-59.75C285.5,-57.34 259.73,-55.38 228,-55.38 196.27,-55.38 170.5,-57.34 170.5,-59.75"/>
<text xml:space="preserve" text-anchor="middle" x="228" y="-43.11" font-family="Helvetica,sans-Serif" font-size="11.00">Redis</text>
<text xml:space="preserve" text-anchor="middle" x="228" y="-29.61" font-family="Helvetica,sans-Serif" font-size="11.00">(Pub/Sub + State)</text>
</g>
<!-- gateway&#45;&gt;redis -->
<g id="edge3" class="edge">
<g id="edge5" class="edge">
<title>gateway&#45;&gt;redis</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M122.74,-248.35C108.28,-233.68 89.42,-211.2 81.25,-187.12 68.86,-150.62 73.72,-106.03 79.72,-75.79"/>
<polygon fill="black" stroke="black" points="83.14,-76.56 81.82,-66.04 76.29,-75.08 83.14,-76.56"/>
<text xml:space="preserve" text-anchor="middle" x="95.88" y="-172.38" font-family="Helvetica,sans-Serif" font-size="10.00">State</text>
<text xml:space="preserve" text-anchor="middle" x="95.88" y="-159.62" font-family="Helvetica,sans-Serif" font-size="10.00">Query</text>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M251.25,-248.13C238.89,-233.15 222.67,-210.37 215.75,-187.12 204.73,-150.09 211.09,-105.6 218.09,-75.53"/>
<polygon fill="black" stroke="black" points="221.49,-76.39 220.51,-65.84 214.7,-74.69 221.49,-76.39"/>
<text xml:space="preserve" text-anchor="middle" x="230.38" y="-172.38" font-family="Helvetica,sans-Serif" font-size="10.00">State</text>
<text xml:space="preserve" text-anchor="middle" x="230.38" y="-159.62" font-family="Helvetica,sans-Serif" font-size="10.00">Query</text>
</g>
<!-- timescale -->
<g id="node8" class="node">
<g id="node9" class="node">
<title>timescale</title>
<path fill="#ffecb3" stroke="black" d="M252.88,-59.75C252.88,-62.16 232.99,-64.12 208.5,-64.12 184.01,-64.12 164.12,-62.16 164.12,-59.75 164.12,-59.75 164.12,-20.38 164.12,-20.38 164.12,-17.96 184.01,-16 208.5,-16 232.99,-16 252.88,-17.96 252.88,-20.38 252.88,-20.38 252.88,-59.75 252.88,-59.75"/>
<path fill="none" stroke="black" d="M252.88,-59.75C252.88,-57.34 232.99,-55.38 208.5,-55.38 184.01,-55.38 164.12,-57.34 164.12,-59.75"/>
<text xml:space="preserve" text-anchor="middle" x="208.5" y="-43.11" font-family="Helvetica,sans-Serif" font-size="11.00">TimescaleDB</text>
<text xml:space="preserve" text-anchor="middle" x="208.5" y="-29.61" font-family="Helvetica,sans-Serif" font-size="11.00">(Time&#45;series)</text>
<path fill="#ffecb3" stroke="black" d="M392.38,-59.75C392.38,-62.16 372.49,-64.12 348,-64.12 323.51,-64.12 303.62,-62.16 303.62,-59.75 303.62,-59.75 303.62,-20.38 303.62,-20.38 303.62,-17.96 323.51,-16 348,-16 372.49,-16 392.38,-17.96 392.38,-20.38 392.38,-20.38 392.38,-59.75 392.38,-59.75"/>
<path fill="none" stroke="black" d="M392.38,-59.75C392.38,-57.34 372.49,-55.38 348,-55.38 323.51,-55.38 303.62,-57.34 303.62,-59.75"/>
<text xml:space="preserve" text-anchor="middle" x="348" y="-43.11" font-family="Helvetica,sans-Serif" font-size="11.00">TimescaleDB</text>
<text xml:space="preserve" text-anchor="middle" x="348" y="-29.61" font-family="Helvetica,sans-Serif" font-size="11.00">(Time&#45;series)</text>
</g>
<!-- gateway&#45;&gt;timescale -->
<g id="edge4" class="edge">
<g id="edge6" class="edge">
<title>gateway&#45;&gt;timescale</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M143.41,-248.29C146.34,-224.28 152.82,-179.73 164,-143.12 171.19,-119.57 182.25,-94.18 191.54,-74.62"/>
<polygon fill="black" stroke="black" points="194.62,-76.29 195.83,-65.76 188.32,-73.24 194.62,-76.29"/>
<text xml:space="preserve" text-anchor="middle" x="187.25" y="-172.38" font-family="Helvetica,sans-Serif" font-size="10.00">Historical</text>
<text xml:space="preserve" text-anchor="middle" x="187.25" y="-159.62" font-family="Helvetica,sans-Serif" font-size="10.00">Query</text>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M266.24,-248.27C265.63,-224.24 266.08,-179.67 275.5,-143.12 282.34,-116.6 300.01,-91.45 316.2,-72.76"/>
<polygon fill="black" stroke="black" points="318.64,-75.28 322.72,-65.51 313.43,-70.6 318.64,-75.28"/>
<text xml:space="preserve" text-anchor="middle" x="298.75" y="-172.38" font-family="Helvetica,sans-Serif" font-size="10.00">Historical</text>
<text xml:space="preserve" text-anchor="middle" x="298.75" y="-159.62" font-family="Helvetica,sans-Serif" font-size="10.00">Query</text>
</g>
<!-- aggregator&#45;&gt;redis -->
<g id="edge7" class="edge">
<g id="edge9" class="edge">
<title>aggregator&#45;&gt;redis</title>
<path fill="none" stroke="#ffa000" d="M267.27,-150.69C261,-148.11 254.59,-145.52 248.5,-143.12 236.59,-138.44 233.22,-138.25 221.5,-133.12 191.36,-119.95 182.76,-118.04 155.5,-99.62 143.6,-91.59 131.5,-81.66 120.93,-72.28"/>
<polygon fill="#ffa000" stroke="#ffa000" points="123.32,-69.73 113.56,-65.6 118.62,-74.91 123.32,-69.73"/>
<text xml:space="preserve" text-anchor="middle" x="239.5" y="-123.62" font-family="Helvetica,sans-Serif" font-size="10.00">Current</text>
<text xml:space="preserve" text-anchor="middle" x="239.5" y="-110.88" font-family="Helvetica,sans-Serif" font-size="10.00">State</text>
<path fill="none" stroke="#ffa000" d="M348.99,-150.75C340.7,-145.41 331.8,-139.3 324,-133.12 300.45,-114.48 276.05,-91.01 257.74,-72.44"/>
<polygon fill="#ffa000" stroke="#ffa000" points="260.33,-70.08 250.84,-65.37 255.32,-74.97 260.33,-70.08"/>
<text xml:space="preserve" text-anchor="middle" x="342" y="-123.62" font-family="Helvetica,sans-Serif" font-size="10.00">Current</text>
<text xml:space="preserve" text-anchor="middle" x="342" y="-110.88" font-family="Helvetica,sans-Serif" font-size="10.00">State</text>
</g>
<!-- aggregator&#45;&gt;timescale -->
<g id="edge8" class="edge">
<g id="edge10" class="edge">
<title>aggregator&#45;&gt;timescale</title>
<path fill="none" stroke="#ffa000" d="M294.81,-150.72C279.15,-130.84 253.2,-97.86 233.84,-73.25"/>
<polygon fill="#ffa000" stroke="#ffa000" points="236.64,-71.16 227.71,-65.47 231.14,-75.49 236.64,-71.16"/>
<text xml:space="preserve" text-anchor="middle" x="296.95" y="-123.62" font-family="Helvetica,sans-Serif" font-size="10.00">Store</text>
<text xml:space="preserve" text-anchor="middle" x="296.95" y="-110.88" font-family="Helvetica,sans-Serif" font-size="10.00">Metrics</text>
<path fill="none" stroke="#ffa000" d="M373.89,-150.72C369.32,-131.36 361.82,-99.6 356.07,-75.22"/>
<polygon fill="#ffa000" stroke="#ffa000" points="359.53,-74.68 353.83,-65.75 352.72,-76.29 359.53,-74.68"/>
<text xml:space="preserve" text-anchor="middle" x="387.14" y="-123.62" font-family="Helvetica,sans-Serif" font-size="10.00">Store</text>
<text xml:space="preserve" text-anchor="middle" x="387.14" y="-110.88" font-family="Helvetica,sans-Serif" font-size="10.00">Metrics</text>
</g>
<!-- events -->
<g id="node9" class="node">
<g id="node10" class="node">
<title>events</title>
<path fill="#e1bee7" stroke="black" d="M395.63,-407.37C395.63,-407.37 376.5,-421.61 376.5,-421.61 371.69,-425.2 360.88,-428.78 354.88,-428.78 354.88,-428.78 302.12,-428.78 302.12,-428.78 296.12,-428.78 285.31,-425.2 280.5,-421.61 280.5,-421.61 261.37,-407.37 261.37,-407.37 256.56,-403.79 256.56,-396.62 261.37,-393.04 261.37,-393.04 280.5,-378.79 280.5,-378.79 285.31,-375.21 296.12,-371.62 302.12,-371.62 302.12,-371.62 354.88,-371.62 354.88,-371.62 360.88,-371.62 371.69,-375.21 376.5,-378.79 376.5,-378.79 395.63,-393.04 395.63,-393.04 400.44,-396.62 400.44,-403.79 395.63,-407.37"/>
<text xml:space="preserve" text-anchor="middle" x="328.5" y="-403.25" font-family="Helvetica,sans-Serif" font-size="11.00">Redis Pub/Sub</text>
<text xml:space="preserve" text-anchor="middle" x="328.5" y="-389.75" font-family="Helvetica,sans-Serif" font-size="11.00">(Events)</text>
<path fill="#e1bee7" stroke="black" d="M543.13,-407.37C543.13,-407.37 524,-421.61 524,-421.61 519.19,-425.2 508.38,-428.78 502.38,-428.78 502.38,-428.78 449.62,-428.78 449.62,-428.78 443.62,-428.78 432.81,-425.2 428,-421.61 428,-421.61 408.87,-407.37 408.87,-407.37 404.06,-403.79 404.06,-396.62 408.87,-393.04 408.87,-393.04 428,-378.79 428,-378.79 432.81,-375.21 443.62,-371.62 449.62,-371.62 449.62,-371.62 502.38,-371.62 502.38,-371.62 508.38,-371.62 519.19,-375.21 524,-378.79 524,-378.79 543.13,-393.04 543.13,-393.04 547.94,-396.62 547.94,-403.79 543.13,-407.37"/>
<text xml:space="preserve" text-anchor="middle" x="476" y="-403.25" font-family="Helvetica,sans-Serif" font-size="11.00">Redis Pub/Sub</text>
<text xml:space="preserve" text-anchor="middle" x="476" y="-389.75" font-family="Helvetica,sans-Serif" font-size="11.00">(Events)</text>
</g>
<!-- aggregator&#45;&gt;events -->
<g id="edge9" class="edge">
<g id="edge11" class="edge">
<title>aggregator&#45;&gt;events</title>
<path fill="none" stroke="#7b1fa2" d="M333.16,-187.49C339.14,-192.63 345.07,-198.63 349.5,-205.12 361.02,-222.03 361.12,-228.46 364.5,-248.62 369.75,-279.97 371.24,-289.07 364.5,-320.12 361.48,-334.06 355.78,-348.49 349.79,-361.14"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="346.73,-359.44 345.42,-369.95 353,-362.55 346.73,-359.44"/>
<text xml:space="preserve" text-anchor="middle" x="386.64" y="-263.5" font-family="Helvetica,sans-Serif" font-size="10.00">Publish</text>
<path fill="none" stroke="#7b1fa2" d="M416.67,-187.52C441.53,-200.73 472.36,-221.29 490,-248.62 495.8,-257.61 513.79,-323.42 505,-353.62 504.25,-356.21 503.3,-358.78 502.21,-361.32"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="499.15,-359.62 497.75,-370.12 505.39,-362.79 499.15,-359.62"/>
<text xml:space="preserve" text-anchor="middle" x="524.36" y="-263.5" font-family="Helvetica,sans-Serif" font-size="10.00">Publish</text>
</g>
<!-- alerts -->
<g id="node5" class="node">
<g id="node6" class="node">
<title>alerts</title>
<path fill="#c8e6c9" stroke="black" d="M236.75,-284.62C236.75,-284.62 204.25,-284.62 204.25,-284.62 198.25,-284.62 192.25,-278.62 192.25,-272.62 192.25,-272.62 192.25,-260.62 192.25,-260.62 192.25,-254.62 198.25,-248.62 204.25,-248.62 204.25,-248.62 236.75,-248.62 236.75,-248.62 242.75,-248.62 248.75,-254.62 248.75,-260.62 248.75,-260.62 248.75,-272.62 248.75,-272.62 248.75,-278.62 242.75,-284.62 236.75,-284.62"/>
<text xml:space="preserve" text-anchor="middle" x="220.5" y="-269.68" font-family="Helvetica,sans-Serif" font-size="11.00">Alerts</text>
<text xml:space="preserve" text-anchor="middle" x="220.5" y="-256.18" font-family="Helvetica,sans-Serif" font-size="11.00">Service</text>
</g>
<!-- alerts&#45;&gt;timescale -->
<g id="edge12" class="edge">
<title>alerts&#45;&gt;timescale</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M219.58,-248.38C217.61,-211.47 212.94,-124.24 210.34,-75.51"/>
<polygon fill="black" stroke="black" points="213.85,-75.6 209.82,-65.8 206.86,-75.97 213.85,-75.6"/>
<text xml:space="preserve" text-anchor="middle" x="230.53" y="-172.38" font-family="Helvetica,sans-Serif" font-size="10.00">Store</text>
<text xml:space="preserve" text-anchor="middle" x="230.53" y="-159.62" font-family="Helvetica,sans-Serif" font-size="10.00">Alerts</text>
<path fill="#c8e6c9" stroke="black" d="M469.25,-284.62C469.25,-284.62 436.75,-284.62 436.75,-284.62 430.75,-284.62 424.75,-278.62 424.75,-272.62 424.75,-272.62 424.75,-260.62 424.75,-260.62 424.75,-254.62 430.75,-248.62 436.75,-248.62 436.75,-248.62 469.25,-248.62 469.25,-248.62 475.25,-248.62 481.25,-254.62 481.25,-260.62 481.25,-260.62 481.25,-272.62 481.25,-272.62 481.25,-278.62 475.25,-284.62 469.25,-284.62"/>
<text xml:space="preserve" text-anchor="middle" x="453" y="-269.68" font-family="Helvetica,sans-Serif" font-size="11.00">Alerts</text>
<text xml:space="preserve" text-anchor="middle" x="453" y="-256.18" font-family="Helvetica,sans-Serif" font-size="11.00">Service</text>
</g>
<!-- collector&#45;&gt;aggregator -->
<g id="edge6" class="edge">
<g id="edge8" class="edge">
<title>collector&#45;&gt;aggregator</title>
<path fill="none" stroke="#388e3c" d="M310.96,-248.55C310.53,-234.65 309.9,-214.73 309.39,-198.45"/>
<polygon fill="#388e3c" stroke="#388e3c" points="312.9,-198.77 309.09,-188.89 305.91,-198.99 312.9,-198.77"/>
<text xml:space="preserve" text-anchor="middle" x="327.98" y="-221.12" font-family="Helvetica,sans-Serif" font-size="10.00">gRPC</text>
<text xml:space="preserve" text-anchor="middle" x="327.98" y="-208.38" font-family="Helvetica,sans-Serif" font-size="10.00">Stream</text>
<path fill="none" stroke="#388e3c" d="M364.86,-248.55C367.19,-234.65 370.53,-214.73 373.25,-198.45"/>
<polygon fill="#388e3c" stroke="#388e3c" points="376.66,-199.31 374.86,-188.87 369.76,-198.15 376.66,-199.31"/>
<text xml:space="preserve" text-anchor="middle" x="389.53" y="-221.12" font-family="Helvetica,sans-Serif" font-size="10.00">gRPC</text>
<text xml:space="preserve" text-anchor="middle" x="389.53" y="-208.38" font-family="Helvetica,sans-Serif" font-size="10.00">Stream</text>
</g>
<!-- events&#45;&gt;gateway -->
<g id="edge11" class="edge">
<g id="edge13" class="edge">
<title>events&#45;&gt;gateway</title>
<path fill="none" stroke="#7b1fa2" d="M281.13,-378.02C267.86,-372.71 253.29,-367.44 239.5,-363.62 212.49,-356.16 199.25,-370.98 177.25,-353.62 159.49,-339.61 150.46,-315.21 145.93,-295.98"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="149.38,-295.39 143.95,-286.29 142.52,-296.79 149.38,-295.39"/>
<text xml:space="preserve" text-anchor="middle" x="200.88" y="-337.75" font-family="Helvetica,sans-Serif" font-size="10.00">Subscribe</text>
<path fill="none" stroke="#7b1fa2" d="M427.23,-379.02C385.24,-361.12 328.44,-335.5 309,-320.12 299.76,-312.82 291.28,-303.11 284.39,-294.03"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="287.4,-292.22 278.71,-286.15 281.72,-296.31 287.4,-292.22"/>
<text xml:space="preserve" text-anchor="middle" x="391.72" y="-337.75" font-family="Helvetica,sans-Serif" font-size="10.00">Subscribe</text>
</g>
<!-- events&#45;&gt;alerts -->
<g id="edge10" class="edge">
<g id="edge12" class="edge">
<title>events&#45;&gt;alerts</title>
<path fill="none" stroke="#7b1fa2" d="M277.27,-380.98C264.23,-374.18 251.36,-365.21 242.25,-353.62 229.43,-337.32 224.08,-314.36 221.89,-296.26"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="225.38,-296.07 220.98,-286.43 218.41,-296.71 225.38,-296.07"/>
<text xml:space="preserve" text-anchor="middle" x="265.88" y="-337.75" font-family="Helvetica,sans-Serif" font-size="10.00">Subscribe</text>
<path fill="none" stroke="#7b1fa2" d="M463.14,-371.21C460.99,-365.5 459.04,-359.45 457.75,-353.62 453.59,-334.83 452.41,-313.19 452.27,-296.31"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="455.77,-296.51 452.31,-286.49 448.77,-296.48 455.77,-296.51"/>
<text xml:space="preserve" text-anchor="middle" x="481.38" y="-337.75" font-family="Helvetica,sans-Serif" font-size="10.00">Subscribe</text>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -60,7 +60,16 @@ digraph DataFlow {
alerts [label="Alert\nService", fillcolor="#C5CAE9"];
gateway [label="Gateway\n(WebSocket)", fillcolor="#9FA8DA"];
lambda [label="Lambda\nAggregator", fillcolor="#7986CB", style="rounded,filled,dashed"];
}
// Edge + Browser
subgraph cluster_delivery {
label="Delivery (AWS)";
style=filled;
fillcolor="#F3E5F5";
edge_relay [label="Edge\n(WS Relay)", fillcolor="#E1BEE7"];
browser [label="Browser\n(Dashboard)", fillcolor="#CE93D8"];
}
// Flow
@@ -75,9 +84,9 @@ digraph DataFlow {
redis_pubsub -> alerts [label="metrics.*"];
redis_pubsub -> gateway [label="metrics.*"];
gateway -> edge_relay [label="WebSocket\nForward"];
edge_relay -> browser [label="WebSocket"];
raw -> agg_1m [label="Continuous\nAggregate", style=dashed];
agg_1m -> agg_1h [label="Hourly\nJob", style=dashed];
raw -> lambda [label="SQS\nTrigger", style=dotted];
lambda -> agg_1m [label="Batch\nWrite", style=dotted];
}

View File

@@ -1,134 +1,139 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.1 (0)
<!-- Generated by graphviz version 14.1.2 (0)
-->
<!-- Title: DataFlow Pages: 1 -->
<svg width="1087pt" height="329pt"
viewBox="0.00 0.00 1087.00 329.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 325.25)">
<svg width="1270pt" height="305pt"
viewBox="0.00 0.00 1270.00 305.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 301.25)">
<title>DataFlow</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-325.25 1082.5,-325.25 1082.5,4 -4,4"/>
<text xml:space="preserve" text-anchor="middle" x="539.25" y="-303.95" font-family="Helvetica,sans-Serif" font-size="14.00">Metrics Data Flow Pipeline</text>
<polygon fill="white" stroke="none" points="-4,4 -4,-301.25 1265.75,-301.25 1265.75,4 -4,4"/>
<text xml:space="preserve" text-anchor="middle" x="630.88" y="-279.95" font-family="Helvetica,sans-Serif" font-size="14.00">Metrics Data Flow Pipeline</text>
<g id="clust1" class="cluster">
<title>cluster_collect</title>
<polygon fill="#e3f2fd" stroke="black" points="8,-111 8,-188 254,-188 254,-111 8,-111"/>
<text xml:space="preserve" text-anchor="middle" x="131" y="-170.7" font-family="Helvetica,sans-Serif" font-size="14.00">Collection (5s)</text>
<polygon fill="#e3f2fd" stroke="black" points="8,-87 8,-164 254,-164 254,-87 8,-87"/>
<text xml:space="preserve" text-anchor="middle" x="131" y="-146.7" font-family="Helvetica,sans-Serif" font-size="14.00">Collection (5s)</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_ingest</title>
<polygon fill="#e8f5e9" stroke="black" points="307,-95 307,-204 562.5,-204 562.5,-95 307,-95"/>
<text xml:space="preserve" text-anchor="middle" x="434.75" y="-186.7" font-family="Helvetica,sans-Serif" font-size="14.00">Ingestion</text>
<polygon fill="#e8f5e9" stroke="black" points="307,-71 307,-180 562.5,-180 562.5,-71 307,-71"/>
<text xml:space="preserve" text-anchor="middle" x="434.75" y="-162.7" font-family="Helvetica,sans-Serif" font-size="14.00">Ingestion</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_hot</title>
<polygon fill="#fff3e0" stroke="black" points="614.75,-34 614.75,-193 769.5,-193 769.5,-34 614.75,-34"/>
<text xml:space="preserve" text-anchor="middle" x="692.12" y="-175.7" font-family="Helvetica,sans-Serif" font-size="14.00">Hot Path (Real&#45;time)</text>
<polygon fill="#fff3e0" stroke="black" points="614.75,-10 614.75,-169 769.5,-169 769.5,-10 614.75,-10"/>
<text xml:space="preserve" text-anchor="middle" x="692.12" y="-151.7" font-family="Helvetica,sans-Serif" font-size="14.00">Hot Path (Real&#45;time)</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_warm</title>
<polygon fill="#fce4ec" stroke="black" points="645.62,-201 645.62,-288 1070.5,-288 1070.5,-201 645.62,-201"/>
<text xml:space="preserve" text-anchor="middle" x="858.06" y="-270.7" font-family="Helvetica,sans-Serif" font-size="14.00">Warm Path (Historical)</text>
<polygon fill="#fce4ec" stroke="black" points="645.62,-177 645.62,-264 1091.5,-264 1091.5,-177 645.62,-177"/>
<text xml:space="preserve" text-anchor="middle" x="868.56" y="-246.7" font-family="Helvetica,sans-Serif" font-size="14.00">Warm Path (Historical)</text>
</g>
<g id="clust5" class="cluster">
<title>cluster_consume</title>
<polygon fill="#e8eaf6" stroke="black" points="840.5,-8 840.5,-193 935.25,-193 935.25,-8 840.5,-8"/>
<text xml:space="preserve" text-anchor="middle" x="887.88" y="-175.7" font-family="Helvetica,sans-Serif" font-size="14.00">Consumers</text>
<polygon fill="#e8eaf6" stroke="black" points="840.5,-8 840.5,-139 935.25,-139 935.25,-8 840.5,-8"/>
<text xml:space="preserve" text-anchor="middle" x="887.88" y="-121.7" font-family="Helvetica,sans-Serif" font-size="14.00">Consumers</text>
</g>
<g id="clust6" class="cluster">
<title>cluster_delivery</title>
<polygon fill="#f3e5f5" stroke="black" points="1005.5,-8 1005.5,-85 1253.75,-85 1253.75,-8 1005.5,-8"/>
<text xml:space="preserve" text-anchor="middle" x="1129.62" y="-67.7" font-family="Helvetica,sans-Serif" font-size="14.00">Delivery (AWS)</text>
</g>
<!-- psutil -->
<g id="node1" class="node">
<title>psutil</title>
<polygon fill="#bbdefb" stroke="black" points="118.25,-155 16,-155 16,-151 12,-151 12,-147 16,-147 16,-127 12,-127 12,-123 16,-123 16,-119 118.25,-119 118.25,-155"/>
<polyline fill="none" stroke="black" points="16,-151 20,-151 20,-147 16,-147"/>
<polygon fill="#bbdefb" stroke="black" points="118.25,-131 16,-131 16,-127 12,-127 12,-123 16,-123 16,-103 12,-103 12,-99 16,-99 16,-95 118.25,-95 118.25,-131"/>
<polyline fill="none" stroke="black" points="16,-127 20,-127 20,-123 16,-123"/>
<text xml:space="preserve" text-anchor="middle" x="67.13" y="-140.25" font-family="Helvetica,sans-Serif" font-size="10.00">psutil</text>
<text xml:space="preserve" text-anchor="middle" x="67.13" y="-127.5" font-family="Helvetica,sans-Serif" font-size="10.00">(CPU, Mem, Disk)</text>
<polyline fill="none" stroke="black" points="16,-103 20,-103 20,-99 16,-99"/>
<text xml:space="preserve" text-anchor="middle" x="67.12" y="-116.25" font-family="Helvetica,sans-Serif" font-size="10.00">psutil</text>
<text xml:space="preserve" text-anchor="middle" x="67.12" y="-103.5" font-family="Helvetica,sans-Serif" font-size="10.00">(CPU, Mem, Disk)</text>
</g>
<!-- collector -->
<g id="node2" class="node">
<title>collector</title>
<path fill="#90caf9" stroke="black" d="M234,-155C234,-155 198.5,-155 198.5,-155 192.5,-155 186.5,-149 186.5,-143 186.5,-143 186.5,-131 186.5,-131 186.5,-125 192.5,-119 198.5,-119 198.5,-119 234,-119 234,-119 240,-119 246,-125 246,-131 246,-131 246,-143 246,-143 246,-149 240,-155 234,-155"/>
<text xml:space="preserve" text-anchor="middle" x="216.25" y="-140.25" font-family="Helvetica,sans-Serif" font-size="10.00">Collector</text>
<text xml:space="preserve" text-anchor="middle" x="216.25" y="-127.5" font-family="Helvetica,sans-Serif" font-size="10.00">Service</text>
<path fill="#90caf9" stroke="black" d="M234,-131C234,-131 198.5,-131 198.5,-131 192.5,-131 186.5,-125 186.5,-119 186.5,-119 186.5,-107 186.5,-107 186.5,-101 192.5,-95 198.5,-95 198.5,-95 234,-95 234,-95 240,-95 246,-101 246,-107 246,-107 246,-119 246,-119 246,-125 240,-131 234,-131"/>
<text xml:space="preserve" text-anchor="middle" x="216.25" y="-116.25" font-family="Helvetica,sans-Serif" font-size="10.00">Collector</text>
<text xml:space="preserve" text-anchor="middle" x="216.25" y="-103.5" font-family="Helvetica,sans-Serif" font-size="10.00">Service</text>
</g>
<!-- psutil&#45;&gt;collector -->
<g id="edge1" class="edge">
<title>psutil&#45;&gt;collector</title>
<path fill="none" stroke="black" d="M118.35,-137C136.74,-137 157.31,-137 174.75,-137"/>
<polygon fill="black" stroke="black" points="174.75,-140.5 184.75,-137 174.75,-133.5 174.75,-140.5"/>
<text xml:space="preserve" text-anchor="middle" x="152.38" y="-139.7" font-family="Helvetica,sans-Serif" font-size="9.00">Metrics</text>
<path fill="none" stroke="black" d="M118.35,-113C136.74,-113 157.31,-113 174.75,-113"/>
<polygon fill="black" stroke="black" points="174.75,-116.5 184.75,-113 174.75,-109.5 174.75,-116.5"/>
<text xml:space="preserve" text-anchor="middle" x="152.38" y="-115.7" font-family="Helvetica,sans-Serif" font-size="9.00">Metrics</text>
</g>
<!-- aggregator -->
<g id="node3" class="node">
<title>aggregator</title>
<path fill="#a5d6a7" stroke="black" d="M373,-155C373,-155 327,-155 327,-155 321,-155 315,-149 315,-143 315,-143 315,-131 315,-131 315,-125 321,-119 327,-119 327,-119 373,-119 373,-119 379,-119 385,-125 385,-131 385,-131 385,-143 385,-143 385,-149 379,-155 373,-155"/>
<text xml:space="preserve" text-anchor="middle" x="350" y="-140.25" font-family="Helvetica,sans-Serif" font-size="10.00">Aggregator</text>
<text xml:space="preserve" text-anchor="middle" x="350" y="-127.5" font-family="Helvetica,sans-Serif" font-size="10.00">(gRPC)</text>
<path fill="#a5d6a7" stroke="black" d="M373,-131C373,-131 327,-131 327,-131 321,-131 315,-125 315,-119 315,-119 315,-107 315,-107 315,-101 321,-95 327,-95 327,-95 373,-95 373,-95 379,-95 385,-101 385,-107 385,-107 385,-119 385,-119 385,-125 379,-131 373,-131"/>
<text xml:space="preserve" text-anchor="middle" x="350" y="-116.25" font-family="Helvetica,sans-Serif" font-size="10.00">Aggregator</text>
<text xml:space="preserve" text-anchor="middle" x="350" y="-103.5" font-family="Helvetica,sans-Serif" font-size="10.00">(gRPC)</text>
</g>
<!-- collector&#45;&gt;aggregator -->
<g id="edge2" class="edge">
<title>collector&#45;&gt;aggregator</title>
<path fill="none" stroke="black" d="M246.49,-137C263.19,-137 284.49,-137 303.35,-137"/>
<polygon fill="black" stroke="black" points="303.2,-140.5 313.2,-137 303.2,-133.5 303.2,-140.5"/>
<text xml:space="preserve" text-anchor="middle" x="280.5" y="-150.95" font-family="Helvetica,sans-Serif" font-size="9.00">gRPC</text>
<text xml:space="preserve" text-anchor="middle" x="280.5" y="-139.7" font-family="Helvetica,sans-Serif" font-size="9.00">Stream</text>
<path fill="none" stroke="black" d="M246.49,-113C263.19,-113 284.49,-113 303.35,-113"/>
<polygon fill="black" stroke="black" points="303.2,-116.5 313.2,-113 303.2,-109.5 303.2,-116.5"/>
<text xml:space="preserve" text-anchor="middle" x="280.5" y="-126.95" font-family="Helvetica,sans-Serif" font-size="9.00">gRPC</text>
<text xml:space="preserve" text-anchor="middle" x="280.5" y="-115.7" font-family="Helvetica,sans-Serif" font-size="9.00">Stream</text>
</g>
<!-- validate -->
<g id="node4" class="node">
<title>validate</title>
<path fill="#c8e6c9" stroke="black" d="M477.54,-165.08C477.54,-165.08 432.71,-142.42 432.71,-142.42 427.35,-139.71 427.35,-134.29 432.71,-131.58 432.71,-131.58 477.54,-108.92 477.54,-108.92 482.9,-106.21 493.6,-106.21 498.96,-108.92 498.96,-108.92 543.79,-131.58 543.79,-131.58 549.15,-134.29 549.15,-139.71 543.79,-142.42 543.79,-142.42 498.96,-165.08 498.96,-165.08 493.6,-167.79 482.9,-167.79 477.54,-165.08"/>
<text xml:space="preserve" text-anchor="middle" x="488.25" y="-140.25" font-family="Helvetica,sans-Serif" font-size="10.00">Validate &amp;</text>
<text xml:space="preserve" text-anchor="middle" x="488.25" y="-127.5" font-family="Helvetica,sans-Serif" font-size="10.00">Normalize</text>
<path fill="#c8e6c9" stroke="black" d="M477.54,-141.08C477.54,-141.08 432.71,-118.42 432.71,-118.42 427.35,-115.71 427.35,-110.29 432.71,-107.58 432.71,-107.58 477.54,-84.92 477.54,-84.92 482.9,-82.21 493.6,-82.21 498.96,-84.92 498.96,-84.92 543.79,-107.58 543.79,-107.58 549.15,-110.29 549.15,-115.71 543.79,-118.42 543.79,-118.42 498.96,-141.08 498.96,-141.08 493.6,-143.79 482.9,-143.79 477.54,-141.08"/>
<text xml:space="preserve" text-anchor="middle" x="488.25" y="-116.25" font-family="Helvetica,sans-Serif" font-size="10.00">Validate &amp;</text>
<text xml:space="preserve" text-anchor="middle" x="488.25" y="-103.5" font-family="Helvetica,sans-Serif" font-size="10.00">Normalize</text>
</g>
<!-- aggregator&#45;&gt;validate -->
<g id="edge3" class="edge">
<title>aggregator&#45;&gt;validate</title>
<path fill="none" stroke="black" d="M385.38,-137C392.95,-137 401.25,-137 409.76,-137"/>
<polygon fill="black" stroke="black" points="409.49,-140.5 419.49,-137 409.49,-133.5 409.49,-140.5"/>
<path fill="none" stroke="black" d="M385.38,-113C392.95,-113 401.25,-113 409.76,-113"/>
<polygon fill="black" stroke="black" points="409.49,-116.5 419.49,-113 409.49,-109.5 409.49,-116.5"/>
</g>
<!-- redis_state -->
<g id="node5" class="node">
<title>redis_state</title>
<path fill="#ffcc80" stroke="black" d="M731.88,-155.84C731.88,-158.15 713.83,-160.03 691.62,-160.03 669.42,-160.03 651.38,-158.15 651.38,-155.84 651.38,-155.84 651.38,-118.16 651.38,-118.16 651.38,-115.85 669.42,-113.97 691.62,-113.97 713.83,-113.97 731.88,-115.85 731.88,-118.16 731.88,-118.16 731.88,-155.84 731.88,-155.84"/>
<path fill="none" stroke="black" d="M731.88,-155.84C731.88,-153.53 713.83,-151.66 691.62,-151.66 669.42,-151.66 651.38,-153.53 651.38,-155.84"/>
<text xml:space="preserve" text-anchor="middle" x="691.62" y="-140.25" font-family="Helvetica,sans-Serif" font-size="10.00">Redis</text>
<text xml:space="preserve" text-anchor="middle" x="691.62" y="-127.5" font-family="Helvetica,sans-Serif" font-size="10.00">Current State</text>
<path fill="#ffcc80" stroke="black" d="M731.88,-131.84C731.88,-134.15 713.83,-136.03 691.62,-136.03 669.42,-136.03 651.38,-134.15 651.38,-131.84 651.38,-131.84 651.38,-94.16 651.38,-94.16 651.38,-91.85 669.42,-89.97 691.62,-89.97 713.83,-89.97 731.88,-91.85 731.88,-94.16 731.88,-94.16 731.88,-131.84 731.88,-131.84"/>
<path fill="none" stroke="black" d="M731.88,-131.84C731.88,-129.53 713.83,-127.66 691.62,-127.66 669.42,-127.66 651.38,-129.53 651.38,-131.84"/>
<text xml:space="preserve" text-anchor="middle" x="691.62" y="-116.25" font-family="Helvetica,sans-Serif" font-size="10.00">Redis</text>
<text xml:space="preserve" text-anchor="middle" x="691.62" y="-103.5" font-family="Helvetica,sans-Serif" font-size="10.00">Current State</text>
</g>
<!-- validate&#45;&gt;redis_state -->
<g id="edge4" class="edge">
<title>validate&#45;&gt;redis_state</title>
<path fill="none" stroke="black" d="M555.47,-137C582.9,-137 614.22,-137 639.8,-137"/>
<polygon fill="black" stroke="black" points="639.6,-140.5 649.6,-137 639.6,-133.5 639.6,-140.5"/>
<text xml:space="preserve" text-anchor="middle" x="588.62" y="-139.7" font-family="Helvetica,sans-Serif" font-size="9.00">Upsert</text>
<path fill="none" stroke="black" d="M555.47,-113C582.9,-113 614.22,-113 639.8,-113"/>
<polygon fill="black" stroke="black" points="639.6,-116.5 649.6,-113 639.6,-109.5 639.6,-116.5"/>
<text xml:space="preserve" text-anchor="middle" x="588.63" y="-115.7" font-family="Helvetica,sans-Serif" font-size="9.00">Upsert</text>
</g>
<!-- redis_pubsub -->
<g id="node6" class="node">
<title>redis_pubsub</title>
<path fill="#ffb74d" stroke="black" d="M729.05,-78.12C729.05,-78.12 721.56,-87.24 721.56,-87.24 717.82,-91.79 708.18,-96.35 702.28,-96.35 702.28,-96.35 680.97,-96.35 680.97,-96.35 675.07,-96.35 665.43,-91.79 661.69,-87.24 661.69,-87.24 654.2,-78.12 654.2,-78.12 650.46,-73.56 650.46,-64.44 654.2,-59.88 654.2,-59.88 661.69,-50.76 661.69,-50.76 665.43,-46.21 675.07,-41.65 680.97,-41.65 680.97,-41.65 702.28,-41.65 702.28,-41.65 708.18,-41.65 717.82,-46.21 721.56,-50.76 721.56,-50.76 729.05,-59.88 729.05,-59.88 732.79,-64.44 732.79,-73.56 729.05,-78.12"/>
<text xml:space="preserve" text-anchor="middle" x="691.62" y="-72.25" font-family="Helvetica,sans-Serif" font-size="10.00">Redis</text>
<text xml:space="preserve" text-anchor="middle" x="691.62" y="-59.5" font-family="Helvetica,sans-Serif" font-size="10.00">Pub/Sub</text>
<path fill="#ffb74d" stroke="black" d="M729.05,-54.12C729.05,-54.12 721.56,-63.24 721.56,-63.24 717.82,-67.79 708.18,-72.35 702.28,-72.35 702.28,-72.35 680.97,-72.35 680.97,-72.35 675.07,-72.35 665.43,-67.79 661.69,-63.24 661.69,-63.24 654.2,-54.12 654.2,-54.12 650.46,-49.56 650.46,-40.44 654.2,-35.88 654.2,-35.88 661.69,-26.76 661.69,-26.76 665.43,-22.21 675.07,-17.65 680.97,-17.65 680.97,-17.65 702.28,-17.65 702.28,-17.65 708.18,-17.65 717.82,-22.21 721.56,-26.76 721.56,-26.76 729.05,-35.88 729.05,-35.88 732.79,-40.44 732.79,-49.56 729.05,-54.12"/>
<text xml:space="preserve" text-anchor="middle" x="691.62" y="-48.25" font-family="Helvetica,sans-Serif" font-size="10.00">Redis</text>
<text xml:space="preserve" text-anchor="middle" x="691.62" y="-35.5" font-family="Helvetica,sans-Serif" font-size="10.00">Pub/Sub</text>
</g>
<!-- validate&#45;&gt;redis_pubsub -->
<g id="edge5" class="edge">
<title>validate&#45;&gt;redis_pubsub</title>
<path fill="none" stroke="black" d="M529.04,-123.57C562.44,-112.28 610.18,-96.17 645.1,-84.37"/>
<polygon fill="black" stroke="black" points="646.17,-87.71 654.53,-81.19 643.93,-81.07 646.17,-87.71"/>
<text xml:space="preserve" text-anchor="middle" x="588.62" y="-109.77" font-family="Helvetica,sans-Serif" font-size="9.00">Publish</text>
<path fill="none" stroke="black" d="M529.04,-99.57C562.44,-88.28 610.18,-72.17 645.1,-60.37"/>
<polygon fill="black" stroke="black" points="646.17,-63.71 654.53,-57.19 643.93,-57.07 646.17,-63.71"/>
<text xml:space="preserve" text-anchor="middle" x="588.63" y="-85.77" font-family="Helvetica,sans-Serif" font-size="9.00">Publish</text>
</g>
<!-- raw -->
<g id="node7" class="node">
<title>raw</title>
<path fill="#f8bbd9" stroke="black" d="M729.62,-250.84C729.62,-253.15 712.59,-255.03 691.62,-255.03 670.66,-255.03 653.62,-253.15 653.62,-250.84 653.62,-250.84 653.62,-213.16 653.62,-213.16 653.62,-210.85 670.66,-208.97 691.62,-208.97 712.59,-208.97 729.62,-210.85 729.62,-213.16 729.62,-213.16 729.62,-250.84 729.62,-250.84"/>
<path fill="none" stroke="black" d="M729.62,-250.84C729.62,-248.53 712.59,-246.66 691.62,-246.66 670.66,-246.66 653.62,-248.53 653.62,-250.84"/>
<text xml:space="preserve" text-anchor="middle" x="691.62" y="-235.25" font-family="Helvetica,sans-Serif" font-size="10.00">metrics_raw</text>
<text xml:space="preserve" text-anchor="middle" x="691.62" y="-222.5" font-family="Helvetica,sans-Serif" font-size="10.00">(5s, 24h)</text>
<path fill="#f8bbd9" stroke="black" d="M729.62,-226.84C729.62,-229.15 712.59,-231.03 691.62,-231.03 670.66,-231.03 653.62,-229.15 653.62,-226.84 653.62,-226.84 653.62,-189.16 653.62,-189.16 653.62,-186.85 670.66,-184.97 691.62,-184.97 712.59,-184.97 729.62,-186.85 729.62,-189.16 729.62,-189.16 729.62,-226.84 729.62,-226.84"/>
<path fill="none" stroke="black" d="M729.62,-226.84C729.62,-224.53 712.59,-222.66 691.62,-222.66 670.66,-222.66 653.62,-224.53 653.62,-226.84"/>
<text xml:space="preserve" text-anchor="middle" x="691.62" y="-211.25" font-family="Helvetica,sans-Serif" font-size="10.00">metrics_raw</text>
<text xml:space="preserve" text-anchor="middle" x="691.62" y="-198.5" font-family="Helvetica,sans-Serif" font-size="10.00">(5s, 24h)</text>
</g>
<!-- validate&#45;&gt;raw -->
<g id="edge6" class="edge">
<title>validate&#45;&gt;raw</title>
<path fill="none" stroke="black" d="M523.01,-153.3C548.24,-165.44 583.6,-182.37 614.75,-197 623.81,-201.26 633.5,-205.76 642.83,-210.07"/>
<polygon fill="black" stroke="black" points="641.22,-213.19 651.77,-214.2 644.16,-206.83 641.22,-213.19"/>
<text xml:space="preserve" text-anchor="middle" x="588.62" y="-194.9" font-family="Helvetica,sans-Serif" font-size="9.00">Insert</text>
<path fill="none" stroke="black" d="M523.01,-129.3C548.24,-141.44 583.6,-158.37 614.75,-173 623.81,-177.26 633.5,-181.76 642.83,-186.07"/>
<polygon fill="black" stroke="black" points="641.22,-189.19 651.77,-190.2 644.16,-182.83 641.22,-189.19"/>
<text xml:space="preserve" text-anchor="middle" x="588.63" y="-170.9" font-family="Helvetica,sans-Serif" font-size="9.00">Insert</text>
</g>
<!-- alerts -->
<g id="node10" class="node">
@@ -140,9 +145,9 @@
<!-- redis_pubsub&#45;&gt;alerts -->
<g id="edge7" class="edge">
<title>redis_pubsub&#45;&gt;alerts</title>
<path fill="none" stroke="black" d="M733.71,-73.03C767.65,-76.36 815.43,-81.04 848.46,-84.28"/>
<polygon fill="black" stroke="black" points="848.11,-87.76 858.4,-85.26 848.79,-80.8 848.11,-87.76"/>
<text xml:space="preserve" text-anchor="middle" x="805" y="-85.09" font-family="Helvetica,sans-Serif" font-size="9.00">metrics.*</text>
<path fill="none" stroke="black" d="M729.98,-53.29C764.26,-60.9 814.78,-72.11 849.05,-79.72"/>
<polygon fill="black" stroke="black" points="848.01,-83.07 858.53,-81.82 849.52,-76.24 848.01,-83.07"/>
<text xml:space="preserve" text-anchor="middle" x="805" y="-77.99" font-family="Helvetica,sans-Serif" font-size="9.00">metrics.*</text>
</g>
<!-- gateway -->
<g id="node11" class="node">
@@ -154,64 +159,70 @@
<!-- redis_pubsub&#45;&gt;gateway -->
<g id="edge8" class="edge">
<title>redis_pubsub&#45;&gt;gateway</title>
<path fill="none" stroke="black" d="M731.37,-62C761.89,-56.49 804.64,-48.77 837.51,-42.83"/>
<polygon fill="black" stroke="black" points="837.98,-46.3 847.2,-41.08 836.74,-39.41 837.98,-46.3"/>
<text xml:space="preserve" text-anchor="middle" x="805" y="-55.25" font-family="Helvetica,sans-Serif" font-size="9.00">metrics.*</text>
<path fill="none" stroke="black" d="M735.14,-42.59C765.3,-40.87 805.86,-38.57 837.38,-36.78"/>
<polygon fill="black" stroke="black" points="837.25,-40.29 847.03,-36.23 836.85,-33.31 837.25,-40.29"/>
<text xml:space="preserve" text-anchor="middle" x="805" y="-42.53" font-family="Helvetica,sans-Serif" font-size="9.00">metrics.*</text>
</g>
<!-- agg_1m -->
<g id="node8" class="node">
<title>agg_1m</title>
<path fill="#f48fb1" stroke="black" d="M924.25,-250.84C924.25,-253.15 907.72,-255.03 887.38,-255.03 867.03,-255.03 850.5,-253.15 850.5,-250.84 850.5,-250.84 850.5,-213.16 850.5,-213.16 850.5,-210.85 867.03,-208.97 887.38,-208.97 907.72,-208.97 924.25,-210.85 924.25,-213.16 924.25,-213.16 924.25,-250.84 924.25,-250.84"/>
<path fill="none" stroke="black" d="M924.25,-250.84C924.25,-248.53 907.72,-246.66 887.38,-246.66 867.03,-246.66 850.5,-248.53 850.5,-250.84"/>
<text xml:space="preserve" text-anchor="middle" x="887.38" y="-235.25" font-family="Helvetica,sans-Serif" font-size="10.00">metrics_1m</text>
<text xml:space="preserve" text-anchor="middle" x="887.38" y="-222.5" font-family="Helvetica,sans-Serif" font-size="10.00">(1m, 7d)</text>
<path fill="#f48fb1" stroke="black" d="M924.25,-226.84C924.25,-229.15 907.72,-231.03 887.38,-231.03 867.03,-231.03 850.5,-229.15 850.5,-226.84 850.5,-226.84 850.5,-189.16 850.5,-189.16 850.5,-186.85 867.03,-184.97 887.38,-184.97 907.72,-184.97 924.25,-186.85 924.25,-189.16 924.25,-189.16 924.25,-226.84 924.25,-226.84"/>
<path fill="none" stroke="black" d="M924.25,-226.84C924.25,-224.53 907.72,-222.66 887.38,-222.66 867.03,-222.66 850.5,-224.53 850.5,-226.84"/>
<text xml:space="preserve" text-anchor="middle" x="887.38" y="-211.25" font-family="Helvetica,sans-Serif" font-size="10.00">metrics_1m</text>
<text xml:space="preserve" text-anchor="middle" x="887.38" y="-198.5" font-family="Helvetica,sans-Serif" font-size="10.00">(1m, 7d)</text>
</g>
<!-- raw&#45;&gt;agg_1m -->
<g id="edge9" class="edge">
<title>raw&#45;&gt;agg_1m</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M729.98,-232C760.97,-232 805.22,-232 838.74,-232"/>
<polygon fill="black" stroke="black" points="838.6,-235.5 848.6,-232 838.6,-228.5 838.6,-235.5"/>
<text xml:space="preserve" text-anchor="middle" x="805" y="-245.95" font-family="Helvetica,sans-Serif" font-size="9.00">Continuous</text>
<text xml:space="preserve" text-anchor="middle" x="805" y="-234.7" font-family="Helvetica,sans-Serif" font-size="9.00">Aggregate</text>
</g>
<!-- lambda -->
<g id="node12" class="node">
<title>lambda</title>
<path fill="#7986cb" stroke="black" stroke-dasharray="5,2" d="M910.38,-160C910.38,-160 864.38,-160 864.38,-160 858.38,-160 852.38,-154 852.38,-148 852.38,-148 852.38,-136 852.38,-136 852.38,-130 858.38,-124 864.38,-124 864.38,-124 910.38,-124 910.38,-124 916.38,-124 922.38,-130 922.38,-136 922.38,-136 922.38,-148 922.38,-148 922.38,-154 916.38,-160 910.38,-160"/>
<text xml:space="preserve" text-anchor="middle" x="887.38" y="-145.25" font-family="Helvetica,sans-Serif" font-size="10.00">Lambda</text>
<text xml:space="preserve" text-anchor="middle" x="887.38" y="-132.5" font-family="Helvetica,sans-Serif" font-size="10.00">Aggregator</text>
</g>
<!-- raw&#45;&gt;lambda -->
<g id="edge11" class="edge">
<title>raw&#45;&gt;lambda</title>
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M729.81,-215.18C742.43,-209.45 756.59,-202.98 769.5,-197 793.37,-185.95 819.91,-173.48 841.65,-163.21"/>
<polygon fill="black" stroke="black" points="843,-166.44 850.54,-159.01 840,-160.12 843,-166.44"/>
<text xml:space="preserve" text-anchor="middle" x="805" y="-205.05" font-family="Helvetica,sans-Serif" font-size="9.00">SQS</text>
<text xml:space="preserve" text-anchor="middle" x="805" y="-193.8" font-family="Helvetica,sans-Serif" font-size="9.00">Trigger</text>
<title>raw&#45;&gt;agg_1m</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M729.98,-208C760.97,-208 805.22,-208 838.74,-208"/>
<polygon fill="black" stroke="black" points="838.6,-211.5 848.6,-208 838.6,-204.5 838.6,-211.5"/>
<text xml:space="preserve" text-anchor="middle" x="805" y="-221.95" font-family="Helvetica,sans-Serif" font-size="9.00">Continuous</text>
<text xml:space="preserve" text-anchor="middle" x="805" y="-210.7" font-family="Helvetica,sans-Serif" font-size="9.00">Aggregate</text>
</g>
<!-- agg_1h -->
<g id="node9" class="node">
<title>agg_1h</title>
<path fill="#ec407a" stroke="black" d="M1062.5,-250.84C1062.5,-253.15 1046.81,-255.03 1027.5,-255.03 1008.19,-255.03 992.5,-253.15 992.5,-250.84 992.5,-250.84 992.5,-213.16 992.5,-213.16 992.5,-210.85 1008.19,-208.97 1027.5,-208.97 1046.81,-208.97 1062.5,-210.85 1062.5,-213.16 1062.5,-213.16 1062.5,-250.84 1062.5,-250.84"/>
<path fill="none" stroke="black" d="M1062.5,-250.84C1062.5,-248.53 1046.81,-246.66 1027.5,-246.66 1008.19,-246.66 992.5,-248.53 992.5,-250.84"/>
<text xml:space="preserve" text-anchor="middle" x="1027.5" y="-235.25" font-family="Helvetica,sans-Serif" font-size="10.00">metrics_1h</text>
<text xml:space="preserve" text-anchor="middle" x="1027.5" y="-222.5" font-family="Helvetica,sans-Serif" font-size="10.00">(1h, 90d)</text>
<path fill="#ec407a" stroke="black" d="M1083.5,-226.84C1083.5,-229.15 1067.81,-231.03 1048.5,-231.03 1029.19,-231.03 1013.5,-229.15 1013.5,-226.84 1013.5,-226.84 1013.5,-189.16 1013.5,-189.16 1013.5,-186.85 1029.19,-184.97 1048.5,-184.97 1067.81,-184.97 1083.5,-186.85 1083.5,-189.16 1083.5,-189.16 1083.5,-226.84 1083.5,-226.84"/>
<path fill="none" stroke="black" d="M1083.5,-226.84C1083.5,-224.53 1067.81,-222.66 1048.5,-222.66 1029.19,-222.66 1013.5,-224.53 1013.5,-226.84"/>
<text xml:space="preserve" text-anchor="middle" x="1048.5" y="-211.25" font-family="Helvetica,sans-Serif" font-size="10.00">metrics_1h</text>
<text xml:space="preserve" text-anchor="middle" x="1048.5" y="-198.5" font-family="Helvetica,sans-Serif" font-size="10.00">(1h, 90d)</text>
</g>
<!-- agg_1m&#45;&gt;agg_1h -->
<g id="edge10" class="edge">
<title>agg_1m&#45;&gt;agg_1h</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M924.67,-232C941.93,-232 962.74,-232 981.04,-232"/>
<polygon fill="black" stroke="black" points="980.84,-235.5 990.84,-232 980.84,-228.5 980.84,-235.5"/>
<text xml:space="preserve" text-anchor="middle" x="959.88" y="-245.95" font-family="Helvetica,sans-Serif" font-size="9.00">Hourly</text>
<text xml:space="preserve" text-anchor="middle" x="959.88" y="-234.7" font-family="Helvetica,sans-Serif" font-size="9.00">Job</text>
</g>
<!-- lambda&#45;&gt;agg_1m -->
<g id="edge12" class="edge">
<title>lambda&#45;&gt;agg_1m</title>
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M887.38,-160.21C887.38,-170.91 887.38,-184.78 887.38,-197.47"/>
<polygon fill="black" stroke="black" points="883.88,-197.16 887.38,-207.16 890.88,-197.16 883.88,-197.16"/>
<text xml:space="preserve" text-anchor="middle" x="873.12" y="-187.18" font-family="Helvetica,sans-Serif" font-size="9.00">Batch</text>
<text xml:space="preserve" text-anchor="middle" x="873.12" y="-175.93" font-family="Helvetica,sans-Serif" font-size="9.00">Write</text>
<title>agg_1m&#45;&gt;agg_1h</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M924.48,-208C947.44,-208 977.36,-208 1001.94,-208"/>
<polygon fill="black" stroke="black" points="1001.66,-211.5 1011.66,-208 1001.66,-204.5 1001.66,-211.5"/>
<text xml:space="preserve" text-anchor="middle" x="970.38" y="-221.95" font-family="Helvetica,sans-Serif" font-size="9.00">Hourly</text>
<text xml:space="preserve" text-anchor="middle" x="970.38" y="-210.7" font-family="Helvetica,sans-Serif" font-size="9.00">Job</text>
</g>
<!-- edge_relay -->
<g id="node12" class="node">
<title>edge_relay</title>
<path fill="#e1bee7" stroke="black" d="M1071.5,-52C1071.5,-52 1025.5,-52 1025.5,-52 1019.5,-52 1013.5,-46 1013.5,-40 1013.5,-40 1013.5,-28 1013.5,-28 1013.5,-22 1019.5,-16 1025.5,-16 1025.5,-16 1071.5,-16 1071.5,-16 1077.5,-16 1083.5,-22 1083.5,-28 1083.5,-28 1083.5,-40 1083.5,-40 1083.5,-46 1077.5,-52 1071.5,-52"/>
<text xml:space="preserve" text-anchor="middle" x="1048.5" y="-37.25" font-family="Helvetica,sans-Serif" font-size="10.00">Edge</text>
<text xml:space="preserve" text-anchor="middle" x="1048.5" y="-24.5" font-family="Helvetica,sans-Serif" font-size="10.00">(WS Relay)</text>
</g>
<!-- gateway&#45;&gt;edge_relay -->
<g id="edge9" class="edge">
<title>gateway&#45;&gt;edge_relay</title>
<path fill="none" stroke="black" d="M926.09,-34C948.75,-34 977.76,-34 1001.73,-34"/>
<polygon fill="black" stroke="black" points="1001.53,-37.5 1011.53,-34 1001.53,-30.5 1001.53,-37.5"/>
<text xml:space="preserve" text-anchor="middle" x="970.38" y="-47.95" font-family="Helvetica,sans-Serif" font-size="9.00">WebSocket</text>
<text xml:space="preserve" text-anchor="middle" x="970.38" y="-36.7" font-family="Helvetica,sans-Serif" font-size="9.00">Forward</text>
</g>
<!-- browser -->
<g id="node13" class="node">
<title>browser</title>
<path fill="#ce93d8" stroke="black" d="M1233.75,-52C1233.75,-52 1181.75,-52 1181.75,-52 1175.75,-52 1169.75,-46 1169.75,-40 1169.75,-40 1169.75,-28 1169.75,-28 1169.75,-22 1175.75,-16 1181.75,-16 1181.75,-16 1233.75,-16 1233.75,-16 1239.75,-16 1245.75,-22 1245.75,-28 1245.75,-28 1245.75,-40 1245.75,-40 1245.75,-46 1239.75,-52 1233.75,-52"/>
<text xml:space="preserve" text-anchor="middle" x="1207.75" y="-37.25" font-family="Helvetica,sans-Serif" font-size="10.00">Browser</text>
<text xml:space="preserve" text-anchor="middle" x="1207.75" y="-24.5" font-family="Helvetica,sans-Serif" font-size="10.00">(Dashboard)</text>
</g>
<!-- edge_relay&#45;&gt;browser -->
<g id="edge10" class="edge">
<title>edge_relay&#45;&gt;browser</title>
<path fill="none" stroke="black" d="M1083.62,-34C1105.36,-34 1133.86,-34 1157.96,-34"/>
<polygon fill="black" stroke="black" points="1157.88,-37.5 1167.88,-34 1157.88,-30.5 1157.88,-37.5"/>
<text xml:space="preserve" text-anchor="middle" x="1126.62" y="-36.7" font-family="Helvetica,sans-Serif" font-size="9.00">WebSocket</text>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -11,60 +11,36 @@ digraph Deployment {
node [shape=box, style="rounded,filled"];
// Local Development
// Local Stack
subgraph cluster_local {
label="Local Development";
style=filled;
fillcolor="#E3F2FD";
subgraph cluster_kind {
label="Kind Cluster";
style=filled;
fillcolor="#BBDEFB";
tilt [label="Tilt\n(Live Reload)", shape=component, fillcolor="#90CAF9"];
k8s_local [label="K8s Pods\n(via Kustomize)", fillcolor="#64B5F6"];
}
compose [label="Docker Compose\n(Alternative)", fillcolor="#90CAF9", style="rounded,dashed"];
}
// AWS Staging/Demo
subgraph cluster_aws {
label="AWS (sysmonstm.mcrn.ar)";
label="Local Stack (Docker Compose)";
style=filled;
fillcolor="#E8F5E9";
subgraph cluster_ec2 {
label="EC2 t2.small";
style=filled;
fillcolor="#C8E6C9";
compose_ec2 [label="Docker Compose\n(All Services)", fillcolor="#A5D6A7"];
nginx [label="Nginx\n(SSL Termination)", fillcolor="#81C784"];
aggregator [label="Aggregator\n(gRPC Server)", fillcolor="#A5D6A7"];
gateway [label="Gateway\n(FastAPI)", fillcolor="#A5D6A7"];
alerts [label="Alerts\nService", fillcolor="#A5D6A7"];
redis [label="Redis", shape=cylinder, fillcolor="#C8E6C9"];
timescaledb [label="TimescaleDB", shape=cylinder, fillcolor="#C8E6C9"];
}
subgraph cluster_lambda {
label="Lambda (Data Processing)";
// AWS Edge
subgraph cluster_aws {
label="AWS (sysmonstm.mcrn.ar)";
style=filled;
fillcolor="#DCEDC8";
fillcolor="#F3E5F5";
lambda_agg [label="Aggregator\nLambda", fillcolor="#AED581"];
lambda_compact [label="Compactor\nLambda", fillcolor="#9CCC65"];
}
sqs [label="SQS\n(Buffer)", shape=hexagon, fillcolor="#FFE082"];
s3 [label="S3\n(Backup)", shape=cylinder, fillcolor="#FFE082"];
edge_relay [label="Edge\n(WebSocket Relay)", fillcolor="#CE93D8"];
}
// CI/CD
subgraph cluster_cicd {
label="CI/CD";
style=filled;
fillcolor="#F3E5F5";
fillcolor="#E3F2FD";
woodpecker [label="Woodpecker CI", fillcolor="#CE93D8"];
registry [label="Container\nRegistry", shape=cylinder, fillcolor="#BA68C8"];
woodpecker [label="Woodpecker CI", fillcolor="#90CAF9"];
registry [label="Container\nRegistry", shape=cylinder, fillcolor="#64B5F6"];
}
// Collectors (External)
@@ -78,18 +54,22 @@ digraph Deployment {
coll3 [label="Collector\n(Machine N)", fillcolor="#FFCCBC"];
}
// Browser
browser [label="Browser\n(Dashboard)", fillcolor="#FFF3E0"];
// Connections
tilt -> k8s_local [style=invis];
coll1 -> aggregator [label="gRPC"];
coll2 -> aggregator [label="gRPC"];
coll3 -> aggregator [label="gRPC"];
aggregator -> redis [label="State"];
aggregator -> timescaledb [label="Store"];
gateway -> aggregator [label="gRPC"];
gateway -> edge_relay [label="WebSocket\nForward"];
edge_relay -> browser [label="WebSocket", dir=both];
woodpecker -> registry [label="Push"];
registry -> compose_ec2 [label="Pull"];
registry -> k8s_local [label="Pull", style=dashed];
nginx -> compose_ec2 [label="Proxy"];
compose_ec2 -> sqs [label="Events"];
sqs -> lambda_agg [label="Trigger"];
lambda_compact -> s3 [label="Archive"];
coll1 -> compose_ec2 [label="gRPC", lhead=cluster_ec2];
coll2 -> compose_ec2 [label="gRPC", lhead=cluster_ec2];
coll3 -> compose_ec2 [label="gRPC", lhead=cluster_ec2];
registry -> edge_relay [label="Pull", style=dashed];
registry -> aggregator [label="Pull", style=dashed, lhead=cluster_local];
}

View File

@@ -1,221 +1,197 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.1 (0)
<!-- Generated by graphviz version 14.1.2 (0)
-->
<!-- Title: Deployment Pages: 1 -->
<svg width="872pt" height="662pt"
viewBox="0.00 0.00 872.00 662.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 658.3)">
<svg width="743pt" height="439pt"
viewBox="0.00 0.00 743.00 439.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 435.03)">
<title>Deployment</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-658.3 868,-658.3 868,4 -4,4"/>
<text xml:space="preserve" text-anchor="middle" x="432" y="-637" font-family="Helvetica,sans-Serif" font-size="14.00">Deployment Architecture</text>
<polygon fill="white" stroke="none" points="-4,4 -4,-435.03 739,-435.03 739,4 -4,4"/>
<text xml:space="preserve" text-anchor="middle" x="367.5" y="-413.73" font-family="Helvetica,sans-Serif" font-size="14.00">Deployment Architecture</text>
<g id="clust1" class="cluster">
<title>cluster_local</title>
<polygon fill="#e3f2fd" stroke="black" points="8,-307.77 8,-514.55 238,-514.55 238,-307.77 8,-307.77"/>
<text xml:space="preserve" text-anchor="middle" x="123" y="-497.25" font-family="Helvetica,sans-Serif" font-size="14.00">Local Development</text>
<polygon fill="#e8f5e9" stroke="black" points="292,-8 292,-291.28 518,-291.28 518,-8 292,-8"/>
<text xml:space="preserve" text-anchor="middle" x="405" y="-273.98" font-family="Helvetica,sans-Serif" font-size="14.00">Local Stack (Docker Compose)</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_kind</title>
<polygon fill="#bbdefb" stroke="black" points="16,-315.77 16,-481.3 124,-481.3 124,-315.77 16,-315.77"/>
<text xml:space="preserve" text-anchor="middle" x="70" y="-464" font-family="Helvetica,sans-Serif" font-size="14.00">Kind Cluster</text>
<title>cluster_aws</title>
<polygon fill="#f3e5f5" stroke="black" points="526,-91.25 526,-168.5 727,-168.5 727,-91.25 526,-91.25"/>
<text xml:space="preserve" text-anchor="middle" x="626.5" y="-151.2" font-family="Helvetica,sans-Serif" font-size="14.00">AWS (sysmonstm.mcrn.ar)</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_aws</title>
<polygon fill="#e8f5e9" stroke="black" points="642,-8 642,-514.55 856,-514.55 856,-8 642,-8"/>
<text xml:space="preserve" text-anchor="middle" x="749" y="-497.25" font-family="Helvetica,sans-Serif" font-size="14.00">AWS (sysmonstm.mcrn.ar)</text>
<title>cluster_cicd</title>
<polygon fill="#e3f2fd" stroke="black" points="531,-209 531,-397.78 635,-397.78 635,-209 531,-209"/>
<text xml:space="preserve" text-anchor="middle" x="583" y="-380.48" font-family="Helvetica,sans-Serif" font-size="14.00">CI/CD</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_ec2</title>
<polygon fill="#c8e6c9" stroke="black" points="650,-315.77 650,-481.3 768,-481.3 768,-315.77 650,-315.77"/>
<text xml:space="preserve" text-anchor="middle" x="709" y="-464" font-family="Helvetica,sans-Serif" font-size="14.00">EC2 t2.small</text>
</g>
<g id="clust5" class="cluster">
<title>cluster_lambda</title>
<polygon fill="#dcedc8" stroke="black" points="650,-101.31 650,-178.56 848,-178.56 848,-101.31 650,-101.31"/>
<text xml:space="preserve" text-anchor="middle" x="749" y="-161.26" font-family="Helvetica,sans-Serif" font-size="14.00">Lambda (Data Processing)</text>
</g>
<g id="clust6" class="cluster">
<title>cluster_cicd</title>
<polygon fill="#f3e5f5" stroke="black" points="246,-399.02 246,-621.05 350,-621.05 350,-399.02 246,-399.02"/>
<text xml:space="preserve" text-anchor="middle" x="298" y="-603.75" font-family="Helvetica,sans-Serif" font-size="14.00">CI/CD</text>
</g>
<g id="clust7" class="cluster">
<title>cluster_collectors</title>
<polygon fill="none" stroke="gray" stroke-dasharray="5,2" points="358,-404.05 358,-481.3 634,-481.3 634,-404.05 358,-404.05"/>
<text xml:space="preserve" text-anchor="middle" x="496" y="-464" font-family="Helvetica,sans-Serif" font-size="14.00">Monitored Machines</text>
<polygon fill="none" stroke="gray" stroke-dasharray="5,2" points="8,-214.03 8,-291.28 284,-291.28 284,-214.03 8,-214.03"/>
<text xml:space="preserve" text-anchor="middle" x="146" y="-273.98" font-family="Helvetica,sans-Serif" font-size="14.00">Monitored Machines</text>
</g>
<!-- tilt -->
<!-- aggregator -->
<g id="node1" class="node">
<title>tilt</title>
<polygon fill="#90caf9" stroke="black" points="110.25,-448.05 29.75,-448.05 29.75,-444.05 25.75,-444.05 25.75,-440.05 29.75,-440.05 29.75,-420.05 25.75,-420.05 25.75,-416.05 29.75,-416.05 29.75,-412.05 110.25,-412.05 110.25,-448.05"/>
<polyline fill="none" stroke="black" points="29.75,-444.05 33.75,-444.05 33.75,-440.05 29.75,-440.05"/>
<polyline fill="none" stroke="black" points="29.75,-420.05 33.75,-420.05 33.75,-416.05 29.75,-416.05"/>
<text xml:space="preserve" text-anchor="middle" x="70" y="-433.3" font-family="Helvetica,sans-Serif" font-size="10.00">Tilt</text>
<text xml:space="preserve" text-anchor="middle" x="70" y="-420.55" font-family="Helvetica,sans-Serif" font-size="10.00">(Live Reload)</text>
<title>aggregator</title>
<path fill="#a5d6a7" stroke="black" d="M371.75,-135.25C371.75,-135.25 312.25,-135.25 312.25,-135.25 306.25,-135.25 300.25,-129.25 300.25,-123.25 300.25,-123.25 300.25,-111.25 300.25,-111.25 300.25,-105.25 306.25,-99.25 312.25,-99.25 312.25,-99.25 371.75,-99.25 371.75,-99.25 377.75,-99.25 383.75,-105.25 383.75,-111.25 383.75,-111.25 383.75,-123.25 383.75,-123.25 383.75,-129.25 377.75,-135.25 371.75,-135.25"/>
<text xml:space="preserve" text-anchor="middle" x="342" y="-120.5" font-family="Helvetica,sans-Serif" font-size="10.00">Aggregator</text>
<text xml:space="preserve" text-anchor="middle" x="342" y="-107.75" font-family="Helvetica,sans-Serif" font-size="10.00">(gRPC Server)</text>
</g>
<!-- k8s_local -->
<g id="node2" class="node">
<title>k8s_local</title>
<path fill="#64b5f6" stroke="black" d="M104.25,-359.77C104.25,-359.77 35.75,-359.77 35.75,-359.77 29.75,-359.77 23.75,-353.77 23.75,-347.77 23.75,-347.77 23.75,-335.77 23.75,-335.77 23.75,-329.77 29.75,-323.77 35.75,-323.77 35.75,-323.77 104.25,-323.77 104.25,-323.77 110.25,-323.77 116.25,-329.77 116.25,-335.77 116.25,-335.77 116.25,-347.77 116.25,-347.77 116.25,-353.77 110.25,-359.77 104.25,-359.77"/>
<text xml:space="preserve" text-anchor="middle" x="70" y="-345.02" font-family="Helvetica,sans-Serif" font-size="10.00">K8s Pods</text>
<text xml:space="preserve" text-anchor="middle" x="70" y="-332.27" font-family="Helvetica,sans-Serif" font-size="10.00">(via Kustomize)</text>
</g>
<!-- tilt&#45;&gt;k8s_local -->
<!-- compose -->
<g id="node3" class="node">
<title>compose</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M218.25,-448.05C218.25,-448.05 143.75,-448.05 143.75,-448.05 137.75,-448.05 131.75,-442.05 131.75,-436.05 131.75,-436.05 131.75,-424.05 131.75,-424.05 131.75,-418.05 137.75,-412.05 143.75,-412.05 143.75,-412.05 218.25,-412.05 218.25,-412.05 224.25,-412.05 230.25,-418.05 230.25,-424.05 230.25,-424.05 230.25,-436.05 230.25,-436.05 230.25,-442.05 224.25,-448.05 218.25,-448.05"/>
<text xml:space="preserve" text-anchor="middle" x="181" y="-433.3" font-family="Helvetica,sans-Serif" font-size="10.00">Docker Compose</text>
<text xml:space="preserve" text-anchor="middle" x="181" y="-420.55" font-family="Helvetica,sans-Serif" font-size="10.00">(Alternative)</text>
</g>
<!-- compose_ec2 -->
<!-- redis -->
<g id="node4" class="node">
<title>compose_ec2</title>
<path fill="#a5d6a7" stroke="black" d="M744.25,-359.77C744.25,-359.77 669.75,-359.77 669.75,-359.77 663.75,-359.77 657.75,-353.77 657.75,-347.77 657.75,-347.77 657.75,-335.77 657.75,-335.77 657.75,-329.77 663.75,-323.77 669.75,-323.77 669.75,-323.77 744.25,-323.77 744.25,-323.77 750.25,-323.77 756.25,-329.77 756.25,-335.77 756.25,-335.77 756.25,-347.77 756.25,-347.77 756.25,-353.77 750.25,-359.77 744.25,-359.77"/>
<text xml:space="preserve" text-anchor="middle" x="707" y="-345.02" font-family="Helvetica,sans-Serif" font-size="10.00">Docker Compose</text>
<text xml:space="preserve" text-anchor="middle" x="707" y="-332.27" font-family="Helvetica,sans-Serif" font-size="10.00">(All Services)</text>
<title>redis</title>
<path fill="#c8e6c9" stroke="black" d="M361,-48.73C361,-50.53 348.9,-52 334,-52 319.1,-52 307,-50.53 307,-48.73 307,-48.73 307,-19.27 307,-19.27 307,-17.47 319.1,-16 334,-16 348.9,-16 361,-17.47 361,-19.27 361,-19.27 361,-48.73 361,-48.73"/>
<path fill="none" stroke="black" d="M361,-48.73C361,-46.92 348.9,-45.45 334,-45.45 319.1,-45.45 307,-46.92 307,-48.73"/>
<text xml:space="preserve" text-anchor="middle" x="334" y="-30.88" font-family="Helvetica,sans-Serif" font-size="10.00">Redis</text>
</g>
<!-- sqs -->
<g id="node8" class="node">
<title>sqs</title>
<path fill="#ffe082" stroke="black" d="M742.89,-252.28C742.89,-252.28 735.71,-261.4 735.71,-261.4 732.12,-265.96 722.73,-270.52 716.93,-270.52 716.93,-270.52 697.07,-270.52 697.07,-270.52 691.27,-270.52 681.88,-265.96 678.29,-261.4 678.29,-261.4 671.11,-252.28 671.11,-252.28 667.52,-247.72 667.52,-238.61 671.11,-234.05 671.11,-234.05 678.29,-224.93 678.29,-224.93 681.88,-220.37 691.27,-215.81 697.07,-215.81 697.07,-215.81 716.93,-215.81 716.93,-215.81 722.73,-215.81 732.12,-220.37 735.71,-224.93 735.71,-224.93 742.89,-234.05 742.89,-234.05 746.48,-238.61 746.48,-247.72 742.89,-252.28"/>
<text xml:space="preserve" text-anchor="middle" x="707" y="-246.42" font-family="Helvetica,sans-Serif" font-size="10.00">SQS</text>
<text xml:space="preserve" text-anchor="middle" x="707" y="-233.67" font-family="Helvetica,sans-Serif" font-size="10.00">(Buffer)</text>
<!-- aggregator&#45;&gt;redis -->
<g id="edge4" class="edge">
<title>aggregator&#45;&gt;redis</title>
<path fill="none" stroke="black" d="M340.3,-99.02C339.29,-88.75 337.98,-75.45 336.82,-63.64"/>
<polygon fill="black" stroke="black" points="340.33,-63.6 335.87,-53.99 333.37,-64.29 340.33,-63.6"/>
<text xml:space="preserve" text-anchor="middle" x="350.48" y="-72.7" font-family="Helvetica,sans-Serif" font-size="9.00">State</text>
</g>
<!-- compose_ec2&#45;&gt;sqs -->
<g id="edge6" class="edge">
<title>compose_ec2&#45;&gt;sqs</title>
<path fill="none" stroke="black" d="M707,-323.5C707,-311.94 707,-296.26 707,-281.89"/>
<polygon fill="black" stroke="black" points="710.5,-282.27 707,-272.27 703.5,-282.27 710.5,-282.27"/>
<text xml:space="preserve" text-anchor="middle" x="722.38" y="-291.22" font-family="Helvetica,sans-Serif" font-size="9.00">Events</text>
</g>
<!-- nginx -->
<!-- timescaledb -->
<g id="node5" class="node">
<title>nginx</title>
<path fill="#81c784" stroke="black" d="M747.75,-448.05C747.75,-448.05 670.25,-448.05 670.25,-448.05 664.25,-448.05 658.25,-442.05 658.25,-436.05 658.25,-436.05 658.25,-424.05 658.25,-424.05 658.25,-418.05 664.25,-412.05 670.25,-412.05 670.25,-412.05 747.75,-412.05 747.75,-412.05 753.75,-412.05 759.75,-418.05 759.75,-424.05 759.75,-424.05 759.75,-436.05 759.75,-436.05 759.75,-442.05 753.75,-448.05 747.75,-448.05"/>
<text xml:space="preserve" text-anchor="middle" x="709" y="-433.3" font-family="Helvetica,sans-Serif" font-size="10.00">Nginx</text>
<text xml:space="preserve" text-anchor="middle" x="709" y="-420.55" font-family="Helvetica,sans-Serif" font-size="10.00">(SSL Termination)</text>
<title>timescaledb</title>
<path fill="#c8e6c9" stroke="black" d="M459.25,-48.73C459.25,-50.53 441.21,-52 419,-52 396.79,-52 378.75,-50.53 378.75,-48.73 378.75,-48.73 378.75,-19.27 378.75,-19.27 378.75,-17.47 396.79,-16 419,-16 441.21,-16 459.25,-17.47 459.25,-19.27 459.25,-19.27 459.25,-48.73 459.25,-48.73"/>
<path fill="none" stroke="black" d="M459.25,-48.73C459.25,-46.92 441.21,-45.45 419,-45.45 396.79,-45.45 378.75,-46.92 378.75,-48.73"/>
<text xml:space="preserve" text-anchor="middle" x="419" y="-30.88" font-family="Helvetica,sans-Serif" font-size="10.00">TimescaleDB</text>
</g>
<!-- nginx&#45;&gt;compose_ec2 -->
<!-- aggregator&#45;&gt;timescaledb -->
<g id="edge5" class="edge">
<title>nginx&#45;&gt;compose_ec2</title>
<path fill="none" stroke="black" d="M708.6,-411.59C708.33,-400.13 707.98,-384.86 707.67,-371.63"/>
<polygon fill="black" stroke="black" points="711.17,-371.63 707.44,-361.72 704.17,-371.79 711.17,-371.63"/>
<text xml:space="preserve" text-anchor="middle" x="720.43" y="-380.47" font-family="Helvetica,sans-Serif" font-size="9.00">Proxy</text>
<title>aggregator&#45;&gt;timescaledb</title>
<path fill="none" stroke="black" d="M358.33,-99.02C368.88,-87.89 382.79,-73.21 394.63,-60.72"/>
<polygon fill="black" stroke="black" points="397.05,-63.25 401.39,-53.58 391.97,-58.44 397.05,-63.25"/>
<text xml:space="preserve" text-anchor="middle" x="397.11" y="-72.7" font-family="Helvetica,sans-Serif" font-size="9.00">Store</text>
</g>
<!-- lambda_agg -->
<!-- gateway -->
<g id="node2" class="node">
<title>gateway</title>
<path fill="#a5d6a7" stroke="black" d="M383.75,-258.03C383.75,-258.03 348.25,-258.03 348.25,-258.03 342.25,-258.03 336.25,-252.03 336.25,-246.03 336.25,-246.03 336.25,-234.03 336.25,-234.03 336.25,-228.03 342.25,-222.03 348.25,-222.03 348.25,-222.03 383.75,-222.03 383.75,-222.03 389.75,-222.03 395.75,-228.03 395.75,-234.03 395.75,-234.03 395.75,-246.03 395.75,-246.03 395.75,-252.03 389.75,-258.03 383.75,-258.03"/>
<text xml:space="preserve" text-anchor="middle" x="366" y="-243.28" font-family="Helvetica,sans-Serif" font-size="10.00">Gateway</text>
<text xml:space="preserve" text-anchor="middle" x="366" y="-230.53" font-family="Helvetica,sans-Serif" font-size="10.00">(FastAPI)</text>
</g>
<!-- gateway&#45;&gt;aggregator -->
<g id="edge6" class="edge">
<title>gateway&#45;&gt;aggregator</title>
<path fill="none" stroke="black" d="M362.56,-221.73C358.68,-202.17 352.29,-170.05 347.67,-146.77"/>
<polygon fill="black" stroke="black" points="351.11,-146.13 345.73,-137.01 344.24,-147.5 351.11,-146.13"/>
<text xml:space="preserve" text-anchor="middle" x="369.18" y="-184.82" font-family="Helvetica,sans-Serif" font-size="9.00">gRPC</text>
</g>
<!-- edge_relay -->
<g id="node6" class="node">
<title>lambda_agg</title>
<path fill="#aed581" stroke="black" d="M730,-145.31C730,-145.31 684,-145.31 684,-145.31 678,-145.31 672,-139.31 672,-133.31 672,-133.31 672,-121.31 672,-121.31 672,-115.31 678,-109.31 684,-109.31 684,-109.31 730,-109.31 730,-109.31 736,-109.31 742,-115.31 742,-121.31 742,-121.31 742,-133.31 742,-133.31 742,-139.31 736,-145.31 730,-145.31"/>
<text xml:space="preserve" text-anchor="middle" x="707" y="-130.56" font-family="Helvetica,sans-Serif" font-size="10.00">Aggregator</text>
<text xml:space="preserve" text-anchor="middle" x="707" y="-117.81" font-family="Helvetica,sans-Serif" font-size="10.00">Lambda</text>
<title>edge_relay</title>
<path fill="#ce93d8" stroke="black" d="M629.75,-135.25C629.75,-135.25 546.25,-135.25 546.25,-135.25 540.25,-135.25 534.25,-129.25 534.25,-123.25 534.25,-123.25 534.25,-111.25 534.25,-111.25 534.25,-105.25 540.25,-99.25 546.25,-99.25 546.25,-99.25 629.75,-99.25 629.75,-99.25 635.75,-99.25 641.75,-105.25 641.75,-111.25 641.75,-111.25 641.75,-123.25 641.75,-123.25 641.75,-129.25 635.75,-135.25 629.75,-135.25"/>
<text xml:space="preserve" text-anchor="middle" x="588" y="-120.5" font-family="Helvetica,sans-Serif" font-size="10.00">Edge</text>
<text xml:space="preserve" text-anchor="middle" x="588" y="-107.75" font-family="Helvetica,sans-Serif" font-size="10.00">(WebSocket Relay)</text>
</g>
<!-- lambda_compact -->
<g id="node7" class="node">
<title>lambda_compact</title>
<path fill="#9ccc65" stroke="black" d="M822.62,-145.31C822.62,-145.31 777.38,-145.31 777.38,-145.31 771.38,-145.31 765.38,-139.31 765.38,-133.31 765.38,-133.31 765.38,-121.31 765.38,-121.31 765.38,-115.31 771.38,-109.31 777.38,-109.31 777.38,-109.31 822.62,-109.31 822.62,-109.31 828.62,-109.31 834.62,-115.31 834.62,-121.31 834.62,-121.31 834.62,-133.31 834.62,-133.31 834.62,-139.31 828.62,-145.31 822.62,-145.31"/>
<text xml:space="preserve" text-anchor="middle" x="800" y="-130.56" font-family="Helvetica,sans-Serif" font-size="10.00">Compactor</text>
<text xml:space="preserve" text-anchor="middle" x="800" y="-117.81" font-family="Helvetica,sans-Serif" font-size="10.00">Lambda</text>
</g>
<!-- s3 -->
<g id="node9" class="node">
<title>s3</title>
<path fill="#ffe082" stroke="black" d="M829.38,-57.88C829.38,-60.19 816.21,-62.06 800,-62.06 783.79,-62.06 770.62,-60.19 770.62,-57.88 770.62,-57.88 770.62,-20.19 770.62,-20.19 770.62,-17.88 783.79,-16 800,-16 816.21,-16 829.38,-17.88 829.38,-20.19 829.38,-20.19 829.38,-57.88 829.38,-57.88"/>
<path fill="none" stroke="black" d="M829.38,-57.88C829.38,-55.56 816.21,-53.69 800,-53.69 783.79,-53.69 770.62,-55.56 770.62,-57.88"/>
<text xml:space="preserve" text-anchor="middle" x="800" y="-42.28" font-family="Helvetica,sans-Serif" font-size="10.00">S3</text>
<text xml:space="preserve" text-anchor="middle" x="800" y="-29.53" font-family="Helvetica,sans-Serif" font-size="10.00">(Backup)</text>
</g>
<!-- lambda_compact&#45;&gt;s3 -->
<g id="edge8" class="edge">
<title>lambda_compact&#45;&gt;s3</title>
<path fill="none" stroke="black" d="M800,-108.85C800,-98.81 800,-85.84 800,-73.88"/>
<polygon fill="black" stroke="black" points="803.5,-73.9 800,-63.9 796.5,-73.9 803.5,-73.9"/>
<text xml:space="preserve" text-anchor="middle" x="816.88" y="-82.76" font-family="Helvetica,sans-Serif" font-size="9.00">Archive</text>
</g>
<!-- sqs&#45;&gt;lambda_agg -->
<!-- gateway&#45;&gt;edge_relay -->
<g id="edge7" class="edge">
<title>sqs&#45;&gt;lambda_agg</title>
<path fill="none" stroke="black" d="M707,-215.47C707,-197.96 707,-175.06 707,-157.13"/>
<polygon fill="black" stroke="black" points="710.5,-157.15 707,-147.15 703.5,-157.15 710.5,-157.15"/>
<text xml:space="preserve" text-anchor="middle" x="722.75" y="-189.26" font-family="Helvetica,sans-Serif" font-size="9.00">Trigger</text>
<title>gateway&#45;&gt;edge_relay</title>
<path fill="none" stroke="black" d="M384.8,-221.53C401.73,-206.88 428,-186.76 454.75,-176.5 482.85,-165.73 494.1,-179.79 522,-168.5 536.54,-162.62 550.65,-152.65 562.07,-143.13"/>
<polygon fill="black" stroke="black" points="564.23,-145.89 569.46,-136.68 559.62,-140.62 564.23,-145.89"/>
<text xml:space="preserve" text-anchor="middle" x="479.88" y="-190.45" font-family="Helvetica,sans-Serif" font-size="9.00">WebSocket</text>
<text xml:space="preserve" text-anchor="middle" x="479.88" y="-179.2" font-family="Helvetica,sans-Serif" font-size="9.00">Forward</text>
</g>
<!-- alerts -->
<g id="node3" class="node">
<title>alerts</title>
<path fill="#a5d6a7" stroke="black" d="M489,-258.03C489,-258.03 459,-258.03 459,-258.03 453,-258.03 447,-252.03 447,-246.03 447,-246.03 447,-234.03 447,-234.03 447,-228.03 453,-222.03 459,-222.03 459,-222.03 489,-222.03 489,-222.03 495,-222.03 501,-228.03 501,-234.03 501,-234.03 501,-246.03 501,-246.03 501,-252.03 495,-258.03 489,-258.03"/>
<text xml:space="preserve" text-anchor="middle" x="474" y="-243.28" font-family="Helvetica,sans-Serif" font-size="10.00">Alerts</text>
<text xml:space="preserve" text-anchor="middle" x="474" y="-230.53" font-family="Helvetica,sans-Serif" font-size="10.00">Service</text>
</g>
<!-- browser -->
<g id="node12" class="node">
<title>browser</title>
<path fill="#fff3e0" stroke="black" d="M614,-52C614,-52 562,-52 562,-52 556,-52 550,-46 550,-40 550,-40 550,-28 550,-28 550,-22 556,-16 562,-16 562,-16 614,-16 614,-16 620,-16 626,-22 626,-28 626,-28 626,-40 626,-40 626,-46 620,-52 614,-52"/>
<text xml:space="preserve" text-anchor="middle" x="588" y="-37.25" font-family="Helvetica,sans-Serif" font-size="10.00">Browser</text>
<text xml:space="preserve" text-anchor="middle" x="588" y="-24.5" font-family="Helvetica,sans-Serif" font-size="10.00">(Dashboard)</text>
</g>
<!-- edge_relay&#45;&gt;browser -->
<g id="edge8" class="edge">
<title>edge_relay&#45;&gt;browser</title>
<path fill="none" stroke="black" d="M588,-87.54C588,-79.86 588,-71.56 588,-63.88"/>
<polygon fill="black" stroke="black" points="584.5,-87.51 588,-97.51 591.5,-87.51 584.5,-87.51"/>
<polygon fill="black" stroke="black" points="591.5,-64 588,-54 584.5,-64 591.5,-64"/>
<text xml:space="preserve" text-anchor="middle" x="613.12" y="-72.7" font-family="Helvetica,sans-Serif" font-size="9.00">WebSocket</text>
</g>
<!-- woodpecker -->
<g id="node10" class="node">
<g id="node7" class="node">
<title>woodpecker</title>
<path fill="#ce93d8" stroke="black" d="M330,-587.8C330,-587.8 266,-587.8 266,-587.8 260,-587.8 254,-581.8 254,-575.8 254,-575.8 254,-563.8 254,-563.8 254,-557.8 260,-551.8 266,-551.8 266,-551.8 330,-551.8 330,-551.8 336,-551.8 342,-557.8 342,-563.8 342,-563.8 342,-575.8 342,-575.8 342,-581.8 336,-587.8 330,-587.8"/>
<text xml:space="preserve" text-anchor="middle" x="298" y="-566.67" font-family="Helvetica,sans-Serif" font-size="10.00">Woodpecker CI</text>
<path fill="#90caf9" stroke="black" d="M615,-364.53C615,-364.53 551,-364.53 551,-364.53 545,-364.53 539,-358.53 539,-352.53 539,-352.53 539,-340.53 539,-340.53 539,-334.53 545,-328.53 551,-328.53 551,-328.53 615,-328.53 615,-328.53 621,-328.53 627,-334.53 627,-340.53 627,-340.53 627,-352.53 627,-352.53 627,-358.53 621,-364.53 615,-364.53"/>
<text xml:space="preserve" text-anchor="middle" x="583" y="-343.41" font-family="Helvetica,sans-Serif" font-size="10.00">Woodpecker CI</text>
</g>
<!-- registry -->
<g id="node11" class="node">
<g id="node8" class="node">
<title>registry</title>
<path fill="#ba68c8" stroke="black" d="M329.62,-448.89C329.62,-451.2 315.45,-453.08 298,-453.08 280.55,-453.08 266.38,-451.2 266.38,-448.89 266.38,-448.89 266.38,-411.21 266.38,-411.21 266.38,-408.89 280.55,-407.02 298,-407.02 315.45,-407.02 329.62,-408.89 329.62,-411.21 329.62,-411.21 329.62,-448.89 329.62,-448.89"/>
<path fill="none" stroke="black" d="M329.62,-448.89C329.62,-446.58 315.45,-444.71 298,-444.71 280.55,-444.71 266.38,-446.58 266.38,-448.89"/>
<text xml:space="preserve" text-anchor="middle" x="298" y="-433.3" font-family="Helvetica,sans-Serif" font-size="10.00">Container</text>
<text xml:space="preserve" text-anchor="middle" x="298" y="-420.55" font-family="Helvetica,sans-Serif" font-size="10.00">Registry</text>
<path fill="#64b5f6" stroke="black" d="M614.62,-258.88C614.62,-261.19 600.45,-263.06 583,-263.06 565.55,-263.06 551.38,-261.19 551.38,-258.88 551.38,-258.88 551.38,-221.19 551.38,-221.19 551.38,-218.88 565.55,-217 583,-217 600.45,-217 614.62,-218.88 614.62,-221.19 614.62,-221.19 614.62,-258.88 614.62,-258.88"/>
<path fill="none" stroke="black" d="M614.62,-258.88C614.62,-256.56 600.45,-254.69 583,-254.69 565.55,-254.69 551.38,-256.56 551.38,-258.88"/>
<text xml:space="preserve" text-anchor="middle" x="583" y="-243.28" font-family="Helvetica,sans-Serif" font-size="10.00">Container</text>
<text xml:space="preserve" text-anchor="middle" x="583" y="-230.53" font-family="Helvetica,sans-Serif" font-size="10.00">Registry</text>
</g>
<!-- woodpecker&#45;&gt;registry -->
<g id="edge2" class="edge">
<g id="edge9" class="edge">
<title>woodpecker&#45;&gt;registry</title>
<path fill="none" stroke="black" d="M298,-551.35C298,-529.66 298,-492.15 298,-464.77"/>
<polygon fill="black" stroke="black" points="301.5,-464.88 298,-454.88 294.5,-464.88 301.5,-464.88"/>
<text xml:space="preserve" text-anchor="middle" x="308.88" y="-525.25" font-family="Helvetica,sans-Serif" font-size="9.00">Push</text>
<path fill="none" stroke="black" d="M583,-328.28C583,-313.79 583,-292.67 583,-274.84"/>
<polygon fill="black" stroke="black" points="586.5,-274.93 583,-264.93 579.5,-274.93 586.5,-274.93"/>
<text xml:space="preserve" text-anchor="middle" x="593.88" y="-301.98" font-family="Helvetica,sans-Serif" font-size="9.00">Push</text>
</g>
<!-- registry&#45;&gt;k8s_local -->
<g id="edge4" class="edge">
<title>registry&#45;&gt;k8s_local</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M265.9,-410.59C258.2,-406.51 249.91,-402.4 242,-399.02 204.6,-383.02 161.03,-368.81 127.1,-358.68"/>
<polygon fill="black" stroke="black" points="128.47,-355.44 117.89,-355.97 126.49,-362.15 128.47,-355.44"/>
<text xml:space="preserve" text-anchor="middle" x="222.42" y="-380.47" font-family="Helvetica,sans-Serif" font-size="9.00">Pull</text>
<!-- registry&#45;&gt;aggregator -->
<g id="edge11" class="edge">
<title>registry&#45;&gt;aggregator</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M560.24,-217.39C550.98,-209.15 539.58,-199.63 527.42,-190.91"/>
<polygon fill="black" stroke="black" points="529.47,-188.06 519.24,-185.28 525.5,-193.83 529.47,-188.06"/>
<text xml:space="preserve" text-anchor="middle" x="544.69" y="-184.82" font-family="Helvetica,sans-Serif" font-size="9.00">Pull</text>
</g>
<!-- registry&#45;&gt;compose_ec2 -->
<g id="edge3" class="edge">
<title>registry&#45;&gt;compose_ec2</title>
<path fill="none" stroke="black" d="M329.84,-409.93C337.55,-405.88 345.91,-401.95 354,-399.02 452.44,-363.35 574.46,-350.26 646.22,-345.49"/>
<polygon fill="black" stroke="black" points="646.02,-349.01 655.78,-344.88 645.58,-342.02 646.02,-349.01"/>
<text xml:space="preserve" text-anchor="middle" x="427.09" y="-380.47" font-family="Helvetica,sans-Serif" font-size="9.00">Pull</text>
<!-- registry&#45;&gt;edge_relay -->
<g id="edge10" class="edge">
<title>registry&#45;&gt;edge_relay</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M583.93,-216.6C584.74,-196.89 585.94,-168.11 586.82,-146.77"/>
<polygon fill="black" stroke="black" points="590.31,-147.14 587.22,-137 583.31,-146.85 590.31,-147.14"/>
<text xml:space="preserve" text-anchor="middle" x="593.38" y="-184.82" font-family="Helvetica,sans-Serif" font-size="9.00">Pull</text>
</g>
<!-- coll1 -->
<g id="node12" class="node">
<g id="node9" class="node">
<title>coll1</title>
<path fill="#ffccbc" stroke="black" d="M521.88,-448.05C521.88,-448.05 472.12,-448.05 472.12,-448.05 466.12,-448.05 460.12,-442.05 460.12,-436.05 460.12,-436.05 460.12,-424.05 460.12,-424.05 460.12,-418.05 466.12,-412.05 472.12,-412.05 472.12,-412.05 521.88,-412.05 521.88,-412.05 527.88,-412.05 533.88,-418.05 533.88,-424.05 533.88,-424.05 533.88,-436.05 533.88,-436.05 533.88,-442.05 527.88,-448.05 521.88,-448.05"/>
<text xml:space="preserve" text-anchor="middle" x="497" y="-433.3" font-family="Helvetica,sans-Serif" font-size="10.00">Collector</text>
<text xml:space="preserve" text-anchor="middle" x="497" y="-420.55" font-family="Helvetica,sans-Serif" font-size="10.00">(Machine 1)</text>
<path fill="#ffccbc" stroke="black" d="M263.88,-258.03C263.88,-258.03 214.12,-258.03 214.12,-258.03 208.12,-258.03 202.12,-252.03 202.12,-246.03 202.12,-246.03 202.12,-234.03 202.12,-234.03 202.12,-228.03 208.12,-222.03 214.12,-222.03 214.12,-222.03 263.88,-222.03 263.88,-222.03 269.88,-222.03 275.88,-228.03 275.88,-234.03 275.88,-234.03 275.88,-246.03 275.88,-246.03 275.88,-252.03 269.88,-258.03 263.88,-258.03"/>
<text xml:space="preserve" text-anchor="middle" x="239" y="-243.28" font-family="Helvetica,sans-Serif" font-size="10.00">Collector</text>
<text xml:space="preserve" text-anchor="middle" x="239" y="-230.53" font-family="Helvetica,sans-Serif" font-size="10.00">(Machine 1)</text>
</g>
<!-- coll1&#45;&gt;compose_ec2 -->
<g id="edge9" class="edge">
<title>coll1&#45;&gt;compose_ec2</title>
<path fill="none" stroke="black" d="M521.16,-411.67C528.02,-407.19 535.63,-402.62 543,-399.02 576.02,-382.89 614.85,-369.35 646.44,-359.6"/>
<polygon fill="black" stroke="black" points="640.37,-365.52 648.58,-358.82 637.98,-358.94 640.37,-365.52"/>
<text xml:space="preserve" text-anchor="middle" x="602.75" y="-380.47" font-family="Helvetica,sans-Serif" font-size="9.00">gRPC</text>
<!-- coll1&#45;&gt;aggregator -->
<g id="edge1" class="edge">
<title>coll1&#45;&gt;aggregator</title>
<path fill="none" stroke="black" d="M253.76,-221.73C271.04,-201.46 299.85,-167.67 319.83,-144.25"/>
<polygon fill="black" stroke="black" points="322.46,-146.55 326.29,-136.67 317.14,-142.01 322.46,-146.55"/>
<text xml:space="preserve" text-anchor="middle" x="302.12" y="-184.82" font-family="Helvetica,sans-Serif" font-size="9.00">gRPC</text>
</g>
<!-- coll2 -->
<g id="node13" class="node">
<g id="node10" class="node">
<title>coll2</title>
<path fill="#ffccbc" stroke="black" d="M613.88,-448.05C613.88,-448.05 564.12,-448.05 564.12,-448.05 558.12,-448.05 552.12,-442.05 552.12,-436.05 552.12,-436.05 552.12,-424.05 552.12,-424.05 552.12,-418.05 558.12,-412.05 564.12,-412.05 564.12,-412.05 613.88,-412.05 613.88,-412.05 619.88,-412.05 625.88,-418.05 625.88,-424.05 625.88,-424.05 625.88,-436.05 625.88,-436.05 625.88,-442.05 619.88,-448.05 613.88,-448.05"/>
<text xml:space="preserve" text-anchor="middle" x="589" y="-433.3" font-family="Helvetica,sans-Serif" font-size="10.00">Collector</text>
<text xml:space="preserve" text-anchor="middle" x="589" y="-420.55" font-family="Helvetica,sans-Serif" font-size="10.00">(Machine 2)</text>
<path fill="#ffccbc" stroke="black" d="M77.88,-258.03C77.88,-258.03 28.12,-258.03 28.12,-258.03 22.12,-258.03 16.12,-252.03 16.12,-246.03 16.12,-246.03 16.12,-234.03 16.12,-234.03 16.12,-228.03 22.12,-222.03 28.12,-222.03 28.12,-222.03 77.88,-222.03 77.88,-222.03 83.88,-222.03 89.88,-228.03 89.88,-234.03 89.88,-234.03 89.88,-246.03 89.88,-246.03 89.88,-252.03 83.88,-258.03 77.88,-258.03"/>
<text xml:space="preserve" text-anchor="middle" x="53" y="-243.28" font-family="Helvetica,sans-Serif" font-size="10.00">Collector</text>
<text xml:space="preserve" text-anchor="middle" x="53" y="-230.53" font-family="Helvetica,sans-Serif" font-size="10.00">(Machine 2)</text>
</g>
<!-- coll2&#45;&gt;compose_ec2 -->
<g id="edge10" class="edge">
<title>coll2&#45;&gt;compose_ec2</title>
<path fill="none" stroke="black" d="M612.88,-411.59C621.13,-405.55 630.83,-398.47 640.8,-391.17"/>
<polygon fill="black" stroke="black" points="642.77,-394.07 648.78,-385.34 638.64,-388.41 642.77,-394.07"/>
<text xml:space="preserve" text-anchor="middle" x="670.19" y="-380.47" font-family="Helvetica,sans-Serif" font-size="9.00">gRPC</text>
<!-- coll2&#45;&gt;aggregator -->
<g id="edge2" class="edge">
<title>coll2&#45;&gt;aggregator</title>
<path fill="none" stroke="black" d="M77.23,-221.79C84.09,-217.31 91.68,-212.7 99,-209 161.95,-177.15 238.85,-150.3 289.05,-134.25"/>
<polygon fill="black" stroke="black" points="290.05,-137.61 298.53,-131.26 287.94,-130.94 290.05,-137.61"/>
<text xml:space="preserve" text-anchor="middle" x="182.04" y="-184.82" font-family="Helvetica,sans-Serif" font-size="9.00">gRPC</text>
</g>
<!-- coll3 -->
<g id="node14" class="node">
<g id="node11" class="node">
<title>coll3</title>
<path fill="#ffccbc" stroke="black" d="M429.62,-448.05C429.62,-448.05 378.38,-448.05 378.38,-448.05 372.38,-448.05 366.38,-442.05 366.38,-436.05 366.38,-436.05 366.38,-424.05 366.38,-424.05 366.38,-418.05 372.38,-412.05 378.38,-412.05 378.38,-412.05 429.62,-412.05 429.62,-412.05 435.62,-412.05 441.62,-418.05 441.62,-424.05 441.62,-424.05 441.62,-436.05 441.62,-436.05 441.62,-442.05 435.62,-448.05 429.62,-448.05"/>
<text xml:space="preserve" text-anchor="middle" x="404" y="-433.3" font-family="Helvetica,sans-Serif" font-size="10.00">Collector</text>
<text xml:space="preserve" text-anchor="middle" x="404" y="-420.55" font-family="Helvetica,sans-Serif" font-size="10.00">(Machine N)</text>
<path fill="#ffccbc" stroke="black" d="M171.62,-258.03C171.62,-258.03 120.38,-258.03 120.38,-258.03 114.38,-258.03 108.38,-252.03 108.38,-246.03 108.38,-246.03 108.38,-234.03 108.38,-234.03 108.38,-228.03 114.38,-222.03 120.38,-222.03 120.38,-222.03 171.62,-222.03 171.62,-222.03 177.62,-222.03 183.62,-228.03 183.62,-234.03 183.62,-234.03 183.62,-246.03 183.62,-246.03 183.62,-252.03 177.62,-258.03 171.62,-258.03"/>
<text xml:space="preserve" text-anchor="middle" x="146" y="-243.28" font-family="Helvetica,sans-Serif" font-size="10.00">Collector</text>
<text xml:space="preserve" text-anchor="middle" x="146" y="-230.53" font-family="Helvetica,sans-Serif" font-size="10.00">(Machine N)</text>
</g>
<!-- coll3&#45;&gt;compose_ec2 -->
<g id="edge11" class="edge">
<title>coll3&#45;&gt;compose_ec2</title>
<path fill="none" stroke="black" d="M427.53,-411.82C434.78,-407.12 442.97,-402.41 451,-399.02 514.86,-372.07 593.36,-357.28 646.47,-349.71"/>
<polygon fill="black" stroke="black" points="639.16,-354.39 648.5,-349.4 638.08,-347.48 639.16,-354.39"/>
<text xml:space="preserve" text-anchor="middle" x="516.54" y="-380.47" font-family="Helvetica,sans-Serif" font-size="9.00">gRPC</text>
<!-- coll3&#45;&gt;aggregator -->
<g id="edge3" class="edge">
<title>coll3&#45;&gt;aggregator</title>
<path fill="none" stroke="black" d="M172.78,-221.78C179.37,-217.57 186.43,-213.1 193,-209 230.29,-185.74 273.17,-159.69 303.33,-141.49"/>
<polygon fill="black" stroke="black" points="304.99,-144.58 311.75,-136.42 301.38,-138.58 304.99,-144.58"/>
<text xml:space="preserve" text-anchor="middle" x="253.61" y="-184.82" font-family="Helvetica,sans-Serif" font-size="9.00">gRPC</text>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -86,8 +86,7 @@ main {
border-radius: 4px;
padding: 1rem;
margin-bottom: 1rem;
overflow: auto;
max-height: 400px;
overflow: visible;
}
.graph-preview img {

View File

@@ -231,6 +231,31 @@ machine_metrics_cache[machine_id].update(incoming_metrics)
New metrics merge with existing. The broadcast includes the full merged state.
### Edge Relay - Public Dashboard Without the Cost
The full stack (aggregator, Redis, TimescaleDB) runs on local hardware. But the dashboard needs to be publicly accessible at `sysmonstm.mcrn.ar`. Running the full stack on AWS would be expensive and unnecessary.
The solution is an edge relay (`ctrl/edge/edge.py`). It's a minimal FastAPI app that does one thing: relay WebSocket messages. The gateway forwards metrics to the edge via WebSocket, and the edge broadcasts them to connected browsers:
```python
# Gateway forwards to edge when EDGE_URL is configured
async def forward_to_edge(data: dict):
if edge_ws:
await edge_ws.send(json.dumps(data))
```
The edge receives these and broadcasts to all dashboard viewers:
```python
@app.websocket("/ws")
async def dashboard_ws(websocket: WebSocket):
await websocket.accept()
clients.add(websocket)
# ... broadcasts incoming metrics to all clients
```
This keeps heavy processing (gRPC, storage, event evaluation) on local hardware and puts only a lightweight relay in the cloud. The AWS instance has no databases, no gRPC, no storage — just WebSocket in, WebSocket out.
## Phase 3: Alerts - Adding Intelligence
The alerts service subscribes to metric events and evaluates them against rules.
@@ -402,7 +427,8 @@ Set `COLLECTOR_AGGREGATOR_URL=192.168.1.100:50051` and it overrides the default.
| Redis events | `shared/events/redis_pubsub.py` | Redis Pub/Sub implementation |
| Configuration | `shared/config.py` | Pydantic settings for all services |
| DB initialization | `scripts/init-db.sql` | TimescaleDB schema, hypertables |
| Docker setup | `docker-compose.yml` | Full stack orchestration |
| Edge relay | `ctrl/edge/edge.py` | WebSocket relay for AWS dashboard |
| Docker setup | `ctrl/dev/docker-compose.yml` | Full stack orchestration |
## Running It

View File

@@ -80,39 +80,6 @@
</header>
<main>
<!-- Explainer Articles -->
<section class="nav-section">
<h2>Explainer Articles</h2>
<div class="doc-links">
<a
href="explainer/viewer.html?file=sysmonstm-from-start-to-finish.md"
class="doc-link"
>
<h3>sysmonstm: From Start to Finish</h3>
<p>
The complete story of building this monitoring
platform. Architecture decisions, trade-offs, and
code walkthrough from MVP to production patterns.
</p>
<span class="tag">Article</span>
</a>
<a
href="explainer/viewer.html?file=other-applications.md"
class="doc-link"
>
<h3>Same Patterns, Different Domains</h3>
<p>
How the same architecture applies to payment
processing systems and the Deskmeter workspace
timer. Domain mapping and implementation paths.
</p>
<span class="tag">Article</span>
</a>
</div>
</section>
<hr class="section-divider" />
<!-- Architecture Diagrams -->
<section class="graph-section" id="overview">
<div class="graph-header-row">
@@ -155,6 +122,11 @@
<strong>Alerts</strong>: Subscribes to events,
evaluates thresholds, triggers actions
</li>
<li>
<strong>Edge</strong>: Lightweight WebSocket
relay on AWS, serves public dashboard at
sysmonstm.mcrn.ar
</li>
</ul>
</div>
</section>
@@ -245,16 +217,17 @@
<h4>Environments</h4>
<ul>
<li>
<strong>Local Dev</strong>: Kind + Tilt for K8s, or
Docker Compose
<strong>Local</strong>: Docker Compose with
aggregator, gateway, Redis, TimescaleDB, alerts
</li>
<li>
<strong>Demo (EC2)</strong>: Docker Compose on
t2.small at sysmonstm.mcrn.ar
<strong>Edge (AWS)</strong>: Lightweight
WebSocket relay at sysmonstm.mcrn.ar, receives
forwarded metrics from local gateway
</li>
<li>
<strong>Lambda Pipeline</strong>: SQS-triggered
aggregation for data processing experience
<strong>Collectors</strong>: Run on remote
machines, stream to local aggregator via gRPC
</li>
</ul>
</div>
@@ -301,7 +274,7 @@
<hr class="section-divider" />
<section class="findings-section">
<h2>Interview Talking Points</h2>
<h2>Key Design Decisions</h2>
<div class="findings-grid">
<article class="finding-card">
<h3>Domain Mapping</h3>
@@ -366,16 +339,13 @@
<h3>Infrastructure</h3>
<ul>
<li>Docker</li>
<li>Kubernetes</li>
<li>Kind + Tilt</li>
<li>Terraform</li>
<li>Docker Compose</li>
</ul>
</div>
<div class="tech-column">
<h3>CI/CD</h3>
<ul>
<li>Woodpecker CI</li>
<li>Kustomize</li>
<li>Container Registry</li>
</ul>
</div>
@@ -386,7 +356,7 @@
<footer>
<p>System Monitoring Platform - Documentation</p>
<p class="date">
Generated: <time datetime="2025-12-31">December 2025</time>
Generated: <time datetime="2026-03-16">March 2026</time>
</p>
</footer>
</body>