package billing import ( "context" "log" "time" "github.com/jackc/pgx/v5/pgxpool" ) // pgStore — реализация Store поверх PostgreSQL через pgx connection pool. type pgStore struct { pool *pgxpool.Pool } // NewPostgresStore создаёт Store с пулом соединений к PostgreSQL. // dsn — строка вида postgres://user:pass@host:5432/dbname // При ошибке подключения — возвращает ошибку, вызывающий код решает как реагировать. func NewPostgresStore(ctx context.Context, dsn string) (Store, error) { cfg, err := pgxpool.ParseConfig(dsn) if err != nil { return nil, err } cfg.MaxConns = 4 cfg.MinConns = 1 cfg.MaxConnIdleTime = 5 * time.Minute pool, err := pgxpool.NewWithConfig(ctx, cfg) if err != nil { return nil, err } // Проверяем живость соединения if err := pool.Ping(ctx); err != nil { pool.Close() return nil, err } log.Printf("billing: connected to PostgreSQL") return &pgStore{pool: pool}, nil } // RecordInvocation вставляет запись асинхронно — не блокирует основной поток. func (s *pgStore) RecordInvocation(inv Invocation) { go func() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _, err := s.pool.Exec(ctx, ` INSERT INTO invocations ( namespace, function_name, trigger_type, route, http_method, started_at, duration_ms, status_code, cold_start, request_bytes, response_bytes, error_msg, recorded_by, event_type ) VALUES ( $1,$2,$3,$4,$5, $6,$7,$8,$9, $10,$11,$12,$13,$14 )`, inv.Namespace, inv.FunctionName, inv.TriggerType, inv.Route, inv.HTTPMethod, inv.StartedAt, inv.DurationMS, inv.StatusCode, inv.ColdStart, inv.RequestBytes, inv.ResponseBytes, nullIfEmpty(inv.ErrorMsg), inv.RecordedBy, nullIfEmpty(inv.EventType), ) if err != nil { log.Printf("billing: insert invocation: %v", err) } }() } // Close закрывает пул соединений. func (s *pgStore) Close() { s.pool.Close() log.Printf("billing: connection pool closed") } // nullIfEmpty возвращает nil для пустой строки (SQL NULL вместо пустой строки). func nullIfEmpty(s string) interface{} { if s == "" { return nil } return s }