Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions collector/pg_index_size.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright 2023 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"

"github.com/go-kit/log"
"github.com/prometheus/client_golang/prometheus"
)

const indexSizeSubsystem = "index_size"

func init() {
registerCollector(indexSizeSubsystem, defaultDisabled, NewPGIndexSizeCollector)
}

type PGIndexSizeCollector struct {
log log.Logger
}

func NewPGIndexSizeCollector(config collectorConfig) (Collector, error) {
return &PGIndexSizeCollector{log: config.logger}, nil
}

var (
indexSizeDesc = prometheus.NewDesc(
prometheus.BuildFQName(namespace, indexSizeSubsystem, "bytes"),
"Size of the index as per pg_table_size function",
[]string{"schemaname", "relname", "indexrelname"},
prometheus.Labels{},
)

indexSizeQuery = `
SELECT
schemaname,
tablename as relname,
indexname as indexrelname,
pg_class.relpages * 8192::bigint as index_size
FROM
pg_indexes inner join pg_namespace on pg_indexes.schemaname = pg_namespace.nspname
inner join pg_class on pg_class.relnamespace = pg_namespace.oid and pg_class.relname = pg_indexes.indexname
WHERE
pg_indexes.schemaname != 'pg_catalog'
`
)

func (PGIndexSizeCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
db := instance.getDB()
rows, err := db.QueryContext(ctx,
indexSizeQuery)

if err != nil {
return err
}
defer rows.Close()

for rows.Next() {
var schemaname, relname, indexrelname sql.NullString
var indexSize sql.NullFloat64

if err := rows.Scan(&schemaname, &relname, &indexrelname, &indexSize); err != nil {
return err
}
schemanameLabel := "unknown"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm really not sure that these metrics even make sense if all of these are NULL. I understand that we used NULL to fix all of the bugs in the 0.13.1 release, but these are net new collectors and I would like to be much more thoughtful about how we handle them in code. For example, if the indexSize metric is NULL, why would we even report the metric? How is the zero value useful.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, if the labels are null, we should Debug log and continue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to me

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this code is still emitting metrics when these values are NULL.

if schemaname.Valid {
schemanameLabel = schemaname.String
}
relnameLabel := "unknown"
if relname.Valid {
relnameLabel = relname.String
}
indexrelnameLabel := "unknown"
if indexrelname.Valid {
indexrelnameLabel = indexrelname.String
}
labels := []string{schemanameLabel, relnameLabel, indexrelnameLabel}

indexSizeMetric := 0.0
if indexSize.Valid {
indexSizeMetric = indexSize.Float64
}
ch <- prometheus.MustNewConstMetric(
indexSizeDesc,
prometheus.GaugeValue,
indexSizeMetric,
labels...,
)
}
if err := rows.Err(); err != nil {
return err
}
return nil
}
105 changes: 105 additions & 0 deletions collector/pg_index_size_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright 2023 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 TestPgIndexSizeCollector(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
defer db.Close()
inst := &instance{db: db}
columns := []string{
"schemaname",
"relname",
"indexrelname",
"index_size",
}
rows := sqlmock.NewRows(columns).
AddRow("public", "foo", "foo_key", 100)

mock.ExpectQuery(sanitizeQuery(indexSizeQuery)).WillReturnRows(rows)

ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGIndexSizeCollector{}

if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGIndexSizeCollector.Update: %s", err)
}
}()
expected := []MetricResult{
{labels: labelMap{"schemaname": "public", "relname": "foo", "indexrelname": "foo_key"}, value: 100, 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 exceptions: %s", err)
}
}

func TestPgIndexSizeCollectorNull(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
defer db.Close()
inst := &instance{db: db}
columns := []string{
"schemaname",
"relname",
"indexrelname",
"index_size",
}
rows := sqlmock.NewRows(columns).
AddRow(nil, nil, nil, nil)

mock.ExpectQuery(sanitizeQuery(indexSizeQuery)).WillReturnRows(rows)

ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGIndexSizeCollector{}

if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGIndexSizeCollector.Update: %s", err)
}
}()
expected := []MetricResult{
{labels: labelMap{"schemaname": "unknown", "relname": "unknown", "indexrelname": "unknown"}, value: 0, 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 exceptions: %s", err)
}
}