fleettickets/migrations/10_inc_history_capture.sql

120 lines
6.2 KiB
MySQL
Raw Permalink Normal View History

-- 10_inc_history_capture.sql — fleettickets · history capture for time-series
-- ─────────────────────────────────────────────────────────────────────────────
-- tickets.inc is current-state (upsert), so it can't answer "how many were OPEN on
-- day X". This adds two durable history artifacts, populated by the ingest via
-- tickets.capture_history():
--
-- tickets.closure_events — append-only log of observed ticket closures
-- (keyed (ticket_id, closed_at); survives row churn,
-- records observed_at = when WE first saw the closure).
-- tickets.inc_daily_snapshot— one row per EAT day: open backlog (+ SLA split,
-- by cluster/status) and the day's created/closed flow.
-- Upserted each run → backlog-over-time accrues going
-- forward (past backlog can't be reconstructed).
--
-- Idempotent: safe on a fresh DB and re-appliable. The first capture backfills
-- closure_events from existing closed rows (observed_at = first-run time).
-- ─────────────────────────────────────────────────────────────────────────────
SET search_path = tickets, public;
CREATE TABLE IF NOT EXISTS tickets.closure_events (
ticket_id text NOT NULL,
closed_at timestamptz NOT NULL,
cluster text,
region text,
assigned_team text,
owner text,
mttr numeric, -- minutes
sla_status text,
observed_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (ticket_id, closed_at)
);
CREATE INDEX IF NOT EXISTS ix_closure_events_closed_at ON tickets.closure_events (closed_at);
CREATE INDEX IF NOT EXISTS ix_closure_events_team ON tickets.closure_events (assigned_team);
CREATE INDEX IF NOT EXISTS ix_closure_events_cluster ON tickets.closure_events (cluster);
CREATE TABLE IF NOT EXISTS tickets.inc_daily_snapshot (
snapshot_date date PRIMARY KEY, -- EAT calendar day
open_total integer,
open_breached integer,
open_at_risk integer,
open_ok integer,
open_unknown integer,
created_today integer, -- created_at_service on this EAT day
closed_today integer, -- closed_at on this EAT day
by_cluster_open jsonb,
by_status_open jsonb,
captured_at timestamptz NOT NULL DEFAULT now()
);
-- capture: append new closures + upsert today's backlog snapshot. Called by the
-- ingest after each --apply run (and available standalone).
CREATE OR REPLACE FUNCTION tickets.capture_history()
RETURNS jsonb LANGUAGE plpgsql AS $fn$
DECLARE
v_today date := (now() AT TIME ZONE 'Africa/Nairobi')::date;
v_events integer;
v_open integer;
BEGIN
-- 1) append newly-observed closures (closed_at is authoritative; dedup by id+closed_at)
INSERT INTO tickets.closure_events
(ticket_id, closed_at, cluster, region, assigned_team, owner, mttr, sla_status)
SELECT ticket_id, closed_at, cluster, region, assigned_team, owner, mttr, sla_status
FROM tickets.inc
WHERE closed_at IS NOT NULL
ON CONFLICT (ticket_id, closed_at) DO NOTHING;
GET DIAGNOSTICS v_events = ROW_COUNT;
-- 2) upsert today's backlog snapshot (open metrics from inc_open_sla)
INSERT INTO tickets.inc_daily_snapshot
(snapshot_date, open_total, open_breached, open_at_risk, open_ok, open_unknown,
created_today, closed_today, by_cluster_open, by_status_open, captured_at)
SELECT
v_today,
(SELECT count(*) FROM tickets.inc_open_sla),
(SELECT count(*) FROM tickets.inc_open_sla WHERE sla_state = 'breached'),
(SELECT count(*) FROM tickets.inc_open_sla WHERE sla_state = 'at_risk'),
(SELECT count(*) FROM tickets.inc_open_sla WHERE sla_state = 'ok'),
(SELECT count(*) FROM tickets.inc_open_sla WHERE sla_state = 'unknown'),
(SELECT count(*) FROM tickets.inc
WHERE (created_at_service AT TIME ZONE 'Africa/Nairobi')::date = v_today),
(SELECT count(*) FROM tickets.inc
WHERE (closed_at AT TIME ZONE 'Africa/Nairobi')::date = v_today),
(SELECT COALESCE(jsonb_object_agg(cl, c), '{}'::jsonb)
FROM (SELECT COALESCE(cluster, '(none)') cl, count(*) c
FROM tickets.inc_open_sla GROUP BY 1) z),
(SELECT COALESCE(jsonb_object_agg(st, c), '{}'::jsonb)
FROM (SELECT COALESCE(normalized_status, '(none)') st, count(*) c
FROM tickets.inc_open_sla GROUP BY 1) z),
now()
ON CONFLICT (snapshot_date) DO UPDATE SET
open_total = EXCLUDED.open_total, open_breached = EXCLUDED.open_breached,
open_at_risk = EXCLUDED.open_at_risk, open_ok = EXCLUDED.open_ok,
open_unknown = EXCLUDED.open_unknown, created_today = EXCLUDED.created_today,
closed_today = EXCLUDED.closed_today, by_cluster_open = EXCLUDED.by_cluster_open,
by_status_open = EXCLUDED.by_status_open, captured_at = now();
SELECT open_total INTO v_open FROM tickets.inc_daily_snapshot WHERE snapshot_date = v_today;
RETURN jsonb_build_object('closure_events_added', v_events,
'snapshot_date', v_today, 'open_total', v_open);
END $fn$;
COMMENT ON FUNCTION tickets.capture_history() IS
'Append new closures to tickets.closure_events + upsert today''s tickets.inc_daily_snapshot. fleettickets 10.';
-- grants (guarded: roles may not exist on a fresh DB)
DO $grants$
BEGIN
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'tracksolid_owner') THEN
GRANT SELECT, INSERT, UPDATE, DELETE ON tickets.closure_events, tickets.inc_daily_snapshot TO tracksolid_owner;
GRANT EXECUTE ON FUNCTION tickets.capture_history() TO tracksolid_owner;
END IF;
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'dashboard_ro') THEN
GRANT SELECT ON tickets.closure_events, tickets.inc_daily_snapshot TO dashboard_ro;
END IF;
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ro') THEN
GRANT SELECT ON tickets.closure_events, tickets.inc_daily_snapshot TO grafana_ro;
END IF;
END $grants$;