mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-10-28 15:51:23 +00:00
Port IMS telemetry into Primora conventions: - core.components/core.events (migration 00004 + sqlc) scoped to projects - POST /api/v1/ingest — single or <=500-event batches via project prm_ keys (X-Primora-Key alias), auto-registering components, permissive CORS - Issues: fingerprint-grouped errors with count/severity/first+last seen - Reads: events w/ filters + before-cursor, components, stats (1h/24h/7d/30d bucketed series + component health + totals + metric names), metric series (avg/p50/p95/max) - GET .../telemetry/stream — project-scoped SSE through an in-process hub (buffered subs, drop-on-slow, 25s keepalives); ?api_key=/?token= auth for EventSource - Telemetry page: health/issues/events/metrics/integrate tabs, live badge, 390px-clean; demo stubs - @primora/client SDK (createPrimoraClient: capture*/heartbeat/installAuto, queued keepalive flushes) - MCP: primora_list_issues/list_events/telemetry_stats/metric_series - IMS README deprecation note pointing here Also fixes workspace pins to 0.3.0 — npm ci in the frontend image was resolving the unpublished 0.2.0 spec from the registry. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
103 lines
4.0 KiB
SQL
103 lines
4.0 KiB
SQL
-- name: UpsertComponent :one
|
|
INSERT INTO core.components (project_id, name, kind)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (project_id, name) DO UPDATE SET name = EXCLUDED.name
|
|
RETURNING *;
|
|
|
|
-- name: GetComponentByName :one
|
|
SELECT * FROM core.components
|
|
WHERE project_id = $1 AND name = $2;
|
|
|
|
-- name: ListComponents :many
|
|
SELECT * FROM core.components
|
|
WHERE project_id = $1
|
|
ORDER BY name;
|
|
|
|
-- name: DeleteComponent :exec
|
|
DELETE FROM core.components
|
|
WHERE id = $1 AND project_id = $2;
|
|
|
|
-- name: InsertEvent :one
|
|
INSERT INTO core.events (project_id, component_id, type, severity, message, payload, fingerprint, ts)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
RETURNING *;
|
|
|
|
-- name: ListEvents :many
|
|
SELECT e.*, co.name AS component_name
|
|
FROM core.events e
|
|
LEFT JOIN core.components co ON co.id = e.component_id
|
|
WHERE e.project_id = $1
|
|
AND (sqlc.narg('type')::text IS NULL OR e.type = sqlc.narg('type'))
|
|
AND (sqlc.narg('component')::text IS NULL OR co.name = sqlc.narg('component'))
|
|
AND (sqlc.narg('fingerprint')::text IS NULL OR e.fingerprint = sqlc.narg('fingerprint'))
|
|
AND (sqlc.narg('before')::bigint IS NULL OR e.id < sqlc.narg('before'))
|
|
ORDER BY e.id DESC
|
|
LIMIT sqlc.arg('limit');
|
|
|
|
-- name: ListErrorGroups :many
|
|
SELECT e.fingerprint,
|
|
(array_agg(e.message ORDER BY e.ts DESC))[1]::text AS message,
|
|
coalesce((array_agg(e.component_id::text ORDER BY e.ts DESC))[1], '')::text AS component_id,
|
|
coalesce((array_agg(co.name ORDER BY e.ts DESC))[1], '')::text AS component_name,
|
|
count(*) AS count,
|
|
min(e.ts)::timestamptz AS first_seen,
|
|
max(e.ts)::timestamptz AS last_seen,
|
|
(array_agg(e.severity ORDER BY e.ts DESC))[1]::text AS severity
|
|
FROM core.events e
|
|
LEFT JOIN core.components co ON co.id = e.component_id
|
|
WHERE e.project_id = $1 AND e.type = 'error' AND e.fingerprint <> ''
|
|
AND e.ts > now() - (sqlc.narg('days')::text || ' days')::interval
|
|
GROUP BY e.fingerprint
|
|
ORDER BY max(e.ts) DESC
|
|
LIMIT 200;
|
|
|
|
-- name: EventTypeSeries :many
|
|
SELECT to_timestamp(floor(extract(epoch FROM e.ts) / sqlc.arg('bucket_sec')::bigint) * sqlc.arg('bucket_sec')::bigint)::timestamptz AS bucket,
|
|
e.type,
|
|
count(*) AS count
|
|
FROM core.events e
|
|
WHERE e.project_id = $1 AND e.ts > now() - make_interval(secs => sqlc.arg('window_sec')::bigint)
|
|
GROUP BY bucket, e.type
|
|
ORDER BY bucket;
|
|
|
|
-- name: ComponentHealthRows :many
|
|
SELECT co.id, co.name, co.kind, co.meta,
|
|
max(e.ts)::timestamptz AS last_seen_at,
|
|
coalesce(((array_agg(e.payload->>'status' ORDER BY e.ts DESC)
|
|
FILTER (WHERE e.type = 'heartbeat'))[1]), '')::text AS last_status,
|
|
count(*) FILTER (WHERE e.type = 'error' AND e.ts > now() - interval '24 hours') AS errors_24h,
|
|
count(*) FILTER (WHERE e.ts > now() - interval '24 hours') AS events_24h
|
|
FROM core.components co
|
|
LEFT JOIN core.events e ON e.component_id = co.id
|
|
WHERE co.project_id = $1
|
|
GROUP BY co.id
|
|
ORDER BY co.name;
|
|
|
|
-- name: EventTotals :one
|
|
SELECT count(*) FILTER (WHERE type = 'error') AS errors,
|
|
count(*) AS events,
|
|
count(*) FILTER (WHERE type = 'metric') AS metrics
|
|
FROM core.events
|
|
WHERE project_id = $1 AND ts > now() - make_interval(secs => sqlc.arg('window_sec')::bigint);
|
|
|
|
-- name: MetricNames :many
|
|
SELECT DISTINCT (payload->>'name')::text AS name
|
|
FROM core.events
|
|
WHERE project_id = $1 AND type = 'metric' AND payload ? 'name'
|
|
ORDER BY 1
|
|
LIMIT 50;
|
|
|
|
-- name: MetricSeries :many
|
|
SELECT to_timestamp(floor(extract(epoch FROM ts) / sqlc.arg('bucket_sec')::bigint) * sqlc.arg('bucket_sec')::bigint)::timestamptz AS bucket,
|
|
avg((payload->>'value')::float8)::float8 AS avg,
|
|
percentile_cont(0.5) WITHIN GROUP (ORDER BY (payload->>'value')::float8)::float8 AS p50,
|
|
percentile_cont(0.95) WITHIN GROUP (ORDER BY (payload->>'value')::float8)::float8 AS p95,
|
|
max((payload->>'value')::float8)::float8 AS max,
|
|
count(*) AS count
|
|
FROM core.events
|
|
WHERE project_id = $1 AND type = 'metric' AND payload->>'name' = sqlc.arg('name')::text
|
|
AND ts > now() - make_interval(secs => sqlc.arg('window_sec')::bigint)
|
|
AND jsonb_typeof(payload->'value') = 'number'
|
|
GROUP BY bucket
|
|
ORDER BY bucket;
|