package db 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) } }