feat: telemetry alert rules with webhook delivery

core.alert_rules (migration 00012) — heartbeat_silence and error_spike
rules evaluated by a 60s backend ticker. Transition-based firing:
alert.fired on breach, alert.resolved on recovery, both first-class
webhook event types riding the signed dispatcher + realtime stream.
Dashboard manages rules on the Integrations page.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-24 19:09:19 +02:00
1 parent 345cf7918b
commit 2f4de543a9
28 files changed
+1693 -10

No files matched your search

@@ -0,0 +1,290 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// source: alert_rules.sql
package db
import (
"context"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
)
const createAlertRule = `-- name: CreateAlertRule :one
INSERT INTO core.alert_rules (project_id, name, kind, config)
VALUES ($1, $2, $3, $4)
RETURNING id, project_id, name, kind, config, enabled, state, created_at, updated_at
`
type CreateAlertRuleParams struct {
ProjectID uuid.UUID `json:"project_id"`
Name string `json:"name"`
Kind string `json:"kind"`
Config []byte `json:"config"`
}
func (q *Queries) CreateAlertRule(ctx context.Context, arg CreateAlertRuleParams) (CoreAlertRule, error) {
row := q.db.QueryRow(ctx, createAlertRule,
arg.ProjectID,
arg.Name,
arg.Kind,
arg.Config,
)
var i CoreAlertRule
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.Name,
&i.Kind,
&i.Config,
&i.Enabled,
&i.State,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const deleteAlertRule = `-- name: DeleteAlertRule :exec
DELETE FROM core.alert_rules
WHERE id = $1 AND project_id = $2
`
type DeleteAlertRuleParams struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
}
func (q *Queries) DeleteAlertRule(ctx context.Context, arg DeleteAlertRuleParams) error {
_, err := q.db.Exec(ctx, deleteAlertRule, arg.ID, arg.ProjectID)
return err
}
const getAlertRule = `-- name: GetAlertRule :one
SELECT id, project_id, name, kind, config, enabled, state, created_at, updated_at FROM core.alert_rules
WHERE id = $1 AND project_id = $2
`
type GetAlertRuleParams struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
}
func (q *Queries) GetAlertRule(ctx context.Context, arg GetAlertRuleParams) (CoreAlertRule, error) {
row := q.db.QueryRow(ctx, getAlertRule, arg.ID, arg.ProjectID)
var i CoreAlertRule
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.Name,
&i.Kind,
&i.Config,
&i.Enabled,
&i.State,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const lastHeartbeats = `-- name: LastHeartbeats :many
SELECT co.name AS component_name, max(e.ts)::timestamptz AS last_beat
FROM core.components co
JOIN core.events e ON e.component_id = co.id AND e.type = 'heartbeat'
WHERE co.project_id = $1
GROUP BY co.name
`
type LastHeartbeatsRow struct {
ComponentName string `json:"component_name"`
LastBeat pgtype.Timestamptz `json:"last_beat"`
}
// Per-component most recent heartbeat — heartbeat_silence detection.
func (q *Queries) LastHeartbeats(ctx context.Context, projectID uuid.UUID) ([]LastHeartbeatsRow, error) {
rows, err := q.db.Query(ctx, lastHeartbeats, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []LastHeartbeatsRow
for rows.Next() {
var i LastHeartbeatsRow
if err := rows.Scan(&i.ComponentName, &i.LastBeat); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listAlertRules = `-- name: ListAlertRules :many
SELECT id, project_id, name, kind, config, enabled, state, created_at, updated_at FROM core.alert_rules
WHERE project_id = $1
ORDER BY name
`
func (q *Queries) ListAlertRules(ctx context.Context, projectID uuid.UUID) ([]CoreAlertRule, error) {
rows, err := q.db.Query(ctx, listAlertRules, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []CoreAlertRule
for rows.Next() {
var i CoreAlertRule
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.Name,
&i.Kind,
&i.Config,
&i.Enabled,
&i.State,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listEnabledAlertRules = `-- name: ListEnabledAlertRules :many
SELECT id, project_id, name, kind, config, enabled, state, created_at, updated_at FROM core.alert_rules
WHERE enabled
`
func (q *Queries) ListEnabledAlertRules(ctx context.Context) ([]CoreAlertRule, error) {
rows, err := q.db.Query(ctx, listEnabledAlertRules)
if err != nil {
return nil, err
}
defer rows.Close()
var items []CoreAlertRule
for rows.Next() {
var i CoreAlertRule
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.Name,
&i.Kind,
&i.Config,
&i.Enabled,
&i.State,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const recentErrorCounts = `-- name: RecentErrorCounts :many
SELECT co.name AS component_name, count(*) AS errors
FROM core.events e
JOIN core.components co ON co.id = e.component_id
WHERE e.project_id = $1 AND e.type = 'error'
AND e.ts > now() - make_interval(secs => $2::bigint)
GROUP BY co.name
`
type RecentErrorCountsParams struct {
ProjectID uuid.UUID `json:"project_id"`
Column2 int64 `json:"column_2"`
}
type RecentErrorCountsRow struct {
ComponentName string `json:"component_name"`
Errors int64 `json:"errors"`
}
// Per-component error count inside a sliding window — error_spike detection.
func (q *Queries) RecentErrorCounts(ctx context.Context, arg RecentErrorCountsParams) ([]RecentErrorCountsRow, error) {
rows, err := q.db.Query(ctx, recentErrorCounts, arg.ProjectID, arg.Column2)
if err != nil {
return nil, err
}
defer rows.Close()
var items []RecentErrorCountsRow
for rows.Next() {
var i RecentErrorCountsRow
if err := rows.Scan(&i.ComponentName, &i.Errors); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const setAlertRuleState = `-- name: SetAlertRuleState :exec
UPDATE core.alert_rules
SET state = $3, updated_at = NOW()
WHERE id = $1 AND project_id = $2
`
type SetAlertRuleStateParams struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
State []byte `json:"state"`
}
func (q *Queries) SetAlertRuleState(ctx context.Context, arg SetAlertRuleStateParams) error {
_, err := q.db.Exec(ctx, setAlertRuleState, arg.ID, arg.ProjectID, arg.State)
return err
}
const updateAlertRule = `-- name: UpdateAlertRule :one
UPDATE core.alert_rules
SET name = $3, config = $4, enabled = $5, updated_at = NOW()
WHERE id = $1 AND project_id = $2
RETURNING id, project_id, name, kind, config, enabled, state, created_at, updated_at
`
type UpdateAlertRuleParams struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
Name string `json:"name"`
Config []byte `json:"config"`
Enabled bool `json:"enabled"`
}
func (q *Queries) UpdateAlertRule(ctx context.Context, arg UpdateAlertRuleParams) (CoreAlertRule, error) {
row := q.db.QueryRow(ctx, updateAlertRule,
arg.ID,
arg.ProjectID,
arg.Name,
arg.Config,
arg.Enabled,
)
var i CoreAlertRule
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.Name,
&i.Kind,
&i.Config,
&i.Enabled,
&i.State,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
@@ -140,6 +140,18 @@ func (ns NullCoreProjectRole) Value() (driver.Value, error) {
return string(ns.CoreProjectRole), nil
}
type CoreAlertRule struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
Name string `json:"name"`
Kind string `json:"kind"`
Config []byte `json:"config"`
Enabled bool `json:"enabled"`
State []byte `json:"state"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type CoreApiKey struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
@@ -23,6 +23,7 @@ type Querier interface {
CountOrganizations(ctx context.Context) (int64, error)
CountProjectAdmins(ctx context.Context, projectID uuid.UUID) (int64, error)
CreateAPIKey(ctx context.Context, arg CreateAPIKeyParams) (CoreApiKey, error)
CreateAlertRule(ctx context.Context, arg CreateAlertRuleParams) (CoreAlertRule, error)
CreateAuditLog(ctx context.Context, arg CreateAuditLogParams) (CoreAuditLog, error)
CreateBucket(ctx context.Context, arg CreateBucketParams) (CoreBucket, error)
CreateBucketObject(ctx context.Context, arg CreateBucketObjectParams) (CoreBucketObject, error)
@@ -35,6 +36,7 @@ type Querier interface {
CreateProject(ctx context.Context, arg CreateProjectParams) (CoreProject, error)
CreateScheduledJob(ctx context.Context, arg CreateScheduledJobParams) (CoreScheduledJob, error)
CreateWebhook(ctx context.Context, arg CreateWebhookParams) (CoreWebhook, error)
DeleteAlertRule(ctx context.Context, arg DeleteAlertRuleParams) error
DeleteBucketByID(ctx context.Context, id uuid.UUID) (CoreBucket, error)
DeleteBucketObjectByKey(ctx context.Context, arg DeleteBucketObjectByKeyParams) (CoreBucketObject, error)
DeleteCollection(ctx context.Context, arg DeleteCollectionParams) error
@@ -54,6 +56,7 @@ type Querier interface {
FinishScheduledJobRun(ctx context.Context, arg FinishScheduledJobRunParams) (CoreScheduledJobRun, error)
GetAPIKeyByIDForProject(ctx context.Context, arg GetAPIKeyByIDForProjectParams) (CoreApiKey, error)
GetAPIKeyByPrefix(ctx context.Context, prefix string) (GetAPIKeyByPrefixRow, error)
GetAlertRule(ctx context.Context, arg GetAlertRuleParams) (CoreAlertRule, error)
GetBucketByID(ctx context.Context, id uuid.UUID) (GetBucketByIDRow, error)
GetBucketObjectByKey(ctx context.Context, arg GetBucketObjectByKeyParams) (CoreBucketObject, error)
GetCollectionByID(ctx context.Context, id uuid.UUID) (CoreCollection, error)
@@ -80,7 +83,10 @@ type Querier interface {
InsertEvent(ctx context.Context, arg InsertEventParams) (CoreEvent, error)
InsertScheduledJobRun(ctx context.Context, arg InsertScheduledJobRunParams) (CoreScheduledJobRun, error)
InsertWebhookDelivery(ctx context.Context, arg InsertWebhookDeliveryParams) (CoreWebhookDelivery, error)
// Per-component most recent heartbeat — heartbeat_silence detection.
LastHeartbeats(ctx context.Context, projectID uuid.UUID) ([]LastHeartbeatsRow, error)
ListAPIKeysForProject(ctx context.Context, projectID uuid.UUID) ([]CoreApiKey, error)
ListAlertRules(ctx context.Context, projectID uuid.UUID) ([]CoreAlertRule, error)
ListAuditLogsForProject(ctx context.Context, arg ListAuditLogsForProjectParams) ([]CoreAuditLog, error)
ListBucketObjects(ctx context.Context, arg ListBucketObjectsParams) ([]CoreBucketObject, error)
ListBucketsForOrganization(ctx context.Context, organizationID uuid.UUID) ([]uuid.UUID, error)
@@ -90,6 +96,7 @@ type Querier interface {
ListDBConnections(ctx context.Context, projectID uuid.UUID) ([]CoreDbConnection, error)
ListDocuments(ctx context.Context, arg ListDocumentsParams) ([]CoreDocument, error)
ListDueScheduledJobs(ctx context.Context, nextRunAt pgtype.Timestamptz) ([]CoreScheduledJob, error)
ListEnabledAlertRules(ctx context.Context) ([]CoreAlertRule, error)
ListErrorGroups(ctx context.Context, arg ListErrorGroupsParams) ([]ListErrorGroupsRow, error)
ListEvents(ctx context.Context, arg ListEventsParams) ([]ListEventsRow, error)
ListIntegrations(ctx context.Context, projectID uuid.UUID) ([]CoreIntegration, error)
@@ -114,10 +121,13 @@ type Querier interface {
MetricSeries(ctx context.Context, arg MetricSeriesParams) ([]MetricSeriesRow, error)
MoveBucketObject(ctx context.Context, arg MoveBucketObjectParams) (CoreBucketObject, error)
PruneScheduledJobRuns(ctx context.Context, arg PruneScheduledJobRunsParams) error
// Per-component error count inside a sliding window — error_spike detection.
RecentErrorCounts(ctx context.Context, arg RecentErrorCountsParams) ([]RecentErrorCountsRow, error)
RemoveOrganizationMember(ctx context.Context, arg RemoveOrganizationMemberParams) (CoreOrganizationMember, error)
RemoveProjectMember(ctx context.Context, arg RemoveProjectMemberParams) (CoreProjectMember, error)
RemoveProjectMembershipsForOrganizationUser(ctx context.Context, arg RemoveProjectMembershipsForOrganizationUserParams) error
RevokeAPIKey(ctx context.Context, arg RevokeAPIKeyParams) (CoreApiKey, error)
SetAlertRuleState(ctx context.Context, arg SetAlertRuleStateParams) error
SweepExpiredAuditLogs(ctx context.Context) (int64, error)
// Per-project retention sweeps. A retention of 0 days disables the sweep for
// that stream. Pending webhook deliveries are never swept — they may still be
@@ -128,6 +138,7 @@ type Querier interface {
SweepExpiredOrgAuditLogs(ctx context.Context) (int64, error)
SweepExpiredWebhookDeliveries(ctx context.Context) (int64, error)
TouchAPIKey(ctx context.Context, id uuid.UUID) error
UpdateAlertRule(ctx context.Context, arg UpdateAlertRuleParams) (CoreAlertRule, error)
UpdateBucketByID(ctx context.Context, arg UpdateBucketByIDParams) (CoreBucket, error)
UpdateCollection(ctx context.Context, arg UpdateCollectionParams) (CoreCollection, error)
UpdateDocument(ctx context.Context, arg UpdateDocumentParams) (CoreDocument, error)