Skip to content

Commit 83066b4

Browse files
Add a collector for pg_proctab.
Docs: https://github.com/markwkm/pg_proctab/tree/main This collector is useful when: * You have access to postgres, but not the underlying machine the postgres server is running on. * The `pg_proctab` extension is installed. We use this in AWS RDS and GCP CloudSQL.
1 parent 94e8399 commit 83066b4

File tree

2 files changed

+344
-0
lines changed

2 files changed

+344
-0
lines changed

collector/pg_proctab.go

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
package collector
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"log/slog"
7+
8+
"github.com/prometheus/client_golang/prometheus"
9+
)
10+
11+
const proctabSubsystem = "proctab"
12+
13+
func init() {
14+
registerCollector(proctabSubsystem, defaultDisabled, NewPGProctabCollector)
15+
}
16+
17+
type PGProctabCollector struct {
18+
log *slog.Logger
19+
}
20+
21+
func NewPGProctabCollector(config collectorConfig) (Collector, error) {
22+
return &PGProctabCollector{
23+
log: config.logger,
24+
}, nil
25+
}
26+
27+
var (
28+
pgMemusedDesc = prometheus.NewDesc(
29+
prometheus.BuildFQName(
30+
namespace,
31+
proctabSubsystem,
32+
"memused",
33+
),
34+
"used memory (from /proc/meminfo) in bytes",
35+
[]string{}, nil,
36+
)
37+
38+
pgMemfreeDesc = prometheus.NewDesc(
39+
prometheus.BuildFQName(
40+
namespace,
41+
proctabSubsystem,
42+
"memfree",
43+
),
44+
"free memory (from /proc/meminfo) in bytes",
45+
[]string{}, nil,
46+
)
47+
48+
pgMemsharedDesc = prometheus.NewDesc(
49+
prometheus.BuildFQName(
50+
namespace,
51+
proctabSubsystem,
52+
"memshared",
53+
),
54+
"shared memory (from /proc/meminfo) in bytes",
55+
[]string{}, nil,
56+
)
57+
58+
pgMembuffersDesc = prometheus.NewDesc(
59+
prometheus.BuildFQName(
60+
namespace,
61+
proctabSubsystem,
62+
"membuffers",
63+
),
64+
"buffered memory (from /proc/meminfo) in bytes",
65+
[]string{}, nil,
66+
)
67+
68+
pgMemcachedDesc = prometheus.NewDesc(
69+
prometheus.BuildFQName(
70+
namespace,
71+
proctabSubsystem,
72+
"memcached",
73+
),
74+
"cached memory (from /proc/meminfo) in bytes",
75+
[]string{}, nil,
76+
)
77+
pgSwapusedDesc = prometheus.NewDesc(
78+
prometheus.BuildFQName(
79+
namespace,
80+
proctabSubsystem,
81+
"swapused",
82+
),
83+
"swap used (from /proc/meminfo) in bytes",
84+
[]string{}, nil,
85+
)
86+
87+
// Loadavg metrics
88+
pgLoad1Desc = prometheus.NewDesc(
89+
prometheus.BuildFQName(
90+
namespace,
91+
proctabSubsystem,
92+
"load1",
93+
),
94+
"load1 load Average",
95+
[]string{}, nil,
96+
)
97+
98+
// CPU metrics
99+
pgCpuUserDesc = prometheus.NewDesc(
100+
prometheus.BuildFQName(
101+
namespace,
102+
proctabSubsystem,
103+
"cpu_user",
104+
),
105+
"PG User cpu time",
106+
[]string{}, nil,
107+
)
108+
pgCpuNiceDesc = prometheus.NewDesc(
109+
prometheus.BuildFQName(
110+
namespace,
111+
proctabSubsystem,
112+
"cpu_nice",
113+
),
114+
"PG nice cpu time (running queries)",
115+
[]string{}, nil,
116+
)
117+
pgCpuSystemDesc = prometheus.NewDesc(
118+
prometheus.BuildFQName(
119+
namespace,
120+
proctabSubsystem,
121+
"cpu_system",
122+
),
123+
"PG system cpu time",
124+
[]string{}, nil,
125+
)
126+
pgCpuIdleDesc = prometheus.NewDesc(
127+
prometheus.BuildFQName(
128+
namespace,
129+
proctabSubsystem,
130+
"cpu_idle",
131+
),
132+
"PG idle cpu time",
133+
[]string{}, nil,
134+
)
135+
pgCpuIowaitDesc = prometheus.NewDesc(
136+
prometheus.BuildFQName(
137+
namespace,
138+
proctabSubsystem,
139+
"cpu_iowait",
140+
),
141+
"PG iowait time",
142+
[]string{}, nil,
143+
)
144+
145+
memoryQuery = `
146+
select
147+
memused,
148+
memfree,
149+
memshared,
150+
membuffers,
151+
memcached,
152+
swapused
153+
from
154+
pg_memusage()
155+
`
156+
157+
load1Query = `
158+
select
159+
load1
160+
from
161+
pg_loadavg()
162+
`
163+
cpuQuery = `
164+
select
165+
"user",
166+
nice,
167+
system,
168+
idle,
169+
iowait
170+
from
171+
pg_cputime()
172+
`
173+
)
174+
175+
func emitMemMetric(m sql.NullInt64, desc *prometheus.Desc, ch chan<- prometheus.Metric) {
176+
mM := 0.0
177+
if m.Valid {
178+
mM = float64(m.Int64 * 1024)
179+
}
180+
ch <- prometheus.MustNewConstMetric(desc, prometheus.GaugeValue, mM)
181+
}
182+
func emitCpuMetric(m sql.NullInt64, desc *prometheus.Desc, ch chan<- prometheus.Metric) {
183+
mM := 0.0
184+
if m.Valid {
185+
mM = float64(m.Int64)
186+
}
187+
ch <- prometheus.MustNewConstMetric(desc, prometheus.CounterValue, mM)
188+
}
189+
190+
// Update implements Collector and exposes database locks.
191+
// It is called by the Prometheus registry when collecting metrics.
192+
func (c PGProctabCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
193+
db := instance.getDB()
194+
// Query the list of databases
195+
rows, err := db.QueryContext(ctx, memoryQuery)
196+
if err != nil {
197+
return err
198+
}
199+
defer rows.Close()
200+
201+
var memused, memfree, memshared, membuffers, memcached, swapused sql.NullInt64
202+
203+
for rows.Next() {
204+
if err := rows.Scan(&memused, &memfree, &memshared, &membuffers, &memcached, &swapused); err != nil {
205+
return err
206+
}
207+
emitMemMetric(memused, pgMemusedDesc, ch)
208+
emitMemMetric(memfree, pgMemfreeDesc, ch)
209+
emitMemMetric(memshared, pgMemsharedDesc, ch)
210+
emitMemMetric(membuffers, pgMembuffersDesc, ch)
211+
emitMemMetric(memcached, pgMemcachedDesc, ch)
212+
emitMemMetric(swapused, pgSwapusedDesc, ch)
213+
}
214+
215+
if err := rows.Err(); err != nil {
216+
return err
217+
}
218+
219+
rows, err = db.QueryContext(ctx, load1Query)
220+
if err != nil {
221+
return err
222+
}
223+
defer rows.Close()
224+
225+
var load1 sql.NullFloat64
226+
for rows.Next() {
227+
if err := rows.Scan(&load1); err != nil {
228+
return err
229+
}
230+
load1Metric := 0.0
231+
if load1.Valid {
232+
load1Metric = load1.Float64
233+
}
234+
ch <- prometheus.MustNewConstMetric(
235+
pgLoad1Desc,
236+
prometheus.GaugeValue, load1Metric,
237+
)
238+
}
239+
if err := rows.Err(); err != nil {
240+
return err
241+
}
242+
243+
rows, err = db.QueryContext(ctx, cpuQuery)
244+
if err != nil {
245+
return err
246+
}
247+
defer rows.Close()
248+
var user, nice, system, idle, iowait sql.NullInt64
249+
for rows.Next() {
250+
if err := rows.Scan(&user, &nice, &system, &idle, &iowait); err != nil {
251+
return err
252+
}
253+
emitCpuMetric(user, pgCpuUserDesc, ch)
254+
emitCpuMetric(nice, pgCpuNiceDesc, ch)
255+
emitCpuMetric(system, pgCpuSystemDesc, ch)
256+
emitCpuMetric(idle, pgCpuIdleDesc, ch)
257+
emitCpuMetric(iowait, pgCpuIowaitDesc, ch)
258+
}
259+
if err := rows.Err(); err != nil {
260+
return err
261+
}
262+
263+
return nil
264+
265+
}

