diff --git a/README.md b/README.md index 244e14ae..dae27849 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Currently, these metric sources are implemented: - $indexStats - getDiagnosticData - replSetGetStatus +- replSetGetConfig - serverStatus ## Supported MongoDB versions diff --git a/exporter/exporter.go b/exporter/exporter.go index 52af296a..dfed8509 100644 --- a/exporter/exporter.go +++ b/exporter/exporter.go @@ -64,6 +64,7 @@ type Opts struct { EnableDBStatsFreeStorage bool EnableDiagnosticData bool EnableReplicasetStatus bool + EnableReplicasetConfig bool EnableCurrentopMetrics bool EnableTopMetrics bool EnableIndexStats bool @@ -163,6 +164,7 @@ func (e *Exporter) makeRegistry(ctx context.Context, client *mongo.Client, topol e.opts.EnableCollStats = true e.opts.EnableTopMetrics = true e.opts.EnableReplicasetStatus = true + e.opts.EnableReplicasetConfig = true e.opts.EnableIndexStats = true e.opts.EnableCurrentopMetrics = true e.opts.EnableProfile = true @@ -239,6 +241,12 @@ func (e *Exporter) makeRegistry(ctx context.Context, client *mongo.Client, topol registry.MustRegister(rsgsc) } + // replSetGetStatus is not supported through mongos. + if e.opts.EnableReplicasetConfig && nodeType != typeMongos && requestOpts.EnableReplicasetConfig { + rsgsc := newReplicationSetConfigCollector(ctx, client, e.opts.Logger, + e.opts.CompatibleMode, topologyInfo) + registry.MustRegister(rsgsc) + } if e.opts.EnableShards && nodeType == typeMongos && requestOpts.EnableShards { sc := newShardsCollector(ctx, client, e.opts.Logger, e.opts.CompatibleMode) registry.MustRegister(sc) @@ -374,6 +382,8 @@ func GetRequestOpts(filters []string, defaultOpts *Opts) Opts { requestOpts.EnableDiagnosticData = true case "replicasetstatus": requestOpts.EnableReplicasetStatus = true + case "replicasetconfig": + requestOpts.EnableReplicasetConfig = true case "dbstats": requestOpts.EnableDBStats = true case "topmetrics": diff --git a/exporter/metrics.go b/exporter/metrics.go index 68450952..33c25820 100644 --- a/exporter/metrics.go +++ b/exporter/metrics.go @@ -328,6 +328,9 @@ func processSlice(prefix, k string, v []interface{}, commonLabels map[string]str if state, ok := s["stateStr"].(string); ok { labels["member_state"] = state } + if host, ok := s["host"].(string); ok { + labels["member_idx"] = host + } metrics = append(metrics, makeMetrics(prefix+k, s, labels, compatibleMode)...) } diff --git a/exporter/replset_config_collector.go b/exporter/replset_config_collector.go new file mode 100644 index 00000000..6bb76f8f --- /dev/null +++ b/exporter/replset_config_collector.go @@ -0,0 +1,94 @@ +// 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 ( + "context" + + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + "github.com/sirupsen/logrus" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +type replSetGetConfigCollector struct { + ctx context.Context + base *baseCollector + + compatibleMode bool + topologyInfo labelsGetter +} + +// newReplicationSetConfigCollector creates a collector for configuration of replication set. +func newReplicationSetConfigCollector(ctx context.Context, client *mongo.Client, logger *logrus.Logger, compatible bool, topology labelsGetter) *replSetGetConfigCollector { + return &replSetGetConfigCollector{ + ctx: ctx, + base: newBaseCollector(client, logger.WithFields(logrus.Fields{"collector": "replset_config"})), + + compatibleMode: compatible, + topologyInfo: topology, + } +} + +func (d *replSetGetConfigCollector) Describe(ch chan<- *prometheus.Desc) { + d.base.Describe(d.ctx, ch, d.collect) +} + +func (d *replSetGetConfigCollector) Collect(ch chan<- prometheus.Metric) { + d.base.Collect(ch) +} + +func (d *replSetGetConfigCollector) collect(ch chan<- prometheus.Metric) { + defer measureCollectTime(ch, "mongodb", "replset_config")() + + logger := d.base.logger + client := d.base.client + + cmd := bson.D{{Key: "replSetGetConfig", Value: "1"}} + res := client.Database("admin").RunCommand(d.ctx, cmd) + + var m bson.M + + if err := res.Decode(&m); err != nil { + if e, ok := err.(mongo.CommandError); ok { //nolint // https://github.com/percona/mongodb_exporter/pull/295#issuecomment-922874632 + if e.Code == replicationNotYetInitialized || e.Code == replicationNotEnabled { + return + } + } + logger.Errorf("cannot get replSetGetConfig: %s", err) + + return + } + + config, ok := m["config"].(bson.M) + if !ok { + err := errors.Wrapf(errUnexpectedDataType, "%T for data field", m["config"]) + logger.Errorf("cannot decode getDiagnosticData: %s", err) + + return + } + m = config + + logger.Debug("replSetGetConfig result:") + debugResult(logger, m) + + for _, metric := range makeMetrics("rs_cfg", m, d.topologyInfo.baseLabels(), d.compatibleMode) { + ch <- metric + } +} + +var _ prometheus.Collector = (*replSetGetConfigCollector)(nil) diff --git a/exporter/replset_config_collector_test.go b/exporter/replset_config_collector_test.go new file mode 100644 index 00000000..5fe456f2 --- /dev/null +++ b/exporter/replset_config_collector_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 ( + "context" + "strings" + "testing" + "time" + + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + + "github.com/percona/mongodb_exporter/internal/tu" +) + +func TestReplsetConfigCollector(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + client := tu.DefaultTestClient(ctx, t) + + ti := labelsGetterMock{} + + c := newReplicationSetConfigCollector(ctx, client, logrus.New(), false, ti) + + // The last \n at the end of this string is important + expected := strings.NewReader(` + # HELP mongodb_rs_cfg_protocolVersion rs_cfg. + # TYPE mongodb_rs_cfg_protocolVersion untyped + mongodb_rs_cfg_protocolVersion 1` + "\n") + // Filter metrics for 2 reasons: + // 1. The result is huge + // 2. We need to check against know values. Don't use metrics that return counters like uptime + // or counters like the number of transactions because they won't return a known value to compare + filter := []string{ + "mongodb_rs_cfg_protocolVersion", + } + err := testutil.CollectAndCompare(c, expected, filter...) + assert.NoError(t, err) +} + +func TestReplsetConfigCollectorNoSharding(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + client := tu.TestClient(ctx, tu.MongoDBStandAlonePort, t) + + ti := labelsGetterMock{} + + c := newReplicationSetConfigCollector(ctx, client, logrus.New(), false, ti) + + // Replication set metrics should not be generated for unsharded server + count := testutil.CollectAndCount(c) + + metaMetricCount := 1 + assert.Equal(t, metaMetricCount, count, "Mismatch in metric count for collector run on unsharded server") +} diff --git a/main.go b/main.go index ec2c64d5..937b610a 100644 --- a/main.go +++ b/main.go @@ -54,6 +54,7 @@ type GlobalFlags struct { EnableExporterMetrics bool `name:"collector.exporter-metrics" help:"Enable collecting metrics about the exporter itself (process_*, go_*)" negatable:"" default:"True"` EnableDiagnosticData bool `name:"collector.diagnosticdata" help:"Enable collecting metrics from getDiagnosticData"` EnableReplicasetStatus bool `name:"collector.replicasetstatus" help:"Enable collecting metrics from replSetGetStatus"` + EnableReplicasetConfig bool `name:"collector.replicasetconfig" help:"Enable collecting metrics from replSetGetConfig"` EnableDBStats bool `name:"collector.dbstats" help:"Enable collecting metrics from dbStats"` EnableDBStatsFreeStorage bool `name:"collector.dbstatsfreestorage" help:"Enable collecting free space metrics from dbStats"` EnableTopMetrics bool `name:"collector.topmetrics" help:"Enable collecting metrics from top admin command"` @@ -166,6 +167,7 @@ func buildExporter(opts GlobalFlags, uri string, log *logrus.Logger) *exporter.E DisableDefaultRegistry: !opts.EnableExporterMetrics, EnableDiagnosticData: opts.EnableDiagnosticData, EnableReplicasetStatus: opts.EnableReplicasetStatus, + EnableReplicasetConfig: opts.EnableReplicasetConfig, EnableCurrentopMetrics: opts.EnableCurrentopMetrics, EnableTopMetrics: opts.EnableTopMetrics, EnableDBStats: opts.EnableDBStats, diff --git a/main_test.go b/main_test.go index 0ab66bb4..49d077c9 100644 --- a/main_test.go +++ b/main_test.go @@ -112,6 +112,7 @@ func TestBuildExporter(t *testing.T) { EnableDiagnosticData: true, EnableReplicasetStatus: true, + EnableReplicasetConfig: true, CompatibleMode: true, }