infoalloggi-monorepo/apps/backend/db/helpers.go

147 lines
4.6 KiB
Go
Raw Normal View History

package db
2025-08-04 17:45:44 +02:00
import (
"context"
"fmt"
"log/slog"
"regexp"
"strings"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
// obfuscate replaces the first n characters of a string with asterisks.
func obfuscate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + strings.Repeat("*", len(s)-n)
}
// ConnectToDatabaseWithLogging connects to the database with logging. It tests
// the connection and returns the connection pool.
func ConnectToDatabaseWithLogging(ctx context.Context, connectionString string, loggingEnabled bool) (*pgxpool.Pool, error) {
// Parse the connection string
slog.
Info("parsing connection string",
slog.String("connection_string", obfuscate(connectionString, 10)),
)
config, err := pgxpool.ParseConfig(connectionString)
if err != nil {
return nil, fmt.Errorf("failed to parse connection string: %w", err)
}
// If logging is enabled, set the tracer
if loggingEnabled {
config.ConnConfig.Tracer = NewMultiQueryTracer(NewLoggingQueryTracer(slog.Default()))
}
// Create the connection pool
slog.Info("creating connection pool")
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
return nil, fmt.Errorf("failed to create connection pool: %w", err)
}
// Ping the database
slog.Info("pinging database")
if err := pool.Ping(ctx); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return pool, nil
}
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
var (
replaceTabs = regexp.MustCompile(`\t+`)
replaceSpacesBeforeOpeningParens = regexp.MustCompile(`\s+\(`)
replaceSpacesAfterOpeningParens = regexp.MustCompile(`\(\s+`)
replaceSpacesBeforeClosingParens = regexp.MustCompile(`\s+\)`)
replaceSpacesAfterClosingParens = regexp.MustCompile(`\)\s+`)
replaceSpaces = regexp.MustCompile(`\s+`)
)
// prettyPrintSQL removes empty lines and trims spaces.
func prettyPrintSQL(sql string) string {
lines := strings.Split(sql, "\n")
pretty := strings.Join(lines, " ")
pretty = replaceTabs.ReplaceAllString(pretty, "")
pretty = replaceSpacesBeforeOpeningParens.ReplaceAllString(pretty, "(")
pretty = replaceSpacesAfterOpeningParens.ReplaceAllString(pretty, "(")
pretty = replaceSpacesAfterClosingParens.ReplaceAllString(pretty, ")")
pretty = replaceSpacesBeforeClosingParens.ReplaceAllString(pretty, ")")
// Finally, replace multiple spaces with a single space
pretty = replaceSpaces.ReplaceAllString(pretty, " ")
return strings.TrimSpace(pretty)
}
// https://github.com/jackc/pgx/issues/1061#issuecomment-1186250809
type LoggingQueryTracer struct {
logger *slog.Logger
}
func NewLoggingQueryTracer(logger *slog.Logger) *LoggingQueryTracer {
return &LoggingQueryTracer{logger: logger}
}
func (l *LoggingQueryTracer) TraceQueryStart(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryStartData) context.Context {
l.logger.
Info("query start",
slog.String("sql", prettyPrintSQL(data.SQL)),
slog.Any("args", data.Args),
)
return ctx
}
func (l *LoggingQueryTracer) TraceQueryEnd(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryEndData) {
// Failure
if data.Err != nil {
l.logger.
Error("query end",
slog.String("error", data.Err.Error()),
slog.String("command_tag", data.CommandTag.String()),
)
return
}
// Success
l.logger.
Info("query end",
slog.String("command_tag", data.CommandTag.String()),
)
}
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// https://github.com/jackc/pgx/discussions/1677#discussioncomment-8815982
type MultiQueryTracer struct {
Tracers []pgx.QueryTracer
}
func NewMultiQueryTracer(tracers ...pgx.QueryTracer) *MultiQueryTracer {
return &MultiQueryTracer{Tracers: tracers}
}
func (m *MultiQueryTracer) TraceQueryStart(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryStartData) context.Context {
for _, t := range m.Tracers {
ctx = t.TraceQueryStart(ctx, conn, data)
}
return ctx
}
func (m *MultiQueryTracer) TraceQueryEnd(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryEndData) {
for _, t := range m.Tracers {
t.TraceQueryEnd(ctx, conn, data)
}
}