go.d/postgres: allow pg_ls_dir execute privilege for repl slot files (#22488)
Co-authored-by: ilyam8 <ilya@netdata.cloud>
Sergey Lavrinenko committed
May 18, 2026 at 12:56 UTC
356a50d5a87446627a4165b400cc755bf0685cf1
6 files changed
+87
-1
src/go/plugin/go.d/collector/postgres/collect.go
+13
@@ -50,6 +50,15 @@ func (c *Collector) collect() (map[string]int64, error) {
50
c.Debugf("connected as super user: %v", *c.superUser)
51
}
52
53
+ if c.canExecutePgLsDir == nil && c.pgVersion >= pgVersion10 {
54
+ v, err := c.doQueryCanExecutePgLsDir()
55
+ if err != nil {
56
+ return nil, fmt.Errorf("querying can execute pg_ls_dir() error: %v", err)
57
+ }
58
+ c.canExecutePgLsDir = &v
59
+ c.Debugf("can execute pg_ls_dir(): %v", *c.canExecutePgLsDir)
60
+ }
61
+
62
if c.pgIsInRecovery == nil {
63
v, err := c.doQueryPGIsInRecovery()
64
if err != nil {
@@ -225,6 +234,10 @@ func (c *Collector) azureADBeforeConnect(ctx context.Context, cfg *pgx.ConnConfi
234
235
func (c *Collector) isSuperUser() bool { return c.superUser != nil && *c.superUser }
236
237
+func (c *Collector) canQueryReplicationSlotFiles() bool {
238
+ return c.isSuperUser() || (c.canExecutePgLsDir != nil && *c.canExecutePgLsDir)
239
+}
240
+
241
func (c *Collector) isPGInRecovery() bool { return c.pgIsInRecovery != nil && *c.pgIsInRecovery }
242
243
func (c *Collector) getDBMetrics(name string) *dbMetrics {
src/go/plugin/go.d/collector/postgres/collector.go
+1
@@ -118,6 +118,7 @@ type (
118
dbConns map[string]*dbConn
119
120
superUser *bool
121
+ canExecutePgLsDir *bool
122
pgIsInRecovery *bool
123
pgVersion int
124
pgStatStatementsAvail bool // cached positive result only
src/go/plugin/go.d/collector/postgres/collector_test.go
+57
@@ -185,6 +185,7 @@ func TestCollector_Check(t *testing.T) {
185
186
mockExpect(t, m, queryServerVersion(), dataVer140004ServerVersionNum)
187
mockExpect(t, m, queryIsSuperUser(), dataVer140004IsSuperUserTrue)
188
+ mockExpect(t, m, queryCanExecutePgLsDir(), dataVer140004IsSuperUserTrue)
189
mockExpect(t, m, queryPGIsInRecovery(), dataVer140004PGIsInRecoveryTrue)
190
191
mockExpect(t, m, querySettingsMaxConnections(), dataVer140004SettingsMaxConnections)
@@ -225,6 +226,7 @@ func TestCollector_Check(t *testing.T) {
226
prepareMock: func(t *testing.T, collr *Collector, m sqlmock.Sqlmock) {
227
mockExpect(t, m, queryServerVersion(), dataVer140004ServerVersionNum)
228
mockExpect(t, m, queryIsSuperUser(), dataVer140004IsSuperUserTrue)
229
+ mockExpect(t, m, queryCanExecutePgLsDir(), dataVer140004IsSuperUserTrue)
230
mockExpect(t, m, queryPGIsInRecovery(), dataVer140004PGIsInRecoveryTrue)
231
232
mockExpect(t, m, querySettingsMaxConnections(), dataVer140004ServerVersionNum)
@@ -245,6 +247,7 @@ func TestCollector_Check(t *testing.T) {
247
prepareMock: func(t *testing.T, collr *Collector, m sqlmock.Sqlmock) {
248
mockExpect(t, m, queryServerVersion(), dataVer140004ServerVersionNum)
249
mockExpect(t, m, queryIsSuperUser(), dataVer140004IsSuperUserTrue)
250
+ mockExpect(t, m, queryCanExecutePgLsDir(), dataVer140004IsSuperUserTrue)
251
mockExpect(t, m, queryPGIsInRecovery(), dataVer140004PGIsInRecoveryTrue)
252
253
mockExpectErr(m, querySettingsMaxConnections())
@@ -289,6 +292,7 @@ func TestCollector_Collect(t *testing.T) {
292
collr.dbSr = matcher.TRUE()
293
mockExpect(t, m, queryServerVersion(), dataVer140004ServerVersionNum)
294
mockExpect(t, m, queryIsSuperUser(), dataVer140004IsSuperUserTrue)
295
+ mockExpect(t, m, queryCanExecutePgLsDir(), dataVer140004IsSuperUserTrue)
296
mockExpect(t, m, queryPGIsInRecovery(), dataVer140004PGIsInRecoveryTrue)
297
298
mockExpect(t, m, querySettingsMaxConnections(), dataVer140004SettingsMaxConnections)
@@ -657,6 +661,7 @@ func TestCollector_Collect(t *testing.T) {
661
prepareMock: func(t *testing.T, collr *Collector, m sqlmock.Sqlmock) {
662
mockExpect(t, m, queryServerVersion(), dataVer140004ServerVersionNum)
663
mockExpect(t, m, queryIsSuperUser(), dataVer140004IsSuperUserTrue)
664
+ mockExpect(t, m, queryCanExecutePgLsDir(), dataVer140004IsSuperUserTrue)
665
mockExpect(t, m, queryPGIsInRecovery(), dataVer140004PGIsInRecoveryTrue)
666
667
mockExpectErr(m, querySettingsMaxConnections())
@@ -673,6 +678,7 @@ func TestCollector_Collect(t *testing.T) {
678
prepareMock: func(t *testing.T, collr *Collector, m sqlmock.Sqlmock) {
679
mockExpect(t, m, queryServerVersion(), dataVer140004ServerVersionNum)
680
mockExpect(t, m, queryIsSuperUser(), dataVer140004IsSuperUserTrue)
681
+ mockExpect(t, m, queryCanExecutePgLsDir(), dataVer140004IsSuperUserTrue)
682
mockExpect(t, m, queryPGIsInRecovery(), dataVer140004PGIsInRecoveryTrue)
683
684
mockExpect(t, m, querySettingsMaxConnections(), dataVer140004SettingsMaxConnections)
@@ -713,6 +719,57 @@ func TestCollector_Collect(t *testing.T) {
719
}
720
}
721
722
+func TestCollector_doQueryReplicationMetrics_replSlotFilesGate(t *testing.T) {
723
+ boolPtr := func(v bool) *bool { return &v }
724
+
725
+ tests := map[string]struct {
726
+ superUser *bool
727
+ canExecutePgLsDir *bool
728
+ expectSlotFilesCall bool
729
+ }{
730
+ "superuser collects slot files even without pg_ls_dir privilege": {
731
+ superUser: boolPtr(true),
732
+ canExecutePgLsDir: boolPtr(false),
733
+ expectSlotFilesCall: true,
734
+ },
735
+ "non-superuser with pg_ls_dir privilege collects slot files": {
736
+ superUser: boolPtr(false),
737
+ canExecutePgLsDir: boolPtr(true),
738
+ expectSlotFilesCall: true,
739
+ },
740
+ "non-superuser without pg_ls_dir privilege skips slot files": {
741
+ superUser: boolPtr(false),
742
+ canExecutePgLsDir: boolPtr(false),
743
+ expectSlotFilesCall: false,
744
+ },
745
+ }
746
+
747
+ for name, test := range tests {
748
+ t.Run(name, func(t *testing.T) {
749
+ db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
750
+ require.NoError(t, err)
751
+ defer func() { _ = db.Close() }()
752
+
753
+ collr := New()
754
+ collr.db = db
755
+ require.NoError(t, collr.Init(context.Background()))
756
+
757
+ collr.pgVersion = 140004
758
+ collr.superUser = test.superUser
759
+ collr.canExecutePgLsDir = test.canExecutePgLsDir
760
+
761
+ mockExpect(t, mock, queryReplicationStandbyAppDelta(collr.pgVersion), dataVer140004ReplStandbyAppDelta)
762
+ mockExpect(t, mock, queryReplicationStandbyAppLag(), dataVer140004ReplStandbyAppLag)
763
+ if test.expectSlotFilesCall {
764
+ mockExpect(t, mock, queryReplicationSlotFiles(collr.pgVersion), dataVer140004ReplSlotFiles)
765
+ }
766
+
767
+ assert.NoError(t, collr.doQueryReplicationMetrics())
768
+ assert.NoError(t, mock.ExpectationsWereMet())
769
+ })
770
+ }
771
+}
772
+
773
func mockExpect(t *testing.T, mock sqlmock.Sqlmock, query string, rows []byte) {
774
mock.ExpectQuery(query).WillReturnRows(mustMockRows(t, rows)).RowsWillBeClosed()
775
}
src/go/plugin/go.d/collector/postgres/do_query_misc.go
+11
@@ -31,6 +31,17 @@ func (c *Collector) doQueryIsSuperUser() (bool, error) {
31
return v, nil
32
}
33
34
+func (c *Collector) doQueryCanExecutePgLsDir() (bool, error) {
35
+ q := queryCanExecutePgLsDir()
36
+
37
+ var v bool
38
+ if err := c.doQueryRow(q, &v); err != nil {
39
+ return false, err
40
+ }
41
+
42
+ return v, nil
43
+}
44
+
45
func (c *Collector) doQueryPGIsInRecovery() (bool, error) {
46
q := queryPGIsInRecovery()
47
src/go/plugin/go.d/collector/postgres/do_query_replication.go
+1
-1
@@ -17,7 +17,7 @@ func (c *Collector) doQueryReplicationMetrics() error {
17
}
18
}
19
20
- if c.pgVersion >= pgVersion10 && c.isSuperUser() {
20
+ if c.pgVersion >= pgVersion10 && c.canQueryReplicationSlotFiles() {
21
if err := c.doQueryReplSlotFiles(); err != nil {
22
return fmt.Errorf("querying replication slot files error: %v", err)
23
}
src/go/plugin/go.d/collector/postgres/queries.go
+4
@@ -10,6 +10,10 @@ func queryIsSuperUser() string {
10
return "SELECT current_setting('is_superuser') = 'on' AS is_superuser;"
11
}
12
13
+func queryCanExecutePgLsDir() string {
14
+ return "SELECT has_function_privilege(current_user, 'pg_catalog.pg_ls_dir(text)', 'EXECUTE');"
15
+}
16
+
17
func queryPGIsInRecovery() string {
18
return "SELECT pg_is_in_recovery();"
19
}