2025-08-21 13:54:15 +02:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|