From 8bf265dd72c5784fda0671e2066dbc9376f5d0ee Mon Sep 17 00:00:00 2001 From: Thibaut MADELAINE Date: Thu, 2 Oct 2025 17:44:03 +0200 Subject: [PATCH] feat: set log level parameters for a role --- postgresql/resource_postgresql_role.go | 257 ++++++++++++++++++++ postgresql/resource_postgresql_role_test.go | 24 ++ 2 files changed, 281 insertions(+) diff --git a/postgresql/resource_postgresql_role.go b/postgresql/resource_postgresql_role.go index 5b346f3b..cda82780 100644 --- a/postgresql/resource_postgresql_role.go +++ b/postgresql/resource_postgresql_role.go @@ -22,6 +22,10 @@ const ( roleCreateRoleAttr = "create_role" roleEncryptedPassAttr = "encrypted_password" roleIdleInTransactionSessionTimeoutAttr = "idle_in_transaction_session_timeout" + roleLogMinDurationStatementAttr = "log_min_duration_statement" + roleLogMinDurationSampleAttr = "log_min_duration_sample" + roleLogStatementSampleRateAttr = "log_statement_sample_rate" + roleLogTransactionSampleRateAttr = "log_transaction_sample_rate" roleInheritAttr = "inherit" roleLoginAttr = "login" roleNameAttr = "name" @@ -126,6 +130,30 @@ func resourcePostgreSQLRole() *schema.Resource { Description: "Terminate any session with an open transaction that has been idle for longer than the specified duration in milliseconds", ValidateFunc: validation.IntAtLeast(0), }, + roleLogMinDurationStatementAttr: { + Type: schema.TypeInt, + Optional: true, + Description: "Log a completed statement if it ran for at least the specified amount of time in milliseconds.", + ValidateFunc: validation.IntAtLeast(-1), + }, + roleLogMinDurationSampleAttr: { + Type: schema.TypeInt, + Optional: true, + Description: "Allows sampling the duration of completed statements that ran for at least the specified amount of time. Sample rate is controlled by log_statement_sample_rate.", + ValidateFunc: validation.IntAtLeast(-1), + }, + roleLogStatementSampleRateAttr: { + Type: schema.TypeFloat, + Optional: true, + Description: "Determines the fraction of statements with duration exceeding log_min_duration_sample that will be logged.", + ValidateFunc: validation.FloatBetween(0, 1), + }, + roleLogTransactionSampleRateAttr: { + Type: schema.TypeFloat, + Optional: true, + Description: "Sets the fraction of transactions whose statements are all logged, in addition to statements logged for other reasons.", + ValidateFunc: validation.FloatBetween(0, 1), + }, roleInheritAttr: { Type: schema.TypeBool, Optional: true, @@ -307,6 +335,22 @@ func resourcePostgreSQLRoleCreate(db *DBConnection, d *schema.ResourceData) erro return err } + if err = setLogMinDurationStatement(txn, d); err != nil { + return err + } + + if err = setLogMinDurationSample(txn, d); err != nil { + return err + } + + if err = setLogStatementSampleRate(txn, d); err != nil { + return err + } + + if err = setLogTransactionSampleRate(txn, d); err != nil { + return err + } + if err = setAssumeRole(txn, d); err != nil { return err } @@ -472,6 +516,34 @@ func resourcePostgreSQLRoleReadImpl(db *DBConnection, d *schema.ResourceData) er d.Set(roleIdleInTransactionSessionTimeoutAttr, idleInTransactionSessionTimeout) + logMinDurationStatement, err := readLogMinDurationStatement(roleConfig) + if err != nil { + return err + } + + d.Set(roleLogMinDurationStatementAttr, logMinDurationStatement) + + logMinDurationSample, err := readLogMinDurationSample(roleConfig) + if err != nil { + return err + } + + d.Set(roleLogMinDurationSampleAttr, logMinDurationSample) + + logStatementSampleRate, err := readLogStatementSampleRate(roleConfig) + if err != nil { + return err + } + + d.Set(roleLogStatementSampleRateAttr, logStatementSampleRate) + + logTransactionSampleRate, err := readLogTransactionSampleRate(roleConfig) + if err != nil { + return err + } + + d.Set(roleLogTransactionSampleRateAttr, logTransactionSampleRate) + d.SetId(roleName) password, err := readRolePassword(db, d, roleCanLogin) @@ -516,6 +588,75 @@ func readIdleInTransactionSessionTimeout(roleConfig pq.ByteaArray) (int, error) return 0, nil } +// readLogMinDurationStatement searches for an log_min_duration_statement entry in the rolconfig array. +// In case no such value is present, it returns nil. +func readLogMinDurationStatement(roleConfig pq.ByteaArray) (int, error) { + for _, v := range roleConfig { + config := string(v) + if strings.HasPrefix(config, rolelogMinDurationStatementAttr) { + var result = strings.Split(strings.TrimPrefix(config, rolelogMinDurationStatementAttr+"="), ", ") + res, err := strconv.Atoi(result[0]) + if err != nil { + return -1, fmt.Errorf("Error reading log_min_duration_statement: %w", err) + } + return res, nil + } + } + return 0, nil +} + +// readLogMinDurationSample searches for an log_statement_sample_rate entry in the rolconfig array. +// In case no such value is present, it returns nil. +func readLogMinDurationSample(roleConfig pq.ByteaArray) (int, error) { + for _, v := range roleConfig { + config := string(v) + if strings.HasPrefix(config, rolelogMinDurationSampleAttr) { + var result = strings.Split(strings.TrimPrefix(config, rolelogMinDurationSampleAttr+"="), ", ") + res, err := strconv.Atoi(result[0]) + if err != nil { + return -1, fmt.Errorf("Error reading log_min_duration_sample: %w", err) + } + return res, nil + } + } + return 0, nil +} + +// readLogStatementSampleRate searches for an log_transaction_sample_rate entry in the rolconfig array. +// In case no such value is present, it returns nil. +func readLogStatementSampleRate(roleConfig pq.ByteaArray) (int, error) { + for _, v := range roleConfig { + config := string(v) + if strings.HasPrefix(config, rolelogStatementSampleRateAttr) { + var result = strings.Split(strings.TrimPrefix(config, rolelogStatementSampleRateAttr+"="), ", ") + res, err := strconv.Atoi(result[0]) + if err != nil { + return -1, fmt.Errorf("Error reading log_statement_sample_rate", err) + } + return res, nil + } + } + return 0, nil +} + + +// readLogTransactionSampleRate searches for an log_transaction_sample_rate entry in the rolconfig array. +// In case no such value is present, it returns nil. +func readLogTransactionSampleRate(roleConfig pq.ByteaArray) (int, error) { + for _, v := range roleConfig { + config := string(v) + if strings.HasPrefix(config, rolelogTransactionSampleRateAttr) { + var result = strings.Split(strings.TrimPrefix(config, rolelogTransactionSampleRateAttr+"="), ", ") + res, err := strconv.Atoi(result[0]) + if err != nil { + return -1, fmt.Errorf("Error reading log_transaction_sample_rate", err) + } + return res, nil + } + } + return 0, nil +} + // readStatementTimeout searches for a statement_timeout entry in the rolconfig array. // In case no such value is present, it returns nil. func readStatementTimeout(roleConfig pq.ByteaArray) (int, error) { @@ -685,6 +826,22 @@ func resourcePostgreSQLRoleUpdate(db *DBConnection, d *schema.ResourceData) erro return err } + if err = setLogMinDurationStatement(txn, d); err != nil { + return err + } + + if err = setLogMinDurationSample(txn, d); err != nil { + return err + } + + if err = setLogStatementSampleRate(txn, d); err != nil { + return err + } + + if err = setLogTransactionSampleRate(txn, d); err != nil { + return err + } + if err = setAssumeRole(txn, d); err != nil { return err } @@ -1038,6 +1195,106 @@ func setIdleInTransactionSessionTimeout(txn *sql.Tx, d *schema.ResourceData) err return nil } +func setLogMinDurationStatement(txn *sql.Tx, d *schema.ResourceData) error { + if !d.HasChange(roleLogMinDurationStatementAttr) { + return nil + } + + roleName := d.Get(roleNameAttr).(string) + logMinDurationStatement := d.Get(roleLogMinDurationStatementAttr).(int) + if logMinDurationStatement != 0 { + sql := fmt.Sprintf( + "ALTER ROLE %s SET log_min_duration_statement TO %d", pq.QuoteIdentifier(roleName), logMinDurationStatement, + ) + if _, err := txn.Exec(sql); err != nil { + return fmt.Errorf("could not set log_min_duration_statement %d for %s: %w", logMinDurationStatement, roleName, err) + } + } else { + sql := fmt.Sprintf( + "ALTER ROLE %s RESET log_min_duration_statement", pq.QuoteIdentifier(roleName), + ) + if _, err := txn.Exec(sql); err != nil { + return fmt.Errorf("could not reset log_min_duration_statement for %s: %w", roleName, err) + } + } + return nil +} + +func setLogMinDurationSample(txn *sql.Tx, d *schema.ResourceData) error { + if !d.HasChange(roleLogMinDurationSampleAttr) { + return nil + } + + roleName := d.Get(roleNameAttr).(string) + logMinDurationSample := d.Get(roleLogMinDurationSampleAttr).(int) + if logMinDurationSample != 0 { + sql := fmt.Sprintf( + "ALTER ROLE %s SET log_min_duration_sample TO %d", pq.QuoteIdentifier(roleName), logMinDurationSample, + ) + if _, err := txn.Exec(sql); err != nil { + return fmt.Errorf("could not set log_min_duration_sample %d for %s: %w", logMinDurationSample, roleName, err) + } + } else { + sql := fmt.Sprintf( + "ALTER ROLE %s RESET log_min_duration_sample", pq.QuoteIdentifier(roleName), + ) + if _, err := txn.Exec(sql); err != nil { + return fmt.Errorf("could not reset log_min_duration_sample for %s: %w", roleName, err) + } + } + return nil +} + +func setLogStatementSampleRate(txn *sql.Tx, d *schema.ResourceData) error { + if !d.HasChange(roleLogStatementSampleRateAttr) { + return nil + } + + roleName := d.Get(roleNameAttr).(string) + logStatementSampleRate := d.Get(roleLogStatementSampleRateAttr).(int) + if logStatementSampleRate != 0 { + sql := fmt.Sprintf( + "ALTER ROLE %s SET log_statement_sample_rate TO %d", pq.QuoteIdentifier(roleName), logStatementSampleRate, + ) + if _, err := txn.Exec(sql); err != nil { + return fmt.Errorf("could not set log_statement_sample_rate %d for %s: %w", logStatementSampleRate, roleName, err) + } + } else { + sql := fmt.Sprintf( + "ALTER ROLE %s RESET log_statement_sample_rate", pq.QuoteIdentifier(roleName), + ) + if _, err := txn.Exec(sql); err != nil { + return fmt.Errorf("could not reset log_statement_sample_rate for %s: %w", roleName, err) + } + } + return nil +} + +func setLogTransactionSampleRate(txn *sql.Tx, d *schema.ResourceData) error { + if !d.HasChange(roleLogTransactionSampleRateAttr) { + return nil + } + + roleName := d.Get(roleNameAttr).(string) + logTransactionSampleRate := d.Get(roleLogTransactionSampleRateAttr).(int) + if logTransactionSampleRate != 0 { + sql := fmt.Sprintf( + "ALTER ROLE %s SET log_transaction_sample_rate TO %d", pq.QuoteIdentifier(roleName), logTransactionSampleRate, + ) + if _, err := txn.Exec(sql); err != nil { + return fmt.Errorf("could not set log_transaction_sample_rate %d for %s: %w", logTransactionSampleRate, roleName, err) + } + } else { + sql := fmt.Sprintf( + "ALTER ROLE %s RESET log_transaction_sample_rate", pq.QuoteIdentifier(roleName), + ) + if _, err := txn.Exec(sql); err != nil { + return fmt.Errorf("could not reset log_transaction_sample_rate for %s: %w", roleName, err) + } + } + return nil +} + func setAssumeRole(txn *sql.Tx, d *schema.ResourceData) error { if !d.HasChange(roleAssumeRoleAttr) { return nil diff --git a/postgresql/resource_postgresql_role_test.go b/postgresql/resource_postgresql_role_test.go index fc958840..88ccce0a 100644 --- a/postgresql/resource_postgresql_role_test.go +++ b/postgresql/resource_postgresql_role_test.go @@ -46,6 +46,10 @@ func TestAccPostgresqlRole_Basic(t *testing.T) { resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "skip_reassign_owned", "false"), resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "statement_timeout", "0"), resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "idle_in_transaction_session_timeout", "0"), + resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "log_min_duration_statement", "-1"), + resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "log_min_duration_sample", "-1"), + resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "log_statement_sample_rate", "1.0"), + resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "log_transaction_sample_rate", "0.0"), resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "assume_role", ""), resource.TestCheckResourceAttr("postgresql_role.role_with_create_database", "name", "role_with_create_database"), @@ -121,6 +125,10 @@ resource "postgresql_role" "update_role" { search_path = ["mysearchpath"] statement_timeout = 30000 idle_in_transaction_session_timeout = 60000 + log_min_duration_statement = 30000 + log_min_duration_sample = 1000 + log_statement_sample_rate = 0.01 + log_transaction_sample_rate = 0.001 assume_role = "${postgresql_role.group_role.name}" } ` @@ -145,6 +153,10 @@ resource "postgresql_role" "update_role" { resource.TestCheckResourceAttr("postgresql_role.update_role", "search_path.#", "0"), resource.TestCheckResourceAttr("postgresql_role.update_role", "statement_timeout", "0"), resource.TestCheckResourceAttr("postgresql_role.update_role", "idle_in_transaction_session_timeout", "0"), + resource.TestCheckResourceAttr("postgresql_role.update_role", "log_min_duration_statement", "-1"), + resource.TestCheckResourceAttr("postgresql_role.update_role", "log_min_duration_sample", "-1"), + resource.TestCheckResourceAttr("postgresql_role.update_role", "log_statement_sample_rate", "1.0"), + resource.TestCheckResourceAttr("postgresql_role.update_role", "log_transaction_sample_rate", "0.0"), resource.TestCheckResourceAttr("postgresql_role.update_role", "assume_role", ""), testAccCheckRoleCanLogin(t, "update_role", "toto"), ), @@ -166,6 +178,10 @@ resource "postgresql_role" "update_role" { resource.TestCheckResourceAttr("postgresql_role.update_role", "search_path.0", "mysearchpath"), resource.TestCheckResourceAttr("postgresql_role.update_role", "statement_timeout", "30000"), resource.TestCheckResourceAttr("postgresql_role.update_role", "idle_in_transaction_session_timeout", "60000"), + resource.TestCheckResourceAttr("postgresql_role.update_role", "log_min_duration_statement", "30000"), + resource.TestCheckResourceAttr("postgresql_role.update_role", "log_min_duration_sample", "1000"), + resource.TestCheckResourceAttr("postgresql_role.update_role", "log_statement_sample_rate", "0.01"), + resource.TestCheckResourceAttr("postgresql_role.update_role", "log_transaction_sample_rate", "0.001"), resource.TestCheckResourceAttr("postgresql_role.update_role", "assume_role", "group_role"), testAccCheckRoleCanLogin(t, "update_role2", "titi"), ), @@ -184,6 +200,10 @@ resource "postgresql_role" "update_role" { resource.TestCheckResourceAttr("postgresql_role.update_role", "search_path.#", "0"), resource.TestCheckResourceAttr("postgresql_role.update_role", "statement_timeout", "0"), resource.TestCheckResourceAttr("postgresql_role.update_role", "idle_in_transaction_session_timeout", "0"), + resource.TestCheckResourceAttr("postgresql_role.update_role", "log_min_duration_statement", "-1"), + resource.TestCheckResourceAttr("postgresql_role.update_role", "log_min_duration_sample", "-1"), + resource.TestCheckResourceAttr("postgresql_role.update_role", "log_statement_sample_rate", "1.0"), + resource.TestCheckResourceAttr("postgresql_role.update_role", "log_transaction_sample_rate", "0.0"), resource.TestCheckResourceAttr("postgresql_role.update_role", "assume_role", ""), testAccCheckRoleCanLogin(t, "update_role", "toto"), ), @@ -426,6 +446,10 @@ resource "postgresql_role" "role_with_defaults" { valid_until = "infinity" statement_timeout = 0 idle_in_transaction_session_timeout = 0 + log_min_duration_statement = -1 + log_min_duration_sample = -1 + log_statement_sample_rate = 1.0 + log_transaction_sample_rate = 0.0 assume_role = "" }