Skip to content

Commit 0894859

Browse files
author
Shashank Sinha
authored
PMM-9459 Remove usage of DescribeByCollect (#443)
DescribeByCollect method internally uses Collect method. This causes exporter to retrieve metric data from database twice per scrape. This patch reduces monitoring load on database server, as well as reduces CPU and memory usage of exporter. Changes to Describe and Collect methods has introduced a dependency. Before a call to Collect method, Describe method needs to be called. Tests need to be updated to handle this assumption.
1 parent 6df2afa commit 0894859

19 files changed

+433
-337
lines changed

exporter/base_collector.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// mongodb_exporter
2+
// Copyright (C) 2022 Percona LLC
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
17+
package exporter
18+
19+
import (
20+
"sync"
21+
22+
"github.com/prometheus/client_golang/prometheus"
23+
"github.com/sirupsen/logrus"
24+
"go.mongodb.org/mongo-driver/mongo"
25+
)
26+
27+
type baseCollector struct {
28+
client *mongo.Client
29+
logger *logrus.Logger
30+
31+
lock sync.Mutex
32+
metricsCache []prometheus.Metric
33+
}
34+
35+
// newBaseCollector creates a skeletal collector, which is used to create other collectors.
36+
func newBaseCollector(client *mongo.Client, logger *logrus.Logger) *baseCollector {
37+
return &baseCollector{
38+
client: client,
39+
logger: logger,
40+
}
41+
}
42+
43+
func (d *baseCollector) Describe(ch chan<- *prometheus.Desc, collect func(mCh chan<- prometheus.Metric)) {
44+
d.lock.Lock()
45+
defer d.lock.Unlock()
46+
47+
d.metricsCache = make([]prometheus.Metric, 0, defaultCacheSize)
48+
49+
// This is a copy/paste of prometheus.DescribeByCollect(d, ch) with the aggreated functionality
50+
// to populate the metrics cache. Since on each scrape Prometheus will call Describe and inmediatelly
51+
// after it will call Collect, it is safe to populate the cache here.
52+
metrics := make(chan prometheus.Metric)
53+
go func() {
54+
collect(metrics)
55+
close(metrics)
56+
}()
57+
58+
for m := range metrics {
59+
d.metricsCache = append(d.metricsCache, m) // populate the cache
60+
ch <- m.Desc()
61+
}
62+
}
63+
64+
func (d *baseCollector) Collect(ch chan<- prometheus.Metric) {
65+
d.lock.Lock()
66+
defer d.lock.Unlock()
67+
68+
for _, metric := range d.metricsCache {
69+
ch <- metric
70+
}
71+
}

exporter/collstats_collector.go

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,48 @@ import (
2727
)
2828

2929
type collstatsCollector struct {
30-
ctx context.Context
31-
client *mongo.Client
32-
collections []string
30+
ctx context.Context
31+
base *baseCollector
32+
3333
compatibleMode bool
3434
discoveringMode bool
35-
logger *logrus.Logger
3635
topologyInfo labelsGetter
36+
37+
collections []string
38+
}
39+
40+
// newCollectionStatsCollector creates a collector for statistics about collections.
41+
func newCollectionStatsCollector(ctx context.Context, client *mongo.Client, logger *logrus.Logger, compatible, discovery bool, topology labelsGetter, collections []string) *collstatsCollector {
42+
return &collstatsCollector{
43+
ctx: ctx,
44+
base: newBaseCollector(client, logger),
45+
46+
compatibleMode: compatible,
47+
discoveringMode: discovery,
48+
topologyInfo: topology,
49+
50+
collections: collections,
51+
}
3752
}
3853

3954
func (d *collstatsCollector) Describe(ch chan<- *prometheus.Desc) {
40-
prometheus.DescribeByCollect(d, ch)
55+
d.base.Describe(ch, d.collect)
4156
}
4257

4358
func (d *collstatsCollector) Collect(ch chan<- prometheus.Metric) {
59+
d.base.Collect(ch)
60+
}
61+
62+
func (d *collstatsCollector) collect(ch chan<- prometheus.Metric) {
4463
collections := d.collections
4564

65+
client := d.base.client
66+
logger := d.base.logger
67+
4668
if d.discoveringMode {
47-
namespaces, err := listAllCollections(d.ctx, d.client, d.collections, systemDBs)
69+
namespaces, err := listAllCollections(d.ctx, client, d.collections, systemDBs)
4870
if err != nil {
49-
d.logger.Errorf("cannot auto discover databases and collections: %s", err.Error())
71+
logger.Errorf("cannot auto discover databases and collections: %s", err.Error())
5072

5173
return
5274
}
@@ -81,22 +103,22 @@ func (d *collstatsCollector) Collect(ch chan<- prometheus.Metric) {
81103
},
82104
}
83105

84-
cursor, err := d.client.Database(database).Collection(collection).Aggregate(d.ctx, mongo.Pipeline{aggregation, project})
106+
cursor, err := client.Database(database).Collection(collection).Aggregate(d.ctx, mongo.Pipeline{aggregation, project})
85107
if err != nil {
86-
d.logger.Errorf("cannot get $collstats cursor for collection %s.%s: %s", database, collection, err)
108+
logger.Errorf("cannot get $collstats cursor for collection %s.%s: %s", database, collection, err)
87109

88110
continue
89111
}
90112

91113
var stats []bson.M
92114
if err = cursor.All(d.ctx, &stats); err != nil {
93-
d.logger.Errorf("cannot get $collstats for collection %s.%s: %s", database, collection, err)
115+
logger.Errorf("cannot get $collstats for collection %s.%s: %s", database, collection, err)
94116

95117
continue
96118
}
97119

98-
d.logger.Debugf("$collStats metrics for %s.%s", database, collection)
99-
debugResult(d.logger, stats)
120+
logger.Debugf("$collStats metrics for %s.%s", database, collection)
121+
debugResult(logger, stats)
100122

101123
// Since all collections will have the same fields, we need to use a metric prefix (db+col)
102124
// to differentiate metrics between collection. Labels are being set only to matke it easier

exporter/collstats_collector_test.go

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -53,25 +53,20 @@ func TestCollStatsCollector(t *testing.T) {
5353

5454
ti := labelsGetterMock{}
5555

56-
c := &collstatsCollector{
57-
client: client,
58-
collections: []string{"testdb.testcol_00", "testdb.testcol_01", "testdb.testcol_02"},
59-
logger: logrus.New(),
60-
topologyInfo: ti,
61-
}
56+
collection := []string{"testdb.testcol_00", "testdb.testcol_01", "testdb.testcol_02"}
57+
c := newCollectionStatsCollector(ctx, client, logrus.New(), false, false, ti, collection)
6258

6359
// The last \n at the end of this string is important
6460
expected := strings.NewReader(`
65-
# HELP mongodb_testdb_testcol_00_latencyStats_commands_latency testdb.testcol_00.latencyStats.commands.
66-
# TYPE mongodb_testdb_testcol_00_latencyStats_commands_latency untyped
67-
mongodb_testdb_testcol_00_latencyStats_commands_latency{collection="testcol_00",database="testdb"} 0
68-
# HELP mongodb_testdb_testcol_01_latencyStats_commands_latency testdb.testcol_01.latencyStats.commands.
69-
# TYPE mongodb_testdb_testcol_01_latencyStats_commands_latency untyped
70-
mongodb_testdb_testcol_01_latencyStats_commands_latency{collection="testcol_01",database="testdb"} 0
71-
# HELP mongodb_testdb_testcol_02_latencyStats_commands_latency testdb.testcol_02.latencyStats.commands.
72-
# TYPE mongodb_testdb_testcol_02_latencyStats_commands_latency untyped
73-
mongodb_testdb_testcol_02_latencyStats_commands_latency{collection="testcol_02",database="testdb"} 0` +
74-
"\n")
61+
# HELP mongodb_testdb_testcol_00_latencyStats_commands_latency testdb.testcol_00.latencyStats.commands.
62+
# TYPE mongodb_testdb_testcol_00_latencyStats_commands_latency untyped
63+
mongodb_testdb_testcol_00_latencyStats_commands_latency{collection="testcol_00",database="testdb"} 0
64+
# HELP mongodb_testdb_testcol_01_latencyStats_commands_latency testdb.testcol_01.latencyStats.commands.
65+
# TYPE mongodb_testdb_testcol_01_latencyStats_commands_latency untyped
66+
mongodb_testdb_testcol_01_latencyStats_commands_latency{collection="testcol_01",database="testdb"} 0
67+
# HELP mongodb_testdb_testcol_02_latencyStats_commands_latency testdb.testcol_02.latencyStats.commands.
68+
# TYPE mongodb_testdb_testcol_02_latencyStats_commands_latency untyped
69+
mongodb_testdb_testcol_02_latencyStats_commands_latency{collection="testcol_02",database="testdb"} 0` + "\n")
7570

7671
// Filter metrics for 2 reasons:
7772
// 1. The result is huge

exporter/dbstats_collector.go

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,40 +26,61 @@ import (
2626
)
2727

2828
type dbstatsCollector struct {
29-
ctx context.Context
30-
client *mongo.Client
29+
ctx context.Context
30+
base *baseCollector
31+
3132
compatibleMode bool
32-
logger *logrus.Logger
3333
topologyInfo labelsGetter
34+
35+
databaseFilter []string
36+
}
37+
38+
// newDBStatsCollector creates a collector for statistics on database storage.
39+
func newDBStatsCollector(ctx context.Context, client *mongo.Client, logger *logrus.Logger, compatible bool, topology labelsGetter, databaseRegex []string) *dbstatsCollector {
40+
return &dbstatsCollector{
41+
ctx: ctx,
42+
base: newBaseCollector(client, logger),
43+
44+
compatibleMode: compatible,
45+
topologyInfo: topology,
46+
47+
databaseFilter: databaseRegex,
48+
}
3449
}
3550

3651
func (d *dbstatsCollector) Describe(ch chan<- *prometheus.Desc) {
37-
prometheus.DescribeByCollect(d, ch)
52+
d.base.Describe(ch, d.collect)
3853
}
3954

4055
func (d *dbstatsCollector) Collect(ch chan<- prometheus.Metric) {
41-
// List all databases names
42-
dbNames, err := d.client.ListDatabaseNames(d.ctx, bson.M{})
56+
d.base.Collect(ch)
57+
}
58+
59+
func (d *dbstatsCollector) collect(ch chan<- prometheus.Metric) {
60+
logger := d.base.logger
61+
client := d.base.client
62+
63+
dbNames, err := databases(d.ctx, client, d.databaseFilter, nil)
4364
if err != nil {
44-
d.logger.Errorf("Failed to get database names: %s", err)
65+
logger.Errorf("Failed to get database names: %s", err)
4566

4667
return
4768
}
4869

49-
d.logger.Debugf("getting stats for databases: %v", dbNames)
70+
logger.Debugf("getting stats for databases: %v", dbNames)
5071
for _, db := range dbNames {
5172
var dbStats bson.M
5273
cmd := bson.D{{Key: "dbStats", Value: 1}, {Key: "scale", Value: 1}}
53-
r := d.client.Database(db).RunCommand(d.ctx, cmd)
74+
r := client.Database(db).RunCommand(d.ctx, cmd)
5475
err := r.Decode(&dbStats)
5576
if err != nil {
56-
d.logger.Errorf("Failed to get $dbstats for database %s: %s", db, err)
77+
logger.Errorf("Failed to get $dbstats for database %s: %s", db, err)
5778

5879
continue
5980
}
6081

61-
d.logger.Debugf("$dbStats metrics for %s", db)
62-
debugResult(d.logger, dbStats)
82+
logger.Debugf("$dbStats metrics for %s", db)
83+
debugResult(logger, dbStats)
6384

6485
prefix := "dbstats"
6586

@@ -69,7 +90,8 @@ func (d *dbstatsCollector) Collect(ch chan<- prometheus.Metric) {
6990
// to differentiate metrics between different databases.
7091
labels["database"] = db
7192

72-
for _, metric := range makeMetrics(prefix, dbStats, labels, d.compatibleMode) {
93+
newMetrics := makeMetrics(prefix, dbStats, labels, d.compatibleMode)
94+
for _, metric := range newMetrics {
7395
ch <- metric
7496
}
7597
}

exporter/dbstats_collector_test.go

Lines changed: 21 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,29 @@ package exporter
1919
import (
2020
"context"
2121
"fmt"
22-
"sort"
22+
"strings"
2323
"testing"
2424
"time"
2525

26-
"github.com/percona/exporter_shared/helpers"
26+
"github.com/prometheus/client_golang/prometheus/testutil"
2727
"github.com/sirupsen/logrus"
2828
"github.com/stretchr/testify/assert"
2929
"go.mongodb.org/mongo-driver/bson"
3030

3131
"github.com/percona/mongodb_exporter/internal/tu"
3232
)
3333

34+
const (
35+
dbName = "testdb"
36+
)
37+
3438
func TestDBStatsCollector(t *testing.T) {
3539
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
3640
defer cancel()
3741

3842
client := tu.DefaultTestClient(ctx, t)
3943

40-
database := client.Database("testdb")
44+
database := client.Database(dbName)
4145
database.Drop(ctx) //nolint
4246

4347
defer func() {
@@ -55,47 +59,24 @@ func TestDBStatsCollector(t *testing.T) {
5559

5660
ti := labelsGetterMock{}
5761

58-
c := &dbstatsCollector{
59-
client: client,
60-
logger: logrus.New(),
61-
topologyInfo: ti,
62-
}
63-
64-
expected := []string{
65-
"# HELP mongodb_dbstats_collections dbstats.",
66-
"# TYPE mongodb_dbstats_collections untyped",
67-
"mongodb_dbstats_collections{database=\"testdb\"} 3",
68-
"# HELP mongodb_dbstats_dataSize dbstats.",
69-
"# TYPE mongodb_dbstats_dataSize untyped",
70-
"mongodb_dbstats_dataSize{database=\"testdb\"} 1200",
71-
"# HELP mongodb_dbstats_indexSize dbstats.",
72-
"# TYPE mongodb_dbstats_indexSize untyped",
73-
"mongodb_dbstats_indexSize{database=\"testdb\"} 12288",
74-
"# HELP mongodb_dbstats_indexes dbstats.",
75-
"# TYPE mongodb_dbstats_indexes untyped",
76-
"mongodb_dbstats_indexes{database=\"testdb\"} 3",
77-
"# HELP mongodb_dbstats_objects dbstats.",
78-
"# TYPE mongodb_dbstats_objects untyped",
79-
"mongodb_dbstats_objects{database=\"testdb\"} 30",
80-
}
62+
c := newDBStatsCollector(ctx, client, logrus.New(), false, ti, []string{dbName})
63+
expected := strings.NewReader(`
64+
# HELP mongodb_dbstats_collections dbstats.
65+
# TYPE mongodb_dbstats_collections untyped
66+
mongodb_dbstats_collections{database="testdb"} 3
67+
# HELP mongodb_dbstats_indexes dbstats.
68+
# TYPE mongodb_dbstats_indexes untyped
69+
mongodb_dbstats_indexes{database="testdb"} 3
70+
# HELP mongodb_dbstats_objects dbstats.
71+
# TYPE mongodb_dbstats_objects untyped
72+
mongodb_dbstats_objects{database="testdb"} 30` + "\n")
8173

82-
metrics := helpers.CollectMetrics(c)
83-
actualMetrics := helpers.ReadMetrics(metrics)
74+
// Only look at metrics created by our activity
8475
filters := []string{
8576
"mongodb_dbstats_collections",
86-
"mongodb_dbstats_dataSize",
87-
"mongodb_dbstats_indexSize",
8877
"mongodb_dbstats_indexes",
8978
"mongodb_dbstats_objects",
9079
}
91-
labels := map[string]string{
92-
"database": "testdb",
93-
}
94-
actualMetrics = filterMetricsWithLabels(actualMetrics,
95-
filters,
96-
labels)
97-
actualLines := helpers.Format(helpers.WriteMetrics(actualMetrics))
98-
sort.Strings(actualLines)
99-
sort.Strings(expected)
100-
assert.Equal(t, expected, actualLines)
80+
err := testutil.CollectAndCompare(c, expected, filters...)
81+
assert.NoError(t, err)
10182
}

0 commit comments

Comments
 (0)