diff --git a/collector/pg_shared_preload_libraries.go b/collector/pg_shared_preload_libraries.go new file mode 100644 index 000000000..79d1d6a06 --- /dev/null +++ b/collector/pg_shared_preload_libraries.go @@ -0,0 +1,87 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collector + +import ( + "context" + "database/sql" + "sort" + "strings" + + "github.com/prometheus/client_golang/prometheus" +) + +const sharedPreloadLibrariesSubsystem = "settings" + +func init() { + registerCollector(sharedPreloadLibrariesSubsystem, defaultEnabled, NewPGSharedPreloadLibrariesCollector) +} + +type PGSharedPreloadLibrariesCollector struct{} + +func NewPGSharedPreloadLibrariesCollector(collectorConfig) (Collector, error) { + return &PGSharedPreloadLibrariesCollector{}, nil +} + +var ( + pgSharedPreloadLibrariesLibraryEnabled = prometheus.NewDesc( + prometheus.BuildFQName( + namespace, + sharedPreloadLibrariesSubsystem, + "shared_preload_library_enabled", + ), + "Whether a library is listed in shared_preload_libraries (1=yes).", + []string{"library"}, nil, + ) + + pgSharedPreloadLibrariesQuery = "SELECT setting FROM pg_settings WHERE name = 'shared_preload_libraries'" +) + +func (c *PGSharedPreloadLibrariesCollector) Update(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric) error { + db := instance.getDB() + row := db.QueryRowContext(ctx, pgSharedPreloadLibrariesQuery) + + var setting sql.NullString + err := row.Scan(&setting) + if err != nil { + return err + } + + // Parse, trim, dedupe and sort libraries for stable series emission. + libsSet := map[string]struct{}{} + if setting.Valid && setting.String != "" { + for _, raw := range strings.Split(setting.String, ",") { + lib := strings.TrimSpace(raw) + if lib == "" { + continue + } + libsSet[lib] = struct{}{} + } + } + libs := make([]string, 0, len(libsSet)) + for lib := range libsSet { + libs = append(libs, lib) + } + sort.Strings(libs) + + for _, lib := range libs { + ch <- prometheus.MustNewConstMetric( + pgSharedPreloadLibrariesLibraryEnabled, + prometheus.GaugeValue, + 1, + lib, + ) + } + return nil +} diff --git a/collector/pg_shared_preload_libraries_test.go b/collector/pg_shared_preload_libraries_test.go new file mode 100644 index 000000000..1a8e2629e --- /dev/null +++ b/collector/pg_shared_preload_libraries_test.go @@ -0,0 +1,187 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collector + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" + "github.com/smartystreets/goconvey/convey" +) + +func TestPGSharedPreloadLibrariesCollector(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Error opening a stub database connection: %s", err) + } + defer db.Close() + + inst := &Instance{db: db} + + columns := []string{"setting"} + rows := sqlmock.NewRows(columns). + AddRow("pg_stat_statements, auto_explain, pg_hint_plan") + + mock.ExpectQuery(sanitizeQuery(pgSharedPreloadLibrariesQuery)).WillReturnRows(rows) + + ch := make(chan prometheus.Metric) + go func() { + defer close(ch) + c := PGSharedPreloadLibrariesCollector{} + + if err := c.Update(context.Background(), inst, ch); err != nil { + t.Errorf("Error calling PGSharedPreloadLibrariesCollector.Update: %s", err) + } + }() + + expected := []MetricResult{ + // Emitted in sorted order: auto_explain, pg_hint_plan, pg_stat_statements + {labels: labelMap{"library": "auto_explain"}, value: 1, metricType: dto.MetricType_GAUGE}, + {labels: labelMap{"library": "pg_hint_plan"}, value: 1, metricType: dto.MetricType_GAUGE}, + {labels: labelMap{"library": "pg_stat_statements"}, value: 1, metricType: dto.MetricType_GAUGE}, + } + + convey.Convey("Metrics comparison", t, func() { + for _, expect := range expected { + m := readMetric(<-ch) + convey.So(expect, convey.ShouldResemble, m) + } + }) + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } +} + +func TestPGSharedPreloadLibrariesCollectorEmpty(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Error opening a stub database connection: %s", err) + } + defer db.Close() + + inst := &Instance{db: db} + + columns := []string{"setting"} + rows := sqlmock.NewRows(columns). + AddRow("") + + mock.ExpectQuery(sanitizeQuery(pgSharedPreloadLibrariesQuery)).WillReturnRows(rows) + + ch := make(chan prometheus.Metric) + go func() { + defer close(ch) + c := PGSharedPreloadLibrariesCollector{} + + if err := c.Update(context.Background(), inst, ch); err != nil { + t.Errorf("Error calling PGSharedPreloadLibrariesCollector.Update: %s", err) + } + }() + + expected := []MetricResult{} + + convey.Convey("Metrics comparison", t, func() { + for _, expect := range expected { + m := readMetric(<-ch) + convey.So(expect, convey.ShouldResemble, m) + } + }) + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } +} + +func TestPGSharedPreloadLibrariesCollectorSingle(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Error opening a stub database connection: %s", err) + } + defer db.Close() + + inst := &Instance{db: db} + + columns := []string{"setting"} + rows := sqlmock.NewRows(columns). + AddRow("pg_stat_statements") + + mock.ExpectQuery(sanitizeQuery(pgSharedPreloadLibrariesQuery)).WillReturnRows(rows) + + ch := make(chan prometheus.Metric) + go func() { + defer close(ch) + c := PGSharedPreloadLibrariesCollector{} + + if err := c.Update(context.Background(), inst, ch); err != nil { + t.Errorf("Error calling PGSharedPreloadLibrariesCollector.Update: %s", err) + } + }() + + expected := []MetricResult{ + {labels: labelMap{"library": "pg_stat_statements"}, value: 1, metricType: dto.MetricType_GAUGE}, + } + + convey.Convey("Metrics comparison", t, func() { + for _, expect := range expected { + m := readMetric(<-ch) + convey.So(expect, convey.ShouldResemble, m) + } + }) + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } +} + +func TestPGSharedPreloadLibrariesCollectorWhitespaceAndDuplicates(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Error opening a stub database connection: %s", err) + } + defer db.Close() + + inst := &Instance{db: db} + + columns := []string{"setting"} + rows := sqlmock.NewRows(columns). + AddRow("pg_stat_statements, auto_explain, pg_hint_plan , auto_explain , pg_stat_statements ") + + mock.ExpectQuery(sanitizeQuery(pgSharedPreloadLibrariesQuery)).WillReturnRows(rows) + + ch := make(chan prometheus.Metric) + go func() { + defer close(ch) + c := PGSharedPreloadLibrariesCollector{} + + if err := c.Update(context.Background(), inst, ch); err != nil { + t.Errorf("Error calling PGSharedPreloadLibrariesCollector.Update: %s", err) + } + }() + + expected := []MetricResult{ + {labels: labelMap{"library": "auto_explain"}, value: 1, metricType: dto.MetricType_GAUGE}, + {labels: labelMap{"library": "pg_hint_plan"}, value: 1, metricType: dto.MetricType_GAUGE}, + {labels: labelMap{"library": "pg_stat_statements"}, value: 1, metricType: dto.MetricType_GAUGE}, + } + + convey.Convey("Metrics comparison", t, func() { + for _, expect := range expected { + m := readMetric(<-ch) + convey.So(expect, convey.ShouldResemble, m) + } + }) + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } +}