diff --git a/collector/pg_locks.go b/collector/pg_locks.go index add3e6d42..655e7ffdf 100644 --- a/collector/pg_locks.go +++ b/collector/pg_locks.go @@ -49,39 +49,39 @@ var ( ) pgLocksQuery = ` - SELECT + SELECT pg_database.datname as datname, tmp.mode as mode, - COALESCE(count, 0) as count - FROM + COALESCE(count, 0) as count + FROM ( - VALUES - ('accesssharelock'), - ('rowsharelock'), - ('rowexclusivelock'), - ('shareupdateexclusivelock'), - ('sharelock'), - ('sharerowexclusivelock'), - ('exclusivelock'), - ('accessexclusivelock'), + VALUES + ('accesssharelock'), + ('rowsharelock'), + ('rowexclusivelock'), + ('shareupdateexclusivelock'), + ('sharelock'), + ('sharerowexclusivelock'), + ('exclusivelock'), + ('accessexclusivelock'), ('sireadlock') ) AS tmp(mode) - CROSS JOIN pg_database + CROSS JOIN pg_database LEFT JOIN ( - SELECT - database, - lower(mode) AS mode, - count(*) AS count - FROM - pg_locks - WHERE - database IS NOT NULL - GROUP BY - database, + SELECT + database, + lower(mode) AS mode, + count(*) AS count + FROM + pg_locks + WHERE + database IS NOT NULL + GROUP BY + database, lower(mode) - ) AS tmp2 ON tmp.mode = tmp2.mode - and pg_database.oid = tmp2.database - ORDER BY + ) AS tmp2 ON tmp.mode = tmp2.mode + and pg_database.oid = tmp2.database + ORDER BY 1 ` ) diff --git a/collector/pg_proctab.go b/collector/pg_proctab.go new file mode 100644 index 000000000..79b542429 --- /dev/null +++ b/collector/pg_proctab.go @@ -0,0 +1,278 @@ +// Copyright 2025 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" + "log/slog" + + "github.com/prometheus/client_golang/prometheus" +) + +const proctabSubsystem = "proctab" + +func init() { + registerCollector(proctabSubsystem, defaultDisabled, NewPGProctabCollector) +} + +type PGProctabCollector struct { + log *slog.Logger +} + +func NewPGProctabCollector(config collectorConfig) (Collector, error) { + return &PGProctabCollector{ + log: config.logger, + }, nil +} + +var ( + pgMemusedDesc = prometheus.NewDesc( + prometheus.BuildFQName( + namespace, + proctabSubsystem, + "memused", + ), + "used memory (from /proc/meminfo) in bytes", + []string{}, nil, + ) + + pgMemfreeDesc = prometheus.NewDesc( + prometheus.BuildFQName( + namespace, + proctabSubsystem, + "memfree", + ), + "free memory (from /proc/meminfo) in bytes", + []string{}, nil, + ) + + pgMemsharedDesc = prometheus.NewDesc( + prometheus.BuildFQName( + namespace, + proctabSubsystem, + "memshared", + ), + "shared memory (from /proc/meminfo) in bytes", + []string{}, nil, + ) + + pgMembuffersDesc = prometheus.NewDesc( + prometheus.BuildFQName( + namespace, + proctabSubsystem, + "membuffers", + ), + "buffered memory (from /proc/meminfo) in bytes", + []string{}, nil, + ) + + pgMemcachedDesc = prometheus.NewDesc( + prometheus.BuildFQName( + namespace, + proctabSubsystem, + "memcached", + ), + "cached memory (from /proc/meminfo) in bytes", + []string{}, nil, + ) + pgSwapusedDesc = prometheus.NewDesc( + prometheus.BuildFQName( + namespace, + proctabSubsystem, + "swapused", + ), + "swap used (from /proc/meminfo) in bytes", + []string{}, nil, + ) + + // Loadavg metrics + pgLoad1Desc = prometheus.NewDesc( + prometheus.BuildFQName( + namespace, + proctabSubsystem, + "load1", + ), + "load1 load Average", + []string{}, nil, + ) + + // CPU metrics + pgCpuUserDesc = prometheus.NewDesc( + prometheus.BuildFQName( + namespace, + proctabSubsystem, + "cpu_user", + ), + "PG User cpu time", + []string{}, nil, + ) + pgCpuNiceDesc = prometheus.NewDesc( + prometheus.BuildFQName( + namespace, + proctabSubsystem, + "cpu_nice", + ), + "PG nice cpu time (running queries)", + []string{}, nil, + ) + pgCpuSystemDesc = prometheus.NewDesc( + prometheus.BuildFQName( + namespace, + proctabSubsystem, + "cpu_system", + ), + "PG system cpu time", + []string{}, nil, + ) + pgCpuIdleDesc = prometheus.NewDesc( + prometheus.BuildFQName( + namespace, + proctabSubsystem, + "cpu_idle", + ), + "PG idle cpu time", + []string{}, nil, + ) + pgCpuIowaitDesc = prometheus.NewDesc( + prometheus.BuildFQName( + namespace, + proctabSubsystem, + "cpu_iowait", + ), + "PG iowait time", + []string{}, nil, + ) + + memoryQuery = ` + select + memused, + memfree, + memshared, + membuffers, + memcached, + swapused + from + pg_memusage() + ` + + load1Query = ` + select + load1 + from + pg_loadavg() + ` + cpuQuery = ` + select + "user", + nice, + system, + idle, + iowait + from + pg_cputime() + ` +) + +func emitMemMetric(m sql.NullInt64, desc *prometheus.Desc, ch chan<- prometheus.Metric) { + mM := 0.0 + if m.Valid { + mM = float64(m.Int64 * 1024) + } + ch <- prometheus.MustNewConstMetric(desc, prometheus.GaugeValue, mM) +} +func emitCpuMetric(m sql.NullInt64, desc *prometheus.Desc, ch chan<- prometheus.Metric) { + mM := 0.0 + if m.Valid { + mM = float64(m.Int64) + } + ch <- prometheus.MustNewConstMetric(desc, prometheus.CounterValue, mM) +} + +// Update implements Collector and exposes database locks. +// It is called by the Prometheus registry when collecting metrics. +func (c PGProctabCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error { + db := instance.getDB() + // Query the list of databases + rows, err := db.QueryContext(ctx, memoryQuery) + if err != nil { + return err + } + defer rows.Close() + + var memused, memfree, memshared, membuffers, memcached, swapused sql.NullInt64 + + for rows.Next() { + if err := rows.Scan(&memused, &memfree, &memshared, &membuffers, &memcached, &swapused); err != nil { + return err + } + emitMemMetric(memused, pgMemusedDesc, ch) + emitMemMetric(memfree, pgMemfreeDesc, ch) + emitMemMetric(memshared, pgMemsharedDesc, ch) + emitMemMetric(membuffers, pgMembuffersDesc, ch) + emitMemMetric(memcached, pgMemcachedDesc, ch) + emitMemMetric(swapused, pgSwapusedDesc, ch) + } + + if err := rows.Err(); err != nil { + return err + } + + rows, err = db.QueryContext(ctx, load1Query) + if err != nil { + return err + } + defer rows.Close() + + var load1 sql.NullFloat64 + for rows.Next() { + if err := rows.Scan(&load1); err != nil { + return err + } + load1Metric := 0.0 + if load1.Valid { + load1Metric = load1.Float64 + } + ch <- prometheus.MustNewConstMetric( + pgLoad1Desc, + prometheus.GaugeValue, load1Metric, + ) + } + if err := rows.Err(); err != nil { + return err + } + + rows, err = db.QueryContext(ctx, cpuQuery) + if err != nil { + return err + } + defer rows.Close() + var user, nice, system, idle, iowait sql.NullInt64 + for rows.Next() { + if err := rows.Scan(&user, &nice, &system, &idle, &iowait); err != nil { + return err + } + emitCpuMetric(user, pgCpuUserDesc, ch) + emitCpuMetric(nice, pgCpuNiceDesc, ch) + emitCpuMetric(system, pgCpuSystemDesc, ch) + emitCpuMetric(idle, pgCpuIdleDesc, ch) + emitCpuMetric(iowait, pgCpuIowaitDesc, ch) + } + if err := rows.Err(); err != nil { + return err + } + + return nil + +} diff --git a/collector/pg_proctab_test.go b/collector/pg_proctab_test.go new file mode 100644 index 000000000..e60c7ef26 --- /dev/null +++ b/collector/pg_proctab_test.go @@ -0,0 +1,79 @@ +// Copyright 2025 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 TestPGProctabCollector(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} + + rows := sqlmock.NewRows([]string{"memused", "memfree", "memshared", "membuffers", "memcached", "swapused"}). + AddRow(123, 456, 789, 234, 567, 89) + mock.ExpectQuery(sanitizeQuery(memoryQuery)).WillReturnRows(rows) + + rows = sqlmock.NewRows([]string{"load1"}).AddRow(123.456) + mock.ExpectQuery(sanitizeQuery(load1Query)).WillReturnRows(rows) + + rows = sqlmock.NewRows([]string{"user", "nice", "system", "idle", "iowait"}).AddRow( + 345, 678, 9, 1234, 56) + mock.ExpectQuery(sanitizeQuery(cpuQuery)).WillReturnRows(rows) + + ch := make(chan prometheus.Metric) + go func() { + defer close(ch) + c := PGProctabCollector{} + if err := c.Update(context.Background(), inst, ch); err != nil { + t.Errorf("Error calling PGProctabCollector .Update: %s", err) + } + }() + + expected := []MetricResult{ + {labels: labelMap{}, value: 123 * 1024, metricType: dto.MetricType_GAUGE}, + {labels: labelMap{}, value: 456 * 1024, metricType: dto.MetricType_GAUGE}, + {labels: labelMap{}, value: 789 * 1024, metricType: dto.MetricType_GAUGE}, + {labels: labelMap{}, value: 234 * 1024, metricType: dto.MetricType_GAUGE}, + {labels: labelMap{}, value: 567 * 1024, metricType: dto.MetricType_GAUGE}, + {labels: labelMap{}, value: 89 * 1024, metricType: dto.MetricType_GAUGE}, + // load + {labels: labelMap{}, value: 123.456, metricType: dto.MetricType_GAUGE}, + // cpu + {labels: labelMap{}, value: 345, metricType: dto.MetricType_COUNTER}, + {labels: labelMap{}, value: 678, metricType: dto.MetricType_COUNTER}, + {labels: labelMap{}, value: 9, metricType: dto.MetricType_COUNTER}, + {labels: labelMap{}, value: 1234, metricType: dto.MetricType_COUNTER}, + {labels: labelMap{}, value: 56, metricType: dto.MetricType_COUNTER}, + } + 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) + } +}