Skip to content

Commit a0dceab

Browse files
feat: add support for pg_stat_user_indexes
Signed-off-by: Michael Todorovic <[email protected]>
1 parent 98f75c7 commit a0dceab

File tree

3 files changed

+276
-0
lines changed

3 files changed

+276
-0
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ This will build the docker image as `prometheuscommunity/postgres_exporter:${bra
147147
* `[no-]collector.stat_statements`
148148
Enable the `stat_statements` collector (default: disabled).
149149

150+
* `[no-]collector.stat_user_indexes`
151+
Enable the `stat_user_indexes` collector (default: disabled).
152+
150153
* `[no-]collector.stat_user_tables`
151154
Enable the `stat_user_tables` collector (default: enabled).
152155

collector/pg_stat_user_indexes.go

+192
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// Copyright 2023 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+
package collector
14+
15+
import (
16+
"context"
17+
"database/sql"
18+
"fmt"
19+
"strings"
20+
21+
"github.com/blang/semver/v4"
22+
"github.com/go-kit/log"
23+
"github.com/go-kit/log/level"
24+
"github.com/prometheus/client_golang/prometheus"
25+
)
26+
27+
func init() {
28+
registerCollector(statUserIndexesSubsystem, defaultDisabled, NewPGStatUserIndexesCollector)
29+
}
30+
31+
type PGStatUserIndexesCollector struct {
32+
log log.Logger
33+
}
34+
35+
const statUserIndexesSubsystem = "stat_user_indexes"
36+
37+
func NewPGStatUserIndexesCollector(config collectorConfig) (Collector, error) {
38+
return &PGStatUserIndexesCollector{log: config.logger}, nil
39+
}
40+
41+
var (
42+
statUserIndexesIdxScan = prometheus.NewDesc(
43+
prometheus.BuildFQName(namespace, statUserIndexesSubsystem, "idx_scan_total"),
44+
"Number of scans for this index",
45+
[]string{"datname", "schemaname", "relname", "indexrelname"},
46+
prometheus.Labels{},
47+
)
48+
49+
statUserIndexesLastIdxScan = prometheus.NewDesc(
50+
prometheus.BuildFQName(namespace, statUserIndexesSubsystem, "last_idx_scan_time"),
51+
"Last timestamp of scan for this index",
52+
[]string{"datname", "schemaname", "relname", "indexrelname"},
53+
prometheus.Labels{},
54+
)
55+
56+
statUserIndexesIdxTupRead = prometheus.NewDesc(
57+
prometheus.BuildFQName(namespace, statUserIndexesSubsystem, "idx_tup_read"),
58+
"Number of tuples read for this index",
59+
[]string{"datname", "schemaname", "relname", "indexrelname"},
60+
prometheus.Labels{},
61+
)
62+
63+
statUserIndexesIdxTupFetch = prometheus.NewDesc(
64+
prometheus.BuildFQName(namespace, statUserIndexesSubsystem, "idx_tup_fetch"),
65+
"Number of tuples fetch for this index",
66+
[]string{"datname", "schemaname", "relname", "indexrelname"},
67+
prometheus.Labels{},
68+
)
69+
)
70+
71+
func statUserIndexesQuery(columns []string) string {
72+
return fmt.Sprintf("SELECT %s FROM pg_stat_user_indexes;", strings.Join(columns, ","))
73+
}
74+
75+
func (c *PGStatUserIndexesCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
76+
db := instance.getDB()
77+
78+
columns := []string{
79+
"current_database() datname",
80+
"schemaname",
81+
"relname",
82+
"indexrelname",
83+
"idx_scan",
84+
"idx_tup_read",
85+
"idx_tup_fetch",
86+
}
87+
88+
lastIdxScanAvail := instance.version.GTE(semver.MustParse("16.0.0"))
89+
if lastIdxScanAvail {
90+
columns = append(columns, "date_part('epoch', last_idx_scan) as last_idx_scan")
91+
}
92+
93+
rows, err := db.QueryContext(ctx,
94+
statUserIndexesQuery(columns),
95+
)
96+
97+
if err != nil {
98+
return err
99+
}
100+
defer rows.Close()
101+
for rows.Next() {
102+
var datname, schemaname, relname, indexrelname sql.NullString
103+
var idxScan, lastIdxScan, idxTupRead, idxTupFetch sql.NullFloat64
104+
105+
r := []any{
106+
&datname,
107+
&schemaname,
108+
&relname,
109+
&indexrelname,
110+
&idxScan,
111+
&idxTupRead,
112+
&idxTupFetch,
113+
}
114+
115+
if lastIdxScanAvail {
116+
r = append(r, &lastIdxScan)
117+
}
118+
119+
if err := rows.Scan(r...); err != nil {
120+
return err
121+
}
122+
datnameLabel := "unknown"
123+
if datname.Valid {
124+
datnameLabel = datname.String
125+
}
126+
schemanameLabel := "unknown"
127+
if schemaname.Valid {
128+
schemanameLabel = schemaname.String
129+
}
130+
relnameLabel := "unknown"
131+
if relname.Valid {
132+
relnameLabel = relname.String
133+
}
134+
indexrelnameLabel := "unknown"
135+
if indexrelname.Valid {
136+
indexrelnameLabel = indexrelname.String
137+
}
138+
139+
if lastIdxScanAvail && !lastIdxScan.Valid {
140+
level.Debug(c.log).Log("msg", "Skipping collecting metric because it has no active_time")
141+
continue
142+
}
143+
144+
labels := []string{datnameLabel, schemanameLabel, relnameLabel, indexrelnameLabel}
145+
146+
idxScanMetric := 0.0
147+
if idxScan.Valid {
148+
idxScanMetric = idxScan.Float64
149+
}
150+
ch <- prometheus.MustNewConstMetric(
151+
statUserIndexesIdxScan,
152+
prometheus.CounterValue,
153+
idxScanMetric,
154+
labels...,
155+
)
156+
157+
idxTupReadMetric := 0.0
158+
if idxTupRead.Valid {
159+
idxTupReadMetric = idxTupRead.Float64
160+
}
161+
ch <- prometheus.MustNewConstMetric(
162+
statUserIndexesIdxTupRead,
163+
prometheus.CounterValue,
164+
idxTupReadMetric,
165+
labels...,
166+
)
167+
168+
idxTupFetchMetric := 0.0
169+
if idxTupFetch.Valid {
170+
idxTupFetchMetric = idxTupFetch.Float64
171+
}
172+
ch <- prometheus.MustNewConstMetric(
173+
statUserIndexesIdxTupFetch,
174+
prometheus.CounterValue,
175+
idxTupFetchMetric,
176+
labels...,
177+
)
178+
179+
if lastIdxScanAvail {
180+
ch <- prometheus.MustNewConstMetric(
181+
statUserIndexesLastIdxScan,
182+
prometheus.CounterValue,
183+
lastIdxScan.Float64,
184+
labels...,
185+
)
186+
}
187+
}
188+
if err := rows.Err(); err != nil {
189+
return err
190+
}
191+
return nil
192+
}
+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright 2023 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+
package collector
14+
15+
import (
16+
"context"
17+
"testing"
18+
19+
"github.com/DATA-DOG/go-sqlmock"
20+
"github.com/prometheus/client_golang/prometheus"
21+
dto "github.com/prometheus/client_model/go"
22+
"github.com/smartystreets/goconvey/convey"
23+
)
24+
25+
func TestPgStatUserIndexesCollector(t *testing.T) {
26+
db, mock, err := sqlmock.New()
27+
if err != nil {
28+
t.Fatalf("Error opening a stub db connection: %s", err)
29+
}
30+
defer db.Close()
31+
inst := &instance{db: db}
32+
33+
columns := []string{
34+
"datname",
35+
"schemaname",
36+
"relname",
37+
"indexrelname",
38+
"idx_scan",
39+
"idx_tup_read",
40+
"idx_tup_fetch",
41+
}
42+
rows := sqlmock.NewRows(columns).
43+
AddRow("postgres", "public", "pgtest_accounts", "pgtest_accounts_pkey", "8", "9", "5")
44+
45+
cols := []string{
46+
"current_database() datname",
47+
"schemaname",
48+
"relname",
49+
"indexrelname",
50+
"idx_scan",
51+
"idx_tup_read",
52+
"idx_tup_fetch",
53+
}
54+
55+
mock.ExpectQuery(sanitizeQuery(statUserIndexesQuery(cols))).WillReturnRows(rows)
56+
57+
ch := make(chan prometheus.Metric)
58+
go func() {
59+
defer close(ch)
60+
c := PGStatUserIndexesCollector{}
61+
62+
if err := c.Update(context.Background(), inst, ch); err != nil {
63+
t.Errorf("Error calling PGStatUserIndexesCollector.Update: %s", err)
64+
}
65+
}()
66+
67+
expected := []MetricResult{
68+
{labels: labelMap{"datname": "postgres", "indexrelname": "pgtest_accounts_pkey", "schemaname": "public", "relname": "pgtest_accounts"}, value: 8, metricType: dto.MetricType_COUNTER},
69+
{labels: labelMap{"datname": "postgres", "indexrelname": "pgtest_accounts_pkey", "schemaname": "public", "relname": "pgtest_accounts"}, value: 9, metricType: dto.MetricType_COUNTER},
70+
{labels: labelMap{"datname": "postgres", "indexrelname": "pgtest_accounts_pkey", "schemaname": "public", "relname": "pgtest_accounts"}, value: 5, metricType: dto.MetricType_COUNTER},
71+
}
72+
convey.Convey("Metrics comparison", t, func() {
73+
for _, expect := range expected {
74+
m := readMetric(<-ch)
75+
convey.So(expect, convey.ShouldResemble, m)
76+
}
77+
})
78+
if err := mock.ExpectationsWereMet(); err != nil {
79+
t.Errorf("there were unfulfilled exceptions: %s", err)
80+
}
81+
}

0 commit comments

Comments
 (0)