Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gitlab Collector: Autovacuum collector and test #840

85 changes: 85 additions & 0 deletions collector/pg_stat_activity_autovacuum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// 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"

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

const statActivityAutovacuumSubsystem = "stat_activity_autovacuum"

func init() {
registerCollector(statActivityAutovacuumSubsystem, defaultDisabled, NewPGStatActivityAutovacuumCollector)
}

type PGStatActivityAutovacuumCollector struct {
log log.Logger
}

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

var (
statActivityAutovacuumAgeInSeconds = prometheus.NewDesc(
prometheus.BuildFQName(namespace, statActivityAutovacuumSubsystem, "age_in_seconds"),
Sticksman marked this conversation as resolved.
Show resolved Hide resolved
"The age of the vacuum process in seconds",
[]string{"relname"},
prometheus.Labels{},
)

statActivityAutovacuumQuery = `
SELECT
SPLIT_PART(query, '.', 2) AS relname,
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems like it could be fragile. How do we guarantee that this will be the relname?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess because since the query is autogenerated it'll always look something like autovacuum: COMMAND schema.relname

EXTRACT(EPOCH FROM (clock_timestamp() - xact_start)) AS age_in_seconds
Sticksman marked this conversation as resolved.
Show resolved Hide resolved
FROM
pg_catalog.pg_stat_activity
WHERE
query like 'autovacuum:%' AND
Sticksman marked this conversation as resolved.
Show resolved Hide resolved
EXTRACT(EPOCH FROM (clock_timestamp() - xact_start)) > 300
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we should drop this filter and always show active autovacuums. This way users can chose what to do with the age in PromQL.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would work for me

`
)

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

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

for rows.Next() {
var relname string
var ageInSeconds float64

if err := rows.Scan(&relname, &ageInSeconds); err != nil {
return err
}

ch <- prometheus.MustNewConstMetric(
statActivityAutovacuumAgeInSeconds,
prometheus.GaugeValue,
ageInSeconds, relname,
)
}
if err := rows.Err(); err != nil {
return err
}
return nil
}
62 changes: 62 additions & 0 deletions collector/pg_stat_activity_autovacuum_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// 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 TestPGStatActivityAutovacuumCollector(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{
"relname",
"timestamp_seconds",
}
rows := sqlmock.NewRows(columns).
AddRow("test", 3600)

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

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

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