From e98175b1a67528ea466df5244880b580451a3855 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 18:44:06 +0000 Subject: [PATCH] docs(protocol): document the real checkMethod health-check contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TypeScript example under "Custom Health Checks" declared `healthChecks`, a map of async functions taking `({ context })`, on the plugin default export. No such field exists: measured as an author-writable field it has 0 occurrences (the hits in packages/ are PluginHealthMonitor's own private Map plus one example reading it). The real configuration is PluginHealthCheckSchema.checkMethod — a method NAME, invoked with no arguments, whose return is read only for `false` / `{ status: 'unhealthy' }`. Rewrite the example against that contract, in the two halves it actually has: the plugin exposes a plain method, and the embedding application constructs PluginHealthMonitor and names the method in the parsed config. No declarative plugin field is shown, which keeps the section's callout ("not yet ... as a declarative plugin field") true — that callout was verified correct and is left untouched, as is the PluginHealthReport block below. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx --- content/docs/protocol/kernel/lifecycle.mdx | 69 +++++++++++++++------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/content/docs/protocol/kernel/lifecycle.mdx b/content/docs/protocol/kernel/lifecycle.mdx index 3f277a923a..abb6d95a40 100644 --- a/content/docs/protocol/kernel/lifecycle.mdx +++ b/content/docs/protocol/kernel/lifecycle.mdx @@ -665,34 +665,59 @@ health-monitor model below, not to this body. ### Custom Health Checks -Plugins can register custom health checks: +A plugin can expose **one** custom health check, and it takes two halves: the +plugin defines an ordinary method, and whoever runs the monitor names that +method in the plugin's `PluginHealthCheck` config. There is no `healthChecks` +field to declare — the monitor resolves the method dynamically off the plugin +object (`plugin[checkMethod]`), so it is not a member of the `Plugin` interface +either. ```typescript -// Plugin registers a custom health check (internal health-monitor model) -export default { +import { PluginHealthMonitor } from '@objectstack/core'; +import { PluginHealthCheckSchema } from '@objectstack/spec/kernel'; + +// 1. The plugin side — a plain method, invoked with NO arguments. +export const salesforcePlugin = { name: '@vendor/salesforce', - healthChecks: { - salesforce_connection: async ({ context }) => { - try { - // Test Salesforce API - const response = await salesforce.query('SELECT Id FROM Account LIMIT 1'); - - return { - status: 'healthy', - latency_ms: response.duration, - records_synced_last_hour: await getSyncCount(), - }; - } catch (error) { - return { - status: 'unhealthy', - error: error.message, - }; - } - }, + // The name is free; `checkMethod` below is what points at it. + async healthCheck() { + try { + await salesforce.query('SELECT Id FROM Account LIMIT 1'); + return true; + } catch (error) { + return { status: 'unhealthy', message: error.message }; + } }, }; -``` + +// 2. The host side — the embedding application owns the monitor. +const monitor = new PluginHealthMonitor(kernel.logger); + +// `registerPlugin` takes the PARSED config, so parse it: the schema fills in +// interval 30000, timeout 5000, failureThreshold 3, successThreshold 1, +// autoRestart false, maxRestartAttempts 3, restartBackoff 'exponential'. +monitor.registerPlugin( + salesforcePlugin.name, + PluginHealthCheckSchema.parse({ checkMethod: 'healthCheck' }), +); + +monitor.startMonitoring(salesforcePlugin.name, salesforcePlugin); +``` + +`startMonitoring` runs one check immediately, then repeats every `interval` +milliseconds; each run is raced against `timeout`, and the method may be +synchronous or return a promise. + +Only two returned shapes count as a failure: `false`, and an object whose +`status` is exactly `'unhealthy'` (whose `message`, if any, becomes the +report's). **Everything else passes** — `true`, `undefined`, +`{ status: 'healthy' }` and a rich object of metrics alike, so a check has +nowhere to publish latency or row counts: no key beyond `status` and `message` +is read. Consecutive returned failures move the plugin to `degraded` first, and +to `unhealthy` only once `failureThreshold` of them accumulate. A check that +**throws** — including one that exceeds `timeout` — is the separate `failed` +status, applied immediately with no threshold. The monitor keeps one report per plugin rather than one aggregate document. Each round of checks builds a `PluginHealthReport` (`@objectstack/spec/kernel`,