Skip to content

Commit b90dc9e

Browse files
authored
Merge branch 'main' into PMM-7-merge-release-branch-back
2 parents d368da1 + 686b313 commit b90dc9e

File tree

15 files changed

+619
-62
lines changed

15 files changed

+619
-62
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,19 @@ If your URI is prefixed by mongodb:// or mongodb+srv:// schema, any host not pre
104104
--mongodb.uri=mongodb+srv://user:pass@host1:27017,host2:27017,host3:27017/admin,mongodb://user2:pass2@host4:27018/admin
105105
```
106106

107+
You can use the --split-cluster option to split all cluster nodes into separate targets. This mode is useful when cluster nodes are defined as SRV records and the mongodb_exporter is running with mongodb+srv domain specified. In this case SRV records will be queried upon mongodb_exporter start and each cluster node can be queried using the **target** parameter of multitarget endpoint.
108+
109+
#### Overall targets request endpoint
110+
111+
There is an overall targets endpoint **/scrapeall** that queries all the targets in one request. It can be used to store multiple node metrics without separate target requests. In this case, each node metric will have a **instance** label containing the node name as a host:port pair (or just host if no port was not specified). For example, for mongodb_exporter running with the options:
112+
```
113+
--mongodb.uri="mongodb://host1:27015,host2:27016" --split-cluster=true
114+
```
115+
we get metrics like this:
116+
```
117+
mongodb_up{instance="host1:27015"} 1
118+
mongodb_up{instance="host2:27016"} 1
119+
```
107120

108121
#### Enabling collstats metrics gathering
109122
`--mongodb.collstats-colls` receives a list of databases and collections to monitor using collstats.

REFERENCE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
| --[no-]mongodb.direct-connect | Whether or not a direct connect should be made. Direct connections are not valid if multiple hosts are specified or an SRV URI is used | |
1212
| --[no-]mongodb.global-conn-pool | Use global connection pool instead of creating new pool for each http request | |
1313
| --mongodb.uri | MongoDB connection URI ($MONGODB_URI) | --mongodb.uri=mongodb://user:pass@127.0.0.1:27017/admin?ssl=true |
14+
| --split-cluster | Whether to treat cluster members from the connection URI as separate targets |
1415
| --web.listen-address | Address to listen on for web interface and telemetry | --web.listen-address=":9216" |
1516
| --web.telemetry-path | Metrics expose path | --web.telemetry-path="/metrics" |
1617
| --web.config | Path to the file having Prometheus TLS config for basic auth | --web.config=STRING |

exporter/exporter.go

Lines changed: 42 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ type Opts struct {
8080
IndexStatsCollections []string
8181
Logger *logrus.Logger
8282

83-
URI string
83+
URI string
84+
NodeName string
8485
}
8586

8687
var (
@@ -290,7 +291,7 @@ func (e *Exporter) getClient(ctx context.Context) (*mongo.Client, error) {
290291
func (e *Exporter) Handler() http.Handler {
291292
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
292293
seconds, err := strconv.Atoi(r.Header.Get("X-Prometheus-Scrape-Timeout-Seconds"))
293-
// To support also older ones vmagents.
294+
// To support older ones vmagents.
294295
if err != nil {
295296
seconds = 10
296297
}
@@ -300,40 +301,7 @@ func (e *Exporter) Handler() http.Handler {
300301
ctx, cancel := context.WithTimeout(r.Context(), time.Duration(seconds)*time.Second)
301302
defer cancel()
302303

303-
filters := r.URL.Query()["collect[]"]
304-
305-
requestOpts := Opts{}
306-
307-
if len(filters) == 0 {
308-
requestOpts = *e.opts
309-
}
310-
311-
for _, filter := range filters {
312-
switch filter {
313-
case "diagnosticdata":
314-
requestOpts.EnableDiagnosticData = true
315-
case "replicasetstatus":
316-
requestOpts.EnableReplicasetStatus = true
317-
case "dbstats":
318-
requestOpts.EnableDBStats = true
319-
case "topmetrics":
320-
requestOpts.EnableTopMetrics = true
321-
case "currentopmetrics":
322-
requestOpts.EnableCurrentopMetrics = true
323-
case "indexstats":
324-
requestOpts.EnableIndexStats = true
325-
case "collstats":
326-
requestOpts.EnableCollStats = true
327-
case "profile":
328-
requestOpts.EnableProfile = true
329-
case "shards":
330-
requestOpts.EnableShards = true
331-
case "fcv":
332-
requestOpts.EnableFCV = true
333-
case "pbm":
334-
requestOpts.EnablePBMMetrics = true
335-
}
336-
}
304+
requestOpts := GetRequestOpts(r.URL.Query()["collect[]"], e.opts)
337305

338306
client, err = e.getClient(ctx)
339307
if err != nil {
@@ -386,6 +354,44 @@ func (e *Exporter) Handler() http.Handler {
386354
})
387355
}
388356

357+
// GetRequestOpts makes exporter.Opts structure from request filters and default options.
358+
func GetRequestOpts(filters []string, defaultOpts *Opts) Opts {
359+
requestOpts := Opts{}
360+
361+
if len(filters) == 0 {
362+
requestOpts = *defaultOpts
363+
}
364+
365+
for _, filter := range filters {
366+
switch filter {
367+
case "diagnosticdata":
368+
requestOpts.EnableDiagnosticData = true
369+
case "replicasetstatus":
370+
requestOpts.EnableReplicasetStatus = true
371+
case "dbstats":
372+
requestOpts.EnableDBStats = true
373+
case "topmetrics":
374+
requestOpts.EnableTopMetrics = true
375+
case "currentopmetrics":
376+
requestOpts.EnableCurrentopMetrics = true
377+
case "indexstats":
378+
requestOpts.EnableIndexStats = true
379+
case "collstats":
380+
requestOpts.EnableCollStats = true
381+
case "profile":
382+
requestOpts.EnableProfile = true
383+
case "shards":
384+
requestOpts.EnableShards = true
385+
case "fcv":
386+
requestOpts.EnableFCV = true
387+
case "pbm":
388+
requestOpts.EnablePBMMetrics = true
389+
}
390+
}
391+
392+
return requestOpts
393+
}
394+
389395
func connect(ctx context.Context, opts *Opts) (*mongo.Client, error) {
390396
clientOpts, err := dsn_fix.ClientOptionsForDSN(opts.URI)
391397
if err != nil {

exporter/gatherer_wrapper.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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+
"github.com/pkg/errors"
20+
"github.com/prometheus/client_golang/prometheus"
21+
io_prometheus_client "github.com/prometheus/client_model/go"
22+
)
23+
24+
// GathererWrapped is a wrapper for prometheus.Gatherer that adds labels to all metrics.
25+
type GathererWrapped struct {
26+
originalGatherer prometheus.Gatherer
27+
labels prometheus.Labels
28+
}
29+
30+
// NewGathererWrapper creates a new GathererWrapped with the given Gatherer and additional labels.
31+
func NewGathererWrapper(gs prometheus.Gatherer, labels prometheus.Labels) *GathererWrapped {
32+
return &GathererWrapped{
33+
originalGatherer: gs,
34+
labels: labels,
35+
}
36+
}
37+
38+
// Gather implements prometheus.Gatherer interface.
39+
func (g *GathererWrapped) Gather() ([]*io_prometheus_client.MetricFamily, error) {
40+
metrics, err := g.originalGatherer.Gather()
41+
if err != nil {
42+
return nil, errors.Wrap(err, "failed to gather metrics")
43+
}
44+
45+
for _, metric := range metrics {
46+
for _, m := range metric.GetMetric() {
47+
for k, v := range g.labels {
48+
v := v
49+
k := k
50+
m.Label = append(m.Label, &io_prometheus_client.LabelPair{
51+
Name: &k,
52+
Value: &v,
53+
})
54+
}
55+
}
56+
}
57+
58+
return metrics, nil
59+
}

exporter/multi_target_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ package exporter
1717

1818
import (
1919
"fmt"
20+
"io"
2021
"net"
22+
"net/http"
23+
"net/http/httptest"
24+
"regexp"
2125
"testing"
2226

2327
"github.com/sirupsen/logrus"
@@ -70,3 +74,61 @@ func TestMultiTarget(t *testing.T) {
7074
assert.HTTPBodyContains(t, multiTargetHandler(serverMap), "GET", fmt.Sprintf("?target=%s", opt.URI), nil, expected[sn])
7175
}
7276
}
77+
78+
func TestOverallHandler(t *testing.T) {
79+
t.Parallel()
80+
81+
opts := []*Opts{
82+
{
83+
NodeName: "standalone",
84+
URI: fmt.Sprintf("mongodb://127.0.0.1:%s", tu.GetenvDefault("TEST_MONGODB_STANDALONE_PORT", "27017")),
85+
DirectConnect: true,
86+
ConnectTimeoutMS: 1000,
87+
},
88+
{
89+
NodeName: "s1",
90+
URI: fmt.Sprintf("mongodb://127.0.0.1:%s", tu.GetenvDefault("TEST_MONGODB_S1_PRIMARY_PORT", "17001")),
91+
DirectConnect: true,
92+
ConnectTimeoutMS: 1000,
93+
},
94+
{
95+
NodeName: "s2",
96+
URI: fmt.Sprintf("mongodb://127.0.0.1:%s", tu.GetenvDefault("TEST_MONGODB_S2_PRIMARY_PORT", "17004")),
97+
DirectConnect: true,
98+
ConnectTimeoutMS: 1000,
99+
},
100+
{
101+
NodeName: "s3",
102+
URI: "mongodb://127.0.0.1:12345",
103+
DirectConnect: true,
104+
ConnectTimeoutMS: 1000,
105+
},
106+
}
107+
expected := []*regexp.Regexp{
108+
regexp.MustCompile(`mongodb_up{[^\}]*instance="standalone"[^\}]*} 1\n`),
109+
regexp.MustCompile(`mongodb_up{[^\}]*instance="s1"[^\}]*} 1\n`),
110+
regexp.MustCompile(`mongodb_up{[^\}]*instance="s2"[^\}]*} 1\n`),
111+
regexp.MustCompile(`mongodb_up{[^\}]*instance="s3"[^\}]*} 0\n`),
112+
}
113+
exporters := make([]*Exporter, len(opts))
114+
115+
logger := logrus.New()
116+
117+
for i, opt := range opts {
118+
exporters[i] = New(opt)
119+
}
120+
121+
rr := httptest.NewRecorder()
122+
req := httptest.NewRequest(http.MethodGet, "/", nil)
123+
OverallTargetsHandler(exporters, logger)(rr, req)
124+
res := rr.Result()
125+
resBody, _ := io.ReadAll(res.Body)
126+
err := res.Body.Close()
127+
assert.NoError(t, err)
128+
129+
assert.Equal(t, http.StatusOK, res.StatusCode)
130+
131+
for _, expected := range expected {
132+
assert.Regexp(t, expected, string(resBody))
133+
}
134+
}

exporter/seedlist.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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+
"net"
20+
"net/url"
21+
"strconv"
22+
"strings"
23+
24+
"github.com/sirupsen/logrus"
25+
)
26+
27+
// GetSeedListFromSRV converts mongodb+srv URI to flat connection string.
28+
func GetSeedListFromSRV(uri string, log *logrus.Logger) string {
29+
uriParsed, err := url.Parse(uri)
30+
if err != nil {
31+
log.Fatalf("Failed to parse URI %s: %v", uri, err)
32+
}
33+
34+
cname, srvRecords, err := net.LookupSRV("mongodb", "tcp", uriParsed.Hostname())
35+
if err != nil {
36+
log.Errorf("Failed to lookup SRV records for %s: %v", uri, err)
37+
return uri
38+
}
39+
40+
if len(srvRecords) == 0 {
41+
log.Errorf("No SRV records found for %s", uri)
42+
return uri
43+
}
44+
45+
queryString := uriParsed.RawQuery
46+
47+
txtRecords, err := net.LookupTXT(uriParsed.Hostname())
48+
if err != nil {
49+
log.Errorf("Failed to lookup TXT records for %s: %v", cname, err)
50+
}
51+
if len(txtRecords) > 1 {
52+
log.Errorf("Multiple TXT records found for %s, thus were not applied", cname)
53+
}
54+
if len(txtRecords) == 1 {
55+
// We take connection parameters from the TXT record
56+
uriParams, err := url.ParseQuery(txtRecords[0])
57+
if err != nil {
58+
log.Errorf("Failed to parse TXT record %s: %v", txtRecords[0], err)
59+
} else {
60+
// Override connection parameters with ones from URI query string
61+
for p, v := range uriParsed.Query() {
62+
uriParams[p] = v
63+
}
64+
queryString = uriParams.Encode()
65+
}
66+
}
67+
68+
// Build final connection URI
69+
servers := make([]string, len(srvRecords))
70+
for i, srv := range srvRecords {
71+
servers[i] = net.JoinHostPort(strings.TrimSuffix(srv.Target, "."), strconv.FormatUint(uint64(srv.Port), 10))
72+
}
73+
uri = "mongodb://"
74+
if uriParsed.User != nil {
75+
uri += uriParsed.User.String() + "@"
76+
}
77+
uri += strings.Join(servers, ",")
78+
if uriParsed.Path != "" {
79+
uri += uriParsed.Path
80+
} else {
81+
uri += "/"
82+
}
83+
if queryString != "" {
84+
uri += "?" + queryString
85+
}
86+
87+
return uri
88+
}

0 commit comments

Comments
 (0)