Skip to content

Commit 193afae

Browse files
authored
export metrics to track shared preload libraries in postgres (#8)
exports one new metric, `pg_shared_preload_libraries_library_enabled{library=<name}=1` per library across the fleet This helps us understand and track rollouts
1 parent 227898d commit 193afae

File tree

2 files changed

+274
-0
lines changed

2 files changed

+274
-0
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright 2025 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package collector
15+
16+
import (
17+
"context"
18+
"database/sql"
19+
"sort"
20+
"strings"
21+
22+
"github.com/prometheus/client_golang/prometheus"
23+
)
24+
25+
const sharedPreloadLibrariesSubsystem = "settings"
26+
27+
func init() {
28+
registerCollector(sharedPreloadLibrariesSubsystem, defaultEnabled, NewPGSharedPreloadLibrariesCollector)
29+
}
30+
31+
type PGSharedPreloadLibrariesCollector struct{}
32+
33+
func NewPGSharedPreloadLibrariesCollector(collectorConfig) (Collector, error) {
34+
return &PGSharedPreloadLibrariesCollector{}, nil
35+
}
36+
37+
var (
38+
pgSharedPreloadLibrariesLibraryEnabled = prometheus.NewDesc(
39+
prometheus.BuildFQName(
40+
namespace,
41+
sharedPreloadLibrariesSubsystem,
42+
"shared_preload_library_enabled",
43+
),
44+
"Whether a library is listed in shared_preload_libraries (1=yes).",
45+
[]string{"library"}, nil,
46+
)
47+
48+
pgSharedPreloadLibrariesQuery = "SELECT setting FROM pg_settings WHERE name = 'shared_preload_libraries'"
49+
)
50+
51+
func (c *PGSharedPreloadLibrariesCollector) Update(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric) error {
52+
db := instance.getDB()
53+
row := db.QueryRowContext(ctx, pgSharedPreloadLibrariesQuery)
54+
55+
var setting sql.NullString
56+
err := row.Scan(&setting)
57+
if err != nil {
58+
return err
59+
}
60+
61+
// Parse, trim, dedupe and sort libraries for stable series emission.
62+
libsSet := map[string]struct{}{}
63+
if setting.Valid && setting.String != "" {
64+
for _, raw := range strings.Split(setting.String, ",") {
65+
lib := strings.TrimSpace(raw)
66+
if lib == "" {
67+
continue
68+
}
69+
libsSet[lib] = struct{}{}
70+
}
71+
}
72+
libs := make([]string, 0, len(libsSet))
73+
for lib := range libsSet {
74+
libs = append(libs, lib)
75+
}
76+
sort.Strings(libs)
77+
78+
for _, lib := range libs {
79+
ch <- prometheus.MustNewConstMetric(
80+
pgSharedPreloadLibrariesLibraryEnabled,
81+
prometheus.GaugeValue,
82+
1,
83+
lib,
84+
)
85+
}
86+
return nil
87+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// Copyright 2025 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package collector
15+
16+
import (
17+
"context"
18+
"testing"
19+
20+
"github.com/DATA-DOG/go-sqlmock"
21+
"github.com/prometheus/client_golang/prometheus"
22+
dto "github.com/prometheus/client_model/go"
23+
"github.com/smartystreets/goconvey/convey"
24+
)
25+
26+
func TestPGSharedPreloadLibrariesCollector(t *testing.T) {
27+
db, mock, err := sqlmock.New()
28+
if err != nil {
29+
t.Fatalf("Error opening a stub database connection: %s", err)
30+
}
31+
defer db.Close()
32+
33+
inst := &Instance{db: db}
34+
35+
columns := []string{"setting"}
36+
rows := sqlmock.NewRows(columns).
37+
AddRow("pg_stat_statements, auto_explain, pg_hint_plan")
38+
39+
mock.ExpectQuery(sanitizeQuery(pgSharedPreloadLibrariesQuery)).WillReturnRows(rows)
40+
41+
ch := make(chan prometheus.Metric)
42+
go func() {
43+
defer close(ch)
44+
c := PGSharedPreloadLibrariesCollector{}
45+
46+
if err := c.Update(context.Background(), inst, ch); err != nil {
47+
t.Errorf("Error calling PGSharedPreloadLibrariesCollector.Update: %s", err)
48+
}
49+
}()
50+
51+
expected := []MetricResult{
52+
// Emitted in sorted order: auto_explain, pg_hint_plan, pg_stat_statements
53+
{labels: labelMap{"library": "auto_explain"}, value: 1, metricType: dto.MetricType_GAUGE},
54+
{labels: labelMap{"library": "pg_hint_plan"}, value: 1, metricType: dto.MetricType_GAUGE},
55+
{labels: labelMap{"library": "pg_stat_statements"}, value: 1, metricType: dto.MetricType_GAUGE},
56+
}
57+
58+
convey.Convey("Metrics comparison", t, func() {
59+
for _, expect := range expected {
60+
m := readMetric(<-ch)
61+
convey.So(expect, convey.ShouldResemble, m)
62+
}
63+
})
64+
if err := mock.ExpectationsWereMet(); err != nil {
65+
t.Errorf("there were unfulfilled expectations: %s", err)
66+
}
67+
}
68+
69+
func TestPGSharedPreloadLibrariesCollectorEmpty(t *testing.T) {
70+
db, mock, err := sqlmock.New()
71+
if err != nil {
72+
t.Fatalf("Error opening a stub database connection: %s", err)
73+
}
74+
defer db.Close()
75+
76+
inst := &Instance{db: db}
77+
78+
columns := []string{"setting"}
79+
rows := sqlmock.NewRows(columns).
80+
AddRow("")
81+
82+
mock.ExpectQuery(sanitizeQuery(pgSharedPreloadLibrariesQuery)).WillReturnRows(rows)
83+
84+
ch := make(chan prometheus.Metric)
85+
go func() {
86+
defer close(ch)
87+
c := PGSharedPreloadLibrariesCollector{}
88+
89+
if err := c.Update(context.Background(), inst, ch); err != nil {
90+
t.Errorf("Error calling PGSharedPreloadLibrariesCollector.Update: %s", err)
91+
}
92+
}()
93+
94+
expected := []MetricResult{}
95+
96+
convey.Convey("Metrics comparison", t, func() {
97+
for _, expect := range expected {
98+
m := readMetric(<-ch)
99+
convey.So(expect, convey.ShouldResemble, m)
100+
}
101+
})
102+
if err := mock.ExpectationsWereMet(); err != nil {
103+
t.Errorf("there were unfulfilled expectations: %s", err)
104+
}
105+
}
106+
107+
func TestPGSharedPreloadLibrariesCollectorSingle(t *testing.T) {
108+
db, mock, err := sqlmock.New()
109+
if err != nil {
110+
t.Fatalf("Error opening a stub database connection: %s", err)
111+
}
112+
defer db.Close()
113+
114+
inst := &Instance{db: db}
115+
116+
columns := []string{"setting"}
117+
rows := sqlmock.NewRows(columns).
118+
AddRow("pg_stat_statements")
119+
120+
mock.ExpectQuery(sanitizeQuery(pgSharedPreloadLibrariesQuery)).WillReturnRows(rows)
121+
122+
ch := make(chan prometheus.Metric)
123+
go func() {
124+
defer close(ch)
125+
c := PGSharedPreloadLibrariesCollector{}
126+
127+
if err := c.Update(context.Background(), inst, ch); err != nil {
128+
t.Errorf("Error calling PGSharedPreloadLibrariesCollector.Update: %s", err)
129+
}
130+
}()
131+
132+
expected := []MetricResult{
133+
{labels: labelMap{"library": "pg_stat_statements"}, value: 1, metricType: dto.MetricType_GAUGE},
134+
}
135+
136+
convey.Convey("Metrics comparison", t, func() {
137+
for _, expect := range expected {
138+
m := readMetric(<-ch)
139+
convey.So(expect, convey.ShouldResemble, m)
140+
}
141+
})
142+
if err := mock.ExpectationsWereMet(); err != nil {
143+
t.Errorf("there were unfulfilled expectations: %s", err)
144+
}
145+
}
146+
147+
func TestPGSharedPreloadLibrariesCollectorWhitespaceAndDuplicates(t *testing.T) {
148+
db, mock, err := sqlmock.New()
149+
if err != nil {
150+
t.Fatalf("Error opening a stub database connection: %s", err)
151+
}
152+
defer db.Close()
153+
154+
inst := &Instance{db: db}
155+
156+
columns := []string{"setting"}
157+
rows := sqlmock.NewRows(columns).
158+
AddRow("pg_stat_statements, auto_explain, pg_hint_plan , auto_explain , pg_stat_statements ")
159+
160+
mock.ExpectQuery(sanitizeQuery(pgSharedPreloadLibrariesQuery)).WillReturnRows(rows)
161+
162+
ch := make(chan prometheus.Metric)
163+
go func() {
164+
defer close(ch)
165+
c := PGSharedPreloadLibrariesCollector{}
166+
167+
if err := c.Update(context.Background(), inst, ch); err != nil {
168+
t.Errorf("Error calling PGSharedPreloadLibrariesCollector.Update: %s", err)
169+
}
170+
}()
171+
172+
expected := []MetricResult{
173+
{labels: labelMap{"library": "auto_explain"}, value: 1, metricType: dto.MetricType_GAUGE},
174+
{labels: labelMap{"library": "pg_hint_plan"}, value: 1, metricType: dto.MetricType_GAUGE},
175+
{labels: labelMap{"library": "pg_stat_statements"}, value: 1, metricType: dto.MetricType_GAUGE},
176+
}
177+
178+
convey.Convey("Metrics comparison", t, func() {
179+
for _, expect := range expected {
180+
m := readMetric(<-ch)
181+
convey.So(expect, convey.ShouldResemble, m)
182+
}
183+
})
184+
if err := mock.ExpectationsWereMet(); err != nil {
185+
t.Errorf("there were unfulfilled expectations: %s", err)
186+
}
187+
}

0 commit comments

Comments
 (0)