Files
Primora/apps/backend/internal/database/db/telemetry.sql.go
T
Tomas DvorakandDevin 8cfddf9184 feat: phase 3 observability — IMS telemetry merge
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>
2026-09-16 16:21:03 +02:00

505 lines
13 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// source: telemetry.sql
package db
import (
"context"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
)
const componentHealthRows = `-- 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
`
type ComponentHealthRowsRow struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Kind string `json:"kind"`
Meta []byte `json:"meta"`
LastSeenAt pgtype.Timestamptz `json:"last_seen_at"`
LastStatus string `json:"last_status"`
Errors24h int64 `json:"errors_24h"`
Events24h int64 `json:"events_24h"`
}
func (q *Queries) ComponentHealthRows(ctx context.Context, projectID uuid.UUID) ([]ComponentHealthRowsRow, error) {
rows, err := q.db.Query(ctx, componentHealthRows, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ComponentHealthRowsRow
for rows.Next() {
var i ComponentHealthRowsRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Kind,
&i.Meta,
&i.LastSeenAt,
&i.LastStatus,
&i.Errors24h,
&i.Events24h,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const deleteComponent = `-- name: DeleteComponent :exec
DELETE FROM core.components
WHERE id = $1 AND project_id = $2
`
type DeleteComponentParams struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
}
func (q *Queries) DeleteComponent(ctx context.Context, arg DeleteComponentParams) error {
_, err := q.db.Exec(ctx, deleteComponent, arg.ID, arg.ProjectID)
return err
}
const eventTotals = `-- 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 => $2::bigint)
`
type EventTotalsParams struct {
ProjectID uuid.UUID `json:"project_id"`
WindowSec int64 `json:"window_sec"`
}
type EventTotalsRow struct {
Errors int64 `json:"errors"`
Events int64 `json:"events"`
Metrics int64 `json:"metrics"`
}
func (q *Queries) EventTotals(ctx context.Context, arg EventTotalsParams) (EventTotalsRow, error) {
row := q.db.QueryRow(ctx, eventTotals, arg.ProjectID, arg.WindowSec)
var i EventTotalsRow
err := row.Scan(&i.Errors, &i.Events, &i.Metrics)
return i, err
}
const eventTypeSeries = `-- name: EventTypeSeries :many
SELECT to_timestamp(floor(extract(epoch FROM e.ts) / $2::bigint) * $2::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 => $3::bigint)
GROUP BY bucket, e.type
ORDER BY bucket
`
type EventTypeSeriesParams struct {
ProjectID uuid.UUID `json:"project_id"`
BucketSec int64 `json:"bucket_sec"`
WindowSec int64 `json:"window_sec"`
}
type EventTypeSeriesRow struct {
Bucket pgtype.Timestamptz `json:"bucket"`
Type string `json:"type"`
Count int64 `json:"count"`
}
func (q *Queries) EventTypeSeries(ctx context.Context, arg EventTypeSeriesParams) ([]EventTypeSeriesRow, error) {
rows, err := q.db.Query(ctx, eventTypeSeries, arg.ProjectID, arg.BucketSec, arg.WindowSec)
if err != nil {
return nil, err
}
defer rows.Close()
var items []EventTypeSeriesRow
for rows.Next() {
var i EventTypeSeriesRow
if err := rows.Scan(&i.Bucket, &i.Type, &i.Count); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getComponentByName = `-- name: GetComponentByName :one
SELECT id, project_id, name, kind, meta, created_at FROM core.components
WHERE project_id = $1 AND name = $2
`
type GetComponentByNameParams struct {
ProjectID uuid.UUID `json:"project_id"`
Name string `json:"name"`
}
func (q *Queries) GetComponentByName(ctx context.Context, arg GetComponentByNameParams) (CoreComponent, error) {
row := q.db.QueryRow(ctx, getComponentByName, arg.ProjectID, arg.Name)
var i CoreComponent
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.Name,
&i.Kind,
&i.Meta,
&i.CreatedAt,
)
return i, err
}
const insertEvent = `-- 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 id, project_id, component_id, type, severity, message, payload, fingerprint, ts
`
type InsertEventParams struct {
ProjectID uuid.UUID `json:"project_id"`
ComponentID pgtype.UUID `json:"component_id"`
Type string `json:"type"`
Severity string `json:"severity"`
Message string `json:"message"`
Payload []byte `json:"payload"`
Fingerprint string `json:"fingerprint"`
Ts pgtype.Timestamptz `json:"ts"`
}
func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) (CoreEvent, error) {
row := q.db.QueryRow(ctx, insertEvent,
arg.ProjectID,
arg.ComponentID,
arg.Type,
arg.Severity,
arg.Message,
arg.Payload,
arg.Fingerprint,
arg.Ts,
)
var i CoreEvent
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.ComponentID,
&i.Type,
&i.Severity,
&i.Message,
&i.Payload,
&i.Fingerprint,
&i.Ts,
)
return i, err
}
const listComponents = `-- name: ListComponents :many
SELECT id, project_id, name, kind, meta, created_at FROM core.components
WHERE project_id = $1
ORDER BY name
`
func (q *Queries) ListComponents(ctx context.Context, projectID uuid.UUID) ([]CoreComponent, error) {
rows, err := q.db.Query(ctx, listComponents, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []CoreComponent
for rows.Next() {
var i CoreComponent
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.Name,
&i.Kind,
&i.Meta,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listErrorGroups = `-- 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() - ($2::text || ' days')::interval
GROUP BY e.fingerprint
ORDER BY max(e.ts) DESC
LIMIT 200
`
type ListErrorGroupsParams struct {
ProjectID uuid.UUID `json:"project_id"`
Days *string `json:"days"`
}
type ListErrorGroupsRow struct {
Fingerprint string `json:"fingerprint"`
Message string `json:"message"`
ComponentID string `json:"component_id"`
ComponentName string `json:"component_name"`
Count int64 `json:"count"`
FirstSeen pgtype.Timestamptz `json:"first_seen"`
LastSeen pgtype.Timestamptz `json:"last_seen"`
Severity string `json:"severity"`
}
func (q *Queries) ListErrorGroups(ctx context.Context, arg ListErrorGroupsParams) ([]ListErrorGroupsRow, error) {
rows, err := q.db.Query(ctx, listErrorGroups, arg.ProjectID, arg.Days)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListErrorGroupsRow
for rows.Next() {
var i ListErrorGroupsRow
if err := rows.Scan(
&i.Fingerprint,
&i.Message,
&i.ComponentID,
&i.ComponentName,
&i.Count,
&i.FirstSeen,
&i.LastSeen,
&i.Severity,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listEvents = `-- name: ListEvents :many
SELECT e.id, e.project_id, e.component_id, e.type, e.severity, e.message, e.payload, e.fingerprint, e.ts, 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 ($2::text IS NULL OR e.type = $2)
AND ($3::text IS NULL OR co.name = $3)
AND ($4::text IS NULL OR e.fingerprint = $4)
AND ($5::bigint IS NULL OR e.id < $5)
ORDER BY e.id DESC
LIMIT $6
`
type ListEventsParams struct {
ProjectID uuid.UUID `json:"project_id"`
Type *string `json:"type"`
Component *string `json:"component"`
Fingerprint *string `json:"fingerprint"`
Before *int64 `json:"before"`
Limit int32 `json:"limit"`
}
type ListEventsRow struct {
ID int64 `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
ComponentID pgtype.UUID `json:"component_id"`
Type string `json:"type"`
Severity string `json:"severity"`
Message string `json:"message"`
Payload []byte `json:"payload"`
Fingerprint string `json:"fingerprint"`
Ts pgtype.Timestamptz `json:"ts"`
ComponentName *string `json:"component_name"`
}
func (q *Queries) ListEvents(ctx context.Context, arg ListEventsParams) ([]ListEventsRow, error) {
rows, err := q.db.Query(ctx, listEvents,
arg.ProjectID,
arg.Type,
arg.Component,
arg.Fingerprint,
arg.Before,
arg.Limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListEventsRow
for rows.Next() {
var i ListEventsRow
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.ComponentID,
&i.Type,
&i.Severity,
&i.Message,
&i.Payload,
&i.Fingerprint,
&i.Ts,
&i.ComponentName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const metricNames = `-- 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
`
func (q *Queries) MetricNames(ctx context.Context, projectID uuid.UUID) ([]string, error) {
rows, err := q.db.Query(ctx, metricNames, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []string
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, err
}
items = append(items, name)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const metricSeries = `-- name: MetricSeries :many
SELECT to_timestamp(floor(extract(epoch FROM ts) / $2::bigint) * $2::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' = $3::text
AND ts > now() - make_interval(secs => $4::bigint)
AND jsonb_typeof(payload->'value') = 'number'
GROUP BY bucket
ORDER BY bucket
`
type MetricSeriesParams struct {
ProjectID uuid.UUID `json:"project_id"`
BucketSec int64 `json:"bucket_sec"`
Name string `json:"name"`
WindowSec int64 `json:"window_sec"`
}
type MetricSeriesRow struct {
Bucket pgtype.Timestamptz `json:"bucket"`
Avg float64 `json:"avg"`
P50 float64 `json:"p50"`
P95 float64 `json:"p95"`
Max float64 `json:"max"`
Count int64 `json:"count"`
}
func (q *Queries) MetricSeries(ctx context.Context, arg MetricSeriesParams) ([]MetricSeriesRow, error) {
rows, err := q.db.Query(ctx, metricSeries,
arg.ProjectID,
arg.BucketSec,
arg.Name,
arg.WindowSec,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []MetricSeriesRow
for rows.Next() {
var i MetricSeriesRow
if err := rows.Scan(
&i.Bucket,
&i.Avg,
&i.P50,
&i.P95,
&i.Max,
&i.Count,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const upsertComponent = `-- 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 id, project_id, name, kind, meta, created_at
`
type UpsertComponentParams struct {
ProjectID uuid.UUID `json:"project_id"`
Name string `json:"name"`
Kind string `json:"kind"`
}
func (q *Queries) UpsertComponent(ctx context.Context, arg UpsertComponentParams) (CoreComponent, error) {
row := q.db.QueryRow(ctx, upsertComponent, arg.ProjectID, arg.Name, arg.Kind)
var i CoreComponent
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.Name,
&i.Kind,
&i.Meta,
&i.CreatedAt,
)
return i, err
}