@cryptotaxi247 / netdata / commits / 356a50d5a

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 }