Monitoring

System, app & DB metrics + alerts

The monitoring block collects operational metrics — host resources, application traffic, database health — persists a rolling history, and raises alerts when thresholds are breached. It exposes everything over HTTP, including a live SSE stream you can wire straight into a dashboard.

Each collector is independently toggleable, so you pay only for what you watch. Alerts add email fan-out with a cooldown so a flapping metric doesn't bury your inbox.

Collectors#

Four metric sources, each switched on independently with its own sub-metric checklist. collectInterval controls how often samples are taken.

config.nucleus.json — monitoring
1{2  "monitoring": {3    "enabled": true,4    "system": {5      "enabled": true,6      "collectInterval": "30s",7      "metrics": { "cpu": true, "memory": true, "disk": true, "process": true }8    },9    "application": {10      "enabled": true,11      "metrics": { "requests": true, "responseTime": true, "errors": true, "rateLimits": true }12    },13    "database": {14      "enabled": true,15      "metrics": { "connections": true, "queryTime": true, "slowQueryThreshold": "500ms" }16    },17    "redis": { "enabled": true },18    "persistence": { "enabled": true, "flushInterval": "60s", "retentionDays": 30 },19    "alerts": {20      "enabled": true,21      "email": { "enabled": true, "recipients": ["[email protected]"] },22      "thresholds": { "cpuPercent": 80, "memoryPercent": 85, "errorRatePercent": 5, "responseTimeMs": 1000 },23      "cooldown": "10m"24    },25    "endpoints": {26      "enabled": true,27      "basePath": "/monitoring",28      "stream": { "enabled": true, "path": "/stream", "interval": "5s" },29      "snapshot": { "enabled": true, "path": "/snapshot" },30      "history": { "enabled": true, "path": "/history", "maxMinutes": 1440 }31    }32  }33}
enabledbooleanOptional

Master switch for the monitoring subsystem. Note: a configured Redis (config.redis) is a HARD precondition — the entire block (MonitoringService construction, collectors, alerts and the HTTP endpoints) is gated on `redis && monitoring.enabled`, so with no Redis wired the subsystem silently does not start even when enabled is true. (config.liveMonitoring is gated independently on its own enabled flag and needs no Redis.)

Defaultfalse
systemobject (enabled default true)Optional

Host metrics (on by default). collectInterval sets the sampling cadence (default 10s — the 30s in the sample above is just an override example); metrics toggles cpu, memory, disk and process (all on by default) plus network (opt-in, defaults false — collected only when explicitly set true, whereas the other four are collected unless explicitly set false).

applicationobject (enabled default true)Optional

In-process metrics (on by default). metrics toggles requests, responseTime, errors and rateLimits — your real-time picture of API behaviour.

databaseobject (enabled default false)Optional

Postgres health — OPT-IN (default off). metrics toggles connections and queryTime, with slowQueryThreshold (default 100ms — the 500ms in the sample above is just an override example) marking the boundary for the slow-query count.

redis{ enabled?: boolean } (default false)Optional

Toggle collection of Redis health metrics — OPT-IN (default off).

Persistence#

Metrics are buffered in memory and periodically flushed to the monitoring_metrics system table so you keep history across restarts. Persistence is ON by default: with a database wired and persistence.enabled not explicitly false, monitoring adds monitoring_metrics to the generated schema definition (feature_set 'monitoring') — but the table is NOT auto-created at runtime. You must re-run nucleus-generate (and migrate) for it to exist; until then the plugin logs a startup warning and keeps metrics in Redis only, skipping the DB flush. The table is exposed read-only through the standard entity routes — GET only, mutating methods are not generated. Tune the flush cadence and how long history is retained.

persistenceobjectOptional

History storage configuration.

enabledbooleanOptional

Persist collected metrics to the database. Defaults to true — persistence is ON whenever monitoring is enabled with a DB wired. This adds monitoring_metrics to the generated schema, but the table is only created when you re-run nucleus-generate; if it is still missing at startup the plugin warns and keeps metrics in Redis only (no DB flush). Set it false to opt out, which keeps monitoring_metrics out of the generated schema and skips writing to it.

Defaulttrue
flushIntervalstringOptional

How often buffered metrics are written (e.g. 60s).

Default1m
retentionDaysnumberOptional

How many days of history to keep before pruning.

Default30

Alerts#

Fire notifications when a metric crosses a threshold. Email recipients receive the alert, and a cooldown prevents alert storms from a metric hovering at the boundary.

alertsobjectOptional

Threshold-based alerting.

enabledbooleanOptional

Master switch for threshold alerting — OFF by default even when monitoring collection is enabled; set true to run AlertService after each collect (checkAndAlert early-returns while this is false).

Defaultfalse
email{ enabled?; recipients?: string[] }Optional

Email delivery of alerts to a recipient list.

thresholdsobjectOptional

The trip points: cpuPercent, memoryPercent, diskPercent, errorRatePercent, responseTimeMs and rateLimitBlocksPerMinute.

cooldownstringOptional

Minimum gap between repeat alerts for the same condition.

Endpoints#

Monitoring is exposed over HTTP under a base path, with a live stream, a point-in-time snapshot, a history query and an alerts feed.

endpointsobjectOptional

HTTP surface for metrics.

enabledbooleanOptional

Gates the whole monitoring HTTP surface — defaults OFF and must be set to true explicitly. When false (the default) the routes are not mounted at all (checked before createMonitoringRoutes runs), so enabling monitoring collection alone exposes no HTTP endpoints. The stream/snapshot/history/alerts sub-endpoints (which each default on) are only reachable once this parent gate is truthy.

