Skip to content

Commit

Permalink
Multi target support (#653)
Browse files Browse the repository at this point in the history
* added multi target feature

* added connect timeout opts

* added tests

* fixed test

* fixed dockerfile

* Bump github.com/golangci/golangci-lint from 1.47.3 to 1.52.2 in /tools (#639)

Bumps [github.com/golangci/golangci-lint](https://github.com/golangci/golangci-lint) from 1.47.3 to 1.52.2.
- [Release notes](https://github.com/golangci/golangci-lint/releases)
- [Changelog](https://github.com/golangci/golangci-lint/blob/master/CHANGELOG.md)
- [Commits](golangci/golangci-lint@v1.47.3...v1.52.2)

---
updated-dependencies:
- dependency-name: github.com/golangci/golangci-lint
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump github.com/golangci/golangci-lint from 1.52.2 to 1.53.2 in /tools (#665)

Bumps [github.com/golangci/golangci-lint](https://github.com/golangci/golangci-lint) from 1.52.2 to 1.53.2.
- [Release notes](https://github.com/golangci/golangci-lint/releases)
- [Changelog](https://github.com/golangci/golangci-lint/blob/master/CHANGELOG.md)
- [Commits](golangci/golangci-lint@v1.52.2...v1.53.2)

---
updated-dependencies:
- dependency-name: github.com/golangci/golangci-lint
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Remove `prometheus/client_golang` replace (#682)

* Remove prometheus/client_golang replace

* Apply formater

* Downgrade client_golang to previous version

* Upgrade exporter-toolkit to v0.10.0

* added multi target feature

* added connect timeout opts

* fixed connect

* formatted code

* fixed linter warnings

* Update README.md

* Update README.md

* updated license

* Update exporter/server.go

Co-authored-by: Nurlan Moldomurov <[email protected]>

* fixed according to review

* formatted the code

* minor changes according to the linters

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Marc Tudurí <[email protected]>
Co-authored-by: Jiří Čtvrtka <[email protected]>
Co-authored-by: Nurlan Moldomurov <[email protected]>
Co-authored-by: Nurlan Moldomurov <[email protected]>
  • Loading branch information
6 people authored Oct 10, 2023
1 parent 8597f3a commit e02fe4f
Show file tree
Hide file tree
Showing 10 changed files with 371 additions and 111 deletions.
15 changes: 12 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:[email protected]:27017/admin,mongodb://user2:[email protected]: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`
Expand Down
5 changes: 4 additions & 1 deletion exporter/base_collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
}

Expand Down
72 changes: 20 additions & 52 deletions exporter/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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
}
Expand All @@ -56,6 +51,7 @@ type Opts struct {
CollStatsLimit int
CompatibleMode bool
DirectConnect bool
ConnectTimeoutMS int
DisableDefaultRegistry bool
DiscoveringMode bool
GlobalConnPool bool
Expand All @@ -76,10 +72,8 @@ type Opts struct {

IndexStatsCollections []string
Logger *logrus.Logger
Path string
URI string
WebListenAddress string
TLSConfigPath string

URI string
}

var (
Expand All @@ -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.
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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,
Expand All @@ -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(`<html>
<head><title>MongoDB Exporter</title></head>
<body>
<h1>MongoDB Exporter</h1>
<p><a href='/metrics'>Metrics</a></p>
</body>
</html>`))
})

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)
Expand Down
80 changes: 68 additions & 12 deletions exporter/exporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand All @@ -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)
}
}
Loading

0 comments on commit e02fe4f

Please sign in to comment.