Skip to content

Commit 69223b3

Browse files
feat: add pg_index_properties and pg_index_size_bytes
Signed-off-by: Michael Todorovic <[email protected]>
1 parent 96b6bbc commit 69223b3

File tree

2 files changed

+254
-0
lines changed

2 files changed

+254
-0
lines changed

Diff for: collector/pg_index.go

+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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/go-kit/log"
22+
"github.com/prometheus/client_golang/prometheus"
23+
)
24+
25+
func init() {
26+
registerCollector(pgIndexSubsystem, defaultDisabled, NewPgIndexCollector)
27+
}
28+
29+
type PGIndexCollector struct {
30+
log log.Logger
31+
}
32+
33+
const pgIndexSubsystem = "index"
34+
35+
func NewPgIndexCollector(config collectorConfig) (Collector, error) {
36+
return &PGIndexCollector{log: config.logger}, nil
37+
}
38+
39+
var (
40+
pgIndexProperties = prometheus.NewDesc(
41+
prometheus.BuildFQName(namespace, pgIndexSubsystem, "properties"),
42+
"Postgresql index properties",
43+
[]string{"datname", "schemaname", "relname", "indexrelname", "is_unique", "is_primary", "is_valid", "is_ready"},
44+
prometheus.Labels{},
45+
)
46+
pgIndexSize = prometheus.NewDesc(
47+
prometheus.BuildFQName(namespace, pgIndexSubsystem, "size_bytes"),
48+
"Postgresql index size in bytes",
49+
[]string{"datname", "schemaname", "relname", "indexrelname"},
50+
prometheus.Labels{},
51+
)
52+
)
53+
54+
func pgIndexQuery(columns []string) string {
55+
return fmt.Sprintf("SELECT %s FROM pg_catalog.pg_stat_user_indexes s JOIN pg_catalog.pg_index i ON s.indexrelid = i.indexrelid WHERE i.indislive='1';", strings.Join(columns, ","))
56+
}
57+
58+
func boolToString(b bool) string {
59+
if b {
60+
return "1"
61+
}
62+
return "0"
63+
}
64+
65+
func (c *PGIndexCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
66+
db := instance.getDB()
67+
68+
columns := []string{
69+
"current_database() datname",
70+
"s.schemaname",
71+
"s.relname",
72+
"s.indexrelname",
73+
"i.indisunique",
74+
"i.indisprimary",
75+
"i.indisvalid",
76+
"i.indisready",
77+
"pg_relation_size(i.indexrelid) AS indexsize",
78+
}
79+
80+
rows, err := db.QueryContext(ctx,
81+
pgIndexQuery(columns),
82+
)
83+
84+
if err != nil {
85+
return err
86+
}
87+
defer rows.Close()
88+
for rows.Next() {
89+
var datname, schemaname, relname, indexrelname sql.NullString
90+
var idxIsUnique, idxIsPrimary, idxIsValid, idxIsReady sql.NullBool
91+
var idxSize sql.NullFloat64
92+
93+
r := []any{
94+
&datname,
95+
&schemaname,
96+
&relname,
97+
&indexrelname,
98+
&idxIsUnique,
99+
&idxIsPrimary,
100+
&idxIsValid,
101+
&idxIsReady,
102+
&idxSize,
103+
}
104+
105+
if err := rows.Scan(r...); err != nil {
106+
return err
107+
}
108+
datnameLabel := "unknown"
109+
if datname.Valid {
110+
datnameLabel = datname.String
111+
}
112+
schemanameLabel := "unknown"
113+
if schemaname.Valid {
114+
schemanameLabel = schemaname.String
115+
}
116+
relnameLabel := "unknown"
117+
if relname.Valid {
118+
relnameLabel = relname.String
119+
}
120+
indexrelnameLabel := "unknown"
121+
if indexrelname.Valid {
122+
indexrelnameLabel = indexrelname.String
123+
}
124+
125+
indexIsUniqueLabel := "unknown"
126+
if idxIsUnique.Valid {
127+
indexIsUniqueLabel = boolToString(idxIsUnique.Bool)
128+
}
129+
130+
indexIsPrimaryLabel := "unknown"
131+
if idxIsPrimary.Valid {
132+
indexIsPrimaryLabel = boolToString(idxIsPrimary.Bool)
133+
}
134+
135+
indexIsValidLabel := "unknown"
136+
if idxIsValid.Valid {
137+
indexIsValidLabel = boolToString(idxIsValid.Bool)
138+
}
139+
140+
indexIsReadyLabel := "unknown"
141+
if idxIsReady.Valid {
142+
indexIsReadyLabel = boolToString(idxIsReady.Bool)
143+
}
144+
145+
indexSizeMetric := -1.0
146+
if idxSize.Valid {
147+
indexSizeMetric = idxSize.Float64
148+
}
149+
150+
propertiesLabels := []string{datnameLabel, schemanameLabel, relnameLabel, indexrelnameLabel, indexIsUniqueLabel, indexIsPrimaryLabel, indexIsValidLabel, indexIsReadyLabel}
151+
ch <- prometheus.MustNewConstMetric(
152+
pgIndexProperties,
153+
prometheus.CounterValue,
154+
1,
155+
propertiesLabels...,
156+
)
157+
158+
sizeLabels := []string{datnameLabel, schemanameLabel, relnameLabel, indexrelnameLabel}
159+
ch <- prometheus.MustNewConstMetric(
160+
pgIndexSize,
161+
prometheus.GaugeValue,
162+
indexSizeMetric,
163+
sizeLabels...,
164+
)
165+
}
166+
if err := rows.Err(); err != nil {
167+
return err
168+
}
169+
return nil
170+
}

