Skip to content

Commit a69bc25

Browse files
committed
feat: add custom collector for statement summary pre-release
1 parent cd450cd commit a69bc25

File tree

2 files changed

+182
-0
lines changed

2 files changed

+182
-0
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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+
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 statStatementsSummarySubsystem = "stat_statements_summary"
25+
26+
func init() {
27+
registerCollector(statStatementsSummarySubsystem, defaultDisabled, NewPGStatStatementsSummaryCollector)
28+
}
29+
30+
type PGStatStatementsSummaryCollector struct {
31+
log log.Logger
32+
}
33+
34+
func NewPGStatStatementsSummaryCollector(config collectorConfig) (Collector, error) {
35+
return &PGStatStatementsSummaryCollector{log: config.logger}, nil
36+
}
37+
38+
var (
39+
statSTatementsSummaryCallsTotal = prometheus.NewDesc(
40+
prometheus.BuildFQName(namespace, statStatementsSubsystem, "calls_total"),
41+
"Number of times executed",
42+
[]string{"datname"},
43+
prometheus.Labels{},
44+
)
45+
statStatementsSummarySecondsTotal = prometheus.NewDesc(
46+
prometheus.BuildFQName(namespace, statStatementsSubsystem, "seconds_total"),
47+
"Total time spent in the statement, in seconds",
48+
[]string{"datname"},
49+
prometheus.Labels{},
50+
)
51+
52+
pgStatStatementsSummaryQuery = `SELECT
53+
pg_database.datname,
54+
SUM(pg_stat_statements.calls) as calls_total,
55+
SUM(pg_stat_statements.total_exec_time) / 1000.0 as seconds_total
56+
FROM pg_stat_statements
57+
JOIN pg_database
58+
ON pg_database.oid = pg_stat_statements.dbid
59+
WHERE
60+
total_exec_time > (
61+
SELECT percentile_cont(0.1)
62+
WITHIN GROUP (ORDER BY total_exec_time)
63+
FROM pg_stat_statements
64+
)
65+
GROUP BY pg_database.datname;`
66+
)
67+
68+
func (PGStatStatementsSummaryCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
69+
query := pgStatStatementsSummaryQuery
70+
71+
db := instance.getDB()
72+
rows, err := db.QueryContext(ctx, query)
73+
74+
if err != nil {
75+
return err
76+
}
77+
defer rows.Close()
78+
for rows.Next() {
79+
var datname sql.NullString
80+
var callsTotal sql.NullInt64
81+
var secondsTotal sql.NullFloat64
82+
83+
if err := rows.Scan(&datname, &callsTotal, &secondsTotal); err != nil {
84+
return err
85+
}
86+
87+
datnameLabel := "unknown"
88+
if datname.Valid {
89+
datnameLabel = datname.String
90+
}
91+
92+
callsTotalMetric := 0.0
93+
if callsTotal.Valid {
94+
callsTotalMetric = float64(callsTotal.Int64)
95+
}
96+
ch <- prometheus.MustNewConstMetric(
97+
statSTatementsSummaryCallsTotal,
98+
prometheus.CounterValue,
99+
callsTotalMetric,
100+
datnameLabel,
101+
)
102+
103+
secondsTotalMetric := 0.0
104+
if secondsTotal.Valid {
105+
secondsTotalMetric = secondsTotal.Float64
106+
}
107+
ch <- prometheus.MustNewConstMetric(
108+
statStatementsSummarySecondsTotal,
109+
prometheus.CounterValue,
110+
secondsTotalMetric,
111+
datnameLabel,
112+
)
113+
}
114+
if err := rows.Err(); err != nil {
115+
return err
116+
}
117+
return nil
118+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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/blang/semver/v4"
21+
"github.com/prometheus/client_golang/prometheus"
22+
dto "github.com/prometheus/client_model/go"
23+
"github.com/smartystreets/goconvey/convey"
24+
)
25+
26+
func TestPGStateStatementsSummaryCollector(t *testing.T) {
27+
db, mock, err := sqlmock.New()
28+
if err != nil {
29+
t.Fatalf("Error opening a stub db connection: %s", err)
30+
}
31+
defer db.Close()
32+
33+
inst := &instance{db: db, version: semver.MustParse("13.3.7")}
34+
35+
columns := []string{"datname", "calls_total", "seconds_total"}
36+
rows := sqlmock.NewRows(columns).
37+
AddRow("postgres", 5, 0.4)
38+
mock.ExpectQuery(sanitizeQuery(pgStatStatementsSummaryQuery)).WillReturnRows(rows)
39+
40+
ch := make(chan prometheus.Metric)
41+
go func() {
42+
defer close(ch)
43+
c := PGStatStatementsSummaryCollector{}
44+
45+
if err := c.Update(context.Background(), inst, ch); err != nil {
46+
t.Errorf("Error calling PGStatStatementsSummaryCollector.Update: %s", err)
47+
}
48+
}()
49+
50+
expected := []MetricResult{
51+
{labels: labelMap{"datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 5},
52+
{labels: labelMap{"datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0.4},
53+
}
54+
55+
convey.Convey("Metrics comparison", t, func() {
56+
for _, expect := range expected {
57+
m := readMetric(<-ch)
58+
convey.So(expect, convey.ShouldResemble, m)
59+
}
60+
})
61+
if err := mock.ExpectationsWereMet(); err != nil {
62+
t.Errorf("there were unfulfilled exceptions: %s", err)
63+
}
64+
}

0 commit comments

Comments
 (0)