mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-10-28 15:51:23 +00:00
- core.integrations (migration 00005) with AES-256-GCM-encrypted credentials (PRIMORA_ENCRYPTION_KEY, hex/base64, dev fallback); has_credentials serialization only — secrets never leave the server - /projects/:id/integrations list/create/delete/test; health check hits the stored base URL with a 5s timeout and records status - Rybbit connector: GET …/integrations/:id/analytics proxies /api/organizations + /api/sites/* server-side into a normalized shape - core.webhooks + core.webhook_deliveries; CRUD, deliveries, test, and POST …/deploy-markers. Events: issue.created (new fingerprint on ingest), deploy.marker, webhook.test. X-Primora-Signature HMAC-SHA256 over the raw body; in-process dispatcher, 3 attempts, quadratic backoff, 10s timeout, pending-scan on boot - HTTPS enforced for public targets; HTTP allowed for loopback/private/ self-hosted sinks - Integrations page (connectors + webhooks tabs, inline analytics, delivery log, secret-shown-once), demo-mode stubs - 12 new primora_* MCP tools - workspace 0.4.0 Authored By: TDvorak <[email protected]>
524 lines
14 KiB
Go
524 lines
14 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 hasFingerprintSeen = `-- name: HasFingerprintSeen :one
|
|
SELECT EXISTS(
|
|
SELECT 1 FROM core.events
|
|
WHERE project_id = $1 AND fingerprint = $2 AND type = 'error'
|
|
) AS seen
|
|
`
|
|
|
|
type HasFingerprintSeenParams struct {
|
|
ProjectID uuid.UUID `json:"project_id"`
|
|
Fingerprint string `json:"fingerprint"`
|
|
}
|
|
|
|
func (q *Queries) HasFingerprintSeen(ctx context.Context, arg HasFingerprintSeenParams) (bool, error) {
|
|
row := q.db.QueryRow(ctx, hasFingerprintSeen, arg.ProjectID, arg.Fingerprint)
|
|
var seen bool
|
|
err := row.Scan(&seen)
|
|
return seen, 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
|
|
}
|