A distributed system monitoring solution using Akka.NET Remoting + Streams, ASP.NET Core 8, SignalR, and Angular with Tailwind CSS.
┌─────────────────────────────────────────────────────────────────────────┐ │ Architecture Overview │ ├─────────────────┐ Akka Remote TCP ┌────────────────────────────┐ │ │ Machine A │ ──────────────────► │ MetricsHub │ │ │ Collector │ PushMetrics msg │ (Central Akka Actor) │ │ │ (Akka Actor + │ ◄────────────────── │ Receives from all nodes │ │ │ Streams tick) │ RegisterCollector │ ──────────────────────► │ │ │ REST API │ │ SignalR push to UI │ │ └─────────────────┘ └────────────────────────────┘ │ │ │ ┌─────────────────┐ Akka Remote TCP │ WebSocket │ │ Machine B │ ──────────────────► ▼ │ │ Collector │ ┌────────────────────────────┐ │ │ │ │ Angular UI │ │ │ REST API │ ◄───── HTTP ──────── │ SignalR Client │ │ │ (start/stop) │ │ Tailwind dashboard │ │ └─────────────────┘ └────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ | Project | Description | Ports |
|---|---|---|
Shared | Shared message types for Akka remoting | — |
MetricsHub | Central hub: receives metrics, pushes via SignalR | HTTP: 5000, Akka: 9080 |
MetricsCollector | Per-machine collector with REST API | HTTP: 5001, Akka: 8081 |
angular-ui | Real-time dashboard in Angular + Tailwind | Dev: 4200 |
# From the repo root docker-compose up --build- UI: http://localhost
- Hub API + Swagger: http://localhost:5000/swagger
- Collector 1 API: http://localhost:5001
- Collector 2 API: http://localhost:5002
Start collection on node 1:
curl -X POST http://localhost:5001/api/metrics/start -H "Content-Type: application/json" -d '{"intervalMs": 2000}'1. Start MetricsHub
cd MetricsHub dotnet run # Listening on http://localhost:5000 # Akka TCP on port 90802. Start a Collector (on any machine)
cd MetricsCollector # Edit appsettings.json: set HubAddress to the hub's address dotnet run # REST API on http://localhost:50013. Start the Angular UI
cd angular-ui npm install npm start # Open http://localhost:42004. Start collecting metrics via REST API
# Start collection (default: every 2 seconds) curl -X POST http://localhost:5001/api/metrics/start # With custom interval curl -X POST http://localhost:5001/api/metrics/start -H "Content-Type: application/json" -d '{"intervalMs": 1000}' # Get status curl http://localhost:5001/api/metrics/status # Stop collection curl -X POST http://localhost:5001/api/metrics/stop- Build the collector:
dotnet publish MetricsCollector -c Release - Copy to target machine
- Set environment variables or edit
appsettings.json:{ "Akka": { "NodeId": "machine-name-or-uuid", "Port": "8081", "PublicHostname": "192.168.1.X", "HubAddress": "akka.tcp://MetricsHub@<HUB_IP>:9080/user/metrics-hub" } } - Run:
dotnet MetricsCollector.dll - Start collection:
POST http://<collector-ip>:5001/api/metrics/start
Environment variable override example:
Akka__NodeId=prod-server-1 \ Akka__PublicHostname=10.0.1.50 \ Akka__HubAddress="akka.tcp://MetricsHub@10.0.1.100:9080/user/metrics-hub" \ dotnet MetricsCollector.dllMetricsHubstarts anActorSystemnamedMetricsHublistening on TCP port 9080- Each
MetricsCollectorcreates anActorSystemnamedMetricsCollector - Collectors resolve the hub actor via:
akka.tcp://MetricsHub@<host>:9080/user/metrics-hub - Messages are serialized with
NewtonSoftJsonSerializer
Source.Tick(interval, interval, "tick") .ViaMaterialized(KillSwitches.Single<string>(), Keep.Right) .Select(_ => new CollectTick()) .ToMaterialized(Sink.ActorRef<CollectTick>(self, new StreamCompleted()), Keep.Both) .Run(materializer);The stream ticks at the configured interval, sends messages to the actor itself, which then collects metrics and pushes to the hub.
| Event | Payload |
|---|---|
MetricUpdate | {nodeId, hostname, cpuPercent, ramPercent, ramUsedMb, ramTotalMb, timestamp} |
NodeList | [{nodeId, hostname, remoteAddress, connectedAt}] |
NodeConnected | {nodeId, hostname, remoteAddress, connectedAt} |
NodeDisconnected | {nodeId} |
| Method | Path | Description |
|---|---|---|
| GET | /api/metrics/status | Get collector status |
| POST | /api/metrics/start | Start metric collection |
| POST | /api/metrics/stop | Stop metric collection |
| GET | /health | Health check |
POST /api/metrics/start body:
{ "intervalMs": 2000 }| Method | Path | Description |
|---|---|---|
| GET | /api/nodes | Get connected collectors |
| GET | /health | Health check |
| WS | /hubs/metrics | SignalR WebSocket hub |
{ "Akka": { "Port": "9080", "Hostname": "0.0.0.0", "PublicHostname": "localhost" }, "Kestrel": { "Endpoints": { "Http": { "Url": "http://0.0.0.0:5000" } } } }{ "Akka": { "NodeId": "node-1", "Port": "8081", "PublicHostname": "localhost", "HubAddress": "akka.tcp://MetricsHub@localhost:9080/user/metrics-hub" }, "Kestrel": { "Endpoints": { "Http": { "Url": "http://0.0.0.0:5001" } } } }| Port | Service | Protocol |
|---|---|---|
| 5000 | Hub HTTP/SignalR | TCP |
| 9080 | Hub Akka Remote | TCP |
| 5001 | Collector REST | TCP |
| 8081 | Collector Akka | TCP |
| 80 | Angular UI | TCP |
Collectors need outbound access to hub port 9080. Hub needs inbound access on port 9080.