Skip to content

Commit 20685a3

Browse files
committed
Add connections by client
Signed-off-by: Felix Yuan <felix.yuan@reddit.com>
1 parent a324fe3 commit 20685a3

File tree

2 files changed

+148
-0
lines changed

2 files changed

+148
-0
lines changed

collector/pg_connections_by_client.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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+
"log/slog"
20+
21+
"github.com/prometheus/client_golang/prometheus"
22+
)
23+
24+
const connectionsByClientSubsystem = "connections_by_client"
25+
26+
func init() {
27+
registerCollector(connectionsByClientSubsystem, defaultEnabled, NewPGConnectionsByClientCollector)
28+
}
29+
30+
type PGConnectionByClientCollector struct {
31+
log *slog.Logger
32+
}
33+
34+
func NewPGConnectionsByClientCollector(config collectorConfig) (Collector, error) {
35+
return &PGConnectionByClientCollector{
36+
log: config.logger,
37+
}, nil
38+
}
39+
40+
var (
41+
pgClientCountDesc = prometheus.NewDesc(
42+
prometheus.BuildFQName(
43+
namespace,
44+
connectionsByClientSubsystem,
45+
"count",
46+
),
47+
"Number of clients",
48+
[]string{"name"}, nil,
49+
)
50+
51+
pgConnectionsByClientQuery = `
52+
SELECT
53+
count(*) as count,
54+
client_hostname
55+
FROM pg_stat_activity
56+
WHERE client_hostname is not null
57+
GROUP BY client_hostname;
58+
`
59+
)
60+
61+
func (c PGConnectionByClientCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
62+
db := instance.getDB()
63+
64+
rows, err := db.QueryContext(ctx,
65+
pgConnectionsByClientQuery,
66+
)
67+
if err != nil {
68+
return err
69+
}
70+
defer rows.Close()
71+
var clientCount sql.NullInt64
72+
var clientName sql.NullString
73+
74+
for rows.Next() {
75+
if err := rows.Scan(&clientCount, &clientName); err != nil {
76+
return err
77+
}
78+
79+
if !clientName.Valid {
80+
continue
81+
}
82+
83+
countMetric := 0.0
84+
if clientCount.Valid {
85+
countMetric = float64(clientCount.Int64)
86+
}
87+
88+
ch <- prometheus.MustNewConstMetric(
89+
pgClientCountDesc,
90+
prometheus.GaugeValue,
91+
countMetric,
92+
clientName.String,
93+
)
94+
}
95+
if err := rows.Err(); err != nil {
96+
return err
97+
}
98+
99+
return nil
100+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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 TestPGConnectionsByClientCollector(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{"count", "client_name"}).
23+
AddRow(42, "test")
24+
25+
mock.ExpectQuery(sanitizeQuery(pgConnectionsByClientQuery)).WillReturnRows(rows)
26+
27+
ch := make(chan prometheus.Metric)
28+
go func() {
29+
defer close(ch)
30+
c := PGConnectionByClientCollector{}
31+
if err := c.Update(context.Background(), inst, ch); err != nil {
32+
t.Errorf("Error calling PGConnectionsByClientCollector.Update: %s", err)
33+
}
34+
}()
35+
36+
expected := []MetricResult{
37+
{labels: labelMap{"name": "test"}, value: 42, metricType: dto.MetricType_GAUGE},
38+
}
39+
convey.Convey("Metrics comparison", t, func() {
40+
for _, expect := range expected {
41+
m := readMetric(<-ch)
42+
convey.So(expect, convey.ShouldResemble, m)
43+
}
44+
})
45+
if err := mock.ExpectationsWereMet(); err != nil {
46+
t.Errorf("there were unfulfilled exceptions: %s", err)
47+
}
48+
}

0 commit comments

Comments
 (0)