diff --git a/Makefile b/Makefile index 23e6c5d7..a2eed500 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ REPO ?= percona/$(NAME) GORELEASER_FLAGS ?= UID ?= $(shell id -u) -export TEST_MONGODB_IMAGE?=mongo:4.2 +export TEST_MONGODB_IMAGE?=mongo:4.4 export TEST_MONGODB_ADMIN_USERNAME?= export TEST_MONGODB_ADMIN_PASSWORD?= export TEST_MONGODB_USERNAME?= diff --git a/docker-compose.yml b/docker-compose.yml index eb937ab1..405723a4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: mongo-1-1: container_name: "mongo-1-1" - image: ${TEST_MONGODB_IMAGE:-mongo:4.2} + image: ${TEST_MONGODB_IMAGE:-mongo:4.4} ports: - "${TEST_MONGODB_S1_PRIMARY_PORT:-17001}:27017" command: mongod --replSet rs1 --shardsvr --port 27017 --oplogSize 16 --bind_ip 0.0.0.0 @@ -14,7 +14,7 @@ services: mongo-1-2: container_name: "mongo-1-2" - image: ${TEST_MONGODB_IMAGE:-mongo:4.2} + image: ${TEST_MONGODB_IMAGE:-mongo:4.4} ports: - "${TEST_MONGODB_S1_SECONDARY1_PORT:-17002}:27017" command: mongod --replSet rs1 --shardsvr --port 27017 --oplogSize 16 --bind_ip 0.0.0.0 @@ -23,7 +23,7 @@ services: mongo-1-3: container_name: "mongo-1-3" - image: ${TEST_MONGODB_IMAGE:-mongo:4.2} + image: ${TEST_MONGODB_IMAGE:-mongo:4.4} ports: - "${TEST_MONGODB_S1_SECONDARY2_PORT:-17003}:27017" command: mongod --replSet rs1 --shardsvr --port 27017 --oplogSize 16 --bind_ip 0.0.0.0 @@ -32,7 +32,7 @@ services: mongo-1-arbiter: container_name: "mongo-1-arbiter" - image: ${TEST_MONGODB_IMAGE:-mongo:4.2} + image: ${TEST_MONGODB_IMAGE:-mongo:4.4} ports: - "${TEST_MONGODB_S1_ARBITER:-17011}:27017" command: mongod --replSet rs1 --shardsvr --port 27017 --oplogSize 16 @@ -41,7 +41,7 @@ services: mongo-rs1-setup: container_name: "mongo-rs1-setup" - image: ${TEST_MONGODB_IMAGE:-mongo:4.2} + image: ${TEST_MONGODB_IMAGE:-mongo:4.4} depends_on: - "mongo-1-1" - "mongo-1-2" @@ -150,7 +150,7 @@ services: mongo-rs2-setup: container_name: "mongo-rs2-setup" - image: ${TEST_MONGODB_IMAGE:-mongo:4.2} + image: ${TEST_MONGODB_IMAGE:-mongo:4.4} depends_on: - "mongo-2-1" - "mongo-2-2" @@ -174,7 +174,7 @@ services: # Config servers mongo-cnf-2: container_name: "mongo-cnf-2" - image: ${TEST_MONGODB_IMAGE:-mongo:4.2} + image: ${TEST_MONGODB_IMAGE:-mongo:4.4} ports: - "${TEST_MONGODB_CONFIGSVR1_PORT:-17007}:27017" command: mongod --dbpath /data/db --replSet cnf-serv --configsvr --port 27017 --oplogSize 16 @@ -183,7 +183,7 @@ services: mongo-cnf-3: container_name: "mongo-cnf-3" - image: ${TEST_MONGODB_IMAGE:-mongo:4.2} + image: ${TEST_MONGODB_IMAGE:-mongo:4.4} ports: - "${TEST_MONGODB_CONFIGSVR2_PORT:-17008}:27017" command: mongod --dbpath /data/db --replSet cnf-serv --configsvr --port 27017 --oplogSize 16 @@ -192,7 +192,7 @@ services: mongo-cnf-1: container_name: "mongo-cnf-1" - image: ${TEST_MONGODB_IMAGE:-mongo:4.2} + image: ${TEST_MONGODB_IMAGE:-mongo:4.4} ports: - "${TEST_MONGODB_CONFIGSVR3_PORT:-17009}:27017" command: mongod --dbpath /data/db --replSet cnf-serv --configsvr --port 27017 --oplogSize 16 @@ -204,7 +204,7 @@ services: mongo-cnf-setup: container_name: "mongo-cnf-setup" - image: ${TEST_MONGODB_IMAGE:-mongo:4.2} + image: ${TEST_MONGODB_IMAGE:-mongo:4.4} depends_on: - "mongo-cnf-1" - "mongo-cnf-2" @@ -224,7 +224,7 @@ services: mongos: container_name: "mongos" - image: ${TEST_MONGODB_IMAGE:-mongo:4.2} + image: ${TEST_MONGODB_IMAGE:-mongo:4.4} ports: - "${TEST_MONGODB_MONGOS_PORT:-17000}:27017" networks: @@ -240,7 +240,7 @@ services: mongo-shard-setup: container_name: "mongo-shard-setup" - image: ${TEST_MONGODB_IMAGE:-mongo:4.2} + image: ${TEST_MONGODB_IMAGE:-mongo:4.4} depends_on: - "mongos" networks: @@ -270,7 +270,7 @@ services: standalone: container_name: "standalone" - image: ${TEST_MONGODB_IMAGE:-mongo:4.2} + image: ${TEST_MONGODB_IMAGE:-mongo:4.4} ports: - "${TEST_MONGODB_STANDALONE_PORT:-27017}:27017" command: mongod --port 27017 --oplogSize 16 diff --git a/exporter/exporter.go b/exporter/exporter.go index e554cfb6..5f991d28 100644 --- a/exporter/exporter.go +++ b/exporter/exporter.go @@ -70,6 +70,7 @@ type Opts struct { EnableCollStats bool EnableProfile bool EnableShards bool + EnableFCV bool // Feature Compatibility Version. EnableOverrideDescendingIndex bool @@ -164,6 +165,7 @@ func (e *Exporter) makeRegistry(ctx context.Context, client *mongo.Client, topol e.opts.EnableCurrentopMetrics = true e.opts.EnableProfile = true e.opts.EnableShards = true + e.opts.EnableFCV = true e.opts.EnablePBMMetrics = true } @@ -178,6 +180,7 @@ func (e *Exporter) makeRegistry(ctx context.Context, client *mongo.Client, topol e.opts.EnableCurrentopMetrics = false e.opts.EnableProfile = false e.opts.EnableShards = false + e.opts.EnableFCV = false e.opts.EnablePBMMetrics = false } @@ -239,6 +242,11 @@ func (e *Exporter) makeRegistry(ctx context.Context, client *mongo.Client, topol registry.MustRegister(sc) } + if e.opts.EnableFCV && nodeType != typeMongos { + fcvc := newFeatureCompatibilityCollector(ctx, client, e.opts.Logger) + registry.MustRegister(fcvc) + } + if e.opts.EnablePBMMetrics && requestOpts.EnablePBMMetrics { pbmc := newPbmCollector(ctx, client, e.opts.URI, e.opts.Logger) registry.MustRegister(pbmc) @@ -320,6 +328,8 @@ func (e *Exporter) Handler() http.Handler { requestOpts.EnableProfile = true case "shards": requestOpts.EnableShards = true + case "fcv": + requestOpts.EnableFCV = true case "pbm": requestOpts.EnablePBMMetrics = true } diff --git a/exporter/exporter_test.go b/exporter/exporter_test.go index 4caad72b..8f59c48e 100644 --- a/exporter/exporter_test.go +++ b/exporter/exporter_test.go @@ -199,34 +199,11 @@ func TestMongoS(t *testing.T) { } } -func TestMongoUp(t *testing.T) { - ctx := context.Background() - - exporterOpts := &Opts{ - Logger: logrus.New(), - 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) - nodeType, _ := getNodeType(ctx, client) - gc := newGeneralCollector(ctx, client, nodeType, e.opts.Logger) - - r := e.makeRegistry(ctx, client, new(labelsGetterMock), *e.opts) - - res := r.Unregister(gc) - assert.Equal(t, true, res) -} - func TestMongoUpMetric(t *testing.T) { ctx := context.Background() type testcase struct { + name string URI string clusterRole string Want int @@ -242,39 +219,41 @@ func TestMongoUpMetric(t *testing.T) { } for _, tc := range testCases { - exporterOpts := &Opts{ - Logger: logrus.New(), - URI: tc.URI, - ConnectTimeoutMS: 200, - DirectConnect: true, - GlobalConnPool: false, - CollectAll: true, - } + t.Run(tc.clusterRole+"/"+tc.URI, func(t *testing.T) { + 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) - } + 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) - nodeType, _ := getNodeType(ctx, client) - gc := newGeneralCollector(ctx, client, nodeType, e.opts.Logger) - r := e.makeRegistry(ctx, client, new(labelsGetterMock), *e.opts) + e := New(exporterOpts) + nodeType, _ := getNodeType(ctx, client) + gc := newGeneralCollector(ctx, client, nodeType, e.opts.Logger) + r := e.makeRegistry(ctx, client, new(labelsGetterMock), *e.opts) - expected := strings.NewReader(fmt.Sprintf(` + expected := strings.NewReader(fmt.Sprintf(` # HELP mongodb_up Whether MongoDB is up. # TYPE mongodb_up gauge mongodb_up {cluster_role="%s"} %s`, tc.clusterRole, 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) + 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) + res := r.Unregister(gc) + assert.Equal(t, true, res) + }) } } diff --git a/exporter/feature_compatibility_version_collector.go b/exporter/feature_compatibility_version_collector.go new file mode 100644 index 00000000..96d996f5 --- /dev/null +++ b/exporter/feature_compatibility_version_collector.go @@ -0,0 +1,80 @@ +// 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" + "fmt" + "strconv" + + "github.com/prometheus/client_golang/prometheus" + "github.com/sirupsen/logrus" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +type featureCompatibilityCollector struct { + ctx context.Context + base *baseCollector +} + +// newProfileCollector creates a collector for being processed queries. +func newFeatureCompatibilityCollector(ctx context.Context, client *mongo.Client, logger *logrus.Logger) *featureCompatibilityCollector { + return &featureCompatibilityCollector{ + ctx: ctx, + base: newBaseCollector(client, logger.WithFields(logrus.Fields{"collector": "featureCompatibility"})), + } +} + +func (d *featureCompatibilityCollector) Describe(ch chan<- *prometheus.Desc) { + d.base.Describe(d.ctx, ch, d.collect) +} + +func (d *featureCompatibilityCollector) Collect(ch chan<- prometheus.Metric) { + d.base.Collect(ch) +} + +func (d *featureCompatibilityCollector) collect(ch chan<- prometheus.Metric) { + defer measureCollectTime(ch, "mongodb", "profile")() + + cmd := bson.D{{Key: "getParameter", Value: 1}, {Key: "featureCompatibilityVersion", Value: 1}} + client := d.base.client + if client == nil { + return + } + res := client.Database("admin").RunCommand(d.ctx, cmd) + + m := make(map[string]interface{}) + if err := res.Decode(&m); err != nil { + d.base.logger.Errorf("Failed to decode featureCompatibilityVersion: %v", err) + ch <- prometheus.NewInvalidMetric(prometheus.NewInvalidDesc(err), err) + return + } + + rawValue := walkTo(m, []string{"featureCompatibilityVersion", "version"}) + if rawValue != nil { + versionString := fmt.Sprintf("%v", rawValue) + version, err := strconv.ParseFloat(versionString, 64) + if err != nil { + d.base.logger.Errorf("Failed to parse featureCompatibilityVersion: %v", err) + ch <- prometheus.NewInvalidMetric(prometheus.NewInvalidDesc(err), err) + return + } + + d := prometheus.NewDesc("mongodb_fcv_feature_compatibility_version", "Feature compatibility version", []string{"version"}, map[string]string{}) + ch <- prometheus.MustNewConstMetric(d, prometheus.GaugeValue, version, versionString) + } +} diff --git a/exporter/feature_compatibility_version_collector_test.go b/exporter/feature_compatibility_version_collector_test.go new file mode 100644 index 00000000..5196a888 --- /dev/null +++ b/exporter/feature_compatibility_version_collector_test.go @@ -0,0 +1,89 @@ +// 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" + "fmt" + "strings" + "testing" + "time" + + "github.com/hashicorp/go-version" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/percona/mongodb_exporter/internal/tu" +) + +func TestFCVCollector(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + client := tu.DefaultTestClient(ctx, t) + + database := client.Database("testdb") + database.Drop(ctx) //nolint:errcheck + defer database.Drop(ctx) //nolint:errcheck + + c := newFeatureCompatibilityCollector(ctx, client, logrus.New()) + + sversion, _ := getMongoDBVersionInfo(t, "mongo-1-1") + + v, err := version.NewVersion(sversion) + require.NoError(t, err) + var mversion string + + mmv := fmt.Sprintf("%d.%d", v.Segments()[0], v.Segments()[1]) + switch { + case mmv == "5.0": + mversion = "4.4" + case mmv == "4.4": + mversion = "4.2" + } + + // The last \n at the end of this string is important + expected := strings.NewReader(` +# HELP mongodb_fcv_feature_compatibility_version Feature compatibility version +# TYPE mongodb_fcv_feature_compatibility_version gauge +mongodb_fcv_feature_compatibility_version{version="` + mversion + `"} ` + mversion + + "\n") + + filter := []string{ + "mongodb_fcv_feature_compatibility_version", + } + err = testutil.CollectAndCompare(c, expected, filter...) + assert.NoError(t, err) + + expected = strings.NewReader(` +# HELP mongodb_fcv_feature_compatibility_version Feature compatibility version +# TYPE mongodb_fcv_feature_compatibility_version gauge +mongodb_fcv_feature_compatibility_version{version="` + mversion + `"} ` + mversion + + "\n") + err = testutil.CollectAndCompare(c, expected, filter...) + assert.NoError(t, err) + + expected = strings.NewReader(` +# HELP mongodb_fcv_feature_compatibility_version Feature compatibility version +# TYPE mongodb_fcv_feature_compatibility_version gauge +mongodb_fcv_feature_compatibility_version{version="` + mversion + `"} ` + mversion + + "\n") + + err = testutil.CollectAndCompare(c, expected, filter...) + assert.NoError(t, err) +} diff --git a/go.mod b/go.mod index e344fab8..dfd7423b 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,8 @@ require ( go.mongodb.org/mongo-driver v1.16.1 ) +require github.com/hashicorp/go-version v1.7.0 + require github.com/percona/percona-backup-mongodb v1.8.1-0.20240814130653-5285f7975ff6 require ( diff --git a/go.sum b/go.sum index 27ee6933..0cd1bdc1 100644 --- a/go.sum +++ b/go.sum @@ -46,6 +46,8 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= diff --git a/main.go b/main.go index 0067d06c..ffaf6000 100644 --- a/main.go +++ b/main.go @@ -59,6 +59,7 @@ type GlobalFlags struct { EnableIndexStats bool `name:"collector.indexstats" help:"Enable collecting metrics from $indexStats"` EnableCollStats bool `name:"collector.collstats" help:"Enable collecting metrics from $collStats"` EnableProfile bool `name:"collector.profile" help:"Enable collecting metrics from profile"` + EnableFCV bool `name:"collector.fcv" help:"Enable Feature Compatibility Version collector"` EnableShards bool `help:"Enable collecting metrics from sharded Mongo clusters about chunks" name:"collector.shards"` EnablePBM bool `help:"Enable collecting metrics from Percona Backup for MongoDB" name:"collector.pbm"` @@ -160,6 +161,7 @@ func buildExporter(opts GlobalFlags, uri string, log *logrus.Logger) *exporter.E EnableCollStats: opts.EnableCollStats, EnableProfile: opts.EnableProfile, EnableShards: opts.EnableShards, + EnableFCV: opts.EnableFCV, EnablePBMMetrics: opts.EnablePBM, EnableOverrideDescendingIndex: opts.EnableOverrideDescendingIndex,