Creating an Event¶
SELECT pghf.create_event(
p_check_id text,
p_event_type text, -- 'notify' or 'function'
p_min_severity text, -- 'info', 'warning', or 'critical'
p_channel_name text DEFAULT NULL, -- required for 'notify'
p_routine regprocedure DEFAULT NULL, -- required for 'function'
p_function_args jsonb DEFAULT NULL, -- optional, 'function' only
p_is_active boolean DEFAULT true
);
-- => returns the new event_id (bigint)
| Parameter | Meaning |
|---|---|
p_check_id |
The check (for example 'PGHF11-009', 'X01-001') this event watches. Any existing check — built-in or your own namespace, no restriction either way. |
p_event_type |
'notify' sends a database notification; 'function' calls a routine you wrote. See the two subsections below. |
p_min_severity |
The floor this event watches for. One of 'info' / 'warning' / 'critical' — not 'ok' (never a meaningful breach floor) and not 'unknown'/'not_evaluable' (not real severities — see Before You Start). |
p_channel_name |
The channel the notification is sent on. Required when the event type is 'notify'; must be left empty otherwise. |
p_routine |
The function to call. Required when the event type is 'function'; must be left empty otherwise. Must have exactly the signature described in Function Events below. |
p_function_args |
Free-form structured configuration passed through verbatim as that routine's second argument. Only meaningful for 'function' events. |
p_is_active |
Defaults to true. Pass false to register an event that exists but won't fire until you turn it on later — useful for staging a channel or consumer before going live. |
Both event types, and every scope rule above, are validated immediately — get one wrong and creating the event raises a specific error message, not a generic constraint violation.
NOTIFY events¶
Any database session that's listening on that channel receives an asynchronous notification the moment the superuser-count check first reaches warning or worse:
LISTEN pghf_alerts;
-- ... later, in another session, PGHF11-009 breaches ...
-- Asynchronous notification "pghf_alerts" with payload "142" received.
SELECT * FROM pghf.get_results_sql('<the run_id whose evaluation_id is 142>');
The payload is deliberately just the evaluation's own internal identifier, as text — not a full row, not even the severity or message. Two reasons: PostgreSQL's own notification mechanism caps a payload at 8000 bytes, and some checks' collected data is unbounded (ranked lists, per-entity breakdowns) — there's no size a fixed-format payload could always safely fit. A listener queries the reporting functions for everything else, the same way any other consumer of this framework's results would.
If nothing is listening on the channel when the event fires, the notification is simply dropped — this is standard PostgreSQL behaviour, nothing this framework changes or works around. A database notification is not a queue; if you need guaranteed delivery to a consumer that isn't always connected, use a 'function' event instead and have your function do the durable handoff (write to a table, call an external endpoint, whatever your own infrastructure needs).
Function events¶
CREATE OR REPLACE FUNCTION pghf.notify_slack(p_evaluation_id bigint, p_args jsonb) RETURNS void
LANGUAGE plpgsql
AS $$
DECLARE
v_webhook_url text := p_args ->> 'webhook_url';
BEGIN
-- however you actually call out from your environment — an HTTP
-- extension, a bridge process listening on a notification channel,
-- whatever fits; the point here is just that p_evaluation_id and
-- p_args are all this function has to work with.
PERFORM pg_notify('slack_bridge', jsonb_build_object(
'evaluation_id', p_evaluation_id, 'webhook_url', v_webhook_url
)::text);
END;
$$;
SELECT pghf.create_event('PGHF11-009', 'function', 'critical',
p_routine => 'pghf.notify_slack(bigint, jsonb)'::regprocedure,
p_function_args => '{"webhook_url": "https://hooks.example.com/..."}'::jsonb
);
Every 'function'-type event's routine must have exactly the signature (p_evaluation_id bigint, p_args jsonb) RETURNS void — creating or updating an event verifies this directly against PostgreSQL's own function catalog (the number and type of arguments, and the return type), the same re-verification a check's own backing function goes through. A function with the right name but the wrong signature or return type is rejected at registration time, not discovered later when it fails to fire correctly.
The function-arguments parameter exists so one function can back many differently-configured events. The example above doesn't hardcode a webhook address or a specific check — the same function, registered against ten different checks with ten different sets of arguments, can send to ten different destinations. This is why, unlike a check's own backing function (which must be unique to it — one function backs exactly one check), an event's routine has no such restriction.
Failure isolation: if a function event raises an error, the framework catches it, logs a warning naming the event and the error text, and moves on to the next event — it never propagates, and it never aborts the rest of the evaluation run. A broken notifier degrades to "this one channel stopped working," not "the whole health-check run failed." That warning shows up wherever your evaluation call's own output is captured — a cron job's logged output, a psql session, an orchestrator's task log. There's no separate table recording dispatch failures; if you need a durable record of them, have your function catch its own errors internally and write them somewhere before re-raising, or don't re-raise at all and record the failure as data instead.
Continue to How Firing Actually Works.