feat(apps): per-app deploy/activity timeline
Every deploy across all four source kinds now writes a workload-scoped
event via a shared plugin.EmitDeployEvent helper (replacing the inline
emit duplicated in static/dockerfile, standardizing static's metadata
key site_id->workload_id, and adding emission to image+compose which
were silent). New indexed event_log.workload_id column, EventLogFilter
.WorkloadID, and GET /api/workloads/{id}/events (id pinned from path).
Frontend: a forge "Activity" panel on /apps/[id] reusing EventLogEntry,
live SSE prepend filtered by workload_id, load-more pagination, an
All/Errors severity filter, and a shared toEventLogEntry mapper. en/ru
i18n parity.
Security: compose's failure status emits a generic reason instead of raw
`docker compose up` output, which can echo app secrets and egresses to
operator webhooks (NotificationURL + event-trigger actions); full detail
stays only in the returned error. Rune-safe 256-rune status cap.
Reviewed: go + typescript APPROVE; security HIGH fixed.
This commit is contained in:
+16
-11
@@ -7,12 +7,13 @@ import (
|
||||
|
||||
// EventLogFilter holds optional filters for listing event log entries.
|
||||
type EventLogFilter struct {
|
||||
Severity string // Filter by severity (info, warn, error).
|
||||
Source string // Filter by source.
|
||||
Since string // Only events created at or after this timestamp.
|
||||
Until string // Only events created at or before this timestamp.
|
||||
Limit int // Maximum number of results (default 50).
|
||||
Offset int // Offset for pagination.
|
||||
Severity string // Filter by severity (info, warn, error).
|
||||
Source string // Filter by source.
|
||||
WorkloadID string // Filter by owning workload (exact match).
|
||||
Since string // Only events created at or after this timestamp.
|
||||
Until string // Only events created at or before this timestamp.
|
||||
Limit int // Maximum number of results (default 50).
|
||||
Offset int // Offset for pagination.
|
||||
}
|
||||
|
||||
// EventLogStats holds counts of event log entries by severity.
|
||||
@@ -31,9 +32,9 @@ func (s *Store) InsertEvent(evt EventLog) (EventLog, error) {
|
||||
}
|
||||
|
||||
result, err := s.db.Exec(
|
||||
`INSERT INTO event_log (source, severity, message, metadata, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
evt.Source, evt.Severity, evt.Message, evt.Metadata, evt.CreatedAt,
|
||||
`INSERT INTO event_log (source, workload_id, severity, message, metadata, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
evt.Source, evt.WorkloadID, evt.Severity, evt.Message, evt.Metadata, evt.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return EventLog{}, fmt.Errorf("insert event: %w", err)
|
||||
@@ -81,6 +82,10 @@ func (s *Store) ListEvents(filter EventLogFilter) ([]EventLog, error) {
|
||||
conditions = append(conditions, "source IN ("+strings.Join(placeholders, ",")+")")
|
||||
}
|
||||
}
|
||||
if filter.WorkloadID != "" {
|
||||
conditions = append(conditions, "workload_id = ?")
|
||||
args = append(args, filter.WorkloadID)
|
||||
}
|
||||
if filter.Since != "" {
|
||||
conditions = append(conditions, "created_at >= ?")
|
||||
args = append(args, filter.Since)
|
||||
@@ -90,7 +95,7 @@ func (s *Store) ListEvents(filter EventLogFilter) ([]EventLog, error) {
|
||||
args = append(args, filter.Until)
|
||||
}
|
||||
|
||||
query := "SELECT id, source, severity, message, metadata, created_at FROM event_log"
|
||||
query := "SELECT id, source, workload_id, severity, message, metadata, created_at FROM event_log"
|
||||
if len(conditions) > 0 {
|
||||
query += " WHERE " + strings.Join(conditions, " AND ")
|
||||
}
|
||||
@@ -114,7 +119,7 @@ func (s *Store) ListEvents(filter EventLogFilter) ([]EventLog, error) {
|
||||
events := []EventLog{}
|
||||
for rows.Next() {
|
||||
var evt EventLog
|
||||
if err := rows.Scan(&evt.ID, &evt.Source, &evt.Severity, &evt.Message, &evt.Metadata, &evt.CreatedAt); err != nil {
|
||||
if err := rows.Scan(&evt.ID, &evt.Source, &evt.WorkloadID, &evt.Severity, &evt.Message, &evt.Metadata, &evt.CreatedAt); err != nil {
|
||||
return nil, fmt.Errorf("scan event: %w", err)
|
||||
}
|
||||
events = append(events, evt)
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInsertEvent_RoundTripsWorkloadID(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
|
||||
in := EventLog{
|
||||
Source: "image",
|
||||
WorkloadID: "wl-abc",
|
||||
Severity: "info",
|
||||
Message: "my-app: deployed",
|
||||
Metadata: `{"workload_id":"wl-abc"}`,
|
||||
}
|
||||
saved, err := s.InsertEvent(in)
|
||||
if err != nil {
|
||||
t.Fatalf("InsertEvent: %v", err)
|
||||
}
|
||||
if saved.ID == 0 {
|
||||
t.Fatal("expected non-zero ID after insert")
|
||||
}
|
||||
if saved.WorkloadID != "wl-abc" {
|
||||
t.Fatalf("returned WorkloadID = %q, want %q", saved.WorkloadID, "wl-abc")
|
||||
}
|
||||
|
||||
rows, err := s.ListEvents(EventLogFilter{WorkloadID: "wl-abc"})
|
||||
if err != nil {
|
||||
t.Fatalf("ListEvents: %v", err)
|
||||
}
|
||||
if len(rows) != 1 {
|
||||
t.Fatalf("got %d rows, want 1", len(rows))
|
||||
}
|
||||
got := rows[0]
|
||||
if got.WorkloadID != "wl-abc" {
|
||||
t.Errorf("WorkloadID = %q, want %q", got.WorkloadID, "wl-abc")
|
||||
}
|
||||
if got.Source != "image" || got.Severity != "info" || got.Message != "my-app: deployed" {
|
||||
t.Errorf("round-trip mismatch: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertEvent_DefaultsWorkloadIDToEmpty(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
|
||||
// Non-deploy callers leave WorkloadID at its zero value; the column
|
||||
// must accept "" (NOT NULL DEFAULT '').
|
||||
saved, err := s.InsertEvent(EventLog{Source: "stale", Severity: "warn", Message: "x"})
|
||||
if err != nil {
|
||||
t.Fatalf("InsertEvent: %v", err)
|
||||
}
|
||||
if saved.WorkloadID != "" {
|
||||
t.Fatalf("WorkloadID = %q, want empty", saved.WorkloadID)
|
||||
}
|
||||
|
||||
rows, err := s.ListEvents(EventLogFilter{Source: "stale"})
|
||||
if err != nil {
|
||||
t.Fatalf("ListEvents: %v", err)
|
||||
}
|
||||
if len(rows) != 1 || rows[0].WorkloadID != "" {
|
||||
t.Fatalf("expected one unscoped row, got %+v", rows)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListEvents_FilterByWorkloadID(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
|
||||
for _, e := range []EventLog{
|
||||
{Source: "image", WorkloadID: "wl-1", Severity: "info", Message: "a"},
|
||||
{Source: "image", WorkloadID: "wl-1", Severity: "error", Message: "b"},
|
||||
{Source: "compose", WorkloadID: "wl-2", Severity: "info", Message: "c"},
|
||||
{Source: "stale", WorkloadID: "", Severity: "warn", Message: "d"},
|
||||
} {
|
||||
if _, err := s.InsertEvent(e); err != nil {
|
||||
t.Fatalf("InsertEvent %q: %v", e.Message, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Filtering by wl-1 returns only its two rows.
|
||||
rows, err := s.ListEvents(EventLogFilter{WorkloadID: "wl-1"})
|
||||
if err != nil {
|
||||
t.Fatalf("ListEvents wl-1: %v", err)
|
||||
}
|
||||
if len(rows) != 2 {
|
||||
t.Fatalf("wl-1: got %d rows, want 2", len(rows))
|
||||
}
|
||||
for _, r := range rows {
|
||||
if r.WorkloadID != "wl-1" {
|
||||
t.Errorf("wl-1 filter leaked row with workload_id %q", r.WorkloadID)
|
||||
}
|
||||
}
|
||||
|
||||
// wl-2 returns exactly one row.
|
||||
rows, err = s.ListEvents(EventLogFilter{WorkloadID: "wl-2"})
|
||||
if err != nil {
|
||||
t.Fatalf("ListEvents wl-2: %v", err)
|
||||
}
|
||||
if len(rows) != 1 || rows[0].Message != "c" {
|
||||
t.Fatalf("wl-2: got %+v, want single row 'c'", rows)
|
||||
}
|
||||
|
||||
// Combined workload + severity filter still narrows correctly.
|
||||
rows, err = s.ListEvents(EventLogFilter{WorkloadID: "wl-1", Severity: "error"})
|
||||
if err != nil {
|
||||
t.Fatalf("ListEvents wl-1+error: %v", err)
|
||||
}
|
||||
if len(rows) != 1 || rows[0].Message != "b" {
|
||||
t.Fatalf("wl-1+error: got %+v, want single row 'b'", rows)
|
||||
}
|
||||
|
||||
// No filter returns all four rows (back-compat: unscoped query intact).
|
||||
rows, err = s.ListEvents(EventLogFilter{})
|
||||
if err != nil {
|
||||
t.Fatalf("ListEvents all: %v", err)
|
||||
}
|
||||
if len(rows) != 4 {
|
||||
t.Fatalf("unfiltered: got %d rows, want 4", len(rows))
|
||||
}
|
||||
}
|
||||
+73
-71
@@ -14,60 +14,60 @@ type Registry struct {
|
||||
|
||||
// Settings holds global application configuration (single-row pattern).
|
||||
type Settings struct {
|
||||
Domain string `json:"domain"`
|
||||
ServerIP string `json:"server_ip"` // Docker host IP (for NPM remote forwarding)
|
||||
PublicIP string `json:"public_ip"` // Public-facing IP for DNS A records (e.g., NPM/proxy host)
|
||||
Network string `json:"network"`
|
||||
SubdomainPattern string `json:"subdomain_pattern"`
|
||||
NotificationURL string `json:"notification_url"`
|
||||
NotificationSecret string `json:"-"` // outgoing-webhook signing secret; never serialized directly
|
||||
NpmURL string `json:"npm_url"`
|
||||
NpmEmail string `json:"npm_email"`
|
||||
NpmPassword string `json:"npm_password"`
|
||||
PollingInterval string `json:"polling_interval"`
|
||||
BaseVolumePath string `json:"base_volume_path"`
|
||||
SSLCertificateID int `json:"ssl_certificate_id"`
|
||||
StaleThresholdDays int `json:"stale_threshold_days"`
|
||||
AllowedVolumePaths string `json:"allowed_volume_paths"` // JSON array of allowed absolute paths
|
||||
WildcardDNS bool `json:"wildcard_dns"`
|
||||
DNSProvider string `json:"dns_provider"`
|
||||
CloudflareAPIToken string `json:"cloudflare_api_token"`
|
||||
CloudflareZoneID string `json:"cloudflare_zone_id"`
|
||||
NpmRemote bool `json:"npm_remote"`
|
||||
NpmAccessListID int `json:"npm_access_list_id"`
|
||||
ProxyProvider string `json:"proxy_provider"`
|
||||
TraefikEntrypoint string `json:"traefik_entrypoint"`
|
||||
TraefikCertResolver string `json:"traefik_cert_resolver"`
|
||||
TraefikNetwork string `json:"traefik_network"`
|
||||
TraefikAPIURL string `json:"traefik_api_url"`
|
||||
ImagePruneThresholdMB int `json:"image_prune_threshold_mb"`
|
||||
BackupEnabled bool `json:"backup_enabled"`
|
||||
BackupIntervalHours int `json:"backup_interval_hours"`
|
||||
BackupRetentionCount int `json:"backup_retention_count"`
|
||||
Domain string `json:"domain"`
|
||||
ServerIP string `json:"server_ip"` // Docker host IP (for NPM remote forwarding)
|
||||
PublicIP string `json:"public_ip"` // Public-facing IP for DNS A records (e.g., NPM/proxy host)
|
||||
Network string `json:"network"`
|
||||
SubdomainPattern string `json:"subdomain_pattern"`
|
||||
NotificationURL string `json:"notification_url"`
|
||||
NotificationSecret string `json:"-"` // outgoing-webhook signing secret; never serialized directly
|
||||
NpmURL string `json:"npm_url"`
|
||||
NpmEmail string `json:"npm_email"`
|
||||
NpmPassword string `json:"npm_password"`
|
||||
PollingInterval string `json:"polling_interval"`
|
||||
BaseVolumePath string `json:"base_volume_path"`
|
||||
SSLCertificateID int `json:"ssl_certificate_id"`
|
||||
StaleThresholdDays int `json:"stale_threshold_days"`
|
||||
AllowedVolumePaths string `json:"allowed_volume_paths"` // JSON array of allowed absolute paths
|
||||
WildcardDNS bool `json:"wildcard_dns"`
|
||||
DNSProvider string `json:"dns_provider"`
|
||||
CloudflareAPIToken string `json:"cloudflare_api_token"`
|
||||
CloudflareZoneID string `json:"cloudflare_zone_id"`
|
||||
NpmRemote bool `json:"npm_remote"`
|
||||
NpmAccessListID int `json:"npm_access_list_id"`
|
||||
ProxyProvider string `json:"proxy_provider"`
|
||||
TraefikEntrypoint string `json:"traefik_entrypoint"`
|
||||
TraefikCertResolver string `json:"traefik_cert_resolver"`
|
||||
TraefikNetwork string `json:"traefik_network"`
|
||||
TraefikAPIURL string `json:"traefik_api_url"`
|
||||
ImagePruneThresholdMB int `json:"image_prune_threshold_mb"`
|
||||
BackupEnabled bool `json:"backup_enabled"`
|
||||
BackupIntervalHours int `json:"backup_interval_hours"`
|
||||
BackupRetentionCount int `json:"backup_retention_count"`
|
||||
// AutoBackupBeforeDeploy creates a "pre-deploy" Tinyforge DB backup
|
||||
// at the start of every project deploy. Independent of BackupEnabled
|
||||
// (which governs the periodic auto-backup cron).
|
||||
AutoBackupBeforeDeploy bool `json:"auto_backup_before_deploy"`
|
||||
StatsIntervalSeconds int `json:"stats_interval_seconds"` // 0 disables collection
|
||||
StatsRetentionHours int `json:"stats_retention_hours"` // 0 disables collection
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
AutoBackupBeforeDeploy bool `json:"auto_backup_before_deploy"`
|
||||
StatsIntervalSeconds int `json:"stats_interval_seconds"` // 0 disables collection
|
||||
StatsRetentionHours int `json:"stats_retention_hours"` // 0 disables collection
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ContainerStatsSample is one persisted sample of container resource usage.
|
||||
// Cumulative counters (network, block I/O) require differencing two samples
|
||||
// to get rates; CPU is already a percent-since-previous-sample value.
|
||||
type ContainerStatsSample struct {
|
||||
ContainerID string `json:"container_id"`
|
||||
OwnerType string `json:"owner_type"` // "instance" or "site"
|
||||
OwnerID string `json:"owner_id"`
|
||||
TS int64 `json:"ts"` // Unix seconds UTC
|
||||
ContainerID string `json:"container_id"`
|
||||
OwnerType string `json:"owner_type"` // "instance" or "site"
|
||||
OwnerID string `json:"owner_id"`
|
||||
TS int64 `json:"ts"` // Unix seconds UTC
|
||||
CPUPercent float64 `json:"cpu_percent"`
|
||||
MemoryUsage int64 `json:"memory_usage"`
|
||||
MemoryLimit int64 `json:"memory_limit"`
|
||||
NetworkRxBytes int64 `json:"network_rx_bytes"`
|
||||
NetworkTxBytes int64 `json:"network_tx_bytes"`
|
||||
BlockReadBytes int64 `json:"block_read_bytes"`
|
||||
BlockWriteBytes int64 `json:"block_write_bytes"`
|
||||
MemoryUsage int64 `json:"memory_usage"`
|
||||
MemoryLimit int64 `json:"memory_limit"`
|
||||
NetworkRxBytes int64 `json:"network_rx_bytes"`
|
||||
NetworkTxBytes int64 `json:"network_tx_bytes"`
|
||||
BlockReadBytes int64 `json:"block_read_bytes"`
|
||||
BlockWriteBytes int64 `json:"block_write_bytes"`
|
||||
}
|
||||
|
||||
// SystemStatsSample is one persisted host-level snapshot that aggregates
|
||||
@@ -106,10 +106,12 @@ type DNSRecord struct {
|
||||
// page. The legacy field names (ProjectID, ProjectName, StageID,
|
||||
// StageName, InstanceID) are retained verbatim for the existing
|
||||
// frontend contract — after the workload-first cutover they map to:
|
||||
// ProjectID/Name → workload id / workload name
|
||||
// StageID/Name → containers.stage_id / containers.role
|
||||
// InstanceID → container row id
|
||||
// Source → "instance" for image/compose, "static_site" for static
|
||||
//
|
||||
// ProjectID/Name → workload id / workload name
|
||||
// StageID/Name → containers.stage_id / containers.role
|
||||
// InstanceID → container row id
|
||||
// Source → "instance" for image/compose, "static_site" for static
|
||||
//
|
||||
// Renaming would require a coordinated frontend change; deferred.
|
||||
type ProxyRoute struct {
|
||||
Source string `json:"source"`
|
||||
@@ -190,12 +192,13 @@ func IsValidVolumeScope(s string) bool {
|
||||
|
||||
// EventLog represents a persistent event log entry.
|
||||
type EventLog struct {
|
||||
ID int64 `json:"id"`
|
||||
Source string `json:"source"`
|
||||
Severity string `json:"severity"` // info, warn, error
|
||||
Message string `json:"message"`
|
||||
Metadata string `json:"metadata"` // JSON-encoded structured data
|
||||
CreatedAt string `json:"created_at"`
|
||||
ID int64 `json:"id"`
|
||||
Source string `json:"source"`
|
||||
WorkloadID string `json:"workload_id"` // "" = unscoped (non-deploy events)
|
||||
Severity string `json:"severity"` // info, warn, error
|
||||
Message string `json:"message"`
|
||||
Metadata string `json:"metadata"` // JSON-encoded structured data
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// EventTrigger is a filter+action rule evaluated against EventLog
|
||||
@@ -245,12 +248,12 @@ const (
|
||||
// for this workload).
|
||||
type LogScanRule struct {
|
||||
ID int64 `json:"id"`
|
||||
WorkloadID string `json:"workload_id"` // "" = global
|
||||
OverridesID int64 `json:"overrides_id"` // 0 = not an override
|
||||
WorkloadID string `json:"workload_id"` // "" = global
|
||||
OverridesID int64 `json:"overrides_id"` // 0 = not an override
|
||||
Name string `json:"name"`
|
||||
Pattern string `json:"pattern"` // regex, compiled at load
|
||||
Severity string `json:"severity"` // info|warn|error
|
||||
Streams string `json:"streams"` // all|stdout|stderr
|
||||
Pattern string `json:"pattern"` // regex, compiled at load
|
||||
Severity string `json:"severity"` // info|warn|error
|
||||
Streams string `json:"streams"` // all|stdout|stderr
|
||||
CooldownSeconds int `json:"cooldown_seconds"`
|
||||
Enabled bool `json:"enabled"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
@@ -308,13 +311,13 @@ type Workload struct {
|
||||
Kind string `json:"kind"` // project | stack | site (legacy discriminator)
|
||||
RefID string `json:"ref_id"`
|
||||
Name string `json:"name"`
|
||||
AppID string `json:"app_id"` // nullable; "" = unassigned (a.k.a. GroupID after rename)
|
||||
SourceKind string `json:"source_kind"` // "" until plugin-mode populated
|
||||
SourceConfig string `json:"source_config"` // JSON-encoded, decoded by the matching Source
|
||||
AppID string `json:"app_id"` // nullable; "" = unassigned (a.k.a. GroupID after rename)
|
||||
SourceKind string `json:"source_kind"` // "" until plugin-mode populated
|
||||
SourceConfig string `json:"source_config"` // JSON-encoded, decoded by the matching Source
|
||||
TriggerKind string `json:"trigger_kind"`
|
||||
TriggerConfig string `json:"trigger_config"` // JSON-encoded, decoded by the matching Trigger
|
||||
PublicFaces string `json:"public_faces"` // JSON-encoded []PublicFace
|
||||
ParentWorkloadID string `json:"parent_workload_id"` // "" = root; non-empty = stage chain
|
||||
TriggerConfig string `json:"trigger_config"` // JSON-encoded, decoded by the matching Trigger
|
||||
PublicFaces string `json:"public_faces"` // JSON-encoded []PublicFace
|
||||
ParentWorkloadID string `json:"parent_workload_id"` // "" = root; non-empty = stage chain
|
||||
NotificationURL string `json:"notification_url"`
|
||||
NotificationSecret string `json:"-"` // never serialized
|
||||
WebhookSecret string `json:"-"` // URL-identifier secret; never serialized
|
||||
@@ -393,11 +396,11 @@ type Container struct {
|
||||
// which workloads to fire.
|
||||
type Trigger struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"` // registry | git | manual | schedule | log_scan | ...
|
||||
Name string `json:"name"` // human-readable, unique
|
||||
Config string `json:"config"` // JSON-encoded, decoded by the matching plugin
|
||||
WebhookSecret string `json:"-"` // URL-identifier secret; never serialized
|
||||
WebhookSigningSecret string `json:"-"` // HMAC key; never serialized
|
||||
Kind string `json:"kind"` // registry | git | manual | schedule | log_scan | ...
|
||||
Name string `json:"name"` // human-readable, unique
|
||||
Config string `json:"config"` // JSON-encoded, decoded by the matching plugin
|
||||
WebhookSecret string `json:"-"` // URL-identifier secret; never serialized
|
||||
WebhookSigningSecret string `json:"-"` // HMAC key; never serialized
|
||||
WebhookRequireSignature bool `json:"webhook_require_signature"`
|
||||
// LastFiredAt is the RFC3339 wall-clock the scheduler last dispatched
|
||||
// this trigger. Empty for never-fired or non-schedule triggers. The
|
||||
@@ -433,4 +436,3 @@ type App struct {
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
|
||||
@@ -178,6 +178,12 @@ func (s *Store) runMigrations() error {
|
||||
// Empty string = never fired. Pre-trigger-split DBs land the column
|
||||
// here so the scheduler can read/write it on first boot.
|
||||
`ALTER TABLE triggers ADD COLUMN last_fired_at TEXT NOT NULL DEFAULT ''`,
|
||||
// Per-app deploy/activity timeline: scope each event_log row to the
|
||||
// workload that produced it so the dashboard can query a workload's
|
||||
// deploy history. Empty string = unscoped (the existing non-deploy
|
||||
// loggers don't set it). Additive ADD COLUMN — the loop below
|
||||
// tolerates the "duplicate column" error on fully-migrated DBs.
|
||||
`ALTER TABLE event_log ADD COLUMN workload_id TEXT NOT NULL DEFAULT ''`,
|
||||
// Hard cutover: drop every legacy table. Idempotent — DROP TABLE
|
||||
// IF EXISTS is a no-op once the table is gone. Operators upgrading
|
||||
// from a pre-cutover build will lose any project / stack / static
|
||||
@@ -432,6 +438,7 @@ func (s *Store) runMigrations() error {
|
||||
`CREATE INDEX IF NOT EXISTS idx_event_log_severity ON event_log(severity)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_event_log_source ON event_log(source)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_event_log_created_at ON event_log(created_at)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_event_log_workload ON event_log(workload_id, created_at)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_dns_records_consumer ON dns_records(consumer_type, consumer_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_container_stats_owner_ts ON container_stats_samples(owner_type, owner_id, ts)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_container_stats_container_ts ON container_stats_samples(container_id, ts)`,
|
||||
|
||||
Reference in New Issue
Block a user