Skip to content
AlphaMergeCase study 02 — WatchdogRelease candidate
Case study 02 · Infrastructure software

Watchdog — a monitoring system we had to be right about.

PRTG-class infrastructure monitoring in Node and strict TypeScript. Probes run checks against your devices; the core stores the results, decides state and notifies. Production is a native service with PostgreSQL as its only always-on dependency — no container, no broker, no cluster.

44
Sensor types
5k
Checks / minute
10×
Disk saved, compressed
1
Always-on dependency
2

The problem

A monitoring system is judged entirely on the times it is wrong. A false alarm costs trust; a missed outage costs the customer. Everything below follows from refusing both — which mostly means being paranoid about what happens when a component of the monitoring system itself goes down.

01A failed check must record no reading — a graph breaks its line, never draws to zero
02A wrapped counter reporting four billion is discarded, not plotted
03Results survive the core being down and the probe restarting
04One stored number, so a chart can never disagree with the alarm that fired
05A typo in config stops the process — silent defaults lie for months
3

Architecture

Two services. ms-core owns the API, storage, notifications, maps, reports and the probe endpoint. ms-probe owns the scheduler, check execution and the protocol listeners. They speak over a mutual-TLS wire protocol, and a probe can sit on a remote network with only outbound access.

Results are written to a disk spool before anything else touches them and drained as the core acknowledges them. Kill the core mid-run and nothing is lost; restart the probe and the queue is still there. The core also serves the built web UI itself — in production there is no second process to keep alive.

Node 24 TypeScript strict PostgreSQL TimescaleDB SNMP · ICMP · SSH · syslog · NetFlow systemd · WinSW pino
Core ↔ probe
ms-core
API · state · notify
Web UI (served)
RBAC · credentials
PostgreSQL + Timescale
←mutual TLS→
ms-probe
scheduler · execute
Protocol listeners
Disk spool ⟳
Remote network OK

The spool is the reason an outage in the monitoring system is not also a hole in its own history.

A dark catalogue page headed “Sensor types”, “44 types in the catalogue”. Each collapsed row names one — core.factory, db.mssql, dns.record, http.transaction — with its category, scan weight and parameter count set to the right.
The catalogue: 44 sensor types, each tagged with its category, its scan weight — light, medium or heavy — and how many parameters its form will ask for.
4

Device tree & state

Stylised UI, then the real one
ms-core/Devices 38 up 3 warning 2 down 1 paused
▾Local probe12 sensors
▾gw-01.core10.0.0.1
ping3.8 ms
snmp · cpu18 %
snmp · if-in912 Mbit/s
▾db-02.tlvdown 2m 14s
disk · free %0.8 %
ssh · load2.1
▾Probe — Haifa DCremote
edge-03 (paused)15 min · PSU swap
edge-046 sensors
Pausing a device pauses everything beneath it, in one request.
Only the object that was paused can be resumed.
State machine — sensor
Unknown → Up → Warning → Down

Two consecutive failures move Up → Warning → Down, and recovery goes straight back to Up. How many warnings come first is an inheritable setting, so a group can set it for everything beneath it. Zero means immediate-down.

State log — db-02.tlv
10:31:04Up → Warning · disk free 4.2 % < 5
10:32:04Warning → Down · disk free 0.8 % < 1
10:32:05Notified · email, 2 recipients
10:33:04No reading — check failed, gap recorded

Every transition is appended with what it changed from, why, and the probe's own message.

A dark object tree. Indented rows nest Local probe over Core services and Lab network, then hosts, then sensors; each row carries its latest reading as plain text and a green “Up” badge on the right.
The object tree, live. Every sensor carries its last reading inline: 3/3 replies at min 0.036 ms, 200 in 7.51 ms, 11% across 2 processors.
5

Channels, limits & the gap

A reading is scaled by its channel's factors and passed through a spike filter before anything judges it — bytes become bits, centidegrees become degrees, and a wrapped counter is discarded rather than flattening the graph. The scaled value is what gets stored, judged and drawn. One number, so the chart can never disagree with the alarm.

db-02.tlv · disk · free % · 24 h limits: warn 5 % · error 1 %
warning · 5 %
error · 1 %
10 %
0 %
no
readings
00:0006:0012:0018:00now
The line breaks. It is not drawn to zero, because zero would have been a lie.
Limits

Judged on the scaled reading itself, never a smoothed one — a disk 99 % full is 99 % full. How many warnings come before a Down is the state machine's business, above. Editing one takes effect on the next scan, not the next restart.

Lookups

A channel reporting a status code rather than a measurement is judged by a lookup: 2 becomes "down", 1 becomes "up". Operators ship their own.

Spike filter

A discarded reading leaves a gap, not a zero — and decides nothing. It cannot trigger an alarm or clear one.

One sensor’s overview page: a breadcrumb, six tabs, then a Channels table listing response time, redirects, response size, status code and time to first byte — each with its reading and a “judged by” column — above Configuration and Parameters panels.
One sensor, six tabs, and the tab lives in the URL. Each channel names what judges it, so “why did this not alert” has an answer on the page.
6

Storage — the licence decision

We depend on the TimescaleDB Community licence for exactly one feature: native compression, worth about ten times on disk with no Apache-licensed substitute. Everything above it — the rollup tiers, the retention policy, the loop that drives them — we wrote ourselves, so nothing else is tied to the engine.

To keep that honest, the database can be started under the Apache-only feature set on demand. Any new dependency on a licensed feature then fails immediately, and the migration that needs it refuses to apply at all — with a message saying why.

TIMESCALEDB_LICENSE=apache
→ migration 004 refuses, states the reason
7

Flow, maps & reports

A device that answers every ping can still be the one saturating the line, so the probe now listens for flow records — NetFlow v5 and v9, IPFIX, sFlow — and decodes them itself. They arrive as one more sensor type, judged by the same limits and the same state machine as everything else.

Above that sit two things every monitoring system is eventually asked for. Maps: a designer with snapping and undo, live graphs on the canvas, a background image or a geographic tile source you point at yourself, and a share link for a screen nobody signs in to. Reports: scheduled, run as a named user so they can only contain what that user may see, and delivered as HTML, CSV, XML or PDF.

PDF laid out in-process
→ no headless browser, fonts embedded, CJK renders
8

Status

Honest version
Done
Object model & schedulerState machine & pausingNotificationsWeb UI44 sensor typesDiscoveryRemote probesRBAC, TOTP, API tokensFlow monitoringMaps & dashboardsScheduled reports
In progress
Hardening & releaseField test — Ubuntu half proved and in CIWindows half — MSI built, waiting on a runner
Enforced, not reviewed
No anyNo console.log in packagesJSON Schema at every boundaryErrors carry contextSmoke install on every commit

Need systems software, not a dashboard?

Schedulers, agents, time-series at scale, native installs. This is the kind of work we take on.