- internal/billing: Store interface + pgStore (pgx/v5 pool) + NoopStore - factory.go: NewStore() from BILLING_DSN env var (NoopStore fallback) - Server.billing: injected into Config, initialized in main - handleInvokeFunction: record invocation (TriggerConsole) - invokeInternalFunction: record invocation (TriggerHTTP) - handleCreateFunction: record event_type=create - handleCloneFunction: record event_type=clone - handleDeleteFunction: record event_type=delete - table: invocations (namespace, function_name, trigger_type, duration_ms, status_code...) - BILLING_DSN added to console.yaml - pgx/v5 added to go.mod/go.sum
91 lines
2.4 KiB
Go
91 lines
2.4 KiB
Go
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
|
|
}
|