C# · .NET 8 · React · TypeScript · ASP.NET Core · MongoDB

SystemMonitor
Monitoring & Logging Platform

A multi-layer monitoring and logging platform in four stages. The worker layer collects evidence from external systems, the rules engine evaluates health and correlates signals into incidents, the interpretation layer renders human-readable Dutch messages, and the notifications engine delivers them by e-mail — all backed by MongoDB and a React dashboard.

4architectural layers
10microservice projects
75unit tests
5trigger types

The dashboard

The React UI reads through the Dashboard API. Each menu item maps to an architectural layer — click through to explore.

SystemMonitor overzicht — system health dashboard

Overzicht (Overview) — the entry point. Shows the health state of every system and recent incidents at a glance. Caldro WMS will turn Onbeschikbaar when a blocking incident fires.

Systemen — system health

One card per system, coloured by its current HealthState. Drill down to see which applications are Unhealthy, Degraded or Healthy.

Systemen — system overview

The Rules engine writes health_states into MongoDB; the Dashboard reads them live.

Incidenten — incident list

A deduplicated, suppressible timeline of incidents grouped by correlation key. Each row shows severity, status, duration, signal count and the owning team.

Incidenten — incident list

Signalen met dezelfde correlatiesleutel ({systemKey}:{applicationKey}:{ruleKey}) worden samengevoegd tot één incident.

Incident detail — drill-down

Open an incident to see the signal, every measurement that fed it, the raw error messages, the timeline and recommended actions. You can acknowledge, resolve or add a note.

Incident detail view

Resolution sets the incident to Resolved, the health jumps back to Gezond, and (with notifyOnResolve) a follow-up e-mail is sent.

Regels — rule configuration

Rules are data, not code. Configure the trigger (threshold, state change, pattern, absence, aggregation), an optional condition tree, and the outcome (severity, health status, templates). Every rule gets a Validate → Dry-run step before saving.

Regels — rule editor

De rules engine leest deze configuratie uit MongoDB en triggers er zonder deployment.

Bronnen — sources & targets

Manage the systems and endpoints the worker layer should monitor: HTTP health-check URLs, polling intervals, timeout, expected status codes and the severity assigned on failure.

Bronnen — source management

Each target is a probe the HttpHealthCollector or bus listener picks up.

Notificaties — notification routing

Build subscriptions that decide who gets what, on which channel, with how much evidence attached, how often (maxNotificationsPerHour) and when to escalate.

Notificaties — subscription management

Een escalatie na  escalateAfterMinutes  breekt de onderdrukking door.

Berichtteksten — templates & labels

Edit the Scriban templates and text labels per key + locale + channel. Use Preview with example incident to see the rendered e-mail immediately.

Berichtteksten — template editor

Alle tekstlogica woont in de interpretatielaag — één plek, consistente berichten.

How it works

Every item flows through an ordered pipeline of layers. Each layer reads its input from MongoDB and writes its output back, with a lightweight message bus (also on MongoDB) signalling the next layer to start.

┌────────────────┐   evidence    ┌──────────────┐   rules   ┌──────────────┐   templates ┌─────────────┐
│  WORKER        │ ───▶          │  RULES       │ ───▶      │  INTER-      │ ───▶        │  NOTIFI-    │
│  layer         │               │  ENGINE      │           │  PRETATION   │             │  CATIONS    │
│                │               │              │           │  layer       │             │  engine     │
│ HTTP probes    │               │ regels uit   │           │ templates +  │             │ delivery    │
│ bus, logs      │               │ Mongo        │           │ teksten      │             │ outbox      │
└────────────────┘               └──────────────┘           └──────────────┘             └─────────────┘
       │                            │                             │                          │
       └────────────────────────── MongoDB ───────────────────────┴──────────────────────────┘
                   (evidence, signals, incidents, health, templates, texts, deliveries, bus)
                            ↳
                            │
                     ┌────────────┐
                     │ Dashboard  │  (read + manage via API + React UI)
                     │ API + UI   │
                     └────────────┘
      

The four layers

Each layer is an independent microservice with its own responsibilities. All configuration lives in MongoDB so a functional administrator can change behaviour without a deployment.

