Add integrated logger-status system with .status() API, health checks and hierarchical view - #372
Add integrated logger-status system with .status() API, health checks and hierarchical view#372kriszyp wants to merge 6 commits into
Conversation
|
This PR's design is now the foundation of the holistic monitoring overhaul: design doc (Holistic Fleet Monitoring & Status Overhaul, incl. a "Design notes: PR #372 re-land" section — keep/extend/reconsider + phasing), tracking issue HarperFast/harper-pro#532, and re-land issue #1639. Plan is to re-land in phases (extended with — Claude (KrAIs), on behalf of Kris |
…, and hierarchical view
Introduces logger.status({ problem: 'key' }).error('msg') API that both logs
and updates component status, enabling real-time health monitoring without
relying on external log shipping to DataDog.
New modules:
- logBridge.ts: bridges logger.status() calls to ComponentStatusRegistry
- hierarchy.ts: builds tree-structured status view from flat component map
- healthChecks.ts: periodic disk/memory/CPU monitoring via status system
Key changes:
- HarperLogger gains .status() method returning a logger-like wrapper
- ComponentStatus extended with source, expiresAt, occurrenceCount fields
- get_status operation now returns hierarchical status tree
- Status API adopted across replication, threads, server, and database code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nsactions Report database.write-queue-overloaded when write queue exceeds duration limit (before throwing 503). Report database.txn-open-too-long when transactions are force-committed due to timeout. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
export { X } from 'Y' re-exports without creating a local binding.
Added a separate import statement so startHealthChecks is available
at module scope.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…, expires })
logger.status({ problem: 'key', expires: 60 }) now registers status immediately
without logging. Useful when throwing errors that will be logged downstream.
Chained calls like .error('msg') still both log and update status.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…to main - Route the module-level harperLogger.status() through module.exports (not mainLogger directly) so callers/tests that stub the exported notify/fatal/error/... functions still observe calls made via .status().fatal()/.error()/etc — fixes handleServerUncaughtException logging as fatal per its stub-based unit test. - Make loggerWithTag()'s taggedLogger.status non-enumerable so the conditional-logger tests (which enumerate active log levels via for...in) aren't thrown off by the new status method. - Add a named `status` export (in addition to the existing default and CJS module.exports variants) to harper_logger.ts so `import harperLogger from './harper_logger.ts'` consumers (e.g. utility/logging/logger.ts) type-check against the new API. - Run prettier over the status-system files that predate this repo's current formatting config. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
5ad1581 to
126995d
Compare
|
Both previously flagged blockers (double-dispatch, missing test coverage) are fixed in commit 13c539a with regression tests. Found 1 new blocker: startHealthChecks runs unconditionally at import time with no config wiring. See inline comment. |
statusLogger() fired the options-only statusHandler dispatch immediately and
unconditionally, so a chained call like .status({problem}).error(msg) always
dispatched twice, doubling occurrenceCount for nearly every problem key. Defer
the options-only dispatch to a microtask and skip it when a chained log method
already fired synchronously.
Add unit test coverage for the previously-untested logger.status()/logBridge
double-dispatch behavior, buildHierarchy's tree-building/roll-up logic, and the
hierarchy field in status.test.js's getAllStatus tests.
Refs #372
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
| // Initialize the log-to-status bridge immediately so logger.status() calls work | ||
| initLogBridge(); | ||
|
|
||
| // Start health checks on the main thread only | ||
| if (isMainThread) { | ||
| startHealthChecks(); | ||
| } |
There was a problem hiding this comment.
Health checks start unconditionally at import time, with no config wiring
File: components/status/internal.ts:35-41 (see also components/status/healthChecks.ts:116-132)
What: initLogBridge() and startHealthChecks() run as side effects of importing internal.ts — which happens transitively whenever anything imports components/status/index.ts (it re-exports * as internal). startHealthChecks() is called with no arguments, so it always uses the hardcoded defaults (60s interval, 80%/95% thresholds) and unconditionally schedules a real setInterval polling disk/memory/CPU via systeminformation on the main thread. There is no wiring to the status.healthChecks.enabled/intervalSeconds/thresholds config this PR's own design describes (Part 5) — grepping the repo for healthChecks/intervalSeconds in .yaml/config schema turns up nothing outside this PR's TS files.
Why it matters: This is a monitoring feature that, per its own design, is supposed to be configurable (including disabled) via harperdb-config.yaml, but as shipped it can never be turned off and always uses the hardcoded thresholds — a real operational regression for any deployment that wants to opt out. It's also reachable from unitTests/server/status/status.test.js (require('#src/components/status/index') at module scope, line 7), which means every run of that suite kicks off real, unmocked systeminformation disk/mem/cpu polling against the shared componentStatusRegistry singleton — writing system.disk/system.memory/system.cpu entries that persist unless a test explicitly resets the registry (only status.test.js itself does, via beforeEach/after).
Suggested fix: Have startHealthChecks() read enabled/intervalSeconds/thresholds from the server config (or accept an explicit config object from the startup path) instead of running unconditionally, and move the module-level initLogBridge()/startHealthChecks() calls out of a plain import side effect and into an explicit startup hook so importing the status module for its types/registry doesn't also start background OS polling.
Introduces logger.status({ problem: 'key' }).error('msg') API that both logs and updates component status, enabling real-time health monitoring without relying on external log shipping to DataDog.
New modules:
Key changes:
The longer design document that Claude is working from:
Integrated Status System: Explicit Logger-Status API + System Health + Hierarchical View
Context
Harper relies on shipping log files to DataDog for problem detection -- expensive and high overhead. Harper already has a component status system (
core/components/status/) used only by the component loader. The goal: let developers explicitly connect log calls to the status system via a.status()API on the logger, add system health monitoring, and expose everything throughget_statusas a hierarchical status tree a front-end can display.Part 1: Logger
.status()APIDesign
Add a
.status()method to the logger that returns a logger-like object. When you log through it, the message is both logged normally AND updates component status:problem: string-- A status key identifying this problem. Sets/updates a status entry.resolves: string-- A status key to clear. Marks the problem as resolved (sets healthy).expires: number-- Optional seconds until the status auto-clears. Only valid withproblem.fatal/error-> error status,warn-> warning status.info/debug/tracewithproblemwould set warning (since they're lower severity). Withresolves, any level works.Implementation
File:
core/utility/logging/harper_logger.jsAdd a
status()method toHarperLogger:Add a
statusLoggerfunction (similar pattern tologgerWithTag):Export
setStatusHandlerfrom the module. ThestatusHandleris a function injected from the TS status bridge module at startup, avoiding circular dependency (JS logger cannot import TS status modules directly).Also add
.status()to:loggerWithTagreturn objectexternalLoggerfacade (so user code can use it)module.exports.status = ...)Status Handler Module
New file:
core/components/status/logBridge.tsInitialization
In the component startup path (likely
componentLoader.tsor a dedicated status start hook), after the status system is ready:External/User Code
The external logger facade already delegates to the internal logger. Add
.status()to the facade:This is also exposed via
_assignPackageExport('logger', ...), so user application code gets it automatically:Part 2: Status Lifecycle Extensions
Extend
ComponentStatus(core/components/status/ComponentStatus.ts)Add optional fields for richer status tracking:
source?: 'log' | 'explicit' | 'health-check'-- how the status was setexpiresAt?: number-- epoch ms for auto-clearoccurrenceCount?: number-- incremented on repeated reports of same problemExtend
ComponentStatusRegistry(core/components/status/ComponentStatusRegistry.ts)Add to
setStatus:sourceandexpiresAtparametersoccurrenceCountNo acknowledge/dismiss/adjustSeverity for now (deferred to later phase).
Extend
ComponentStatusBuilder(core/components/status/api.ts)Add
sourcetracking -- when called from the log bridge, passsource: 'log'; when called from health checks, passsource: 'health-check'; direct calls default tosource: 'explicit'.Part 3: System Health Monitoring
New Module:
core/components/status/healthChecks.tsPeriodic checks using existing
systemInformation.jsfunctions:Checks:
statusForComponent('system.disk')statusForComponent('system.memory')statusForComponent('system.cpu')Thresholds (configurable via
harperdb-config.yaml):Health checks self-heal: when the metric drops below the threshold, status reverts to healthy. No expiry needed.
Runs on main thread only. Uses
.unref()on interval timer.Part 4: Hierarchical Status View
New Module:
core/components/status/hierarchy.tsBuilds a tree from the flat component status map by splitting names on
.:Parent status = worst child status (error > warning > loading > unknown > healthy).
Extend
GET_STATUSResponseFile:
core/server/status/index.tsModify
getAllStatus()to include the hierarchy:Part 5: Configuration
Add to
harperdb-config.yamlschema:The log bridge itself needs no configuration -- it's explicitly opted into per log call via
.status().Implementation Phases
Phase 1: Logger
.status()API + Bridgestatus()method toHarperLoggerclassstatusLogger()function (returns logger-like wrapper)setStatusHandlerexport and module-level variable.status()tologgerWithTagreturn objects.status()toexternalLoggerfacadecore/components/status/logBridge.tswithhandleStatusLogsetStatusHandler(handleStatusLog)during component startupPhase 2: Status Lifecycle Extensions
source,expiresAt,occurrenceCounttoComponentStatusComponentStatusRegistry.setStatusto handle these fieldsPhase 3: Hierarchical Status View
core/components/status/hierarchy.tsgetAllStatus()incore/server/status/index.tsto include hierarchyAllStatusSummarytypePhase 4: System Health Checks
core/components/status/healthChecks.tsPhase 5: Testing
.status()logger wrapperhandleStatusLog(problem, resolves, expires)buildHierarchy(tree construction, status rollup)Files to Modify
core/utility/logging/harper_logger.jsstatus()to HarperLogger,statusLogger(),setStatusHandler, update exports and facadescore/components/status/ComponentStatus.tssource,expiresAt,occurrenceCountfieldscore/components/status/ComponentStatusRegistry.tssetStatus, expiry timer mgmtcore/components/status/api.tssourcethrough builder methodscore/server/status/index.tsgetAllStatus()responseNew Files
core/components/status/logBridge.tshandleStatusLog-- bridges.status()calls to component statuscore/components/status/hierarchy.tsbuildHierarchy-- builds tree from flat status mapcore/components/status/healthChecks.tsVerification
.status({ problem: 'test' }).error('test error')-> verify it appears inget_statusresponse undercomponentStatusandhierarchy.status({ resolves: 'test' }).info('resolved')-> verify status clears to healthy.status({ problem: 'temp', expires: 5 }).warn('temporary')-> verify it auto-clears after 5 secondshierarchyinget_statusresponse groupssystem.disk,system.memory,system.cpuunder asystemparent.status()API