Skip to content

Commit 3eebf9a

Browse files
committed
Add Table Sizes to postgresql exporter
Signed-off-by: Moritz Wirth <mw@flanga.io>
1 parent a4ac0e6 commit 3eebf9a

File tree

2 files changed

+193
-0
lines changed

2 files changed

+193
-0
lines changed

collector/pg_table.go

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright 2024 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+
20+
"github.com/go-kit/log"
21+
"github.com/prometheus/client_golang/prometheus"
22+
)
23+
24+
const tableSizeSubsystem = "table_size"
25+
26+
func init() {
27+
registerCollector(tableSizeSubsystem, defaultEnabled, NewPGTableSizeCollector)
28+
}
29+
30+
type PGTableSizeCollector struct {
31+
log log.Logger
32+
}
33+
34+
func NewPGTableSizeCollector(config collectorConfig) (Collector, error) {
35+
return &PGTableSizeCollector{
36+
log: config.logger,
37+
}, nil
38+
}
39+
40+
var (
41+
pgTableTotalRelationDesc = prometheus.NewDesc(
42+
prometheus.BuildFQName(
43+
namespace,
44+
tableSizeSubsystem,
45+
"total_relation",
46+
),
47+
"Total Relation Size of the table",
48+
[]string{"schemaname", "datname", "relname"}, nil,
49+
)
50+
pgTableIndexSizeDesc = prometheus.NewDesc(
51+
prometheus.BuildFQName(
52+
namespace,
53+
tableSizeSubsystem,
54+
"index",
55+
),
56+
"Indexes Size of the Table",
57+
[]string{"schemaname", "datname", "relname"}, nil,
58+
)
59+
pgRelationSizeDesc = prometheus.NewDesc(
60+
prometheus.BuildFQName(
61+
namespace,
62+
tableSizeSubsystem,
63+
"relation",
64+
),
65+
"Relation Size of the table",
66+
[]string{"schemaname", "datname", "relname"}, nil,
67+
)
68+
pgTableSizeQuery = `SELECT
69+
table_catalog datname,
70+
table_name relname,
71+
table_schema schemaname,
72+
pg_total_relation_size('"'||table_schema||'"."'||table_name||'"') total_relation_size,
73+
pg_relation_size('"'||table_schema||'"."'||table_name||'"') relation_size,
74+
pg_indexes_size('"'||table_schema||'"."'||table_name||'"') indexes_size
75+
FROM information_schema.tables`
76+
)
77+
78+
// Update implements Collector and exposes database locks.
79+
// It is called by the Prometheus registry when collecting metrics.
80+
func (c PGTableSizeCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
81+
db := instance.getDB()
82+
// Query the list of databases
83+
rows, err := db.QueryContext(ctx, pgTableSizeQuery)
84+
if err != nil {
85+
return err
86+
}
87+
defer rows.Close()
88+
for rows.Next() {
89+
var tableSchema, tableName, databaseName sql.NullString
90+
var totalRelationSize, relationSize, indexesSize sql.NullInt64
91+
92+
if err := rows.Scan(&databaseName, &tableName, &tableSchema, &totalRelationSize, &relationSize, &indexesSize); err != nil {
93+
return err
94+
}
95+
96+
if !tableSchema.Valid || !tableName.Valid || !databaseName.Valid {
97+
continue
98+
}
99+
100+
totalRelationsSizeMetric := 0.0
101+
relationSizeMetric := 0.0
102+
indexesSizeMetric := 0.0
103+
if totalRelationSize.Valid {
104+
totalRelationsSizeMetric = float64(totalRelationSize.Int64)
105+
}
106+
107+
if relationSize.Valid {
108+
relationSizeMetric = float64(relationSize.Int64)
109+
}
110+
111+
if indexesSize.Valid {
112+
indexesSizeMetric = float64(indexesSize.Int64)
113+
}
114+
115+
ch <- prometheus.MustNewConstMetric(
116+
pgTableTotalRelationDesc,
117+
prometheus.CounterValue, totalRelationsSizeMetric,
118+
tableSchema.String, databaseName.String, tableName.String,
119+
)
120+
121+
ch <- prometheus.MustNewConstMetric(
122+
pgRelationSizeDesc,
123+
prometheus.CounterValue, relationSizeMetric,
124+
tableSchema.String, databaseName.String, tableName.String,
125+
)
126+
127+
ch <- prometheus.MustNewConstMetric(
128+
pgTableIndexSizeDesc,
129+
prometheus.CounterValue, indexesSizeMetric,
130+
tableSchema.String, databaseName.String, tableName.String,
131+
)
132+
}
133+
134+
if err := rows.Err(); err != nil {
135+
return err
136+
}
137+
138+
return nil
139+
}

collector/pg_table_test.go

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package collector
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/DATA-DOG/go-sqlmock"
8+
"github.com/prometheus/client_golang/prometheus"
9+
dto "github.com/prometheus/client_model/go"
10+
"github.com/smartystreets/goconvey/convey"
11+
)
12+
13+
func TestPGTableSizeCollector(t *testing.T) {
14+
db, mock, err := sqlmock.New()
15+
if err != nil {
16+
t.Fatalf("Error opening a stub db connection: %s", err)
17+
}
18+
defer db.Close()
19+
20+
inst := &instance{db: db}
21+
22+
rows := sqlmock.NewRows([]string{"datname", "relname", "schemaname", "total_relation_size", "relation_size", "indexes_size"}).
23+
AddRow("test", "testrel", "testschema", 69, 42, 27).
24+
AddRow("test2", "testrel2", "testschema2", 14, 10, 2)
25+
26+
mock.ExpectQuery(sanitizeQuery(pgTableSizeQuery)).WillReturnRows(rows)
27+
28+
ch := make(chan prometheus.Metric)
29+
go func() {
30+
defer close(ch)
31+
c := PGTableSizeCollector{}
32+
if err := c.Update(context.Background(), inst, ch); err != nil {
33+
t.Errorf("Error calling PGTableSizeCollector.Update: %s", err)
34+
}
35+
}()
36+
37+
expected := []MetricResult{
38+
{labels: labelMap{"datname": "test", "relname": "testrel", "schemaname": "testschema"}, value: 69, metricType: dto.MetricType_COUNTER},
39+
{labels: labelMap{"datname": "test", "relname": "testrel", "schemaname": "testschema"}, value: 42, metricType: dto.MetricType_COUNTER},
40+
{labels: labelMap{"datname": "test", "relname": "testrel", "schemaname": "testschema"}, value: 27, metricType: dto.MetricType_COUNTER},
41+
{labels: labelMap{"datname": "test2", "relname": "testrel2", "schemaname": "testschema2"}, value: 14, metricType: dto.MetricType_COUNTER},
42+
{labels: labelMap{"datname": "test2", "relname": "testrel2", "schemaname": "testschema2"}, value: 10, metricType: dto.MetricType_COUNTER},
43+
{labels: labelMap{"datname": "test2", "relname": "testrel2", "schemaname": "testschema2"}, value: 2, metricType: dto.MetricType_COUNTER},
44+
}
45+
convey.Convey("Metrics comparison", t, func() {
46+
for _, expect := range expected {
47+
m := readMetric(<-ch)
48+
convey.So(expect, convey.ShouldResemble, m)
49+
}
50+
})
51+
if err := mock.ExpectationsWereMet(); err != nil {
52+
t.Errorf("there were unfulfilled exceptions: %s", err)
53+
}
54+
}

0 commit comments

Comments
 (0)