basePathstringOptional

Prefix for all monitoring routes.

stream{ enabled?; path?; interval? }Optional

Server-Sent Events live feed; interval sets push frequency. enabled defaults on — the route is skipped only when explicitly set to false.

snapshot{ enabled?; path? }Optional

GET {basePath}{snapshot.path} (default GET /monitoring/snapshot) returns the current metric values in a single response. enabled defaults on — the route is skipped only when explicitly set to false.

history{ enabled?; path?; maxMinutes? }Optional

GET /history serves the in-Redis rolling window, capped at roughly the last hour — so maxMinutes effectively tops out around 60 and a larger value (e.g. 1440) yields no extra data. enabled defaults on — the route is skipped only when explicitly set to false. Long-term history persisted in monitoring_metrics is queried via the read-only entity routes on that table.

alerts{ enabled?; path? }Optional

GET {basePath}{alerts.path} (default GET /monitoring/alerts) reads the ever-triggered alerts (latest event per type). Note active alerts NEVER auto-clear: once a metric type breaches it stays in the in-memory list — and keeps streaming over SSE — for the process lifetime, even after the metric normalizes (getActiveAlerts returns every type ever fired; the clearAlert method that would remove one is unused dead code). Enabling it also mounts a companion POST {basePath}{alerts.path}/:alertId/acknowledge (default POST /monitoring/alerts/:alertId/acknowledge) that returns isSuccess:false with 'Alert not found' when the alertId is unknown — there is no generated client action, call it by method+path. Acknowledge only sets acknowledged:true on the matching alert; it is a no-op for delivery — the alert is neither removed nor stopped from streaming, so a dashboard cannot dismiss an alert through this endpoint. enabled defaults on — both routes are skipped only when explicitly set to false.

From the frontend (live-monitoring actions)#

These four actions belong to the live-monitoring subsystem (config.liveMonitoring), not the persisted metrics feature. MONITORING_GET_LOGS returns the live in-memory activity feed (memory/CPU/Dapr/WebSocket/request), NOT persisted metric history for charts; MONITORING_GET_SETTINGS / MONITORING_CHANGE_SETTINGS read and patch the live-monitoring settings; MONITORING_HEALTH_CHECK hits its health route. The live SSE feed is read straight from the stream path.

MONITORING_HEALTH_CHECKGET · /monitoring/health

A simple status + timestamp probe for an uptime badge.

MONITORING_GET_LOGSGET · /monitoring/logs

Pull the recent live in-memory activity feed (memory/CPU/Dapr/WebSocket/request) — this is the live-monitoring feed, not persisted metric history for charts.

MONITORING_GET_SETTINGS / MONITORING_CHANGE_SETTINGSGET|PATCH · /monitoring/settings

Read or tune which feeds are captured and at what cadence, from an admin panel.

Under the hood — the collect/flush loop#

MonitoringService runs two timers: one collects a snapshot on collectInterval, the other flushes accumulated metric points to the database on flushInterval. Redis holds the hot data; the database holds history.

collectorssystem · application · database · redisOptional

SystemCollector reads CPU/memory/disk/network/process (network via /proc/net/dev on Linux); ApplicationCollector tallies requests/response-time/errors/rate-limit blocks (fed by recordRequest / recordRateLimitBlock from the middleware); DatabaseCollector queries pg_stat_activity/pg_stat_database for connections and query timings/slow-query count (avg query time on PG14+); RedisCollector parses INFO for memory/clients/ops and hit rate (direct connection mode only — skipped under Dapr). Each enabled metric is flattened into dotted MetricPoints (e.g. system.memory.usagePercent).

hot data in Redislatest + 1h historyOptional

Every snapshot is written to monitoring:{appId}:latest (1h TTL) and appended to a monitoring:{appId}:history list trimmed to the last hour — so the stream/snapshot/history endpoints get instant recent data without touching the database. (This Redis cache is separate from the live-monitoring GET_LOGS feed, which reads its own in-memory store, not these keys.)

persistence flushflushToDb + retentionDaysOptional

Collected MetricPoints buffer in memory and are flushed in batches into the monitoring_metrics table on flushInterval (default 1m); a failed flush re-queues them. A daily prune deletes rows older than retentionDays.

alertsthresholds + cooldownOptional

After each collect, AlertService compares the snapshot against thresholds (cpu 80%, memory 85%, disk 90%, error-rate 5%, response-time 1000ms, rate-limit blocks 100/min by default). A breach raises an alert and, if configured, emails the recipients — then a cooldown (default 5m) suppresses re-alerting of that type for the window. The cooldown throttles the ENTIRE re-alert, not just email: within the window createAlert returns null before building the event, so neither the activeAlerts entry is refreshed nor an email is sent. Streaming persists purely because the alert entry is never auto-cleared, so getActiveAlerts (backing GET /alerts and the SSE 'alerts' event) keeps returning it for the process lifetime even after the metric normalizes. Note the retained entry is NOT frozen at the first breach: each breach outside the cooldown window overwrites the map entry with a fresh value/timestamp (createAlert sets lastAlertTime[type] and activeAlerts.set(type, alert)), while breaches inside the window early-return null without updating. So the surviving entry reflects the LAST breach before normalization — equal to the first breach only when the metric normalizes within a single cooldown window. Acknowledge flips acknowledged:true but does not remove it, and clearAlert (the only method that deletes an entry) is unused.

Related sections