// Изменено: 2026-03-07 // PostgreSQL storage — хранение логов вызовов функций. // Состояние функций (phase, imageRef) хранится в k8s CRD, не здесь. // Здесь только invocations: логи, статусы, время выполнения. package postgres import ( "context" "database/sql" "fmt" "time" _ "github.com/lib/pq" // драйвер PostgreSQL ) // Invocation — запись о вызове функции. type Invocation struct { ID string FunctionName string Namespace string Status string // success, error, timeout DurationMs int32 HTTPStatus *int32 // nil для cron триггеров Logs string TriggerType string CreatedAt time.Time } // Store — клиент для работы с PostgreSQL. type Store struct { db *sql.DB } // New открывает подключение к PostgreSQL и проверяет его. func New(dsn string) (*Store, error) { db, err := sql.Open("postgres", dsn) if err != nil { return nil, fmt.Errorf("open postgres: %w", err) } // Проверяем что подключение реально работает ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := db.PingContext(ctx); err != nil { return nil, fmt.Errorf("ping postgres: %w", err) } db.SetMaxOpenConns(10) db.SetMaxIdleConns(5) db.SetConnMaxLifetime(5 * time.Minute) return &Store{db: db}, nil } // Close закрывает подключение к БД. func (s *Store) Close() error { return s.db.Close() } // SaveInvocation записывает лог вызова функции в БД. func (s *Store) SaveInvocation(ctx context.Context, inv *Invocation) error { _, err := s.db.ExecContext(ctx, ` INSERT INTO invocations (function_name, namespace, status, duration_ms, http_status, logs, trigger_type) VALUES ($1, $2, $3, $4, $5, $6, $7) `, inv.FunctionName, inv.Namespace, inv.Status, inv.DurationMs, inv.HTTPStatus, inv.Logs, inv.TriggerType) if err != nil { return fmt.Errorf("save invocation: %w", err) } return nil } // ListInvocations возвращает последние N вызовов для указанной функции. func (s *Store) ListInvocations(ctx context.Context, functionName, namespace string, limit int) ([]*Invocation, error) { rows, err := s.db.QueryContext(ctx, ` SELECT id, function_name, namespace, status, duration_ms, http_status, logs, trigger_type, created_at FROM invocations WHERE function_name = $1 AND namespace = $2 ORDER BY created_at DESC LIMIT $3 `, functionName, namespace, limit) if err != nil { return nil, fmt.Errorf("list invocations: %w", err) } defer rows.Close() var result []*Invocation for rows.Next() { inv := &Invocation{} if err := rows.Scan( &inv.ID, &inv.FunctionName, &inv.Namespace, &inv.Status, &inv.DurationMs, &inv.HTTPStatus, &inv.Logs, &inv.TriggerType, &inv.CreatedAt, ); err != nil { return nil, fmt.Errorf("scan invocation: %w", err) } result = append(result, inv) } return result, rows.Err() } // RunMigrations применяет SQL файлы из директории migrations. // Простая реализация без внешних зависимостей — выполняем один файл. // Используем IF NOT EXISTS в SQL поэтому безопасно запускать повторно. func (s *Store) RunMigrations(ctx context.Context, sql string) error { _, err := s.db.ExecContext(ctx, sql) if err != nil { return fmt.Errorf("run migration: %w", err) } return nil }