collector/pg_proctab_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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/prometheus/client_golang/prometheus"
21+
dto "github.com/prometheus/client_model/go"
22+
"github.com/smartystreets/goconvey/convey"
23+
)
24+
25+
func TestPGProctabCollector(t *testing.T) {
26+
db, mock, err := sqlmock.New()
27+
if err != nil {
28+
t.Fatalf("Error opening a stub db connection: %s", err)
29+
}
30+
defer db.Close()
31+
32+
inst := &instance{db: db}
33+
34+
rows := sqlmock.NewRows([]string{"memused", "memfree", "memshared", "membuffers", "memcached", "swapused"}).
35+
AddRow(123, 456, 789, 234, 567, 89)
36+
mock.ExpectQuery(sanitizeQuery(memoryQuery)).WillReturnRows(rows)
37+
38+
rows = sqlmock.NewRows([]string{"load1"}).AddRow(123.456)
39+
mock.ExpectQuery(sanitizeQuery(load1Query)).WillReturnRows(rows)
40+
41+
rows = sqlmock.NewRows([]string{"user", "nice", "system", "idle", "iowait"}).AddRow(
42+
345, 678, 9, 1234, 56)
43+
mock.ExpectQuery(sanitizeQuery(cpuQuery)).WillReturnRows(rows)
44+
45+
ch := make(chan prometheus.Metric)
46+
go func() {
47+
defer close(ch)
48+
c := PGProctabCollector{}
49+
if err := c.Update(context.Background(), inst, ch); err != nil {
50+
t.Errorf("Error calling PGProctabCollector .Update: %s", err)
51+
}
52+
}()
53+
54+
expected := []MetricResult{
55+
{labels: labelMap{}, value: 123 * 1024, metricType: dto.MetricType_GAUGE},
56+
{labels: labelMap{}, value: 456 * 1024, metricType: dto.MetricType_GAUGE},
57+
{labels: labelMap{}, value: 789 * 1024, metricType: dto.MetricType_GAUGE},
58+
{labels: labelMap{}, value: 234 * 1024, metricType: dto.MetricType_GAUGE},
59+
{labels: labelMap{}, value: 567 * 1024, metricType: dto.MetricType_GAUGE},
60+
{labels: labelMap{}, value: 89 * 1024, metricType: dto.MetricType_GAUGE},
61+
// load
62+
{labels: labelMap{}, value: 123.456, metricType: dto.MetricType_GAUGE},
63+
// cpu
64+
{labels: labelMap{}, value: 345, metricType: dto.MetricType_COUNTER},
65+
{labels: labelMap{}, value: 678, metricType: dto.MetricType_COUNTER},
66+
{labels: labelMap{}, value: 9, metricType: dto.MetricType_COUNTER},
67+
{labels: labelMap{}, value: 1234, metricType: dto.MetricType_COUNTER},
68+
{labels: labelMap{}, value: 56, metricType: dto.MetricType_COUNTER},
69+
}
70+
convey.Convey("Metrics comparison", t, func() {
71+
for _, expect := range expected {
72+
m := readMetric(<-ch)
73+
convey.So(expect, convey.ShouldResemble, m)
74+
}
75+
})
76+
if err := mock.ExpectationsWereMet(); err != nil {
77+
t.Errorf("there were unfulfilled exceptions: %s", err)
78+
}
79+
}

0 commit comments

Comments
 (0)