mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-10-28 15:51:23 +00:00
Security:
- Graceful shutdown: http.Server + signal.NotifyContext, 10s drain,
retention sweeper cancelled before pool close
- Scoped API keys (migration 00006): ingest/read/write/admin enforced in
auth middleware; scope picker + badges in Settings UI; scopeless legacy
keys act as admin. Read-only keys now get 403 on ingest (verified live)
- nginx: baseline security headers on all responses, CSP scoped to SPA;
fixed add_header inheritance shadowing
- better-auth rate limit enabled unconditionally (was gated on NODE_ENV,
which the shipped .env sets to development)
- Mailpit moved to docker-compose.dev.yml; prod compose cannot expose it
- setup.sh generates PRIMORA_ENCRYPTION_KEY
Reliability/ops:
- Retention: per-project retention_{events,audit,webhook}_days
(migration 00007, 0=off) + hourly RetentionSweeper; edit UI in Projects
- GET /api/v1/metrics Prometheus exposition
- compose: healthchecks + restart policies everywhere, dragonfly pinned
to v2.0.0, depends_on service_healthy
- backup.sh/restore.sh (pg_dump -Fc + storage volume); restore verified
against scratch postgres+volume
- .dockerignore: exclude target dirs (build context was 4.7GB)
Correctness/testing:
- DBX transfer endpoint: POST /db-connections/:id/transfer — SQL or Redis
target, 1000-row bound, server-side creds (verified live pg→dragonfly)
- Pagination audit: ListComponents capped at 500 (auto-registers)
- CI: goose up/reset/up job + compose e2e-smoke job (smoke-e2e.sh
verified locally: signup, bootstrap, scoped keys, ingest, metrics)
- k6 load sanity script: 6650 reqs, ~217/s, 0 real failures, p95 221ms
(429s = configured rate limiter working)
Docs: DEPLOYMENT_GUIDE.md, PRODUCTION_READINESS.md; verify script
repaired (stale refs + set -e arithmetic abort). ROADMAP: phase 2 +
phase 6 marked complete.
Authored By: TDvorak <[email protected]>
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
525 lines
14 KiB
Go
525 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
|
|
LIMIT 500
|
|
`
|
|
|
|
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
|
|
}
|