Compare commits

..

17 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
buenosairesam
754d3e55fb Use ctrl/edge/deploy.sh for CI-agnostic deployment 2026-01-26 17:34:53 -03:00
buenosairesam
761bca20b0 pipeline updates 2026-01-26 12:38:52 -03:00
buenosairesam
9ddcb68131 fix proto import in docker
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-22 18:45:46 -03:00
buenosairesam
f854d6d399 sysmonstm setup
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-22 18:31:47 -03:00
buenosairesam
0cd8d1516f sysmonstm setup
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-01-22 18:30:30 -03:00
buenosairesam
82c4551e71 simple is better 2026-01-22 16:22:15 -03:00
buenosairesam
dc3518f138 new three layer deployment 2026-01-22 12:55:50 -03:00
buenosairesam
174bc15368 add readme 2026-01-22 06:02:01 -03:00
36 changed files with 1718 additions and 1361 deletions

4
.gitignore vendored
View File

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

View File

@@ -1,17 +0,0 @@
# sysmonstm Pipeline
when:
- event: push
- event: manual
steps:
- name: build
image: plugins/docker
settings:
repo: registry.mcrn.ar/sysmonstm
registry: registry.mcrn.ar
tags:
- latest
- ${CI_COMMIT_SHA:0:7}
dockerfile: ctrl/standalone/Dockerfile
context: ctrl/standalone

25
.woodpecker/build.yml Normal file
View File

@@ -0,0 +1,25 @@
# sysmonstm Pipeline
when:
- event: push
- event: manual
steps:
- name: build
image: plugins/docker
settings:
repo: registry.mcrn.ar/sysmonstm/edge
registry: registry.mcrn.ar
tags:
- latest
- ${CI_COMMIT_SHA:0:7}
dockerfile: ctrl/edge/Dockerfile
context: ctrl/edge
- name: deploy
image: docker:24-cli
commands:
- 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

533
CLAUDE.md
View File

@@ -2,491 +2,90 @@
## Project Overview ## Project Overview
A real-time system monitoring platform that streams metrics from multiple machines to a central hub with live web dashboard. Built to demonstrate production microservices patterns (gRPC, FastAPI, streaming, event-driven architecture) while solving a real problem: monitoring development infrastructure across multiple machines. A real-time system monitoring platform that streams metrics from multiple machines to a central hub with live web dashboard. Built to demonstrate production microservices patterns (gRPC, FastAPI, streaming, event-driven architecture).
**Primary Goal:** Interview demonstration project for Python Microservices Engineer position **Primary Goal:** Portfolio project demonstrating real-time streaming with gRPC
**Secondary Goal:** Actually useful tool for managing multi-machine development environment **Status:** Working, deployed at sysmonstm.mcrn.ar
**Time Investment:** Phased approach - MVP in weekend, polish over 2-3 weeks
## Why This Project
**Interview Alignment:**
- Demonstrates gRPC-based microservices architecture (core requirement)
- Shows streaming patterns (server-side and bidirectional)
- Real-time data aggregation and processing
- Alert/threshold monitoring (maps to fraud detection)
- Event-driven patterns
- Multiple data sources requiring normalization (maps to multiple payment processors)
**Personal Utility:**
- Monitors existing multi-machine dev setup
- Dashboard stays open, provides real value
- Solves actual pain point
- Will continue running post-interview
**Domain Mapping for Interview:**
- Machine = Payment Processor
- Metrics Stream = Transaction Stream
- Resource Thresholds = Fraud/Limit Detection
- Alert System = Risk Management
- Aggregation Service = Payment Processing Hub
## Technical Stack
### Core Technologies (Must Use - From JD)
- **Python 3.11+** - Primary language
- **FastAPI** - Web gateway, REST endpoints, WebSocket streaming
- **gRPC** - Inter-service communication, metric streaming
- **PostgreSQL/TimescaleDB** - Time-series historical data
- **Redis** - Current state, caching, alert rules
- **Docker Compose** - Orchestration
### Supporting Technologies
- **Protocol Buffers** - gRPC message definitions
- **WebSockets** - Browser streaming
- **htmx + Alpine.js** - Lightweight reactive frontend (avoid heavy SPA)
- **Chart.js or Apache ECharts** - Real-time graphs
- **asyncio** - Async patterns throughout
### Development Tools
- **grpcio & grpcio-tools** - Python gRPC
- **psutil** - System metrics collection
- **uvicorn** - FastAPI server
- **pytest** - Testing
- **docker-compose** - Local orchestration
## Architecture ## Architecture
``` ```
┌─────────────────────────────────────────────────────────────┐ ┌─────────────┐ ┌─────────────────────────────────────┐ ┌─────────────┐
Browser │ Collector │────▶│ Aggregator + Gateway + Redis + TS │────▶│ Edge │────▶ Browser
┌──────────────────────────────────────────────────────┐ │ (mcrn) │gRPC │ (LOCAL) │ WS │ (AWS) │ WS
Dashboard (htmx + Alpine.js + WebSockets) │ │ └─────────────┘ └─────────────────────────────────────┘ └─────────────┘
│ └──────────────────────────────────────────────────────┘ ┌─────────────┐
└────────────────────────┬────────────────────────────────────┘ │ Collector │────────────────────┘
│ WebSocket │ (nfrt) │gRPC
└─────────────┘
┌─────────────────────────────────────────────────────────────┐ ```
│ Web Gateway Service │
│ (FastAPI + WebSockets) │ - **Collectors** (`services/collector/`) - gRPC clients on each monitored machine
│ - Serves dashboard │ - **Aggregator** (`services/aggregator/`) - gRPC server, stores in Redis/TimescaleDB
│ - Streams updates to browser │ - **Gateway** (`services/gateway/`) - FastAPI, bridges gRPC to WebSocket, forwards to edge
│ - REST API for historical queries - **Edge** (`ctrl/edge/`) - Simple WebSocket relay for AWS, serves public dashboard
└────────────────────────┬────────────────────────────────────┘
│ gRPC ## Directory Structure
┌─────────────────────────────────────────────────────────────┐ ```
│ Aggregator Service (gRPC) │ sms/
│ - Receives metric streams from all collectors │ ├── services/ # gRPC-based microservices
- Normalizes data from different sources │ ├── collector/ # gRPC client, streams to aggregator
- Enriches with machine context │ ├── aggregator/ # gRPC server, stores in Redis/TimescaleDB
- Publishes to event stream │ ├── gateway/ # FastAPI, WebSocket, forwards to edge
- Checks alert thresholds │ └── alerts/ # Event subscriber for threshold alerts
└─────┬───────────────────────────────────┬───────────────────┘
│ │
│ Stores │ Publishes events
▼ ▼
┌──────────────┐ ┌────────────────┐
│ TimescaleDB │ │ Event Stream │
│ (historical)│ │ (Redis Pub/Sub│
└──────────────┘ │ or RabbitMQ) │
└────────┬───────┘
┌──────────────┐ │
│ Redis │ │ Subscribes
│ (current │◄───────────────────────────┘
│ state) │ │
└──────────────┘ ▼
┌────────────────┐
▲ │ Alert Service │
│ │ - Processes │
│ │ events │
│ gRPC Streaming │ - Triggers │
│ │ actions │
┌─────┴────────────────────────────┴────────────────┘
│ Multiple Collector Services (one per machine) ├── ctrl/ # Deployment configurations
┌───────────────────────────────────────┐ ├── dev/ # Full stack docker-compose
│ Metrics Collector (gRPC Client) │ └── edge/ # Cloud dashboard (AWS)
│ │ - Gathers system metrics (psutil) │
│ │ - Streams to Aggregator via gRPC │
│ │ - CPU, Memory, Disk, Network │
│ │ - Process list │
│ │ - Docker container stats (optional) │
│ └───────────────────────────────────────┘
──► Machine 1, Machine 2, Machine 3, ... ── proto/ # Protocol Buffer definitions
├── shared/ # Shared Python modules (config, logging, events)
└── web/ # Dashboard templates and static files
``` ```
## Implementation Phases ## Running
### Phase 1: MVP - Core Streaming (Weekend - 8-12 hours) ### Local Development
```bash
**Goal:** Prove the gRPC streaming works end-to-end docker compose up
**Deliverables:**
1. Metrics Collector Service (gRPC client)
- Collects CPU, memory, disk on localhost
- Streams to aggregator every 5 seconds
2. Aggregator Service (gRPC server)
- Receives metric stream
- Stores current state in Redis
- Logs to console
3. Proto definitions for metric messages
4. Docker Compose setup
**Success Criteria:**
- Run collector, see metrics flowing to aggregator
- Redis contains current state
- Can query Redis manually for latest metrics
### Phase 2: Web Dashboard (1 week)
**Goal:** Make it visible and useful
**Deliverables:**
1. Web Gateway Service (FastAPI)
- WebSocket endpoint for streaming
- REST endpoints for current/historical data
2. Dashboard UI
- Real-time CPU/Memory graphs per machine
- Current state table
- Simple, clean design
3. WebSocket bridge (Gateway ↔ Aggregator)
4. TimescaleDB integration
- Store historical metrics
- Query endpoints for time ranges
**Success Criteria:**
- Open dashboard, see live graphs updating
- Graphs show last hour of data
- Multiple machines displayed separately
### Phase 3: Alerts & Intelligence (1 week)
**Goal:** Add decision-making layer (interview focus)
**Deliverables:**
1. Alert Service
- Subscribes to event stream
- Evaluates threshold rules
- Triggers notifications
2. Configuration Service (gRPC)
- Dynamic threshold management
- Alert rule CRUD
- Stored in PostgreSQL
3. Event Stream implementation (Redis Pub/Sub or RabbitMQ)
4. Enhanced dashboard
- Alert indicators
- Alert history
- Threshold configuration UI
**Success Criteria:**
- Set CPU threshold at 80%
- Generate load (stress-ng)
- See alert trigger in dashboard
- Alert logged to database
### Phase 4: Interview Polish (Final week)
**Goal:** Demo-ready, production patterns visible
**Deliverables:**
1. Observability
- OpenTelemetry tracing (optional)
- Structured logging
- Health check endpoints
2. "Synthetic Transactions"
- Simulate business operations through system
- Track end-to-end latency
- Maps directly to payment processing demo
3. Documentation
- Architecture diagram
- Service interaction flows
- Deployment guide
4. Demo script
- Story to walk through
- Key talking points
- Domain mapping explanations
**Success Criteria:**
- Can deploy entire stack with one command
- Can explain every service's role
- Can map architecture to payment processing
- Demo runs smoothly without hiccups
## Key Technical Patterns to Demonstrate
### 1. gRPC Streaming Patterns
**Server-Side Streaming:**
```python
# Collector streams metrics to aggregator
service MetricsService {
rpc StreamMetrics(MetricsRequest) returns (stream Metric) {}
}
``` ```
**Bidirectional Streaming:** ### With Edge Forwarding (to AWS)
```python ```bash
# Two-way communication between services EDGE_URL=wss://sysmonstm.mcrn.ar/ws docker compose up
service ControlService {
rpc ManageStream(stream Command) returns (stream Response) {}
}
``` ```
### 2. Service Communication Patterns ### Collector on Remote Machine
```bash
- **Synchronous (gRPC):** Query current state, configuration docker run -d --network host \
- **Asynchronous (Events):** Metric updates, alerts, audit logs -e AGGREGATOR_URL=<local-ip>:50051 \
- **Streaming (gRPC + WebSocket):** Real-time data flow -e MACHINE_ID=$(hostname) \
registry.mcrn.ar/sysmonstm/collector:latest
### 3. Data Storage Patterns
- **Hot data (Redis):** Current state, recent metrics (last 5 minutes)
- **Warm data (TimescaleDB):** Historical metrics (last 30 days)
- **Cold data (Optional):** Archive to S3-compatible storage
### 4. Error Handling & Resilience
- gRPC retry logic with exponential backoff
- Circuit breaker pattern for service calls
- Graceful degradation (continue if one collector fails)
- Dead letter queue for failed events
## Proto Definitions (Starting Point)
```protobuf
syntax = "proto3";
package monitoring;
service MetricsService {
rpc StreamMetrics(MetricsRequest) returns (stream Metric) {}
rpc GetCurrentState(StateRequest) returns (MachineState) {}
}
message MetricsRequest {
string machine_id = 1;
int32 interval_seconds = 2;
}
message Metric {
string machine_id = 1;
int64 timestamp = 2;
MetricType type = 3;
double value = 4;
map<string, string> labels = 5;
}
enum MetricType {
CPU_PERCENT = 0;
MEMORY_PERCENT = 1;
MEMORY_USED_GB = 2;
DISK_PERCENT = 3;
NETWORK_SENT_MBPS = 4;
NETWORK_RECV_MBPS = 5;
}
message MachineState {
string machine_id = 1;
int64 last_seen = 2;
repeated Metric current_metrics = 3;
HealthStatus health = 4;
}
enum HealthStatus {
HEALTHY = 0;
WARNING = 1;
CRITICAL = 2;
UNKNOWN = 3;
}
``` ```
## Project Structure ## Technical Stack
``` - **Python 3.11+**
system-monitor/ - **gRPC** - Collector to aggregator communication (showcased)
├── docker-compose.yml - **FastAPI** - Gateway REST/WebSocket
├── proto/ - **Redis** - Pub/Sub events, current state cache
│ └── metrics.proto - **TimescaleDB** - Historical metrics storage
├── services/ - **WebSocket** - Gateway to edge, edge to browser
│ ├── collector/
│ │ ├── Dockerfile
│ │ ├── requirements.txt
│ │ ├── main.py
│ │ └── metrics.py
│ ├── aggregator/
│ │ ├── Dockerfile
│ │ ├── requirements.txt
│ │ ├── main.py
│ │ └── storage.py
│ ├── gateway/
│ │ ├── Dockerfile
│ │ ├── requirements.txt
│ │ ├── main.py
│ │ └── websocket.py
│ └── alerts/
│ ├── Dockerfile
│ ├── requirements.txt
│ ├── main.py
│ └── rules.py
├── web/
│ ├── static/
│ │ ├── css/
│ │ └── js/
│ └── templates/
│ └── dashboard.html
└── README.md
```
## Interview Talking Points ## Key Files
### Domain Mapping to Payments | File | Purpose |
|------|---------|
| `proto/metrics.proto` | gRPC service and message definitions |
| `services/collector/main.py` | gRPC streaming client |
| `services/aggregator/main.py` | gRPC server, metric processing |
| `services/gateway/main.py` | WebSocket bridge, edge forwarding |
| `ctrl/edge/edge.py` | Simple WebSocket relay for AWS |
**What you say:** ## Portfolio Talking Points
- "I built this to monitor my dev machines, but the architecture directly maps to payment processing"
- "Each machine streaming metrics is like a payment processor streaming transactions"
- "The aggregator normalizes data from different sources - same as aggregating from Stripe, PayPal, bank APIs"
- "Alert thresholds on resource usage are structurally identical to fraud detection thresholds"
- "The event stream for audit trails maps directly to payment audit logs"
### Technical Decisions to Highlight - **gRPC streaming** - Efficient binary protocol for real-time metrics
- **Event-driven** - Redis Pub/Sub decouples processing from delivery
**gRPC vs REST:** - **Edge pattern** - Heavy processing local, lightweight relay in cloud
- "I use gRPC between services for efficiency and strong typing" - **Cost optimization** - ~$10/mo for public dashboard (data transfer, not requests)
- "FastAPI gateway exposes REST/WebSocket for browser clients"
- "This pattern is common - internal gRPC, external REST"
**Streaming vs Polling:**
- "Server-side streaming reduces network overhead"
- "Bidirectional streaming allows dynamic configuration updates"
- "WebSocket to browser maintains single connection"
**State Management:**
- "Redis for hot data - current state, needs fast access"
- "TimescaleDB for historical analysis - optimized for time-series"
- "This tiered storage approach scales to payment transaction volumes"
**Resilience:**
- "Each collector is independent - one failing doesn't affect others"
- "Circuit breaker prevents cascade failures"
- "Event stream decouples alert processing from metric ingestion"
### What NOT to Say
- Don't call it a "toy project" or "learning exercise"
- Don't apologize for running locally vs AWS
- Don't over-explain obvious things
- Don't claim it's production-ready when it's not
### What TO Say
- "I built this to solve a real problem I have"
- "Locally it uses PostgreSQL/Redis, in production these become Aurora/ElastiCache"
- "I focused on the architectural patterns since those transfer directly"
- "I'd keep developing this - it's genuinely useful"
## Development Guidelines
### Code Quality Standards
- Type hints throughout (Python 3.11+ syntax)
- Async/await patterns consistently
- Structured logging (JSON format)
- Error handling at all boundaries
- Unit tests for business logic
- Integration tests for service interactions
### Docker Best Practices
- Multi-stage builds
- Non-root users
- Health checks
- Resource limits
- Volume mounts for development
### Configuration Management
- Environment variables for all config
- Sensible defaults
- Config validation on startup
- No secrets in code
## AWS Mapping (For Interview Discussion)
**What you have → What it becomes:**
- PostgreSQL → Aurora PostgreSQL
- Redis → ElastiCache
- Docker Containers → ECS/Fargate or Lambda
- RabbitMQ/Redis Pub/Sub → SQS/SNS
- Docker Compose → CloudFormation/Terraform
- Local networking → VPC, Security Groups
**Key point:** "The architecture and patterns are production-ready, the infrastructure is local for development convenience"
## Common Pitfalls to Avoid
1. **Over-engineering Phase 1** - Resist adding features, just get streaming working
2. **Ugly UI** - Don't waste time on design, htmx + basic CSS is fine
3. **Perfect metrics** - Mock data is OK early on, real psutil data comes later
4. **Complete coverage** - Better to have 3 services working perfectly than 10 half-done
5. **AWS deployment** - Local is fine, AWS costs money and adds complexity
## Success Metrics
**For Yourself:**
- [ ] Actually use the dashboard daily
- [ ] Catches a real issue before you notice
- [ ] Runs stable for 1+ week without intervention
**For Interview:**
- [ ] Can demo end-to-end in 5 minutes
- [ ] Can explain every service interaction
- [ ] Can map to payment domain fluently
- [ ] Shows understanding of production patterns
## Next Steps
1. Set up project structure
2. Define proto messages
3. Build Phase 1 MVP
4. Iterate based on what feels useful
5. Polish for demo when interview approaches
## Resources
- gRPC Python docs: https://grpc.io/docs/languages/python/
- FastAPI WebSockets: https://fastapi.tiangolo.com/advanced/websockets/
- TimescaleDB: https://docs.timescale.com/
- htmx: https://htmx.org/
## Questions to Ask Yourself During Development
- "Would I actually use this feature?"
- "How does this map to payments?"
- "Can I explain why I built it this way?"
- "What would break if X service failed?"
- "How would this scale to 1000 machines?"
---
## Final Note
This project works because it's:
1. **Real** - You'll use it
2. **Focused** - Shows specific patterns they care about
3. **Mappable** - Clear connection to their domain
4. **Yours** - Not a tutorial copy, demonstrates your thinking
Build it in phases, use it daily, and by interview time you'll have natural stories about trade-offs, failures, and learnings. That authenticity is more valuable than perfect code.
Good luck! 🚀

