package postgresql import ( "fmt" "strings" "testing" "github.com/docker/go-connections/nat" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go/wait" "github.com/influxdata/telegraf/config" "github.com/influxdata/telegraf/testutil" ) const servicePort = "5432" func launchTestContainer(t *testing.T) *testutil.Container { container := testutil.Container{ Image: "postgres:alpine", ExposedPorts: []string{servicePort}, Env: map[string]string{ "POSTGRES_HOST_AUTH_METHOD": "trust", }, WaitingFor: wait.ForAll( // the database comes up twice, once right away, then again a second // time after the docker entrypoint starts configuration wait.ForLog("database system is ready to accept connections").WithOccurrence(2), wait.ForListeningPort(nat.Port(servicePort)), ), } err := container.Start() require.NoError(t, err, "failed to start container") return &container } func TestPostgresqlGeneratesMetricsIntegration(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } container := launchTestContainer(t) defer container.Terminate() addr := fmt.Sprintf( "host=%s port=%s user=postgres sslmode=disable", container.Address, container.Ports[servicePort], ) p := &Postgresql{ Service: Service{ Address: config.NewSecret([]byte(addr)), IsPgBouncer: false, }, Databases: []string{"postgres"}, } var acc testutil.Accumulator require.NoError(t, p.Start(&acc)) require.NoError(t, p.Gather(&acc)) intMetrics := []string{ "xact_commit", "xact_rollback", "blks_read", "blks_hit", "tup_returned", "tup_fetched", "tup_inserted", "tup_updated", "tup_deleted", "conflicts", "temp_files", "temp_bytes", "deadlocks", "buffers_alloc", "buffers_backend", "buffers_backend_fsync", "buffers_checkpoint", "buffers_clean", "checkpoints_req", "checkpoints_timed", "maxwritten_clean", "datid", "numbackends", } int32Metrics := []string{} floatMetrics := []string{ "blk_read_time", "blk_write_time", "checkpoint_write_time", "checkpoint_sync_time", } stringMetrics := []string{ "datname", } metricsCounted := 0 for _, metric := range intMetrics { require.True(t, acc.HasInt64Field("postgresql", metric)) metricsCounted++ } for _, metric := range int32Metrics { require.True(t, acc.HasInt32Field("postgresql", metric)) metricsCounted++ } for _, metric := range floatMetrics { require.True(t, acc.HasFloatField("postgresql", metric)) metricsCounted++ } for _, metric := range stringMetrics { require.True(t, acc.HasStringField("postgresql", metric)) metricsCounted++ } require.Greater(t, metricsCounted, 0) require.Equal(t, len(floatMetrics)+len(intMetrics)+len(int32Metrics)+len(stringMetrics), metricsCounted) } func TestPostgresqlTagsMetricsWithDatabaseNameIntegration(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } container := launchTestContainer(t) defer container.Terminate() addr := fmt.Sprintf( "host=%s port=%s user=postgres sslmode=disable", container.Address, container.Ports[servicePort], ) p := &Postgresql{ Service: Service{ Address: config.NewSecret([]byte(addr)), }, Databases: []string{"postgres"}, } var acc testutil.Accumulator require.NoError(t, p.Start(&acc)) require.NoError(t, p.Gather(&acc)) point, ok := acc.Get("postgresql") require.True(t, ok) require.Equal(t, "postgres", point.Tags["db"]) } func TestPostgresqlDefaultsToAllDatabasesIntegration(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } container := launchTestContainer(t) defer container.Terminate() addr := fmt.Sprintf( "host=%s port=%s user=postgres sslmode=disable", container.Address, container.Ports[servicePort], ) p := &Postgresql{ Service: Service{ Address: config.NewSecret([]byte(addr)), }, } var acc testutil.Accumulator require.NoError(t, p.Start(&acc)) require.NoError(t, p.Gather(&acc)) var found bool for _, pnt := range acc.Metrics { if pnt.Measurement == "postgresql" { if pnt.Tags["db"] == "postgres" { found = true break } } } require.True(t, found) } func TestPostgresqlIgnoresUnwantedColumnsIntegration(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } container := launchTestContainer(t) defer container.Terminate() addr := fmt.Sprintf( "host=%s port=%s user=postgres sslmode=disable", container.Address, container.Ports[servicePort], ) p := &Postgresql{ Service: Service{ Address: config.NewSecret([]byte(addr)), }, } var acc testutil.Accumulator require.NoError(t, p.Start(&acc)) require.NoError(t, p.Gather(&acc)) for col := range p.IgnoredColumns() { require.False(t, acc.HasMeasurement(col)) } } func TestPostgresqlDatabaseWhitelistTestIntegration(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } container := launchTestContainer(t) defer container.Terminate() addr := fmt.Sprintf( "host=%s port=%s user=postgres sslmode=disable", container.Address, container.Ports[servicePort], ) p := &Postgresql{ Service: Service{ Address: config.NewSecret([]byte(addr)), }, Databases: []string{"template0"}, } var acc testutil.Accumulator require.NoError(t, p.Start(&acc)) require.NoError(t, p.Gather(&acc)) var foundTemplate0 = false var foundTemplate1 = false for _, pnt := range acc.Metrics { if pnt.Measurement == "postgresql" { if pnt.Tags["db"] == "template0" { foundTemplate0 = true } } if pnt.Measurement == "postgresql" { if pnt.Tags["db"] == "template1" { foundTemplate1 = true } } } require.True(t, foundTemplate0) require.False(t, foundTemplate1) } func TestPostgresqlDatabaseBlacklistTestIntegration(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } container := launchTestContainer(t) defer container.Terminate() addr := fmt.Sprintf( "host=%s port=%s user=postgres sslmode=disable", container.Address, container.Ports[servicePort], ) p := &Postgresql{ Service: Service{ Address: config.NewSecret([]byte(addr)), }, IgnoredDatabases: []string{"template0"}, } var acc testutil.Accumulator require.NoError(t, p.Start(&acc)) require.NoError(t, p.Gather(&acc)) var foundTemplate0 = false var foundTemplate1 = false for _, pnt := range acc.Metrics { if pnt.Measurement == "postgresql" { if pnt.Tags["db"] == "template0" { foundTemplate0 = true } } if pnt.Measurement == "postgresql" { if pnt.Tags["db"] == "template1" { foundTemplate1 = true } } } require.False(t, foundTemplate0) require.True(t, foundTemplate1) } func TestURIParsing(t *testing.T) { tests := []struct { name string uri string expected string }{ { name: "short", uri: `postgres://localhost`, expected: "host=localhost", }, { name: "with port", uri: `postgres://localhost:5432`, expected: "host=localhost port=5432", }, { name: "with database", uri: `postgres://localhost/mydb`, expected: "dbname=mydb host=localhost", }, { name: "with additional parameters", uri: `postgres://localhost/mydb?application_name=pgxtest&search_path=myschema&connect_timeout=5`, expected: "application_name=pgxtest connect_timeout=5 dbname=mydb host=localhost search_path=myschema", }, { name: "with database setting in params", uri: `postgres://localhost:5432/?database=mydb`, expected: "database=mydb host=localhost port=5432", }, { name: "with authentication", uri: `postgres://jack:secret@localhost:5432/mydb?sslmode=prefer`, expected: "dbname=mydb host=localhost password=secret port=5432 sslmode=prefer user=jack", }, { name: "with spaces", uri: `postgres://jack%20hunter:secret@localhost/mydb?application_name=pgx%20test`, expected: "application_name='pgx test' dbname=mydb host=localhost password=secret user='jack hunter'", }, { name: "with equal signs", uri: `postgres://jack%20hunter:secret@localhost/mydb?application_name=pgx%3Dtest`, expected: "application_name='pgx=test' dbname=mydb host=localhost password=secret user='jack hunter'", }, { name: "multiple hosts", uri: `postgres://jack:secret@foo:1,bar:2,baz:3/mydb?sslmode=disable`, expected: "dbname=mydb host=foo,bar,baz password=secret port=1,2,3 sslmode=disable user=jack", }, { name: "multiple hosts without ports", uri: `postgres://jack:secret@foo,bar,baz/mydb?sslmode=disable`, expected: "dbname=mydb host=foo,bar,baz password=secret sslmode=disable user=jack", }, } for _, tt := range tests { // Key value without spaces around equal sign t.Run(tt.name, func(t *testing.T) { actual, err := toKeyValue(tt.uri) require.NoError(t, err) require.Equalf(t, tt.expected, actual, "initial: %s", tt.uri) }) } } func TestSanitizeAddressKeyValue(t *testing.T) { keys := []string{"password", "sslcert", "sslkey", "sslmode", "sslrootcert"} tests := []struct { name string value string }{ { name: "simple text", value: `foo`, }, { name: "empty values", value: `''`, }, { name: "space in value", value: `'foo bar'`, }, { name: "equal sign in value", value: `'foo=bar'`, }, { name: "escaped quote", value: `'foo\'s bar'`, }, { name: "escaped quote no space", value: `\'foobar\'s\'`, }, { name: "escaped backslash", value: `'foo bar\\'`, }, { name: "escaped quote and backslash", value: `'foo\\\'s bar'`, }, { name: "two escaped backslashes", value: `'foo bar\\\\'`, }, { name: "multiple inline spaces", value: "'foo \t bar'", }, { name: "leading space", value: `' foo bar'`, }, { name: "trailing space", value: `'foo bar '`, }, { name: "multiple equal signs", value: `'foo===bar'`, }, { name: "leading equal sign", value: `'=foo bar'`, }, { name: "trailing equal sign", value: `'foo bar='`, }, { name: "mix of equal signs and spaces", value: "'foo = a\t===\tbar'", }, } for _, tt := range tests { // Key value without spaces around equal sign t.Run(tt.name, func(t *testing.T) { // Generate the DSN from the given keys and value parts := make([]string, 0, len(keys)) for _, k := range keys { parts = append(parts, k+"="+tt.value) } dsn := strings.Join(parts, " canary=ok ") plugin := &Postgresql{ Service: Service{ Address: config.NewSecret([]byte(dsn)), }, } expected := strings.Join(make([]string, len(keys)), "canary=ok ") expected = strings.TrimSpace(expected) actual, err := plugin.SanitizedAddress() require.NoError(t, err) require.Equalf(t, expected, actual, "initial: %s", dsn) }) // Key value with spaces around equal sign t.Run("spaced "+tt.name, func(t *testing.T) { // Generate the DSN from the given keys and value parts := make([]string, 0, len(keys)) for _, k := range keys { parts = append(parts, k+" = "+tt.value) } dsn := strings.Join(parts, " canary=ok ") plugin := &Postgresql{ Service: Service{ Address: config.NewSecret([]byte(dsn)), }, } expected := strings.Join(make([]string, len(keys)), "canary=ok ") expected = strings.TrimSpace(expected) actual, err := plugin.SanitizedAddress() require.NoError(t, err) require.Equalf(t, expected, actual, "initial: %s", dsn) }) } } func TestSanitizeAddressURI(t *testing.T) { keys := []string{"password", "sslcert", "sslkey", "sslmode", "sslrootcert"} tests := []struct { name string value string }{ { name: "simple text", value: `foo`, }, { name: "empty values", value: ``, }, { name: "space in value", value: `foo bar`, }, { name: "equal sign in value", value: `foo=bar`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Generate the DSN from the given keys and value value := strings.ReplaceAll(tt.value, "=", "%3D") value = strings.ReplaceAll(value, " ", "%20") parts := make([]string, 0, len(keys)) for _, k := range keys { parts = append(parts, k+"="+value) } dsn := "postgresql://user:passwd@localhost:5432/db?" + strings.Join(parts, "&") plugin := &Postgresql{ Service: Service{ Address: config.NewSecret([]byte(dsn)), }, } expected := "dbname=db host=localhost port=5432 user=user" actual, err := plugin.SanitizedAddress() require.NoError(t, err) require.Equalf(t, expected, actual, "initial: %s", dsn) }) } }