Diff for: collector/pg_index_test.go

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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 TestPgIndexesCollector(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+
"indisunique",
39+
"indisprimary",
40+
"indisvalid",
41+
"indisready",
42+
"indexsize",
43+
}
44+
rows := sqlmock.NewRows(columns).
45+
AddRow("postgres", "public", "pgtest_accounts", "pgtest_accounts_pkey", "0", "1", "1", "1", "123456789")
46+
47+
cols := []string{
48+
"current_database() datname",
49+
"s.schemaname",
50+
"s.relname",
51+
"s.indexrelname",
52+
"i.indisunique",
53+
"i.indisprimary",
54+
"i.indisvalid",
55+
"i.indisready",
56+
"pg_relation_size(i.indexrelid) AS indexsize",
57+
}
58+
59+
mock.ExpectQuery(sanitizeQuery(pgIndexQuery(cols))).WillReturnRows(rows)
60+
61+
ch := make(chan prometheus.Metric)
62+
go func() {
63+
defer close(ch)
64+
c := PGIndexCollector{}
65+
66+
if err := c.Update(context.Background(), inst, ch); err != nil {
67+
t.Errorf("Error calling PGIndexCollector.Update: %s", err)
68+
}
69+
}()
70+
71+
expected := []MetricResult{
72+
{labels: labelMap{"datname": "postgres", "indexrelname": "pgtest_accounts_pkey", "schemaname": "public", "relname": "pgtest_accounts", "is_unique": "0", "is_primary": "1", "is_valid": "1", "is_ready": "1"}, value: 1, metricType: dto.MetricType_COUNTER},
73+
{labels: labelMap{"datname": "postgres", "indexrelname": "pgtest_accounts_pkey", "schemaname": "public", "relname": "pgtest_accounts"}, value: 123456789, metricType: dto.MetricType_GAUGE},
74+
}
75+
convey.Convey("Metrics comparison", t, func() {
76+
for _, expect := range expected {
77+
m := readMetric(<-ch)
78+
convey.So(expect, convey.ShouldResemble, m)
79+
}
80+
})
81+
if err := mock.ExpectationsWereMet(); err != nil {
82+
t.Errorf("there were unfulfilled exceptions: %s", err)
83+
}
84+
}

0 commit comments

Comments
 (0)