Skip to content

Commit c8360b4

Browse files
PMM-8770 enable safe metrics collection (#389)
* PMM-8489 Fixed topology labels on mongos * Updated mog for go 1.16 * Updated dependency * Updated toolkit dep to 3.x * Updated go.sum for go 1.16 In my local I have go 1.17 so the sum was different and tests were failing. * Fixed mod conflicts * Revert "Merge branch 'PMM-8489_topology_labels_on_mongos'" This reverts commit 8905fce, reversing changes made to 4553084. * PMM-8770 Updated discovery mode Now discovering mode automatically search for all databases and all collections. It might receive a filter list and ignores system collections. * PMM-8770 Added common functions * PMM-8770 New collstats limit * Fixed race condition * Top collector won't be enabled on mongos * Fixes for CR * Renamed collector.topmetric param Co-authored-by: JiriCtvrtka <[email protected]>
1 parent 705db98 commit c8360b4

File tree

7 files changed

+248
-53
lines changed

7 files changed

+248
-53
lines changed

exporter/collstats_collector.go

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,26 +41,27 @@ func (d *collstatsCollector) Describe(ch chan<- *prometheus.Desc) {
4141
}
4242

4343
func (d *collstatsCollector) Collect(ch chan<- prometheus.Metric) {
44+
collections := d.collections
45+
4446
if d.discoveringMode {
45-
databases := map[string][]string{}
46-
for _, dbCollection := range d.collections {
47-
parts := strings.Split(dbCollection, ".")
48-
if _, ok := databases[parts[0]]; !ok {
49-
db := parts[0]
50-
databases[db], _ = d.client.Database(parts[0]).ListCollectionNames(d.ctx, bson.D{})
51-
}
47+
namespaces, err := listAllCollections(d.ctx, d.client, d.collections)
48+
if err != nil {
49+
d.logger.Errorf("cannot auto discover databases and collections")
50+
51+
return
5252
}
5353

54-
d.collections = fromMapToSlice(databases)
54+
collections = fromMapToSlice(namespaces)
5555
}
56-
for _, dbCollection := range d.collections {
56+
57+
for _, dbCollection := range collections {
5758
parts := strings.Split(dbCollection, ".")
58-
if len(parts) != 2 { //nolint:gomnd
59+
if len(parts) < 2 { //nolint:gomnd
5960
continue
6061
}
6162

6263
database := parts[0]
63-
collection := parts[1]
64+
collection := strings.Join(parts[1:], ".") // support collections having a .
6465

6566
aggregation := bson.D{
6667
{
@@ -82,12 +83,14 @@ func (d *collstatsCollector) Collect(ch chan<- prometheus.Metric) {
8283
cursor, err := d.client.Database(database).Collection(collection).Aggregate(d.ctx, mongo.Pipeline{aggregation, project})
8384
if err != nil {
8485
d.logger.Errorf("cannot get $collstats cursor for collection %s.%s: %s", database, collection, err)
86+
8587
continue
8688
}
8789

8890
var stats []bson.M
8991
if err = cursor.All(d.ctx, &stats); err != nil {
9092
d.logger.Errorf("cannot get $collstats for collection %s.%s: %s", database, collection, err)
93+
9194
continue
9295
}
9396

exporter/common.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package exporter
2+
3+
import (
4+
"context"
5+
6+
"github.com/AlekSi/pointer"
7+
"github.com/pkg/errors"
8+
"go.mongodb.org/mongo-driver/bson"
9+
"go.mongodb.org/mongo-driver/bson/primitive"
10+
"go.mongodb.org/mongo-driver/mongo"
11+
"go.mongodb.org/mongo-driver/mongo/options"
12+
)
13+
14+
var systemDBs = []string{"admin", "config", "local"} //nolint:gochecknoglobals
15+
16+
func listCollections(ctx context.Context, client *mongo.Client, collections []string, database string) ([]string, error) {
17+
filter := bson.D{} // Default=empty -> list all collections
18+
19+
// if there is a filter with the list of collections we want, create a filter like
20+
// $or: {
21+
// {"$regex": "collection1"},
22+
// {"$regex": "collection2"},
23+
// }
24+
if len(collections) > 0 {
25+
matchExpressions := []bson.D{}
26+
27+
for _, collection := range collections {
28+
matchExpressions = append(matchExpressions,
29+
bson.D{{Key: "name", Value: primitive.Regex{Pattern: collection, Options: "i"}}})
30+
}
31+
32+
filter = bson.D{{Key: "$or", Value: matchExpressions}}
33+
}
34+
35+
databases, err := client.Database(database).ListCollectionNames(ctx, filter)
36+
if err != nil {
37+
return nil, errors.Wrap(err, "cannot get the list of collections for discovery")
38+
}
39+
40+
return databases, nil
41+
}
42+
43+
func databases(ctx context.Context, client *mongo.Client, exclude []string) ([]string, error) {
44+
opts := &options.ListDatabasesOptions{NameOnly: pointer.ToBool(true), AuthorizedDatabases: pointer.ToBool(true)}
45+
filterExpressions := []bson.D{}
46+
for _, dbname := range exclude {
47+
filterExpressions = append(filterExpressions,
48+
bson.D{{Key: "name", Value: bson.D{{Key: "$ne", Value: dbname}}}},
49+
)
50+
}
51+
52+
filter := bson.D{{Key: "$and", Value: filterExpressions}}
53+
54+
dbNames, err := client.ListDatabaseNames(ctx, filter, opts)
55+
if err != nil {
56+
return nil, errors.Wrap(err, "cannot get the database names list")
57+
}
58+
59+
return dbNames, nil
60+
}
61+
62+
func listAllCollections(ctx context.Context, client *mongo.Client, filter []string) (map[string][]string, error) {
63+
namespaces := make(map[string][]string)
64+
// exclude system databases
65+
dbnames, err := databases(ctx, client, systemDBs)
66+
if err != nil {
67+
return nil, errors.Wrap(err, "cannot get the list of all collections in the server")
68+
}
69+
70+
for _, dbname := range dbnames {
71+
colls, err := listCollections(ctx, client, filter, dbname)
72+
if err != nil {
73+
return nil, errors.Wrapf(err, "cannot list the collections for %q", dbname)
74+
}
75+
namespaces[dbname] = colls
76+
}
77+
78+
return namespaces, nil
79+
}
80+
81+
func allCollectionsCount(ctx context.Context, client *mongo.Client, filter []string) (int, error) {
82+
databases, err := databases(ctx, client, systemDBs)
83+
if err != nil {
84+
return 0, errors.Wrap(err, "cannot retrieve the collection names for count collections")
85+
}
86+
87+
var count int
88+
89+
for _, dbname := range databases {
90+
colls, err := listCollections(ctx, client, filter, dbname)
91+
if err != nil {
92+
return 0, errors.Wrap(err, "cannot get collections count")
93+
}
94+
count += len(colls)
95+
}
96+
97+
return count, nil
98+
}

exporter/common_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package exporter
2+
3+
import (
4+
"context"
5+
"sort"
6+
"testing"
7+
"time"
8+
9+
"github.com/stretchr/testify/assert"
10+
"go.mongodb.org/mongo-driver/bson"
11+
12+
"github.com/percona/mongodb_exporter/internal/tu"
13+
)
14+
15+
func TestListCollections(t *testing.T) {
16+
t.Parallel()
17+
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
18+
defer cancel()
19+
20+
client := tu.DefaultTestClient(ctx, t)
21+
22+
databases := []string{"testdb01", "testdb02"}
23+
collections := []string{"col01", "col02", "colxx", "colyy"}
24+
25+
defer func() {
26+
for _, dbname := range databases {
27+
client.Database(dbname).Drop(ctx) //nolint:errcheck
28+
}
29+
}()
30+
31+
for _, dbname := range databases {
32+
for _, coll := range collections {
33+
for j := 0; j < 10; j++ {
34+
_, err := client.Database(dbname).Collection(coll).InsertOne(ctx, bson.M{"f1": j, "f2": "2"})
35+
assert.NoError(t, err)
36+
}
37+
}
38+
}
39+
40+
want := []string{"col01", "col02", "colxx"}
41+
collections, err := listCollections(ctx, client, []string{"col0", "colx"}, databases[0])
42+
sort.Strings(collections)
43+
44+
assert.NoError(t, err)
45+
assert.Equal(t, want, collections)
46+
47+
count, err := allCollectionsCount(ctx, client, nil)
48+
assert.NoError(t, err)
49+
assert.True(t, count > 8)
50+
51+
count, err = allCollectionsCount(ctx, client, []string{"col0", "colx"})
52+
assert.NoError(t, err)
53+
assert.Equal(t, 6, count)
54+
}

exporter/exporter.go

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,31 +33,35 @@ import (
3333

3434
// Exporter holds Exporter methods and attributes.
3535
type Exporter struct {
36-
path string
37-
client *mongo.Client
38-
clientMu sync.Mutex
39-
logger *logrus.Logger
40-
opts *Opts
41-
webListenAddress string
36+
path string
37+
client *mongo.Client
38+
clientMu sync.Mutex
39+
logger *logrus.Logger
40+
opts *Opts
41+
webListenAddress string
42+
lock *sync.Mutex
43+
totalCollectionsCount int
4244
}
4345

4446
// Opts holds new exporter options.
4547
type Opts struct {
48+
CollStatsCollections []string
49+
CollStatsLimit int
50+
CollectorTopMetrics bool
4651
CompatibleMode bool
47-
DiscoveringMode bool
48-
GlobalConnPool bool
4952
DirectConnect bool
50-
URI string
51-
Path string
52-
WebListenAddress string
53-
IndexStatsCollections []string
54-
CollStatsCollections []string
55-
Logger *logrus.Logger
53+
DisableDefaultRegistry bool
5654
DisableDiagnosticData bool
5755
DisableReplicasetStatus bool
58-
DisableDefaultRegistry bool
56+
DiscoveringMode bool
5957
EnableDBStats bool
60-
CollectorTopMetrics bool
58+
EnableTop bool
59+
GlobalConnPool bool
60+
IndexStatsCollections []string
61+
Logger *logrus.Logger
62+
Path string
63+
URI string
64+
WebListenAddress string
6165
}
6266

6367
var (
@@ -78,21 +82,31 @@ func New(opts *Opts) *Exporter {
7882
ctx := context.Background()
7983

8084
exp := &Exporter{
81-
path: opts.Path,
82-
logger: opts.Logger,
83-
opts: opts,
84-
webListenAddress: opts.WebListenAddress,
85+
path: opts.Path,
86+
logger: opts.Logger,
87+
opts: opts,
88+
webListenAddress: opts.WebListenAddress,
89+
lock: &sync.Mutex{},
90+
totalCollectionsCount: -1, // not calculated yet. waiting the db connection.
8591
}
8692
// Try initial connect. Connection will be retried with every scrape.
8793
go func() {
88-
if _, err := exp.getClient(ctx); err != nil {
94+
_, err := exp.getClient(ctx)
95+
if err != nil {
8996
exp.logger.Errorf("Cannot connect to MongoDB: %v", err)
9097
}
9198
}()
9299

93100
return exp
94101
}
95102

103+
func (e *Exporter) getTotalCollectionsCount() int {
104+
e.lock.Lock()
105+
defer e.lock.Unlock()
106+
107+
return e.totalCollectionsCount
108+
}
109+
96110
func (e *Exporter) makeRegistry(ctx context.Context, client *mongo.Client, topologyInfo labelsGetter) *prometheus.Registry {
97111
registry := prometheus.NewRegistry()
98112

@@ -108,7 +122,17 @@ func (e *Exporter) makeRegistry(ctx context.Context, client *mongo.Client, topol
108122
e.logger.Errorf("Cannot get node type to check if this is a mongos: %s", err)
109123
}
110124

111-
if len(e.opts.CollStatsCollections) > 0 {
125+
// enable collection dependant collectors like collstats and indexstats
126+
enableCollStats := false
127+
if e.opts.CollStatsLimit == -1 {
128+
enableCollStats = true
129+
}
130+
if e.getTotalCollectionsCount() > 0 && e.getTotalCollectionsCount() < e.opts.CollStatsLimit {
131+
enableCollStats = true
132+
}
133+
134+
// if we manually set the collection names we want or auto discovery is set
135+
if (len(e.opts.CollStatsCollections) > 0 || e.opts.DiscoveringMode) && enableCollStats {
112136
cc := collstatsCollector{
113137
ctx: ctx,
114138
client: client,
@@ -121,7 +145,8 @@ func (e *Exporter) makeRegistry(ctx context.Context, client *mongo.Client, topol
121145
registry.MustRegister(&cc)
122146
}
123147

124-
if len(e.opts.IndexStatsCollections) > 0 {
148+
// if we manually set the collection names we want or auto discovery is set
149+
if (len(e.opts.IndexStatsCollections) > 0 || e.opts.DiscoveringMode) && enableCollStats {
125150
ic := indexstatsCollector{
126151
ctx: ctx,
127152
client: client,
@@ -155,7 +180,7 @@ func (e *Exporter) makeRegistry(ctx context.Context, client *mongo.Client, topol
155180
registry.MustRegister(&cc)
156181
}
157182

158-
if e.opts.CollectorTopMetrics {
183+
if e.opts.CollectorTopMetrics && nodeType != typeMongos {
159184
tc := topCollector{
160185
ctx: ctx,
161186
client: client,
@@ -229,6 +254,16 @@ func (e *Exporter) Handler() http.Handler {
229254

230255
return
231256
}
257+
258+
if e.getTotalCollectionsCount() < 0 {
259+
count, err := allCollectionsCount(ctx, client, nil)
260+
if err == nil {
261+
e.lock.Lock()
262+
e.totalCollectionsCount = count
263+
e.lock.Unlock()
264+
}
265+
}
266+
232267
// Close client after usage
233268
if !e.opts.GlobalConnPool {
234269
defer func() {

0 commit comments

Comments
 (0)