Skip to content

Commit bedbd21

Browse files
Export currentOP uptime query metrics (#706)
* Export currentOP uptime query metrics #704 * Update exporter/currentop_collector.go Co-authored-by: Artem Gavrilov <[email protected]> * Export currentOP uptime query metrics --------- Co-authored-by: Artem Gavrilov <[email protected]>
1 parent 9571d21 commit bedbd21

File tree

5 files changed

+235
-0
lines changed

5 files changed

+235
-0
lines changed

REFERENCE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
|--collector.replicasetstatus|Enable collecting metrics from replSetGetStatus|
2020
|--collector.dbstats|Enable collecting metrics from dbStats||
2121
|--collector.topmetrics|Enable collecting metrics from top admin command|
22+
|--collector.currentopmetrics|Enable collecting metrics from currentop admin command|
2223
|--collector.indexstats|Enable collecting metrics from $indexStats|
2324
|--collector.collstats|Enable collecting metrics from $collStats|
2425
|--collect-all|Enable all collectors. Same as specifying all --collector.\<name\>|

exporter/currentop_collector.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// mongodb_exporter
2+
// Copyright (C) 2017 Percona LLC
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package exporter
17+
18+
import (
19+
"context"
20+
"errors"
21+
"strconv"
22+
23+
"github.com/prometheus/client_golang/prometheus"
24+
"github.com/sirupsen/logrus"
25+
"go.mongodb.org/mongo-driver/bson"
26+
"go.mongodb.org/mongo-driver/bson/primitive"
27+
"go.mongodb.org/mongo-driver/mongo"
28+
)
29+
30+
type currentopCollector struct {
31+
ctx context.Context
32+
base *baseCollector
33+
compatibleMode bool
34+
topologyInfo labelsGetter
35+
}
36+
37+
var ErrInvalidOrMissingInprogEntry = errors.New("invalid or missing inprog entry in currentop results")
38+
39+
// newCurrentopCollector creates a collector for being processed queries.
40+
func newCurrentopCollector(ctx context.Context, client *mongo.Client, logger *logrus.Logger,
41+
compatible bool, topology labelsGetter,
42+
) *currentopCollector {
43+
return &currentopCollector{
44+
ctx: ctx,
45+
base: newBaseCollector(client, logger),
46+
compatibleMode: compatible,
47+
topologyInfo: topology,
48+
}
49+
}
50+
51+
func (d *currentopCollector) Describe(ch chan<- *prometheus.Desc) {
52+
d.base.Describe(d.ctx, ch, d.collect)
53+
}
54+
55+
func (d *currentopCollector) Collect(ch chan<- prometheus.Metric) {
56+
d.base.Collect(ch)
57+
}
58+
59+
func (d *currentopCollector) collect(ch chan<- prometheus.Metric) {
60+
defer measureCollectTime(ch, "mongodb", "currentop")()
61+
62+
logger := d.base.logger
63+
client := d.base.client
64+
65+
// Get all requests that are being processed except system requests (admin and local).
66+
cmd := bson.D{
67+
{Key: "currentOp", Value: true},
68+
{Key: "active", Value: true},
69+
{Key: "microsecs_running", Value: bson.D{
70+
{Key: "$exists", Value: true},
71+
}},
72+
{Key: "op", Value: bson.D{{Key: "$ne", Value: ""}}},
73+
{Key: "ns", Value: bson.D{
74+
{Key: "$ne", Value: ""},
75+
{Key: "$not", Value: bson.D{{Key: "$regex", Value: "^admin.*|^local.*"}}},
76+
}},
77+
}
78+
res := client.Database("admin").RunCommand(d.ctx, cmd)
79+
80+
var r primitive.M
81+
if err := res.Decode(&r); err != nil {
82+
ch <- prometheus.NewInvalidMetric(prometheus.NewInvalidDesc(err), err)
83+
return
84+
}
85+
86+
logger.Debug("currentop response from MongoDB:")
87+
debugResult(logger, r)
88+
89+
inprog, ok := r["inprog"].(primitive.A)
90+
91+
if !ok {
92+
ch <- prometheus.NewInvalidMetric(prometheus.NewInvalidDesc(ErrInvalidOrMissingInprogEntry),
93+
ErrInvalidOrMissingInprogEntry)
94+
}
95+
96+
for _, bsonMap := range inprog {
97+
98+
bsonMapElement, ok := bsonMap.(primitive.M)
99+
if !ok {
100+
logger.Errorf("Invalid type primitive.M assertion for bsonMap: %T", bsonMapElement)
101+
continue
102+
}
103+
opid, ok := bsonMapElement["opid"].(int32)
104+
if !ok {
105+
logger.Errorf("Invalid type int32 assertion for 'opid': %T", bsonMapElement)
106+
continue
107+
}
108+
namespace, ok := bsonMapElement["ns"].(string)
109+
if !ok {
110+
logger.Errorf("Invalid type string assertion for 'ns': %T", bsonMapElement)
111+
continue
112+
}
113+
db, collection := splitNamespace(namespace)
114+
op, ok := bsonMapElement["op"].(string)
115+
if !ok {
116+
logger.Errorf("Invalid type string assertion for 'op': %T", bsonMapElement)
117+
continue
118+
}
119+
desc, ok := bsonMapElement["desc"].(string)
120+
if !ok {
121+
logger.Errorf("Invalid type string assertion for 'desc': %T", bsonMapElement)
122+
continue
123+
}
124+
microsecs_running, ok := bsonMapElement["microsecs_running"].(int64)
125+
if !ok {
126+
logger.Errorf("Invalid type int64 assertion for 'microsecs_running': %T", bsonMapElement)
127+
continue
128+
}
129+
130+
labels := d.topologyInfo.baseLabels()
131+
labels["opid"] = strconv.Itoa(int(opid))
132+
labels["op"] = op
133+
labels["desc"] = desc
134+
labels["database"] = db
135+
labels["collection"] = collection
136+
labels["ns"] = namespace
137+
138+
m := primitive.M{"uptime": microsecs_running}
139+
140+
for _, metric := range makeMetrics("currentop_query", m, labels, d.compatibleMode) {
141+
ch <- metric
142+
}
143+
}
144+
}

exporter/currentop_collector_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// mongodb_exporter
2+
// Copyright (C) 2017 Percona LLC
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package exporter
17+
18+
import (
19+
"context"
20+
"fmt"
21+
"sync"
22+
"testing"
23+
"time"
24+
25+
"github.com/prometheus/client_golang/prometheus/testutil"
26+
"github.com/sirupsen/logrus"
27+
"github.com/stretchr/testify/assert"
28+
"go.mongodb.org/mongo-driver/bson"
29+
30+
"github.com/percona/mongodb_exporter/internal/tu"
31+
)
32+
33+
func TestCurrentopCollector(t *testing.T) {
34+
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
35+
defer cancel()
36+
37+
var wg sync.WaitGroup
38+
39+
client := tu.DefaultTestClient(ctx, t)
40+
41+
database := client.Database("testdb")
42+
database.Drop(ctx)
43+
44+
defer func() {
45+
err := database.Drop(ctx)
46+
assert.NoError(t, err)
47+
}()
48+
wg.Add(1)
49+
go func() {
50+
defer wg.Done()
51+
for i := 0; i < 3; i++ {
52+
coll := fmt.Sprintf("testcol_%02d", i)
53+
_, err := database.Collection(coll).InsertOne(ctx, bson.M{"f1": 1, "f2": "2"})
54+
assert.NoError(t, err)
55+
}
56+
}()
57+
58+
ti := labelsGetterMock{}
59+
60+
c := newCurrentopCollector(ctx, client, logrus.New(), false, ti)
61+
62+
// Filter metrics by reason:
63+
// 1. The result will be different on different hardware
64+
// 2. Can't check labels like 'decs' and 'opid' because they don't return a known value for comparison
65+
// It looks like:
66+
// # HELP mongodb_currentop_query_uptime currentop_query.
67+
// # TYPE mongodb_currentop_query_uptime untyped
68+
// mongodb_currentop_query_uptime{collection="testcol_00",database="testdb",decs="conn6365",ns="testdb.testcol_00",op="insert",opid="448307"} 2524
69+
70+
filter := []string{
71+
"mongodb_currentop_query_uptime",
72+
}
73+
74+
count := testutil.CollectAndCount(c, filter...)
75+
assert.True(t, count > 0)
76+
wg.Wait()
77+
}

exporter/exporter.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ type Opts struct {
6565
EnableDBStatsFreeStorage bool
6666
EnableDiagnosticData bool
6767
EnableReplicasetStatus bool
68+
EnableCurrentopMetrics bool
6869
EnableTopMetrics bool
6970
EnableIndexStats bool
7071
EnableCollStats bool
@@ -170,6 +171,7 @@ func (e *Exporter) makeRegistry(ctx context.Context, client *mongo.Client, topol
170171
e.opts.EnableTopMetrics = true
171172
e.opts.EnableReplicasetStatus = true
172173
e.opts.EnableIndexStats = true
174+
e.opts.EnableCurrentopMetrics = true
173175
}
174176

175177
// arbiter only have isMaster privileges
@@ -180,6 +182,7 @@ func (e *Exporter) makeRegistry(ctx context.Context, client *mongo.Client, topol
180182
e.opts.EnableTopMetrics = false
181183
e.opts.EnableReplicasetStatus = false
182184
e.opts.EnableIndexStats = false
185+
e.opts.EnableCurrentopMetrics = false
183186
}
184187

185188
// If we manually set the collection names we want or auto discovery is set.
@@ -210,6 +213,12 @@ func (e *Exporter) makeRegistry(ctx context.Context, client *mongo.Client, topol
210213
registry.MustRegister(cc)
211214
}
212215

216+
if e.opts.EnableCurrentopMetrics && nodeType != typeMongos && limitsOk && requestOpts.EnableCurrentopMetrics {
217+
coc := newCurrentopCollector(ctx, client, e.opts.Logger,
218+
e.opts.CompatibleMode, topologyInfo)
219+
registry.MustRegister(coc)
220+
}
221+
213222
if e.opts.EnableTopMetrics && nodeType != typeMongos && limitsOk && requestOpts.EnableTopMetrics {
214223
tc := newTopCollector(ctx, client, e.opts.Logger,
215224
e.opts.CompatibleMode, topologyInfo)
@@ -288,6 +297,8 @@ func (e *Exporter) Handler() http.Handler {
288297
requestOpts.EnableDBStats = true
289298
case "topmetrics":
290299
requestOpts.EnableTopMetrics = true
300+
case "currentopmetrics":
301+
requestOpts.EnableCurrentopMetrics = true
291302
case "indexstats":
292303
requestOpts.EnableIndexStats = true
293304
case "collstats":

main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ type GlobalFlags struct {
5151
EnableDBStats bool `name:"collector.dbstats" help:"Enable collecting metrics from dbStats"`
5252
EnableDBStatsFreeStorage bool `name:"collector.dbstatsfreestorage" help:"Enable collecting free space metrics from dbStats"`
5353
EnableTopMetrics bool `name:"collector.topmetrics" help:"Enable collecting metrics from top admin command"`
54+
EnableCurrentopMetrics bool `name:"collector.currentopmetrics" help:"Enable collecting metrics currentop admin command"`
5455
EnableIndexStats bool `name:"collector.indexstats" help:"Enable collecting metrics from $indexStats"`
5556
EnableCollStats bool `name:"collector.collstats" help:"Enable collecting metrics from $collStats"`
5657

@@ -139,6 +140,7 @@ func buildExporter(opts GlobalFlags) *exporter.Exporter {
139140

140141
EnableDiagnosticData: opts.EnableDiagnosticData,
141142
EnableReplicasetStatus: opts.EnableReplicasetStatus,
143+
EnableCurrentopMetrics: opts.EnableCurrentopMetrics,
142144
EnableTopMetrics: opts.EnableTopMetrics,
143145
EnableDBStats: opts.EnableDBStats,
144146
EnableDBStatsFreeStorage: opts.EnableDBStatsFreeStorage,

0 commit comments

Comments
 (0)