@@ -18,10 +18,12 @@ package exporter
1818
1919import (
2020 "context"
21+ "fmt"
2122
2223 "github.com/prometheus/client_golang/prometheus"
2324 "github.com/sirupsen/logrus"
2425 "go.mongodb.org/mongo-driver/bson"
26+ "go.mongodb.org/mongo-driver/bson/primitive"
2527 "go.mongodb.org/mongo-driver/mongo"
2628)
2729
@@ -33,12 +35,14 @@ type topCollector struct {
3335 topologyInfo labelsGetter
3436}
3537
36- // newTopCollector creates a collector for statistics on collection usage.
37- func newTopCollector (ctx context.Context , client * mongo.Client , logger * logrus.Logger , compatible bool , topology labelsGetter ) * topCollector {
38- return & topCollector {
39- ctx : ctx ,
40- base : newBaseCollector (client , logger ),
38+ var ErrInvalidOrMissingTotalsEntry = fmt .Errorf ("Invalid or misssing totals entry in top results" )
4139
40+ func newTopCollector (ctx context.Context , client * mongo.Client , logger * logrus.Logger , compatible bool ,
41+ topology labelsGetter ,
42+ ) * topCollector {
43+ return & topCollector {
44+ ctx : ctx ,
45+ base : newBaseCollector (client , logger ),
4246 compatibleMode : compatible ,
4347 topologyInfo : topology ,
4448 }
@@ -59,7 +63,7 @@ func (d *topCollector) collect(ch chan<- prometheus.Metric) {
5963 cmd := bson.D {{Key : "top" , Value : "1" }}
6064 res := client .Database ("admin" ).RunCommand (d .ctx , cmd )
6165
62- var m bson .M
66+ var m primitive .M
6367 if err := res .Decode (& m ); err != nil {
6468 ch <- prometheus .NewInvalidMetric (prometheus .NewInvalidDesc (err ), err )
6569 return
@@ -68,7 +72,85 @@ func (d *topCollector) collect(ch chan<- prometheus.Metric) {
6872 logrus .Debug ("top result:" )
6973 debugResult (logger , m )
7074
71- for _ , metric := range makeMetrics ("top" , m , d .topologyInfo .baseLabels (), d .compatibleMode ) {
72- ch <- metric
75+ totals , ok := m ["totals" ].(primitive.M )
76+ if ! ok {
77+ ch <- prometheus .NewInvalidMetric (prometheus .NewInvalidDesc (ErrInvalidOrMissingTotalsEntry ),
78+ ErrInvalidOrMissingTotalsEntry )
79+ }
80+
81+ /*
82+ The top command will return a structure with a key named totals and it is a map
83+ where the key is the collection namespace and for each collection there are per
84+ collection usage statistics.
85+ Example: rs1:SECONDARY> db.adminCommand({"top": 1});
86+
87+ {
88+ "totals" : {
89+ "note" : "all times in microseconds",
90+ "admin.system.roles" : {
91+ "total" : {
92+ "time" : 41,
93+ "count" : 1
94+ },
95+ "readLock" : {
96+ "time" : 41,
97+ "count" : 1
98+ },
99+ "writeLock" : {
100+ "time" : 0,
101+ "count" : 0
102+ },
103+ "queries" : {
104+ "time" : 41,
105+ "count" : 1
106+ },
107+ "getmore" : {
108+ "time" : 0,
109+ "count" : 0
110+ },
111+ "insert" : {
112+ "time" : 0,
113+ "count" : 0
114+ },
115+ "update" : {
116+ "time" : 0,
117+ "count" : 0
118+ },
119+ "remove" : {
120+ "time" : 0,
121+ "count" : 0
122+ },
123+ "commands" : {
124+ "time" : 0,
125+ "count" : 0
126+ }
127+ },
128+ "admin.system.version" : {
129+ "total" : {
130+ "time" : 63541,
131+ "count" : 218
132+ },
133+
134+ If we pass this structure to the makeMetrics function, we will have metric names with the form of
135+ prefix + namespace + metric like mongodb_top_totals_admin.system.role_readlock_count.
136+ Having the namespace as part of the metric is a Prometheus anti pattern and diffucults grouping
137+ metrics in Grafana. For this reason, we need to manually loop through the metric in the totals key
138+ and pass the namespace as a label to the makeMetrics function.
139+ */
140+
141+ for namespace , metrics := range totals {
142+ labels := d .topologyInfo .baseLabels ()
143+ db , coll := splitNamespace (namespace )
144+ labels ["database" ] = db
145+ labels ["collection" ] = coll
146+
147+ mm , ok := metrics .(primitive.M ) // ingore entries like -> "note" : "all times in microseconds"
148+ if ! ok {
149+ continue
150+ }
151+
152+ for _ , metric := range makeMetrics ("top" , mm , labels , d .compatibleMode ) {
153+ ch <- metric
154+ }
73155 }
74156}
0 commit comments