diff --git a/Dockerfile b/Dockerfile
index 3ed6f8ffb..759ab614a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,9 +1,18 @@
FROM alpine AS builder
RUN apk add --no-cache ca-certificates
-FROM scratch AS final
+FROM golang:alpine as builder2
+
+RUN apk update && apk add make
+RUN mkdir /source
+COPY . /source
+WORKDIR /source
+RUN make init
+RUN make build
+
+FROM alpine AS final
USER 65535:65535
-COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
-COPY ./mongodb_exporter /
+COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
+COPY --from=builder2 /source/mongodb_exporter /
EXPOSE 9216
ENTRYPOINT ["/mongodb_exporter"]
\ No newline at end of file
diff --git a/README.md b/README.md
index f20cefb7a..6fbd05511 100644
--- a/README.md
+++ b/README.md
@@ -86,6 +86,17 @@ export MONGODB_PASSWORD=YYY
mongodb_exporter_linux_amd64/mongodb_exporter --mongodb.uri=mongodb://127.0.0.1:17001 --mongodb.collstats-colls=db1.c1,db2.c2
```
+#### Multi-target support
+You can run the exporter specifying multiple URIs, devided by a comma in --mongodb.uri option or MONGODB_URI environment variable in order to monitor multiple mongodb instances with the a single mongodb_exporter instance.
+```sh
+--mongodb.uri=mongodb://user:pass@127.0.0.1:27017/admin,mongodb://user2:pass2@127.0.0.1:27018/admin
+```
+In this case you can use the **/scrape** endpoint with the **target** parameter to retreive the specified tartget's metrics. When querying the data you can use just mongodb://host:port in the targer parameter without other parameters and, of course without host credentials
+```sh
+GET /scrape?target=mongodb://127.0.0.1:27018
+```
+
+
#### Enabling collstats metrics gathering
`--mongodb.collstats-colls` receives a list of databases and collections to monitor using collstats.
Usage example: `--mongodb.collstats-colls=database1.collection1,database2.collection2`
diff --git a/exporter/base_collector.go b/exporter/base_collector.go
index c16dc34d5..4184112d2 100644
--- a/exporter/base_collector.go
+++ b/exporter/base_collector.go
@@ -43,7 +43,10 @@ func newBaseCollector(client *mongo.Client, logger *logrus.Logger) *baseCollecto
func (d *baseCollector) Describe(ctx context.Context, ch chan<- *prometheus.Desc, collect func(mCh chan<- prometheus.Metric)) {
select {
case <-ctx.Done():
- return
+ // don't interrupt, let mongodb_up metric to be registered if on timeout we still don't have client connected
+ if d.client != nil {
+ return
+ }
default:
}
diff --git a/exporter/exporter.go b/exporter/exporter.go
index 4787abe7a..ce11f0667 100644
--- a/exporter/exporter.go
+++ b/exporter/exporter.go
@@ -21,15 +21,12 @@ import (
"fmt"
"net/http"
_ "net/http/pprof"
- "os"
"strconv"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
- "github.com/prometheus/common/promlog"
- "github.com/prometheus/exporter-toolkit/web"
"github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/mongo"
@@ -38,12 +35,10 @@ import (
// Exporter holds Exporter methods and attributes.
type Exporter struct {
- path string
client *mongo.Client
clientMu sync.Mutex
logger *logrus.Logger
opts *Opts
- webListenAddress string
lock *sync.Mutex
totalCollectionsCount int
}
@@ -56,6 +51,7 @@ type Opts struct {
CollStatsLimit int
CompatibleMode bool
DirectConnect bool
+ ConnectTimeoutMS int
DisableDefaultRegistry bool
DiscoveringMode bool
GlobalConnPool bool
@@ -76,10 +72,8 @@ type Opts struct {
IndexStatsCollections []string
Logger *logrus.Logger
- Path string
- URI string
- WebListenAddress string
- TLSConfigPath string
+
+ URI string
}
var (
@@ -103,16 +97,9 @@ func New(opts *Opts) *Exporter {
ctx := context.Background()
- if opts.Path == "" {
- opts.Logger.Warn("Web telemetry path \"\" invalid, falling back to \"/\" instead")
- opts.Path = "/"
- }
-
exp := &Exporter{
- path: opts.Path,
logger: opts.Logger,
opts: opts,
- webListenAddress: opts.WebListenAddress,
lock: &sync.Mutex{},
totalCollectionsCount: -1, // Not calculated yet. waiting the db connection.
}
@@ -257,7 +244,7 @@ func (e *Exporter) getClient(ctx context.Context) (*mongo.Client, error) {
return e.client, nil
}
- client, err := connect(context.Background(), e.opts.URI, e.opts.DirectConnect)
+ client, err := connect(context.Background(), e.opts)
if err != nil {
return nil, err
}
@@ -267,7 +254,7 @@ func (e *Exporter) getClient(ctx context.Context) (*mongo.Client, error) {
}
// !e.opts.GlobalConnPool: create new client for every scrape.
- client, err := connect(ctx, e.opts.URI, e.opts.DirectConnect)
+ client, err := connect(ctx, e.opts)
if err != nil {
return nil, err
}
@@ -350,14 +337,15 @@ func (e *Exporter) Handler() http.Handler {
gatherers = append(gatherers, prometheus.DefaultGatherer)
}
+ var ti *topologyInfo
if client != nil {
// Topology can change between requests, so we need to get it every time.
- ti := newTopologyInfo(ctx, client, e.logger)
-
- registry := e.makeRegistry(ctx, client, ti, requestOpts)
- gatherers = append(gatherers, registry)
+ ti = newTopologyInfo(ctx, client, e.logger)
}
+ registry := e.makeRegistry(ctx, client, ti, requestOpts)
+ gatherers = append(gatherers, registry)
+
// Delegate http serving to Prometheus client library, which will call collector.Collect.
h := promhttp.HandlerFor(gatherers, promhttp.HandlerOpts{
ErrorHandling: promhttp.ContinueOnError,
@@ -368,41 +356,21 @@ func (e *Exporter) Handler() http.Handler {
})
}
-// Run starts the exporter.
-func (e *Exporter) Run() {
- mux := http.DefaultServeMux
- mux.Handle(e.path, e.Handler())
- mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- w.Write([]byte(`
-
MongoDB Exporter
-
- MongoDB Exporter
- Metrics
-
- `))
- })
-
- server := &http.Server{
- Handler: mux,
- }
- flags := &web.FlagConfig{
- WebListenAddresses: &[]string{e.webListenAddress},
- WebConfigFile: &e.opts.TLSConfigPath,
- }
- if err := web.ListenAndServe(server, flags, promlog.New(&promlog.Config{})); err != nil {
- e.logger.Errorf("error starting server: %v", err)
- os.Exit(1)
- }
-}
-
-func connect(ctx context.Context, dsn string, directConnect bool) (*mongo.Client, error) {
- clientOpts, err := dsn_fix.ClientOptionsForDSN(dsn)
+func connect(ctx context.Context, opts *Opts) (*mongo.Client, error) {
+ clientOpts, err := dsn_fix.ClientOptionsForDSN(opts.URI)
if err != nil {
return nil, fmt.Errorf("invalid dsn: %w", err)
}
- clientOpts.SetDirect(directConnect)
+
+ clientOpts.SetDirect(opts.DirectConnect)
clientOpts.SetAppName("mongodb_exporter")
+ if clientOpts.ConnectTimeout == nil {
+ connectTimeout := time.Duration(opts.ConnectTimeoutMS) * time.Millisecond
+ clientOpts.SetConnectTimeout(connectTimeout)
+ clientOpts.SetServerSelectionTimeout(connectTimeout)
+ }
+
client, err := mongo.Connect(ctx, clientOpts)
if err != nil {
return nil, fmt.Errorf("invalid MongoDB options: %w", err)
diff --git a/exporter/exporter_test.go b/exporter/exporter_test.go
index b340c8810..9dba21159 100644
--- a/exporter/exporter_test.go
+++ b/exporter/exporter_test.go
@@ -19,11 +19,15 @@ import (
"context"
"fmt"
"io"
+ "net"
"net/http"
"net/http/httptest"
+ "strconv"
+ "strings"
"sync"
"testing"
+ "github.com/prometheus/client_golang/prometheus/testutil"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
@@ -61,8 +65,11 @@ func TestConnect(t *testing.T) {
t.Run("Connect without SSL", func(t *testing.T) {
for name, port := range ports {
- dsn := fmt.Sprintf("mongodb://%s:%s/admin", hostname, port)
- client, err := connect(ctx, dsn, true)
+ exporterOpts := &Opts{
+ URI: fmt.Sprintf("mongodb://%s/admin", net.JoinHostPort(hostname, port)),
+ DirectConnect: true,
+ }
+ client, err := connect(ctx, exporterOpts)
assert.NoError(t, err, name)
err = client.Disconnect(ctx)
assert.NoError(t, err, name)
@@ -167,17 +174,17 @@ func TestMongoS(t *testing.T) {
}
for _, test := range tests {
- dsn := fmt.Sprintf("mongodb://%s:%s/admin", hostname, test.port)
- client, err := connect(ctx, dsn, true)
- assert.NoError(t, err)
-
exporterOpts := &Opts{
Logger: logrus.New(),
- URI: dsn,
+ URI: fmt.Sprintf("mongodb://%s/admin", net.JoinHostPort(hostname, test.port)),
+ DirectConnect: true,
GlobalConnPool: false,
EnableReplicasetStatus: true,
}
+ client, err := connect(ctx, exporterOpts)
+ assert.NoError(t, err)
+
e := New(exporterOpts)
rsgsc := newReplicationSetStatusCollector(ctx, client, e.opts.Logger,
@@ -195,17 +202,17 @@ func TestMongoS(t *testing.T) {
func TestMongoUp(t *testing.T) {
ctx := context.Background()
- dsn := "mongodb://127.0.0.1:123456/admin"
- client, err := connect(ctx, dsn, true)
- assert.Error(t, err)
-
exporterOpts := &Opts{
Logger: logrus.New(),
- URI: dsn,
+ URI: "mongodb://127.0.0.1:123456/admin",
+ DirectConnect: true,
GlobalConnPool: false,
CollectAll: true,
}
+ client, err := connect(ctx, exporterOpts)
+ assert.Error(t, err)
+
e := New(exporterOpts)
gc := newGeneralCollector(ctx, client, e.opts.Logger)
@@ -215,3 +222,52 @@ func TestMongoUp(t *testing.T) {
res := r.Unregister(gc)
assert.Equal(t, true, res)
}
+
+func TestMongoUpMetric(t *testing.T) {
+ ctx := context.Background()
+
+ type testcase struct {
+ URI string
+ Want int
+ }
+
+ testCases := []testcase{
+ {URI: "mongodb://127.0.0.1:12345/admin", Want: 0},
+ {URI: fmt.Sprintf("mongodb://127.0.0.1:%s/admin", tu.GetenvDefault("TEST_MONGODB_STANDALONE_PORT", "27017")), Want: 1},
+ }
+
+ for _, tc := range testCases {
+ exporterOpts := &Opts{
+ Logger: logrus.New(),
+ URI: tc.URI,
+ ConnectTimeoutMS: 200,
+ DirectConnect: true,
+ GlobalConnPool: false,
+ CollectAll: true,
+ }
+
+ client, err := connect(ctx, exporterOpts)
+ if tc.Want == 1 {
+ assert.NoError(t, err, "Must be able to connect to %s", tc.URI)
+ } else {
+ assert.Error(t, err, "Must be unable to connect to %s", tc.URI)
+ }
+
+ e := New(exporterOpts)
+ gc := newGeneralCollector(ctx, client, e.opts.Logger)
+ r := e.makeRegistry(ctx, client, new(labelsGetterMock), *e.opts)
+
+ expected := strings.NewReader(`
+ # HELP mongodb_up Whether MongoDB is up.
+ # TYPE mongodb_up gauge
+ mongodb_up ` + strconv.Itoa(tc.Want) + "\n")
+ filter := []string{
+ "mongodb_up",
+ }
+ err = testutil.CollectAndCompare(gc, expected, filter...)
+ assert.NoError(t, err, "mongodb_up metric should be %d", tc.Want)
+
+ res := r.Unregister(gc)
+ assert.Equal(t, true, res)
+ }
+}
diff --git a/exporter/multi_target_test.go b/exporter/multi_target_test.go
new file mode 100644
index 000000000..f689ce7c7
--- /dev/null
+++ b/exporter/multi_target_test.go
@@ -0,0 +1,72 @@
+// mongodb_exporter
+// Copyright (C) 2017 Percona LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package exporter
+
+import (
+ "fmt"
+ "net"
+ "testing"
+
+ "github.com/sirupsen/logrus"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/percona/mongodb_exporter/internal/tu"
+)
+
+func TestMultiTarget(t *testing.T) {
+ hostname := "127.0.0.1"
+ opts := []*Opts{
+ {
+ URI: fmt.Sprintf("mongodb://%s", net.JoinHostPort(hostname, tu.GetenvDefault("TEST_MONGODB_STANDALONE_PORT", "27017"))),
+ DirectConnect: true,
+ ConnectTimeoutMS: 1000,
+ },
+ {
+ URI: fmt.Sprintf("mongodb://%s", net.JoinHostPort(hostname, tu.GetenvDefault("TEST_MONGODB_S1_PRIMARY_PORT", "17001"))),
+ DirectConnect: true,
+ ConnectTimeoutMS: 1000,
+ },
+ {
+ URI: fmt.Sprintf("mongodb://%s", net.JoinHostPort(hostname, tu.GetenvDefault("TEST_MONGODB_S2_PRIMARY_PORT", "17004"))),
+ DirectConnect: true,
+ ConnectTimeoutMS: 1000,
+ },
+ {
+ URI: fmt.Sprintf("mongodb://%s", net.JoinHostPort(hostname, "12345")),
+ DirectConnect: true,
+ ConnectTimeoutMS: 1000,
+ },
+ }
+ exporters := make([]*Exporter, len(opts))
+
+ for i, opt := range opts {
+ exporters[i] = New(opt)
+ }
+ log := logrus.New()
+ serverMap := buildServerMap(exporters, log)
+
+ expected := []string{
+ "mongodb_up 1\n",
+ "mongodb_up 1\n",
+ "mongodb_up 1\n",
+ "mongodb_up 0\n",
+ }
+
+ // Test all targets
+ for sn, opt := range opts {
+ assert.HTTPBodyContains(t, multiTargetHandler(serverMap), "GET", fmt.Sprintf("?target=%s", opt.URI), nil, expected[sn])
+ }
+}
diff --git a/exporter/server.go b/exporter/server.go
new file mode 100644
index 000000000..6788acb31
--- /dev/null
+++ b/exporter/server.go
@@ -0,0 +1,111 @@
+// mongodb_exporter
+// Copyright (C) 2017 Percona LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package exporter
+
+import (
+ "net/http"
+ "net/url"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/prometheus/common/promlog"
+ "github.com/prometheus/exporter-toolkit/web"
+ "github.com/sirupsen/logrus"
+)
+
+// ServerMap stores http handlers for each host
+type ServerMap map[string]http.Handler
+
+// ServerOpts is the options for the main http handler
+type ServerOpts struct {
+ Path string
+ MultiTargetPath string
+ WebListenAddress string
+ TLSConfigPath string
+}
+
+// Runs the main web-server
+func RunWebServer(opts *ServerOpts, exporters []*Exporter, log *logrus.Logger) {
+ mux := http.DefaultServeMux
+
+ if len(exporters) == 0 {
+ panic("No exporters were built. You must specify --mongodb.uri command argument or MONGODB_URI environment variable")
+ }
+
+ serverMap := buildServerMap(exporters, log)
+
+ defaultExporter := exporters[0]
+ mux.Handle(opts.Path, defaultExporter.Handler())
+ mux.HandleFunc(opts.MultiTargetPath, multiTargetHandler(serverMap))
+
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ _, err := w.Write([]byte(`
+ MongoDB Exporter
+
+ MongoDB Exporter
+ Metrics
+
+ `))
+ if err != nil {
+ log.Errorf("error writing response: %v", err)
+ }
+ })
+
+ server := &http.Server{
+ ReadHeaderTimeout: 2 * time.Second,
+ Handler: mux,
+ }
+ flags := &web.FlagConfig{
+ WebListenAddresses: &[]string{opts.WebListenAddress},
+ WebConfigFile: &opts.TLSConfigPath,
+ }
+ if err := web.ListenAndServe(server, flags, promlog.New(&promlog.Config{})); err != nil {
+ log.Errorf("error starting server: %v", err)
+ os.Exit(1)
+ }
+}
+
+func multiTargetHandler(serverMap ServerMap) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ targetHost := r.URL.Query().Get("target")
+ if targetHost != "" {
+ if !strings.HasPrefix(targetHost, "mongodb://") {
+ targetHost = "mongodb://" + targetHost
+ }
+ if uri, err := url.Parse(targetHost); err == nil {
+ if e, ok := serverMap[uri.Host]; ok {
+ e.ServeHTTP(w, r)
+ return
+ }
+ }
+ }
+ http.Error(w, "Unable to find target", http.StatusNotFound)
+ }
+}
+
+func buildServerMap(exporters []*Exporter, log *logrus.Logger) ServerMap {
+ servers := make(ServerMap, len(exporters))
+ for _, e := range exporters {
+ if url, err := url.Parse(e.opts.URI); err == nil {
+ servers[url.Host] = e.Handler()
+ } else {
+ log.Errorf("Unable to parse addr %s as url: %s", e.opts.URI, err)
+ }
+ }
+
+ return servers
+}
diff --git a/internal/tu/testutils.go b/internal/tu/testutils.go
index ce138ab3f..a93ccdfe1 100644
--- a/internal/tu/testutils.go
+++ b/internal/tu/testutils.go
@@ -67,6 +67,7 @@ func GetenvDefault(key, defaultValue string) string {
func DefaultTestClient(ctx context.Context, t *testing.T) *mongo.Client {
port, err := PortForContainer("mongo-1-1")
require.NoError(t, err)
+
return TestClient(ctx, port, t)
}
@@ -103,6 +104,7 @@ func GetImageNameForDefault() (string, string, error) {
break
}
}
+
return imageBaseName, version, nil
}
diff --git a/main.go b/main.go
index 1e5d13561..83948ff27 100644
--- a/main.go
+++ b/main.go
@@ -34,17 +34,18 @@ var (
// GlobalFlags has command line flags to configure the exporter.
type GlobalFlags struct {
- User string `name:"mongodb.user" help:"monitor user, need clusterMonitor role in admin db and read role in local db" env:"MONGODB_USER" placeholder:"monitorUser"`
- Password string `name:"mongodb.password" help:"monitor user password" env:"MONGODB_PASSWORD" placeholder:"monitorPassword"`
- CollStatsNamespaces string `name:"mongodb.collstats-colls" help:"List of comma separared databases.collections to get $collStats" placeholder:"db1,db2.col2"`
- IndexStatsCollections string `name:"mongodb.indexstats-colls" help:"List of comma separared databases.collections to get $indexStats" placeholder:"db1.col1,db2.col2"`
- URI string `name:"mongodb.uri" help:"MongoDB connection URI" env:"MONGODB_URI" placeholder:"mongodb://user:pass@127.0.0.1:27017/admin?ssl=true"`
- GlobalConnPool bool `name:"mongodb.global-conn-pool" help:"Use global connection pool instead of creating new pool for each http request." negatable:""`
- DirectConnect bool `name:"mongodb.direct-connect" help:"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." default:"true" negatable:""`
- WebListenAddress string `name:"web.listen-address" help:"Address to listen on for web interface and telemetry" default:":9216"`
- WebTelemetryPath string `name:"web.telemetry-path" help:"Metrics expose path" default:"/metrics"`
- TLSConfigPath string `name:"web.config" help:"Path to the file having Prometheus TLS config for basic auth"`
- LogLevel string `name:"log.level" help:"Only log messages with the given severity or above. Valid levels: [debug, info, warn, error, fatal]" enum:"debug,info,warn,error,fatal" default:"error"`
+ User string `name:"mongodb.user" help:"monitor user, need clusterMonitor role in admin db and read role in local db" env:"MONGODB_USER" placeholder:"monitorUser"`
+ Password string `name:"mongodb.password" help:"monitor user password" env:"MONGODB_PASSWORD" placeholder:"monitorPassword"`
+ CollStatsNamespaces string `name:"mongodb.collstats-colls" help:"List of comma separared databases.collections to get $collStats" placeholder:"db1,db2.col2"`
+ IndexStatsCollections string `name:"mongodb.indexstats-colls" help:"List of comma separared databases.collections to get $indexStats" placeholder:"db1.col1,db2.col2"`
+ URI []string `name:"mongodb.uri" help:"MongoDB connection URI" env:"MONGODB_URI" placeholder:"mongodb://user:pass@127.0.0.1:27017/admin?ssl=true"`
+ GlobalConnPool bool `name:"mongodb.global-conn-pool" help:"Use global connection pool instead of creating new pool for each http request." negatable:""`
+ DirectConnect bool `name:"mongodb.direct-connect" help:"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." default:"true" negatable:""`
+ WebListenAddress string `name:"web.listen-address" help:"Address to listen on for web interface and telemetry" default:":9216"`
+ WebTelemetryPath string `name:"web.telemetry-path" help:"Metrics expose path" default:"/metrics"`
+ TLSConfigPath string `name:"web.config" help:"Path to the file having Prometheus TLS config for basic auth"`
+ LogLevel string `name:"log.level" help:"Only log messages with the given severity or above. Valid levels: [debug, info, warn, error, fatal]" enum:"debug,info,warn,error,fatal" default:"error"`
+ ConnectTimeoutMS int `name:"mongodb.connect-timeout-ms" help:"Connection timeout in milliseconds" default:"5000"`
EnableDiagnosticData bool `name:"collector.diagnosticdata" help:"Enable collecting metrics from getDiagnosticData"`
EnableReplicasetStatus bool `name:"collector.replicasetstatus" help:"Enable collecting metrics from replSetGetStatus"`
@@ -71,7 +72,7 @@ type GlobalFlags struct {
func main() {
var opts GlobalFlags
- _ = kong.Parse(&opts,
+ ctx := kong.Parse(&opts,
kong.Name("mongodb_exporter"),
kong.Description("MongoDB Prometheus exporter"),
kong.UsageOnError(),
@@ -87,30 +88,9 @@ func main() {
fmt.Printf("Version: %s\n", version)
fmt.Printf("Commit: %s\n", commit)
fmt.Printf("Build date: %s\n", buildDate)
-
return
}
- e := buildExporter(opts)
- e.Run()
-}
-
-func buildURI(uri string, user string, password string) string {
- // IF user@pass not contained in uri AND custom user and pass supplied in arguments
- // DO concat a new uri with user and pass arguments value
- if !strings.Contains(uri, "@") && user != "" && password != "" {
- // trim mongodb:// prefix to handle user and pass logic
- uri = strings.TrimPrefix(uri, "mongodb://")
- // add user and pass to the uri
- uri = fmt.Sprintf("%s:%s@%s", user, password, uri)
- }
- if !strings.HasPrefix(uri, "mongodb") {
- uri = "mongodb://" + uri
- }
- return uri
-}
-
-func buildExporter(opts GlobalFlags) *exporter.Exporter {
log := logrus.New()
levels := map[string]logrus.Level{
@@ -121,12 +101,29 @@ func buildExporter(opts GlobalFlags) *exporter.Exporter {
"warn": logrus.WarnLevel,
}
log.SetLevel(levels[opts.LogLevel])
-
log.Debugf("Compatible mode: %v", opts.CompatibleMode)
- opts.URI = buildURI(opts.URI, opts.User, opts.Password)
+ if opts.WebTelemetryPath == "" {
+ log.Warn("Web telemetry path \"\" invalid, falling back to \"/\" instead")
+ opts.WebTelemetryPath = "/"
+ }
+
+ if len(opts.URI) == 0 {
+ ctx.Fatalf("No MongoDB hosts were specified. You must specify the host(s) with the --mongodb.uri command argument or the MONGODB_URI environment variable")
+ }
+
+ serverOpts := &exporter.ServerOpts{
+ Path: opts.WebTelemetryPath,
+ MultiTargetPath: "/scrape",
+ WebListenAddress: opts.WebListenAddress,
+ TLSConfigPath: opts.TLSConfigPath,
+ }
+ exporter.RunWebServer(serverOpts, buildServers(opts, log), log)
+}
- log.Debugf("Connection URI: %s", opts.URI)
+func buildExporter(opts GlobalFlags, uri string, log *logrus.Logger) *exporter.Exporter {
+ uri = buildURI(uri, opts.User, opts.Password)
+ log.Debugf("Connection URI: %s", uri)
exporterOpts := &exporter.Opts{
CollStatsNamespaces: strings.Split(opts.CollStatsNamespaces, ","),
@@ -134,12 +131,10 @@ func buildExporter(opts GlobalFlags) *exporter.Exporter {
DiscoveringMode: opts.DiscoveringMode,
IndexStatsCollections: strings.Split(opts.IndexStatsCollections, ","),
Logger: log,
- Path: opts.WebTelemetryPath,
- URI: opts.URI,
+ URI: uri,
GlobalConnPool: opts.GlobalConnPool,
- WebListenAddress: opts.WebListenAddress,
- TLSConfigPath: opts.TLSConfigPath,
DirectConnect: opts.DirectConnect,
+ ConnectTimeoutMS: opts.ConnectTimeoutMS,
EnableDiagnosticData: opts.EnableDiagnosticData,
EnableReplicasetStatus: opts.EnableReplicasetStatus,
@@ -162,3 +157,36 @@ func buildExporter(opts GlobalFlags) *exporter.Exporter {
return e
}
+
+func buildServers(opts GlobalFlags, log *logrus.Logger) []*exporter.Exporter {
+ servers := make([]*exporter.Exporter, len(opts.URI))
+
+ for serverIdx := range opts.URI {
+ URI := opts.URI[serverIdx]
+
+ if !strings.HasPrefix(URI, "mongodb") {
+ log.Debugf("Prepending mongodb:// to the URI %s", URI)
+ URI = "mongodb://" + URI
+ }
+
+ servers[serverIdx] = buildExporter(opts, URI, log)
+ }
+
+ return servers
+}
+
+func buildURI(uri string, user string, password string) string {
+ // IF user@pass not contained in uri AND custom user and pass supplied in arguments
+ // DO concat a new uri with user and pass arguments value
+ if !strings.Contains(uri, "@") && user != "" && password != "" {
+ // trim mongodb:// prefix to handle user and pass logic
+ uri = strings.TrimPrefix(uri, "mongodb://")
+ // add user and pass to the uri
+ uri = fmt.Sprintf("%s:%s@%s", user, password, uri)
+ }
+ if !strings.HasPrefix(uri, "mongodb") {
+ uri = "mongodb://" + uri
+ }
+
+ return uri
+}
diff --git a/main_test.go b/main_test.go
index bf37e38a0..a5549e1bb 100644
--- a/main_test.go
+++ b/main_test.go
@@ -18,6 +18,7 @@ package main
import (
"testing"
+ "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
@@ -25,7 +26,6 @@ func TestBuildExporter(t *testing.T) {
opts := GlobalFlags{
CollStatsNamespaces: "c1,c2,c3",
IndexStatsCollections: "i1,i2,i3",
- URI: "mongodb://usr:pwd@127.0.0.1/",
GlobalConnPool: false, // to avoid testing the connection
WebListenAddress: "localhost:12345",
WebTelemetryPath: "/mymetrics",
@@ -36,8 +36,8 @@ func TestBuildExporter(t *testing.T) {
CompatibleMode: true,
}
-
- buildExporter(opts)
+ log := logrus.New()
+ buildExporter(opts, "mongodb://usr:pwd@127.0.0.1/", log)
}
func TestBuildURI(t *testing.T) {