214
README.md Normal file
View File

@@ -0,0 +1,214 @@
# sysmonstm
A real-time distributed system monitoring platform that streams metrics from multiple machines to a central hub with a live web dashboard.
## Overview
sysmonstm demonstrates production microservices patterns (gRPC streaming, FastAPI, event-driven architecture) while solving a real problem: monitoring development infrastructure across multiple machines.
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Collector │ │ Collector │ │ Collector │
│ (Machine 1) │ │ (Machine 2) │ │ (Machine N) │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
│ gRPC Streaming │
└───────────────────────┼───────────────────────┘
┌────────────────────────┐
│ Aggregator │
│ (gRPC Server + Redis │
│ + TimescaleDB) │
└────────────┬───────────┘
┌──────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌──────────────┐ ┌──────────────┐
│ Gateway │ │ Alerts │ │ Event Stream│
│ (FastAPI + WS) │ │ Service │ │ (Redis PubSub│
└────────┬───────┘ └──────────────┘ └──────────────┘
│ WebSocket
┌────────────────┐
│ Browser │
│ Dashboard │
└────────────────┘
```
## Features
- **Real-time streaming**: Collectors stream metrics via gRPC to central aggregator
- **Multi-machine support**: Monitor any number of machines from a single dashboard
- **Live dashboard**: WebSocket-powered updates with real-time graphs
- **Tiered storage**: Redis for hot data, TimescaleDB for historical analysis
- **Threshold alerts**: Configurable rules for CPU, memory, disk usage
- **Event-driven**: Decoupled services via Redis Pub/Sub
## Quick Start
```bash
# Start the full stack
docker compose up
# Open dashboard
open http://localhost:8000
```
Metrics appear within seconds. The collector runs locally by default.
### Monitor Additional Machines
Run the collector on any machine you want to monitor:
```bash
# On a remote machine, point to your aggregator
COLLECTOR_AGGREGATOR_URL=your-server:50051 \
COLLECTOR_MACHINE_ID=my-laptop \
python services/collector/main.py
```
## Architecture
### Services
| Service | Port | Description |
|---------|------|-------------|
| **Collector** | - | gRPC client that streams system metrics (CPU, memory, disk, network) |
| **Aggregator** | 50051 | gRPC server that receives metrics, stores them, publishes events |
| **Gateway** | 8000 | FastAPI server with REST API and WebSocket for dashboard |
| **Alerts** | - | Subscribes to events, evaluates threshold rules, triggers notifications |
### Infrastructure
| Component | Purpose |
|-----------|---------|
| **Redis** | Current state cache, event pub/sub |
| **TimescaleDB** | Historical metrics with automatic downsampling |
### Key Patterns
- **gRPC Streaming**: Collectors stream metrics continuously to the aggregator
- **Event-Driven**: Services communicate via Redis Pub/Sub for decoupling
- **Tiered Storage**: Hot data in Redis, historical in TimescaleDB
- **Graceful Degradation**: System continues partially if storage fails
## Project Structure
```
sysmonstm/
├── proto/
│ └── metrics.proto # gRPC service definitions
├── services/
│ ├── collector/ # Metrics collection (psutil)
│ ├── aggregator/ # Central gRPC server
│ ├── gateway/ # FastAPI + WebSocket
│ └── alerts/ # Threshold evaluation
├── shared/
│ ├── config.py # Pydantic settings
│ ├── logging.py # Structured JSON logging
│ └── events/ # Event pub/sub abstraction
├── web/
│ ├── static/ # CSS, JS
│ └── templates/ # Dashboard HTML
├── scripts/
│ └── init-db.sql # TimescaleDB schema
├── docs/ # Architecture diagrams & explainers
├── docker-compose.yml
└── Tiltfile # Local Kubernetes dev
```
## Configuration
All services use environment variables with sensible defaults:
```bash
# Collector
COLLECTOR_MACHINE_ID=my-machine # Machine identifier
COLLECTOR_AGGREGATOR_URL=localhost:50051
COLLECTOR_COLLECTION_INTERVAL=5 # Seconds between collections
# Common
REDIS_URL=redis://localhost:6379
TIMESCALE_URL=postgresql://monitor:monitor@localhost:5432/monitor
LOG_LEVEL=INFO
LOG_FORMAT=json
```
## Metrics Collected
- CPU: Overall percentage, per-core usage
- Memory: Percentage, used/available bytes
- Disk: Percentage, used bytes, read/write throughput
- Network: Bytes sent/received per second, connection count
- System: Process count, load averages (1m, 5m, 15m)
## Development
### Local Development with Hot Reload
```bash
# Use the override file for volume mounts
docker compose -f docker-compose.yml -f docker-compose.override.yml up
```
### Kubernetes Development with Tilt
```bash
tilt up
```
### Running Services Individually
```bash
# Install dependencies
python -m venv .venv
source .venv/bin/activate
pip install -r services/collector/requirements.txt
# Generate protobuf code
python -m grpc_tools.protoc -I proto --python_out=. --grpc_python_out=. proto/metrics.proto
# Run a service
python services/collector/main.py
```
## API Endpoints
### REST (Gateway)
| Endpoint | Description |
|----------|-------------|
| `GET /` | Dashboard UI |
| `GET /api/machines` | List all monitored machines |
| `GET /api/machines/{id}/metrics` | Current metrics for a machine |
| `GET /api/machines/{id}/history` | Historical metrics |
| `GET /health` | Health check |
| `GET /ready` | Readiness check (includes dependencies) |
### WebSocket
Connect to `ws://localhost:8000/ws` for real-time metric updates.
## Documentation
Detailed documentation is available in the `docs/` folder:
- [Architecture Diagrams](docs/architecture/) - System overview, data flow, deployment
- [Building sysmonstm](docs/explainer/sysmonstm-from-start-to-finish.md) - Deep dive into implementation decisions
- [Domain Applications](docs/explainer/other-applications.md) - How these patterns apply to payment processing and other domains
## Tech Stack
- **Python 3.11+** with async/await throughout
- **gRPC** for inter-service communication
- **FastAPI** for REST API and WebSocket
- **Redis** for caching and pub/sub
- **TimescaleDB** for time-series storage
- **psutil** for system metrics collection
- **Docker Compose** for orchestration
## License
MIT

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

72
ctrl/README.md Normal file
View File

@@ -0,0 +1,72 @@
# Deployment Configurations
## Architecture
```
┌─────────────┐ ┌─────────────────────────────────────┐ ┌─────────────┐
│ Collector │────▶│ Aggregator + Gateway + Redis + TS │────▶│ Edge │────▶ Browser
│ (mcrn) │gRPC │ (LOCAL) │ WS │ (AWS) │ WS
└─────────────┘ └─────────────────────────────────────┘ └─────────────┘
┌─────────────┐ │
│ Collector │────────────────────┘
│ (nfrt) │gRPC
└─────────────┘
```
- **Collectors** use gRPC to stream metrics to the local aggregator
- **Gateway** forwards to edge via WebSocket (if `EDGE_URL` configured)
- **Edge** (AWS) relays to browsers via WebSocket
## Directory Structure
```
ctrl/
├── dev/ # Full stack for local development (docker-compose)
└── edge/ # Cloud dashboard for AWS deployment
```
## Local Development
```bash
# From repo root
docker compose up
```
Runs: aggregator, gateway, collector, alerts, redis, timescaledb
## Production Deployment
### 1. Deploy Edge to AWS
```bash
cd ctrl/edge
docker compose up -d
```
### 2. Run Full Stack Locally with Edge Forwarding
```bash
EDGE_URL=wss://sysmonstm.mcrn.ar/ws EDGE_API_KEY=xxx docker compose up
```
### 3. Run Collectors on Other Machines
```bash
docker run -d --name sysmonstm-collector --network host \
-e AGGREGATOR_URL=<local-gateway-ip>:50051 \
-e MACHINE_ID=$(hostname) \
registry.mcrn.ar/sysmonstm/collector:latest
```
## Environment Variables
### Gateway (for edge forwarding)
- `EDGE_URL` - WebSocket URL of edge (e.g., wss://sysmonstm.mcrn.ar/ws)
- `EDGE_API_KEY` - Authentication key for edge
### Edge
- `API_KEY` - Key required from gateway
### Collector
- `AGGREGATOR_URL` - gRPC URL of aggregator (e.g., localhost:50051)
- `MACHINE_ID` - Identifier for this machine

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

156
ctrl/dev/docker-compose.yml Normal file
View File

@@ -0,0 +1,156 @@
version: "3.8"
# This file works both locally and on EC2 for demo purposes.
# For local dev with hot-reload, use: docker compose -f docker-compose.yml -f docker-compose.override.yml up
x-common-env: &common-env
REDIS_URL: redis://redis:6379
TIMESCALE_URL: postgresql://monitor:monitor@timescaledb:5432/monitor
EVENTS_BACKEND: redis_pubsub
LOG_LEVEL: ${LOG_LEVEL:-INFO}
LOG_FORMAT: json
x-healthcheck-defaults: &healthcheck-defaults
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
services:
# =============================================================================
# Infrastructure
# =============================================================================
redis:
image: redis:7-alpine
ports:
- "${REDIS_PORT:-6379}:6379"
volumes:
- redis-data:/data
healthcheck:
<<: *healthcheck-defaults
test: ["CMD", "redis-cli", "ping"]
deploy:
resources:
limits:
memory: 128M
timescaledb:
image: timescale/timescaledb:latest-pg15
environment:
POSTGRES_USER: monitor
POSTGRES_PASSWORD: monitor
POSTGRES_DB: monitor
ports:
- "${TIMESCALE_PORT:-5432}:5432"
volumes:
- timescale-data:/var/lib/postgresql/data
- ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql:ro
healthcheck:
<<: *healthcheck-defaults
test: ["CMD-SHELL", "pg_isready -U monitor -d monitor"]
deploy:
resources:
limits:
memory: 512M
# =============================================================================
# Application Services
# =============================================================================
aggregator:
build:
context: .
dockerfile: services/aggregator/Dockerfile
environment:
<<: *common-env
GRPC_PORT: 50051
SERVICE_NAME: aggregator
ports:
- "${AGGREGATOR_GRPC_PORT:-50051}:50051"
depends_on:
redis:
condition: service_healthy
timescaledb:
condition: service_healthy
healthcheck:
<<: *healthcheck-defaults
test: ["CMD", "/bin/grpc_health_probe", "-addr=:50051"]
deploy:
resources:
limits:
memory: 256M
gateway:
build:
context: .
dockerfile: services/gateway/Dockerfile
environment:
<<: *common-env
HTTP_PORT: 8000
AGGREGATOR_URL: aggregator:50051
SERVICE_NAME: gateway
EDGE_URL: ${EDGE_URL:-}
EDGE_API_KEY: ${EDGE_API_KEY:-}
ports:
- "${GATEWAY_PORT:-8000}:8000"
depends_on:
- aggregator
- redis
healthcheck:
<<: *healthcheck-defaults
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
deploy:
resources:
limits:
memory: 256M
alerts:
build:
context: .
dockerfile: services/alerts/Dockerfile
environment:
<<: *common-env
SERVICE_NAME: alerts
depends_on:
redis:
condition: service_healthy
timescaledb:
condition: service_healthy
healthcheck:
<<: *healthcheck-defaults
test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
deploy:
resources:
limits:
memory: 128M
# Collector runs separately on each machine being monitored
# For local testing, we run one instance
collector:
build:
context: .
dockerfile: services/collector/Dockerfile
environment:
<<: *common-env
AGGREGATOR_URL: aggregator:50051
MACHINE_ID: ${MACHINE_ID:-local-dev}
COLLECTION_INTERVAL: ${COLLECTION_INTERVAL:-5}
SERVICE_NAME: collector
depends_on:
- aggregator
deploy:
resources:
limits:
memory: 64M
# For actual system metrics, you might need:
# privileged: true
# pid: host
volumes:
redis-data:
timescale-data:
networks:
default:
name: sysmonstm

15
ctrl/edge/Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM python:3.11-slim
WORKDIR /app
RUN pip install --no-cache-dir fastapi uvicorn[standard] websockets jinja2
COPY edge.py .
COPY templates/ templates/
ENV API_KEY=""
ENV LOG_LEVEL=INFO
EXPOSE 8080
CMD ["uvicorn", "edge:app", "--host", "0.0.0.0", "--port", "8080"]

15
ctrl/edge/deploy.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
# Deploy sysmonstm edge service
# Called by Woodpecker or manually
set -e
cd "$(dirname "$0")"
echo "Pulling latest image..."
docker compose pull
echo "Deploying edge service..."
docker compose up -d --remove-orphans
echo "Deploy complete"
docker compose ps

View File

@@ -0,0 +1,16 @@
services:
edge:
image: registry.mcrn.ar/sysmonstm/edge:latest
container_name: sysmonstm-edge
restart: unless-stopped
environment:
- API_KEY=${API_KEY:-}
- LOG_LEVEL=${LOG_LEVEL:-INFO}
ports:
- "8080:8080"
networks:
- gateway
networks:
gateway:
external: true

135
ctrl/edge/edge.py Normal file
View File

@@ -0,0 +1,135 @@
"""Minimal sysmonstm gateway - standalone mode without dependencies."""
import asyncio
import json
import logging
import os
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", "")
LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO")
# Logging setup
logging.basicConfig(
level=getattr(logging, LOG_LEVEL.upper(), logging.INFO),
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
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 = {}
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
return templates.TemplateResponse("dashboard.html", {"request": request})
@app.get("/health")
async def health():
return {"status": "ok", "machines": len(machines)}
@app.get("/api/machines")
async def get_machines():
return machines
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket, key: str = Query(default="")):
# API key validation for collectors (browsers don't need key)
# We validate key only when metrics are received, allowing browsers to connect freely
await websocket.accept()
connections.append(websocket)
client = websocket.client.host if websocket.client else "unknown"
log.info(f"WebSocket connected: {client}")
try:
# Send current state to new connection
for machine_id, data in machines.items():
await websocket.send_json(
{"type": "metrics", "machine_id": machine_id, **data}
)
# Main loop
while True:
try:
msg = await asyncio.wait_for(websocket.receive_text(), timeout=30)
data = json.loads(msg)
if data.get("type") == "metrics":
# Validate API key for metric submissions
if API_KEY and key != API_KEY:
log.warning(f"Invalid API key from {client}")
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(broadcast_data)
except Exception:
pass
except asyncio.TimeoutError:
# Send ping to keep connection alive
await websocket.send_json({"type": "ping"})
except WebSocketDisconnect:
log.info(f"WebSocket disconnected: {client}")
except Exception as e:
log.error(f"WebSocket error: {e}")
finally:
if websocket in connections:
connections.remove(websocket)
if __name__ == "__main__":
import uvicorn
log.info("Starting sysmonstm gateway")
log.info(f" API key: {'configured' if API_KEY else 'not set (open)'}")
uvicorn.run(app, host="0.0.0.0", port=8080)

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

@@ -1,6 +0,0 @@
FROM python:3.11-slim
WORKDIR /app
RUN pip install --no-cache-dir fastapi uvicorn[standard] websockets
COPY main.py .
EXPOSE 8080
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]

View File

@@ -1,13 +0,0 @@
services:
sysmonstm:
build: .
container_name: sysmonstm
restart: unless-stopped
ports:
- "8080:8080"
networks:
- gateway
networks:
gateway:
external: true

View File

@@ -1,198 +0,0 @@
"""Minimal sysmonstm gateway - standalone mode without dependencies."""
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
import json
import asyncio
from datetime import datetime
app = FastAPI(title="sysmonstm")
# 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 ws = new WebSocket(`wss://${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);
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];
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>Updated</span><span>${new Date(m.timestamp).toLocaleTimeString()}</span></div>
</div>
`;
}).join('');
}
connect();
</script>
</body>
</html>
"""
@app.get("/", response_class=HTMLResponse)
async def index():
return HTML
@app.get("/health")
async def health():
return {"status": "ok", "machines": len(machines)}
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
connections.append(websocket)
try:
# Send current state
for machine_id, data in machines.items():
await websocket.send_json({"type": "metrics", "machine_id": machine_id, **data})
# Keep alive
while True:
try:
msg = await asyncio.wait_for(websocket.receive_text(), timeout=30)
data = json.loads(msg)
if data.get("type") == "metrics":
machine_id = data.get("machine_id", "unknown")
machines[machine_id] = {**data, "timestamp": datetime.utcnow().isoformat()}
# Broadcast to all
for conn in connections:
try:
await conn.send_json({"type": "metrics", "machine_id": machine_id, **machines[machine_id]})
except:
pass
except asyncio.TimeoutError:
await websocket.send_json({"type": "ping"})
except WebSocketDisconnect:
pass
finally:
connections.remove(websocket)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8080)

View File

@@ -1,154 +0,0 @@
version: "3.8"
# This file works both locally and on EC2 for demo purposes.
# For local dev with hot-reload, use: docker compose -f docker-compose.yml -f docker-compose.override.yml up
x-common-env: &common-env
REDIS_URL: redis://redis:6379
TIMESCALE_URL: postgresql://monitor:monitor@timescaledb:5432/monitor
EVENTS_BACKEND: redis_pubsub
LOG_LEVEL: ${LOG_LEVEL:-INFO}
LOG_FORMAT: json
x-healthcheck-defaults: &healthcheck-defaults
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
services:
# =============================================================================
# Infrastructure
# =============================================================================
redis:
image: redis:7-alpine
ports:
- "${REDIS_PORT:-6379}:6379"
volumes:
- redis-data:/data
healthcheck:
<<: *healthcheck-defaults
test: ["CMD", "redis-cli", "ping"]
deploy:
resources:
limits:
memory: 128M
timescaledb:
image: timescale/timescaledb:latest-pg15
environment:
POSTGRES_USER: monitor
POSTGRES_PASSWORD: monitor
POSTGRES_DB: monitor
ports:
- "${TIMESCALE_PORT:-5432}:5432"
volumes:
- timescale-data:/var/lib/postgresql/data
- ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql:ro
healthcheck:
<<: *healthcheck-defaults
test: ["CMD-SHELL", "pg_isready -U monitor -d monitor"]
deploy:
resources:
limits:
memory: 512M
# =============================================================================
# Application Services
# =============================================================================
aggregator:
build:
context: .
dockerfile: services/aggregator/Dockerfile
environment:
<<: *common-env
GRPC_PORT: 50051
SERVICE_NAME: aggregator
ports:
- "${AGGREGATOR_GRPC_PORT:-50051}:50051"
depends_on:
redis:
condition: service_healthy
timescaledb:
condition: service_healthy
healthcheck:
<<: *healthcheck-defaults
test: ["CMD", "/bin/grpc_health_probe", "-addr=:50051"]
deploy:
resources:
limits:
memory: 256M
gateway:
build:
context: .
dockerfile: services/gateway/Dockerfile
environment:
<<: *common-env
HTTP_PORT: 8000
AGGREGATOR_URL: aggregator:50051
SERVICE_NAME: gateway
ports:
- "${GATEWAY_PORT:-8000}:8000"
depends_on:
- aggregator
- redis
healthcheck:
<<: *healthcheck-defaults
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
deploy:
resources:
limits:
memory: 256M
alerts:
build:
context: .
dockerfile: services/alerts/Dockerfile
environment:
<<: *common-env
SERVICE_NAME: alerts
depends_on:
redis:
condition: service_healthy
timescaledb:
condition: service_healthy
healthcheck:
<<: *healthcheck-defaults
test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
deploy:
resources:
limits:
memory: 128M
# Collector runs separately on each machine being monitored
# For local testing, we run one instance
collector:
build:
context: .
dockerfile: services/collector/Dockerfile
environment:
<<: *common-env
AGGREGATOR_URL: aggregator:50051
MACHINE_ID: ${MACHINE_ID:-local-dev}
COLLECTION_INTERVAL: ${COLLECTION_INTERVAL:-5}
SERVICE_NAME: collector
depends_on:
- aggregator
deploy:
resources:
limits:
memory: 64M
# For actual system metrics, you might need:
# privileged: true
# pid: host
volumes:
redis-data:
timescale-data:
networks:
default:
name: sysmonstm

1
docker-compose.yml Symbolic link
View File

@@ -0,0 +1 @@
ctrl/dev/docker-compose.yml

View File

@@ -24,9 +24,19 @@ digraph SystemOverview {
machines [label="Monitored\nMachines", fillcolor="#FFF3E0", shape=box3d]; 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 // Core Services
subgraph cluster_services { subgraph cluster_services {
label="Application Services"; label="Local Stack";
style=filled; style=filled;
color="#E8F5E9"; color="#E8F5E9";
fillcolor="#E8F5E9"; fillcolor="#E8F5E9";
@@ -59,7 +69,10 @@ digraph SystemOverview {
} }
// Connections // 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 -> aggregator [label="gRPC", color="#388E3C"];
gateway -> redis [label="State\nQuery", style=dashed]; gateway -> redis [label="State\nQuery", style=dashed];
gateway -> timescale [label="Historical\nQuery", style=dashed]; gateway -> timescale [label="Historical\nQuery", style=dashed];
@@ -73,6 +86,4 @@ digraph SystemOverview {
events -> alerts [label="Subscribe", color="#7B1FA2"]; events -> alerts [label="Subscribe", color="#7B1FA2"];
events -> gateway [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"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> "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 --> <!-- Title: SystemOverview Pages: 1 -->
<svg width="444pt" height="508pt" <svg width="577pt" height="618pt"
viewBox="0.00 0.00 444.00 508.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 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 503.78)"> <g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 614.03)">
<title>SystemOverview</title> <title>SystemOverview</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-503.78 440,-503.78 440,4 -4,4"/> <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="218" y="-480.58" font-family="Helvetica,sans-Serif" font-size="16.00">System Monitoring Platform &#45; Architecture Overview</text> <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"> <g id="clust1" class="cluster">
<title>cluster_external</title> <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"/> <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="140.5" y="-434.5" font-family="Helvetica,sans-Serif" font-size="16.00">External</text> <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>
<g id="clust2" class="cluster"> <g id="clust2" class="cluster">
<title>cluster_services</title> <title>cluster_edge</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"/> <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="232.5" y="-300.93" font-family="Helvetica,sans-Serif" font-size="16.00">Application Services</text> <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>
<g id="clust3" class="cluster"> <g id="clust3" class="cluster">
<title>cluster_data</title> <title>cluster_services</title>
<polygon fill="#fff8e1" stroke="#fff8e1" points="22.5,-8 22.5,-99.62 260.5,-99.62 260.5,-8 22.5,-8"/> <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="141.5" y="-80.42" font-family="Helvetica,sans-Serif" font-size="16.00">Data Layer</text> <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>
<g id="clust4" class="cluster"> <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> <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"/> <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="328.5" y="-445.08" font-family="Helvetica,sans-Serif" font-size="16.00">Event Stream</text> <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> </g>
<!-- browser --> <!-- browser -->
<g id="node1" class="node"> <g id="node1" class="node">
<title>browser</title> <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"/> <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="95.5" y="-403.25" font-family="Helvetica,sans-Serif" font-size="11.00">Browser</text> <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="95.5" y="-389.75" font-family="Helvetica,sans-Serif" font-size="11.00">(Dashboard)</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> </g>
<!-- gateway --> <!-- gateway -->
<g id="node3" class="node"> <g id="node4" class="node">
<title>gateway</title> <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"/> <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="141.5" y="-269.68" font-family="Helvetica,sans-Serif" font-size="11.00">Gateway</text> <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="141.5" y="-256.18" font-family="Helvetica,sans-Serif" font-size="11.00">(FastAPI)</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> </g>
<!-- browser&#45;&gt;gateway --> <!-- browser&#45;&gt;gateway -->
<g id="edge1" class="edge"> <g id="edge3" class="edge">
<title>browser&#45;&gt;gateway</title> <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"/> <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="119.02,-295.94 122.86,-286.06 113.7,-291.39 119.02,-295.94"/> <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="122.88" y="-344.12" font-family="Helvetica,sans-Serif" font-size="10.00">WebSocket</text> <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="122.88" y="-331.38" font-family="Helvetica,sans-Serif" font-size="10.00">REST</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> </g>
<!-- machines --> <!-- machines -->
<g id="node2" class="node"> <g id="node2" class="node">
<title>machines</title> <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"/> <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="223.25,-414.2 155.75,-414.2"/> <polyline fill="none" stroke="black" points="385.75,-535.03 318.25,-535.03"/>
<polyline fill="none" stroke="black" points="223.25,-414.2 223.25,-382.2"/> <polyline fill="none" stroke="black" points="385.75,-535.03 385.75,-503.03"/>
<polyline fill="none" stroke="black" points="223.25,-414.2 227.25,-418.2"/> <polyline fill="none" stroke="black" points="385.75,-535.03 389.75,-539.03"/>
<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="354" y="-524.08" 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> <text xml:space="preserve" text-anchor="middle" x="354" y="-510.58" font-family="Helvetica,sans-Serif" font-size="11.00">Machines</text>
</g> </g>
<!-- collector --> <!-- collector -->
<g id="node6" class="node"> <g id="node7" class="node">
<title>collector</title> <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"/> <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="311.5" y="-269.68" font-family="Helvetica,sans-Serif" font-size="11.00">Collector</text> <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="311.5" y="-256.18" font-family="Helvetica,sans-Serif" font-size="11.00">(gRPC Client)</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> </g>
<!-- machines&#45;&gt;collector --> <!-- machines&#45;&gt;collector -->
<g id="edge5" class="edge"> <g id="edge7" class="edge">
<title>machines&#45;&gt;collector</title> <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"/> <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="315.12,-296.47 312.08,-286.32 308.13,-296.15 315.12,-296.47"/> <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="318.1" y="-337.75" font-family="Helvetica,sans-Serif" font-size="10.00">psutil</text> <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> </g>
<!-- aggregator --> <!-- aggregator -->
<g id="node4" class="node"> <g id="node5" class="node">
<title>aggregator</title> <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"/> <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="308.5" y="-172.18" font-family="Helvetica,sans-Serif" font-size="11.00">Aggregator</text> <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="308.5" y="-158.68" font-family="Helvetica,sans-Serif" font-size="11.00">(gRPC Server)</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> </g>
<!-- gateway&#45;&gt;aggregator --> <!-- gateway&#45;&gt;aggregator -->
<g id="edge2" class="edge"> <g id="edge4" class="edge">
<title>gateway&#45;&gt;aggregator</title> <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"/> <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="269.66,-196.37 276.6,-188.36 266.19,-190.29 269.66,-196.37"/> <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="257.62" y="-214.75" font-family="Helvetica,sans-Serif" font-size="10.00">gRPC</text> <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> </g>
<!-- redis --> <!-- redis -->
<g id="node7" class="node"> <g id="node8" class="node">
<title>redis</title> <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="#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="M146,-59.75C146,-57.34 120.23,-55.38 88.5,-55.38 56.77,-55.38 31,-57.34 31,-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="88.5" y="-43.11" font-family="Helvetica,sans-Serif" font-size="11.00">Redis</text> <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="88.5" y="-29.61" font-family="Helvetica,sans-Serif" font-size="11.00">(Pub/Sub + State)</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> </g>
<!-- gateway&#45;&gt;redis --> <!-- gateway&#45;&gt;redis -->
<g id="edge3" class="edge"> <g id="edge5" class="edge">
<title>gateway&#45;&gt;redis</title> <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"/> <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="83.14,-76.56 81.82,-66.04 76.29,-75.08 83.14,-76.56"/> <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="95.88" 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="-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> <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> </g>
<!-- timescale --> <!-- timescale -->
<g id="node8" class="node"> <g id="node9" class="node">
<title>timescale</title> <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="#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="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"/> <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="208.5" y="-43.11" font-family="Helvetica,sans-Serif" font-size="11.00">TimescaleDB</text> <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="208.5" y="-29.61" font-family="Helvetica,sans-Serif" font-size="11.00">(Time&#45;series)</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> </g>
<!-- gateway&#45;&gt;timescale --> <!-- gateway&#45;&gt;timescale -->
<g id="edge4" class="edge"> <g id="edge6" class="edge">
<title>gateway&#45;&gt;timescale</title> <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"/> <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="194.62,-76.29 195.83,-65.76 188.32,-73.24 194.62,-76.29"/> <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="187.25" 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="-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> <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> </g>
<!-- aggregator&#45;&gt;redis --> <!-- aggregator&#45;&gt;redis -->
<g id="edge7" class="edge"> <g id="edge9" class="edge">
<title>aggregator&#45;&gt;redis</title> <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"/> <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="123.32,-69.73 113.56,-65.6 118.62,-74.91 123.32,-69.73"/> <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="239.5" y="-123.62" font-family="Helvetica,sans-Serif" font-size="10.00">Current</text> <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="239.5" y="-110.88" font-family="Helvetica,sans-Serif" font-size="10.00">State</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> </g>
<!-- aggregator&#45;&gt;timescale --> <!-- aggregator&#45;&gt;timescale -->
<g id="edge8" class="edge"> <g id="edge10" class="edge">
<title>aggregator&#45;&gt;timescale</title> <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"/> <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="236.64,-71.16 227.71,-65.47 231.14,-75.49 236.64,-71.16"/> <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="296.95" 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="-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> <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> </g>
<!-- events --> <!-- events -->
<g id="node9" class="node"> <g id="node10" class="node">
<title>events</title> <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"/> <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="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="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="328.5" y="-389.75" font-family="Helvetica,sans-Serif" font-size="11.00">(Events)</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> </g>
<!-- aggregator&#45;&gt;events --> <!-- aggregator&#45;&gt;events -->
<g id="edge9" class="edge"> <g id="edge11" class="edge">
<title>aggregator&#45;&gt;events</title> <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"/> <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="346.73,-359.44 345.42,-369.95 353,-362.55 346.73,-359.44"/> <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="386.64" y="-263.5" font-family="Helvetica,sans-Serif" font-size="10.00">Publish</text> <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> </g>
<!-- alerts --> <!-- alerts -->
<g id="node5" class="node"> <g id="node6" class="node">
<title>alerts</title> <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"/> <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="220.5" y="-269.68" font-family="Helvetica,sans-Serif" font-size="11.00">Alerts</text> <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="220.5" y="-256.18" font-family="Helvetica,sans-Serif" font-size="11.00">Service</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>
<!-- 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>
</g> </g>
<!-- collector&#45;&gt;aggregator --> <!-- collector&#45;&gt;aggregator -->
<g id="edge6" class="edge"> <g id="edge8" class="edge">
<title>collector&#45;&gt;aggregator</title> <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"/> <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="312.9,-198.77 309.09,-188.89 305.91,-198.99 312.9,-198.77"/> <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="327.98" 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="-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> <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> </g>
<!-- events&#45;&gt;gateway --> <!-- events&#45;&gt;gateway -->
<g id="edge11" class="edge"> <g id="edge13" class="edge">
<title>events&#45;&gt;gateway</title> <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"/> <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="149.38,-295.39 143.95,-286.29 142.52,-296.79 149.38,-295.39"/> <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="200.88" y="-337.75" font-family="Helvetica,sans-Serif" font-size="10.00">Subscribe</text> <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> </g>
<!-- events&#45;&gt;alerts --> <!-- events&#45;&gt;alerts -->
<g id="edge10" class="edge"> <g id="edge12" class="edge">
<title>events&#45;&gt;alerts</title> <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"/> <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="225.38,-296.07 220.98,-286.43 218.41,-296.71 225.38,-296.07"/> <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="265.88" y="-337.75" font-family="Helvetica,sans-Serif" font-size="10.00">Subscribe</text> <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>
</g> </g>
</svg> </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"]; alerts [label="Alert\nService", fillcolor="#C5CAE9"];
gateway [label="Gateway\n(WebSocket)", fillcolor="#9FA8DA"]; 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 // Flow
@@ -75,9 +84,9 @@ digraph DataFlow {
redis_pubsub -> alerts [label="metrics.*"]; redis_pubsub -> alerts [label="metrics.*"];
redis_pubsub -> gateway [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]; raw -> agg_1m [label="Continuous\nAggregate", style=dashed];
agg_1m -> agg_1h [label="Hourly\nJob", 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"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> "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 --> <!-- Title: DataFlow Pages: 1 -->
<svg width="1087pt" height="329pt" <svg width="1270pt" height="305pt"
viewBox="0.00 0.00 1087.00 329.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 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 325.25)"> <g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 301.25)">
<title>DataFlow</title> <title>DataFlow</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-325.25 1082.5,-325.25 1082.5,4 -4,4"/> <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="539.25" y="-303.95" font-family="Helvetica,sans-Serif" font-size="14.00">Metrics Data Flow Pipeline</text> <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"> <g id="clust1" class="cluster">
<title>cluster_collect</title> <title>cluster_collect</title>
<polygon fill="#e3f2fd" stroke="black" points="8,-111 8,-188 254,-188 254,-111 8,-111"/> <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="-170.7" font-family="Helvetica,sans-Serif" font-size="14.00">Collection (5s)</text> <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>
<g id="clust2" class="cluster"> <g id="clust2" class="cluster">
<title>cluster_ingest</title> <title>cluster_ingest</title>
<polygon fill="#e8f5e9" stroke="black" points="307,-95 307,-204 562.5,-204 562.5,-95 307,-95"/> <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="-186.7" font-family="Helvetica,sans-Serif" font-size="14.00">Ingestion</text> <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>
<g id="clust3" class="cluster"> <g id="clust3" class="cluster">
<title>cluster_hot</title> <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"/> <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="-175.7" font-family="Helvetica,sans-Serif" font-size="14.00">Hot Path (Real&#45;time)</text> <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>
<g id="clust4" class="cluster"> <g id="clust4" class="cluster">
<title>cluster_warm</title> <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"/> <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="858.06" y="-270.7" font-family="Helvetica,sans-Serif" font-size="14.00">Warm Path (Historical)</text> <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>
<g id="clust5" class="cluster"> <g id="clust5" class="cluster">
<title>cluster_consume</title> <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"/> <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="-175.7" font-family="Helvetica,sans-Serif" font-size="14.00">Consumers</text> <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> </g>
<!-- psutil --> <!-- psutil -->
<g id="node1" class="node"> <g id="node1" class="node">
<title>psutil</title> <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"/> <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,-151 20,-151 20,-147 16,-147"/>
<polyline fill="none" stroke="black" points="16,-127 20,-127 20,-123 16,-123"/> <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> <polyline fill="none" stroke="black" points="16,-103 20,-103 20,-99 16,-99"/>
<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> <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> </g>
<!-- collector --> <!-- collector -->
<g id="node2" class="node"> <g id="node2" class="node">
<title>collector</title> <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"/> <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="-140.25" font-family="Helvetica,sans-Serif" font-size="10.00">Collector</text> <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="-127.5" font-family="Helvetica,sans-Serif" font-size="10.00">Service</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> </g>
<!-- psutil&#45;&gt;collector --> <!-- psutil&#45;&gt;collector -->
<g id="edge1" class="edge"> <g id="edge1" class="edge">
<title>psutil&#45;&gt;collector</title> <title>psutil&#45;&gt;collector</title>
<path fill="none" stroke="black" d="M118.35,-137C136.74,-137 157.31,-137 174.75,-137"/> <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,-140.5 184.75,-137 174.75,-133.5 174.75,-140.5"/> <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="-139.7" font-family="Helvetica,sans-Serif" font-size="9.00">Metrics</text> <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> </g>
<!-- aggregator --> <!-- aggregator -->
<g id="node3" class="node"> <g id="node3" class="node">
<title>aggregator</title> <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"/> <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="-140.25" font-family="Helvetica,sans-Serif" font-size="10.00">Aggregator</text> <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="-127.5" font-family="Helvetica,sans-Serif" font-size="10.00">(gRPC)</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> </g>
<!-- collector&#45;&gt;aggregator --> <!-- collector&#45;&gt;aggregator -->
<g id="edge2" class="edge"> <g id="edge2" class="edge">
<title>collector&#45;&gt;aggregator</title> <title>collector&#45;&gt;aggregator</title>
<path fill="none" stroke="black" d="M246.49,-137C263.19,-137 284.49,-137 303.35,-137"/> <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,-140.5 313.2,-137 303.2,-133.5 303.2,-140.5"/> <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="-150.95" font-family="Helvetica,sans-Serif" font-size="9.00">gRPC</text> <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="-139.7" font-family="Helvetica,sans-Serif" font-size="9.00">Stream</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> </g>
<!-- validate --> <!-- validate -->
<g id="node4" class="node"> <g id="node4" class="node">
<title>validate</title> <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"/> <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="-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="-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="-127.5" font-family="Helvetica,sans-Serif" font-size="10.00">Normalize</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> </g>
<!-- aggregator&#45;&gt;validate --> <!-- aggregator&#45;&gt;validate -->
<g id="edge3" class="edge"> <g id="edge3" class="edge">
<title>aggregator&#45;&gt;validate</title> <title>aggregator&#45;&gt;validate</title>
<path fill="none" stroke="black" d="M385.38,-137C392.95,-137 401.25,-137 409.76,-137"/> <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,-140.5 419.49,-137 409.49,-133.5 409.49,-140.5"/> <polygon fill="black" stroke="black" points="409.49,-116.5 419.49,-113 409.49,-109.5 409.49,-116.5"/>
</g> </g>
<!-- redis_state --> <!-- redis_state -->
<g id="node5" class="node"> <g id="node5" class="node">
<title>redis_state</title> <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="#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,-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"/> <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="-140.25" font-family="Helvetica,sans-Serif" font-size="10.00">Redis</text> <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="-127.5" font-family="Helvetica,sans-Serif" font-size="10.00">Current State</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> </g>
<!-- validate&#45;&gt;redis_state --> <!-- validate&#45;&gt;redis_state -->
<g id="edge4" class="edge"> <g id="edge4" class="edge">
<title>validate&#45;&gt;redis_state</title> <title>validate&#45;&gt;redis_state</title>
<path fill="none" stroke="black" d="M555.47,-137C582.9,-137 614.22,-137 639.8,-137"/> <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,-140.5 649.6,-137 639.6,-133.5 639.6,-140.5"/> <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.62" y="-139.7" font-family="Helvetica,sans-Serif" font-size="9.00">Upsert</text> <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> </g>
<!-- redis_pubsub --> <!-- redis_pubsub -->
<g id="node6" class="node"> <g id="node6" class="node">
<title>redis_pubsub</title> <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"/> <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="-72.25" font-family="Helvetica,sans-Serif" font-size="10.00">Redis</text> <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="-59.5" font-family="Helvetica,sans-Serif" font-size="10.00">Pub/Sub</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> </g>
<!-- validate&#45;&gt;redis_pubsub --> <!-- validate&#45;&gt;redis_pubsub -->
<g id="edge5" class="edge"> <g id="edge5" class="edge">
<title>validate&#45;&gt;redis_pubsub</title> <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"/> <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,-87.71 654.53,-81.19 643.93,-81.07 646.17,-87.71"/> <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.62" y="-109.77" font-family="Helvetica,sans-Serif" font-size="9.00">Publish</text> <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> </g>
<!-- raw --> <!-- raw -->
<g id="node7" class="node"> <g id="node7" class="node">
<title>raw</title> <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="#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,-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"/> <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="-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="-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="-222.5" font-family="Helvetica,sans-Serif" font-size="10.00">(5s, 24h)</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> </g>
<!-- validate&#45;&gt;raw --> <!-- validate&#45;&gt;raw -->
<g id="edge6" class="edge"> <g id="edge6" class="edge">
<title>validate&#45;&gt;raw</title> <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"/> <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,-213.19 651.77,-214.2 644.16,-206.83 641.22,-213.19"/> <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.62" y="-194.9" font-family="Helvetica,sans-Serif" font-size="9.00">Insert</text> <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> </g>
<!-- alerts --> <!-- alerts -->
<g id="node10" class="node"> <g id="node10" class="node">
@@ -140,9 +145,9 @@
<!-- redis_pubsub&#45;&gt;alerts --> <!-- redis_pubsub&#45;&gt;alerts -->
<g id="edge7" class="edge"> <g id="edge7" class="edge">
<title>redis_pubsub&#45;&gt;alerts</title> <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"/> <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.11,-87.76 858.4,-85.26 848.79,-80.8 848.11,-87.76"/> <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="-85.09" font-family="Helvetica,sans-Serif" font-size="9.00">metrics.*</text> <text xml:space="preserve" text-anchor="middle" x="805" y="-77.99" font-family="Helvetica,sans-Serif" font-size="9.00">metrics.*</text>
</g> </g>
<!-- gateway --> <!-- gateway -->
<g id="node11" class="node"> <g id="node11" class="node">
@@ -154,64 +159,70 @@
<!-- redis_pubsub&#45;&gt;gateway --> <!-- redis_pubsub&#45;&gt;gateway -->
<g id="edge8" class="edge"> <g id="edge8" class="edge">
<title>redis_pubsub&#45;&gt;gateway</title> <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"/> <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.98,-46.3 847.2,-41.08 836.74,-39.41 837.98,-46.3"/> <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="-55.25" font-family="Helvetica,sans-Serif" font-size="9.00">metrics.*</text> <text xml:space="preserve" text-anchor="middle" x="805" y="-42.53" font-family="Helvetica,sans-Serif" font-size="9.00">metrics.*</text>
</g> </g>
<!-- agg_1m --> <!-- agg_1m -->
<g id="node8" class="node"> <g id="node8" class="node">
<title>agg_1m</title> <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="#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,-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"/> <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="-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="-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="-222.5" font-family="Helvetica,sans-Serif" font-size="10.00">(1m, 7d)</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> </g>
<!-- raw&#45;&gt;agg_1m --> <!-- 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"> <g id="edge11" class="edge">
<title>raw&#45;&gt;lambda</title> <title>raw&#45;&gt;agg_1m</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"/> <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="843,-166.44 850.54,-159.01 840,-160.12 843,-166.44"/> <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="-205.05" font-family="Helvetica,sans-Serif" font-size="9.00">SQS</text> <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="-193.8" font-family="Helvetica,sans-Serif" font-size="9.00">Trigger</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> </g>
<!-- agg_1h --> <!-- agg_1h -->
<g id="node9" class="node"> <g id="node9" class="node">
<title>agg_1h</title> <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="#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="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"/> <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="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="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="1027.5" y="-222.5" font-family="Helvetica,sans-Serif" font-size="10.00">(1h, 90d)</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> </g>
<!-- agg_1m&#45;&gt;agg_1h --> <!-- 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"> <g id="edge12" class="edge">
<title>lambda&#45;&gt;agg_1m</title> <title>agg_1m&#45;&gt;agg_1h</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"/> <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="883.88,-197.16 887.38,-207.16 890.88,-197.16 883.88,-197.16"/> <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="873.12" y="-187.18" font-family="Helvetica,sans-Serif" font-size="9.00">Batch</text> <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="873.12" y="-175.93" font-family="Helvetica,sans-Serif" font-size="9.00">Write</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>
</g> </g>
</svg> </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"]; node [shape=box, style="rounded,filled"];
// Local Development // Local Stack
subgraph cluster_local { subgraph cluster_local {
label="Local Development"; label="Local Stack (Docker Compose)";
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)";
style=filled; style=filled;
fillcolor="#E8F5E9"; fillcolor="#E8F5E9";
subgraph cluster_ec2 { aggregator [label="Aggregator\n(gRPC Server)", fillcolor="#A5D6A7"];
label="EC2 t2.small"; gateway [label="Gateway\n(FastAPI)", fillcolor="#A5D6A7"];
style=filled; alerts [label="Alerts\nService", fillcolor="#A5D6A7"];
fillcolor="#C8E6C9"; redis [label="Redis", shape=cylinder, fillcolor="#C8E6C9"];
timescaledb [label="TimescaleDB", shape=cylinder, fillcolor="#C8E6C9"];
compose_ec2 [label="Docker Compose\n(All Services)", fillcolor="#A5D6A7"];
nginx [label="Nginx\n(SSL Termination)", fillcolor="#81C784"];
} }
subgraph cluster_lambda { // AWS Edge
label="Lambda (Data Processing)"; subgraph cluster_aws {
label="AWS (sysmonstm.mcrn.ar)";
style=filled; style=filled;
fillcolor="#DCEDC8"; fillcolor="#F3E5F5";
lambda_agg [label="Aggregator\nLambda", fillcolor="#AED581"]; edge_relay [label="Edge\n(WebSocket Relay)", fillcolor="#CE93D8"];
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"];
} }
// CI/CD // CI/CD
subgraph cluster_cicd { subgraph cluster_cicd {
label="CI/CD"; label="CI/CD";
style=filled; style=filled;
fillcolor="#F3E5F5"; fillcolor="#E3F2FD";
woodpecker [label="Woodpecker CI", fillcolor="#CE93D8"]; woodpecker [label="Woodpecker CI", fillcolor="#90CAF9"];
registry [label="Container\nRegistry", shape=cylinder, fillcolor="#BA68C8"]; registry [label="Container\nRegistry", shape=cylinder, fillcolor="#64B5F6"];
} }
// Collectors (External) // Collectors (External)
@@ -78,18 +54,22 @@ digraph Deployment {
coll3 [label="Collector\n(Machine N)", fillcolor="#FFCCBC"]; coll3 [label="Collector\n(Machine N)", fillcolor="#FFCCBC"];
} }
// Browser
browser [label="Browser\n(Dashboard)", fillcolor="#FFF3E0"];
// Connections // 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"]; woodpecker -> registry [label="Push"];
registry -> compose_ec2 [label="Pull"]; registry -> edge_relay [label="Pull", style=dashed];
registry -> k8s_local [label="Pull", style=dashed]; registry -> aggregator [label="Pull", style=dashed, lhead=cluster_local];
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];
} }

View File

@@ -1,221 +1,197 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> "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 --> <!-- Title: Deployment Pages: 1 -->
<svg width="872pt" height="662pt" <svg width="743pt" height="439pt"
viewBox="0.00 0.00 872.00 662.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 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 658.3)"> <g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 435.03)">
<title>Deployment</title> <title>Deployment</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-658.3 868,-658.3 868,4 -4,4"/> <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="432" y="-637" font-family="Helvetica,sans-Serif" font-size="14.00">Deployment Architecture</text> <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"> <g id="clust1" class="cluster">
<title>cluster_local</title> <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"/> <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="123" y="-497.25" font-family="Helvetica,sans-Serif" font-size="14.00">Local Development</text> <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>
<g id="clust2" class="cluster"> <g id="clust2" class="cluster">
<title>cluster_kind</title> <title>cluster_aws</title>
<polygon fill="#bbdefb" stroke="black" points="16,-315.77 16,-481.3 124,-481.3 124,-315.77 16,-315.77"/> <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="70" y="-464" font-family="Helvetica,sans-Serif" font-size="14.00">Kind Cluster</text> <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>
<g id="clust3" class="cluster"> <g id="clust3" class="cluster">
<title>cluster_aws</title> <title>cluster_cicd</title>
<polygon fill="#e8f5e9" stroke="black" points="642,-8 642,-514.55 856,-514.55 856,-8 642,-8"/> <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="749" y="-497.25" font-family="Helvetica,sans-Serif" font-size="14.00">AWS (sysmonstm.mcrn.ar)</text> <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>
<g id="clust4" class="cluster"> <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> <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"/> <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="496" y="-464" font-family="Helvetica,sans-Serif" font-size="14.00">Monitored Machines</text> <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> </g>
<!-- tilt --> <!-- aggregator -->
<g id="node1" class="node"> <g id="node1" class="node">
<title>tilt</title> <title>aggregator</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"/> <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"/>
<polyline fill="none" stroke="black" points="29.75,-444.05 33.75,-444.05 33.75,-440.05 29.75,-440.05"/> <text xml:space="preserve" text-anchor="middle" x="342" y="-120.5" font-family="Helvetica,sans-Serif" font-size="10.00">Aggregator</text>
<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="342" y="-107.75" font-family="Helvetica,sans-Serif" font-size="10.00">(gRPC Server)</text>
<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>
</g> </g>
<!-- k8s_local --> <!-- redis -->
<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 -->
<g id="node4" class="node"> <g id="node4" class="node">
<title>compose_ec2</title> <title>redis</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"/> <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"/>
<text xml:space="preserve" text-anchor="middle" x="707" y="-345.02" font-family="Helvetica,sans-Serif" font-size="10.00">Docker Compose</text> <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="707" y="-332.27" font-family="Helvetica,sans-Serif" font-size="10.00">(All Services)</text> <text xml:space="preserve" text-anchor="middle" x="334" y="-30.88" font-family="Helvetica,sans-Serif" font-size="10.00">Redis</text>
</g> </g>
<!-- sqs --> <!-- aggregator&#45;&gt;redis -->
<g id="node8" class="node"> <g id="edge4" class="edge">
<title>sqs</title> <title>aggregator&#45;&gt;redis</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"/> <path fill="none" stroke="black" d="M340.3,-99.02C339.29,-88.75 337.98,-75.45 336.82,-63.64"/>
<text xml:space="preserve" text-anchor="middle" x="707" y="-246.42" font-family="Helvetica,sans-Serif" font-size="10.00">SQS</text> <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="707" y="-233.67" font-family="Helvetica,sans-Serif" font-size="10.00">(Buffer)</text> <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> </g>
<!-- compose_ec2&#45;&gt;sqs --> <!-- timescaledb -->
<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 -->
<g id="node5" class="node"> <g id="node5" class="node">
<title>nginx</title> <title>timescaledb</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"/> <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"/>
<text xml:space="preserve" text-anchor="middle" x="709" y="-433.3" font-family="Helvetica,sans-Serif" font-size="10.00">Nginx</text> <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="709" y="-420.55" font-family="Helvetica,sans-Serif" font-size="10.00">(SSL Termination)</text> <text xml:space="preserve" text-anchor="middle" x="419" y="-30.88" font-family="Helvetica,sans-Serif" font-size="10.00">TimescaleDB</text>
</g> </g>
<!-- nginx&#45;&gt;compose_ec2 --> <!-- aggregator&#45;&gt;timescaledb -->
<g id="edge5" class="edge"> <g id="edge5" class="edge">
<title>nginx&#45;&gt;compose_ec2</title> <title>aggregator&#45;&gt;timescaledb</title>
<path fill="none" stroke="black" d="M708.6,-411.59C708.33,-400.13 707.98,-384.86 707.67,-371.63"/> <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="711.17,-371.63 707.44,-361.72 704.17,-371.79 711.17,-371.63"/> <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="720.43" y="-380.47" font-family="Helvetica,sans-Serif" font-size="9.00">Proxy</text> <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> </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"> <g id="node6" class="node">
<title>lambda_agg</title> <title>edge_relay</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"/> <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="707" y="-130.56" font-family="Helvetica,sans-Serif" font-size="10.00">Aggregator</text> <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="707" y="-117.81" font-family="Helvetica,sans-Serif" font-size="10.00">Lambda</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> </g>
<!-- lambda_compact --> <!-- gateway&#45;&gt;edge_relay -->
<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 -->
<g id="edge7" class="edge"> <g id="edge7" class="edge">
<title>sqs&#45;&gt;lambda_agg</title> <title>gateway&#45;&gt;edge_relay</title>
<path fill="none" stroke="black" d="M707,-215.47C707,-197.96 707,-175.06 707,-157.13"/> <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="710.5,-157.15 707,-147.15 703.5,-157.15 710.5,-157.15"/> <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="722.75" y="-189.26" font-family="Helvetica,sans-Serif" font-size="9.00">Trigger</text> <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> </g>
<!-- woodpecker --> <!-- woodpecker -->
<g id="node10" class="node"> <g id="node7" class="node">
<title>woodpecker</title> <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"/> <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="298" y="-566.67" font-family="Helvetica,sans-Serif" font-size="10.00">Woodpecker CI</text> <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> </g>
<!-- registry --> <!-- registry -->
<g id="node11" class="node"> <g id="node8" class="node">
<title>registry</title> <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="#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="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"/> <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="298" y="-433.3" font-family="Helvetica,sans-Serif" font-size="10.00">Container</text> <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="298" y="-420.55" font-family="Helvetica,sans-Serif" font-size="10.00">Registry</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> </g>
<!-- woodpecker&#45;&gt;registry --> <!-- woodpecker&#45;&gt;registry -->
<g id="edge2" class="edge"> <g id="edge9" class="edge">
<title>woodpecker&#45;&gt;registry</title> <title>woodpecker&#45;&gt;registry</title>
<path fill="none" stroke="black" d="M298,-551.35C298,-529.66 298,-492.15 298,-464.77"/> <path fill="none" stroke="black" d="M583,-328.28C583,-313.79 583,-292.67 583,-274.84"/>
<polygon fill="black" stroke="black" points="301.5,-464.88 298,-454.88 294.5,-464.88 301.5,-464.88"/> <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="308.88" y="-525.25" font-family="Helvetica,sans-Serif" font-size="9.00">Push</text> <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> </g>
<!-- registry&#45;&gt;k8s_local --> <!-- registry&#45;&gt;aggregator -->
<g id="edge4" class="edge"> <g id="edge11" class="edge">
<title>registry&#45;&gt;k8s_local</title> <title>registry&#45;&gt;aggregator</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"/> <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="128.47,-355.44 117.89,-355.97 126.49,-362.15 128.47,-355.44"/> <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="222.42" y="-380.47" font-family="Helvetica,sans-Serif" font-size="9.00">Pull</text> <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> </g>
<!-- registry&#45;&gt;compose_ec2 --> <!-- registry&#45;&gt;edge_relay -->
<g id="edge3" class="edge"> <g id="edge10" class="edge">
<title>registry&#45;&gt;compose_ec2</title> <title>registry&#45;&gt;edge_relay</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"/> <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="646.02,-349.01 655.78,-344.88 645.58,-342.02 646.02,-349.01"/> <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="427.09" y="-380.47" font-family="Helvetica,sans-Serif" font-size="9.00">Pull</text> <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> </g>
<!-- coll1 --> <!-- coll1 -->
<g id="node12" class="node"> <g id="node9" class="node">
<title>coll1</title> <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"/> <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="497" y="-433.3" font-family="Helvetica,sans-Serif" font-size="10.00">Collector</text> <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="497" y="-420.55" font-family="Helvetica,sans-Serif" font-size="10.00">(Machine 1)</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> </g>
<!-- coll1&#45;&gt;compose_ec2 --> <!-- coll1&#45;&gt;aggregator -->
<g id="edge9" class="edge"> <g id="edge1" class="edge">
<title>coll1&#45;&gt;compose_ec2</title> <title>coll1&#45;&gt;aggregator</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"/> <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="640.37,-365.52 648.58,-358.82 637.98,-358.94 640.37,-365.52"/> <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="602.75" y="-380.47" font-family="Helvetica,sans-Serif" font-size="9.00">gRPC</text> <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> </g>
<!-- coll2 --> <!-- coll2 -->
<g id="node13" class="node"> <g id="node10" class="node">
<title>coll2</title> <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"/> <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="589" y="-433.3" font-family="Helvetica,sans-Serif" font-size="10.00">Collector</text> <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="589" y="-420.55" font-family="Helvetica,sans-Serif" font-size="10.00">(Machine 2)</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> </g>
<!-- coll2&#45;&gt;compose_ec2 --> <!-- coll2&#45;&gt;aggregator -->
<g id="edge10" class="edge"> <g id="edge2" class="edge">
<title>coll2&#45;&gt;compose_ec2</title> <title>coll2&#45;&gt;aggregator</title>
<path fill="none" stroke="black" d="M612.88,-411.59C621.13,-405.55 630.83,-398.47 640.8,-391.17"/> <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="642.77,-394.07 648.78,-385.34 638.64,-388.41 642.77,-394.07"/> <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="670.19" y="-380.47" font-family="Helvetica,sans-Serif" font-size="9.00">gRPC</text> <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> </g>
<!-- coll3 --> <!-- coll3 -->
<g id="node14" class="node"> <g id="node11" class="node">
<title>coll3</title> <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"/> <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="404" y="-433.3" font-family="Helvetica,sans-Serif" font-size="10.00">Collector</text> <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="404" y="-420.55" font-family="Helvetica,sans-Serif" font-size="10.00">(Machine N)</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> </g>
<!-- coll3&#45;&gt;compose_ec2 --> <!-- coll3&#45;&gt;aggregator -->
<g id="edge11" class="edge"> <g id="edge3" class="edge">
<title>coll3&#45;&gt;compose_ec2</title> <title>coll3&#45;&gt;aggregator</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"/> <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="639.16,-354.39 648.5,-349.4 638.08,-347.48 639.16,-354.39"/> <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="516.54" y="-380.47" font-family="Helvetica,sans-Serif" font-size="9.00">gRPC</text> <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>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -86,8 +86,7 @@ main {
border-radius: 4px; border-radius: 4px;
padding: 1rem; padding: 1rem;
margin-bottom: 1rem; margin-bottom: 1rem;
overflow: auto; overflow: visible;
max-height: 400px;
} }
.graph-preview img { .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. 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 ## Phase 3: Alerts - Adding Intelligence
The alerts service subscribes to metric events and evaluates them against rules. 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 | | Redis events | `shared/events/redis_pubsub.py` | Redis Pub/Sub implementation |
| Configuration | `shared/config.py` | Pydantic settings for all services | | Configuration | `shared/config.py` | Pydantic settings for all services |
| DB initialization | `scripts/init-db.sql` | TimescaleDB schema, hypertables | | 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 ## Running It

View File

@@ -80,39 +80,6 @@
</header> </header>
<main> <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 --> <!-- Architecture Diagrams -->
<section class="graph-section" id="overview"> <section class="graph-section" id="overview">
<div class="graph-header-row"> <div class="graph-header-row">
@@ -155,6 +122,11 @@
<strong>Alerts</strong>: Subscribes to events, <strong>Alerts</strong>: Subscribes to events,
evaluates thresholds, triggers actions evaluates thresholds, triggers actions
</li> </li>
<li>
<strong>Edge</strong>: Lightweight WebSocket
relay on AWS, serves public dashboard at
sysmonstm.mcrn.ar
</li>
</ul> </ul>
</div> </div>
</section> </section>
@@ -245,16 +217,17 @@
<h4>Environments</h4> <h4>Environments</h4>
<ul> <ul>
<li> <li>
<strong>Local Dev</strong>: Kind + Tilt for K8s, or <strong>Local</strong>: Docker Compose with
Docker Compose aggregator, gateway, Redis, TimescaleDB, alerts
</li> </li>
<li> <li>
<strong>Demo (EC2)</strong>: Docker Compose on <strong>Edge (AWS)</strong>: Lightweight
t2.small at sysmonstm.mcrn.ar WebSocket relay at sysmonstm.mcrn.ar, receives
forwarded metrics from local gateway
</li> </li>
<li> <li>
<strong>Lambda Pipeline</strong>: SQS-triggered <strong>Collectors</strong>: Run on remote
aggregation for data processing experience machines, stream to local aggregator via gRPC
</li> </li>
</ul> </ul>
</div> </div>
@@ -301,7 +274,7 @@
<hr class="section-divider" /> <hr class="section-divider" />
<section class="findings-section"> <section class="findings-section">
<h2>Interview Talking Points</h2> <h2>Key Design Decisions</h2>
<div class="findings-grid"> <div class="findings-grid">
<article class="finding-card"> <article class="finding-card">
<h3>Domain Mapping</h3> <h3>Domain Mapping</h3>
@@ -366,16 +339,13 @@
<h3>Infrastructure</h3> <h3>Infrastructure</h3>
<ul> <ul>
<li>Docker</li> <li>Docker</li>
<li>Kubernetes</li> <li>Docker Compose</li>
<li>Kind + Tilt</li>
<li>Terraform</li>
</ul> </ul>
</div> </div>
<div class="tech-column"> <div class="tech-column">
<h3>CI/CD</h3> <h3>CI/CD</h3>
<ul> <ul>
<li>Woodpecker CI</li> <li>Woodpecker CI</li>
<li>Kustomize</li>
<li>Container Registry</li> <li>Container Registry</li>
</ul> </ul>
</div> </div>
@@ -386,7 +356,7 @@
<footer> <footer>
<p>System Monitoring Platform - Documentation</p> <p>System Monitoring Platform - Documentation</p>
<p class="date"> <p class="date">
Generated: <time datetime="2025-12-31">December 2025</time> Generated: <time datetime="2026-03-16">March 2026</time>
</p> </p>
</footer> </footer>
</body> </body>

View File

@@ -22,7 +22,8 @@ RUN python -m grpc_tools.protoc \
-I/app/proto \ -I/app/proto \
--python_out=/app/shared \ --python_out=/app/shared \
--grpc_python_out=/app/shared \ --grpc_python_out=/app/shared \
/app/proto/metrics.proto /app/proto/metrics.proto \
&& sed -i 's/^import metrics_pb2/from shared import metrics_pb2/' /app/shared/metrics_pb2_grpc.py
COPY services/aggregator /app/services/aggregator COPY services/aggregator /app/services/aggregator

View File

@@ -18,7 +18,8 @@ RUN python -m grpc_tools.protoc \
-I/app/proto \ -I/app/proto \
--python_out=/app/shared \ --python_out=/app/shared \
--grpc_python_out=/app/shared \ --grpc_python_out=/app/shared \
/app/proto/metrics.proto /app/proto/metrics.proto \
&& sed -i 's/^import metrics_pb2/from shared import metrics_pb2/' /app/shared/metrics_pb2_grpc.py
COPY services/alerts /app/services/alerts COPY services/alerts /app/services/alerts

View File

@@ -26,7 +26,8 @@ RUN python -m grpc_tools.protoc \
-I/app/proto \ -I/app/proto \
--python_out=/app/shared \ --python_out=/app/shared \
--grpc_python_out=/app/shared \ --grpc_python_out=/app/shared \
/app/proto/metrics.proto /app/proto/metrics.proto \
&& sed -i 's/^import metrics_pb2/from shared import metrics_pb2/' /app/shared/metrics_pb2_grpc.py
# Copy service code # Copy service code
COPY services/collector /app/services/collector COPY services/collector /app/services/collector

View File

@@ -18,7 +18,8 @@ RUN python -m grpc_tools.protoc \
-I/app/proto \ -I/app/proto \
--python_out=/app/shared \ --python_out=/app/shared \
--grpc_python_out=/app/shared \ --grpc_python_out=/app/shared \
/app/proto/metrics.proto /app/proto/metrics.proto \
&& sed -i 's/^import metrics_pb2/from shared import metrics_pb2/' /app/shared/metrics_pb2_grpc.py
COPY services/gateway /app/services/gateway COPY services/gateway /app/services/gateway
COPY services/aggregator/__init__.py /app/services/aggregator/__init__.py COPY services/aggregator/__init__.py /app/services/aggregator/__init__.py

View File

@@ -32,6 +32,11 @@ logger = setup_logging(
log_format=config.log_format, log_format=config.log_format,
) )
# Edge forwarding config
EDGE_URL = config.edge_url if hasattr(config, "edge_url") else None
EDGE_API_KEY = config.edge_api_key if hasattr(config, "edge_api_key") else ""
edge_ws = None
# WebSocket connection manager # WebSocket connection manager
class ConnectionManager: class ConnectionManager:
@@ -77,6 +82,54 @@ timescale: TimescaleStorage | None = None
grpc_channel: grpc.aio.Channel | None = None grpc_channel: grpc.aio.Channel | None = None
grpc_stub: metrics_pb2_grpc.MetricsServiceStub | None = None grpc_stub: metrics_pb2_grpc.MetricsServiceStub | None = None
async def connect_to_edge():
"""Maintain persistent WebSocket connection to edge and forward metrics."""
global edge_ws
if not EDGE_URL:
logger.info("edge_not_configured", msg="No EDGE_URL set, running local only")
return
import websockets
url = EDGE_URL
if EDGE_API_KEY:
separator = "&" if "?" in url else "?"
url = f"{url}{separator}key={EDGE_API_KEY}"
while True:
try:
logger.info("edge_connecting", url=EDGE_URL)
async with websockets.connect(url) as ws:
edge_ws = ws
logger.info("edge_connected")
while True:
try:
msg = await asyncio.wait_for(ws.recv(), timeout=30)
# Ignore messages from edge (pings, etc)
except asyncio.TimeoutError:
await ws.ping()
except asyncio.CancelledError:
break
except Exception as e:
edge_ws = None
logger.warning("edge_connection_error", error=str(e))
await asyncio.sleep(5)
async def forward_to_edge(data: dict):
"""Forward metrics to edge if connected."""
global edge_ws
if edge_ws:
try:
await edge_ws.send(json.dumps(data))
except Exception as e:
logger.warning("edge_forward_error", error=str(e))
# Track recent events for internals view # Track recent events for internals view
recent_events: list[dict] = [] recent_events: list[dict] = []
MAX_RECENT_EVENTS = 100 MAX_RECENT_EVENTS = 100
@@ -133,15 +186,17 @@ async def event_listener():
merged_payload = event.payload merged_payload = event.payload
# Broadcast merged data to dashboard # Broadcast merged data to dashboard
await manager.broadcast( broadcast_msg = {
{
"type": "metrics", "type": "metrics",
"data": merged_payload, "data": merged_payload,
"timestamp": event.timestamp.isoformat(), "timestamp": event.timestamp.isoformat(),
} }
) await manager.broadcast(broadcast_msg)
service_stats["websocket_broadcasts"] += 1 service_stats["websocket_broadcasts"] += 1
# Forward to edge if connected
await forward_to_edge(broadcast_msg)
# Broadcast to internals (show raw event, not merged) # Broadcast to internals (show raw event, not merged)
await internals_manager.broadcast( await internals_manager.broadcast(
{ {
@@ -176,6 +231,9 @@ async def lifespan(app: FastAPI):
# Start event listener in background # Start event listener in background
listener_task = asyncio.create_task(event_listener()) listener_task = asyncio.create_task(event_listener())
# Start edge connection if configured
edge_task = asyncio.create_task(connect_to_edge())
service_stats["started_at"] = datetime.utcnow().isoformat() service_stats["started_at"] = datetime.utcnow().isoformat()
logger.info("gateway_started") logger.info("gateway_started")
@@ -183,10 +241,15 @@ async def lifespan(app: FastAPI):
# Cleanup # Cleanup
listener_task.cancel() listener_task.cancel()
edge_task.cancel()
try: try:
await listener_task await listener_task
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
try:
await edge_task
except asyncio.CancelledError:
pass
if grpc_channel: if grpc_channel:
await grpc_channel.close() await grpc_channel.close()

View File

@@ -74,6 +74,10 @@ class GatewayConfig(BaseConfig):
# TimescaleDB - can be set directly via TIMESCALE_URL # TimescaleDB - can be set directly via TIMESCALE_URL
timescale_url: str = "postgresql://monitor:monitor@localhost:5432/monitor" timescale_url: str = "postgresql://monitor:monitor@localhost:5432/monitor"
# Edge forwarding (optional - for pushing to cloud edge)
edge_url: str = "" # e.g., wss://sysmonstm.mcrn.ar/ws
edge_api_key: str = ""
class AlertsConfig(BaseConfig): class AlertsConfig(BaseConfig):
"""Alerts service configuration.""" """Alerts service configuration."""