Worker layer

Collector.Worker

Listeners on service busses, HTTP health checks/API-calls, and external logging. Writes evidence to MongoDB and publishes systemmonitor.evidence.collected.

Rules engine

RulesEngine.Worker

Evaluates rules from MongoDB, creates Signal documents, deduplicates them into Incidents by correlation key, and recalculates HealthState.

Interpretation

Interpretation.Api

Applies Scriban templates + text labels, enriches the model (owner, actions, links, impact) and renders per channel. Results are cached in interpreted_messages.

Notifications

Notifications.Worker

Matches subscriptions, fills the delivery outbox, fetches rendered messages from the interpretation layer and sends them via SMTP with retry/backoff. Six failures → DeadLettered.

LayerWrites to MongoDBReads from MongoDBPublishes
Workerevidencetargetssystemmonitor.evidence.collected
Rules enginesignals, incidents, health_statesrules, evidencesystemmonitor.incidents.changed
Interpretationinterpreted_messagesincidents, signals, templates, texts—
Notificationsdeliveriessubscriptions, recipient_groups, interpreted_messages—

Walk-through: the Warehouse API is unreachable

A live end-to-end scenario, step by step through every layer. It is also automated as an integration test: WarehouseScenarioTests.cs.

Trigger: the source warehouse-api-health (URL http://localhost:8080/health, interval 30 s, timeout 10 s) gets no response.
STEP 1

Worker collects evidence

HttpHealthCollector runs HttpProbe. No response within 10 s → an evidence record with isSuccessful = false. A systemmonitor.evidence.collected bus message is published.

STEP 2

Rules engine evaluates

RulesEngineHostedService picks up the bus message. Rule warehouse-api.health has trigger.kind = Threshold with consecutiveFailures = 2. Two failed measurements in a 15-minute window → the rule fires.

STEP 3

Deduplication into an incident

IncidentManager.RegisterAsync() looks up an open incident by the correlation key wms:warehouse-api:warehouse-api.health. None exists, so a new Incident is created (status Open) with a 30-minute suppression window. HealthStateCalculator sets the health of wms to Unhealthy.

STEP 4

Interpretation renders the message

Notifications requests GET /api/v1/interpret/incidents/{id}?channel=Email&locale=nl-NL. The interpretation layer loads the incident, signals and evidence, applies template incident.email (nl-NL) and text labels (severity.Blocking → "Blokkerend"), enriches the model with impact, actions, metrics and links. Result is cached in interpreted_messages.

STEP 5

Notifications sends the e-mail

SubscriptionMatcher finds subscription oncall-blocking (min severity Blocking) and developers-with-exceptions (min severity Error, raw payload). Each channel writes a delivery to the outbox and sends via SMTP. The timeline gets a "Notification sent via Email" entry.

What the recipient sees:

Onderwerp: [Blokkerend] Warehouse API is niet bereikbaar - Caldro WMS/Warehouse API

Body:
[BLOKKEREND · ONBESCHIKBAAR]
Warehouse API is niet bereikbaar
Caldro WMS / Warehouse API (production)

Er is een nieuw incident vastgesteld sinds 24-09-2026 16:28.
Er zijn 2 mislukte metingen gemeten volgens regel "Warehouse API niet bereikbaar".
Dit blokkeert de business: start onmiddellijk onderzoek en escalatie.

Foutmeldingen
  System.Net.Http.HttpRequestException: Connection refused (warehouse-api:8080)

Tijdlijn  16:28 created — Warehouse API is niet bereikbaar (2 opeenvolgende mislukte metingen).
Acties      controleer bereikbaarheid · bekijk logregels · verwittig on-call · volg runbook
      

Try it yourself

docker compose up --build, then:

  1. Open http://localhost:8082 → Overzicht → Caldro WMS turns Onbeschikbaar after ~1 minute.
  2. Open the incident → see the message, signals, error messages and timeline (Incident detail).
  3. View e-mails at http://localhost:3000 (smtp4dev): one to on-call + WMS owners, one to developers with raw payload.
  4. Check Notificaties for delivery status: Sent, attempts and provider-id.
  5. Resolve via the Oplossen button → incident goes Resolved, health returns to Gezond, resolution e-mail follows.

Rules are data, not code

The trigger, an optional condition tree, and the outcome are all stored in MongoDB. They are evaluated safely — no code or script is ever executed from the database.

{
  "key": "warehouse-api.health",
  "enabled": true,
  "priority": 100,
  "scope": { "applicationKeys": ["warehouse-api"], "environments": ["production"] },
  "trigger": { "kind": "Threshold", "windowMinutes": 15, "consecutiveFailures": 2 },
  "condition": null,
  "outcome": {
    "severity": "Blocking",
    "healthStatus": "Unhealthy",
    "title": "Warehouse API is niet bereikbaar",
    "summaryTemplate": "De health check van {applicationName} faalt ({failCount} van {measurementCount} metingen).",
    "messageKeys": ["incident.email", "incident.dashboard"],
    "runbookKey": "wms-warehouse-api-down"
  },
  "deduplication": { "keyTemplate": "{systemKey}:{applicationKey}:{ruleKey}", "suppressionWindowMinutes": 30 }
}
        

5 trigger types

Threshold, StateChange, Pattern, Absence, Aggregation.

Condition tree

and / or / not with field & operator — fully nestable.

Deduplication

Signals merge into incidents by correlation key with a suppression window.

Dry-run

Validate → Dry-run (60 min) → Save on every rule.

Reliability built in

The design avoids duplicate processing and message loss even with multiple instances.

Single-run lock

Engine lock

IEngineLockRepository uses an atomic find-and-update on engine_locks with a TTL. Only the lease owner runs the rules cycle — safe horizontal scaling.

Outbox

At-least-once delivery

deliveries is an outbox. Pending → Sent, or Failed/DeadLettered with exponential backoff after 6 attempts.

Leased bus

Replaceable message bus

bus_messages uses leasedUntilUtc + processedAtUtc with a 7-day TTL. Swap in NATS/RabbitMQ/Kafka via IMessageBus — the rest stays the same.

Time bucket

Datum + uur

Every signal, incident and message carries a timeBucket (yyyy-MM-ddTHH in UTC). The engine measures on, the dashboard groups on, digests are built on.

Key features

A small but complete platform. Four layers, one database, one dashboard.

Four layers

Worker, Rules engine, Interpretation, Notifications — each a separate microservice.

Configurable

All targets, rules, subscriptions, groups and templates live in MongoDB — editable in the UI.

Data-driven rules

No code in the database. Safe evaluation of triggers and condition trees.

Deduplication & suppression

Signals merge into incidents with a configurable suppression window and escalation.

At-least-once notifications

Outbox with retry, exponential backoff and dead-lettering.

Dutch messaging

Scriban templates + text labels per locale and channel, cached results.

Docker ready

docker compose up --build starts the full stack including Seq, mongo-express and smtp4dev.

Tested

75 unit tests + a full integration test on the warehouse scenario.

About this project

User-driven, not feature-driven. Built to explore clean architecture, event-driven design and configurable monitoring in a real end-to-end flow.

Don't write tools because you can, write them user-driven and start from actual needs. Think about what a user will do and what information they need to execute their action. You don't want to spend money on unused features.

SystemMonitor is platform-independent — .NET 8, MongoDB and Docker run anywhere, on-prem or in any cloud, and the project comes with no costs and is free to use. The architecture is organised in dependency-inverting layers: SharedKernel, Contracts, Domain, Interfaces, Application and Infrastructure form the foundation; the four layer-specific projects sit on top. The whole thing is runnable with Docker Compose or stand-alone with dotnet run per host.

This is boilerplate code and needs further work to finalise. Considerations about the amount of data storage, retention & cleanups, placeholders for advanced text interpretation, and production-ready authentication are still open. Treat it as a starting point, not a drop-in appliance.

Feel free to download and modify — feedback and improvements are welcome.

Download the source

The full SystemMonitor solution — all four layers, the Dashboard API, the React UI, seed data, tests and documentation — in one archive.

Download SystemMonitor.zip
Complete source · .zip archive