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`,