Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi target support #653

Merged
merged 38 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
152b71a
added multi target feature
adnull Feb 13, 2023
d3e5b24
Merge branch 'percona:main' into multi-target
adnull Mar 18, 2023
7342ce9
Merge branch 'percona:main' into multi-target
adnull May 3, 2023
da9afc3
added connect timeout opts
adnull May 7, 2023
4e861b6
added tests
adnull May 7, 2023
b7c58c3
fixed test
adnull May 7, 2023
0ac7892
Merge branch 'main' into multi-target
adnull May 8, 2023
c5d2b67
Merge branch 'main' into multi-target
adnull May 18, 2023
f7195cf
Merge branch 'main' into multi-target
adnull May 30, 2023
562f57c
Merge branch 'main' into multi-target
adnull Jun 12, 2023
76a8126
Merge branch 'main' into multi-target
adnull Jul 6, 2023
c8cadf1
fixed dockerfile
adnull May 7, 2023
70421c4
Bump github.com/golangci/golangci-lint from 1.47.3 to 1.52.2 in /tool…
dependabot[bot] May 17, 2023
2b5eb69
Bump github.com/golangci/golangci-lint from 1.52.2 to 1.53.2 in /tool…
dependabot[bot] Jun 12, 2023
83dd752
Merge branch 'main' into multi-target
adnull Jul 6, 2023
89d0a7e
Remove `prometheus/client_golang` replace (#682)
marctc Jul 6, 2023
4f1d85e
added multi target feature
adnull Feb 13, 2023
065a847
added connect timeout opts
adnull May 7, 2023
13571dc
Merge remote-tracking branch 'upstream/main' into multi-target
adnull Jul 6, 2023
4a01d30
fixed connect
adnull Jul 6, 2023
fa47514
Merge branch 'main' into multi-target
JiriCtvrtka Aug 3, 2023
3241789
formatted code
adnull Aug 3, 2023
a8d48e8
fixed linter warnings
adnull Aug 8, 2023
60cd695
Merge branch 'main' into multi-target
JiriCtvrtka Aug 16, 2023
6084992
Merge branch 'main' into multi-target
JiriCtvrtka Aug 17, 2023
91d627d
Merge branch 'main' into multi-target
adnull Aug 22, 2023
cc2e803
Merge branch 'main' into multi-target
adnull Sep 5, 2023
cda1aa2
Resolved conflicts with the main branch
adnull Sep 23, 2023
4dc883a
Update README.md
adnull Sep 24, 2023
b00d3cd
Update README.md
adnull Sep 24, 2023
44ec283
updated license
adnull Sep 27, 2023
6a62697
Merge branch 'main' into multi-target
adnull Oct 1, 2023
88f5c89
Update exporter/server.go
adnull Oct 8, 2023
c341b2a
fixed according to review
adnull Oct 8, 2023
9e1e0a0
Merge branch 'main' into multi-target
BupycHuk Oct 10, 2023
8dc058e
formatted the code
adnull Oct 10, 2023
9573d80
minor changes according to the linters
adnull Oct 10, 2023
df09248
Merge branch 'main' into multi-target
adnull Oct 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
78 changes: 26 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,27 @@ 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)
}

if clientOpts.ConnectTimeout == nil {
connectTimeout := time.Duration(opts.ConnectTimeoutMS) * time.Millisecond
clientOpts.SetConnectTimeout(connectTimeout)
clientOpts.SetServerSelectionTimeout(connectTimeout)
}
adnull marked this conversation as resolved.
Show resolved Hide resolved

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