├── VERSION ├── .github ├── CODEOWNERS ├── dependabot.yml ├── workflows │ ├── stale.yml │ ├── lint.yml │ ├── build.yml │ ├── scorecard.yml │ ├── release.yml │ ├── test.yml │ └── version-check.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── pull_request_template.md └── check-license.go ├── test-setup ├── secret │ ├── mongodb_secrets.txt │ └── keyfile ├── pbm │ └── config │ │ └── pbm.yaml ├── kerberos.dockerfile ├── scripts │ ├── init-psmdb-kerberos.sh │ ├── run-mongodb-encrypted.sh │ ├── init-pbm.sh │ ├── setup-krb5-server.sh │ ├── setup-krb5-mongo.sh │ ├── setup-shard.sh │ └── setup.sh └── mongodb-auth.dockerfile ├── .gitignore ├── .scripts ├── default │ └── mongodb_exporter.example ├── postinst └── systemd │ └── mongodb_exporter.service ├── Dockerfile ├── .golangci-required.yml ├── internal ├── proto │ └── proto.go ├── tu │ └── docker_inspect_test.go └── util │ └── util.go ├── tools └── tools.go ├── exporter ├── debug.go ├── exporter_metrics.go ├── debug_test.go ├── dsn_fix │ ├── dsn_fix.go │ └── dsn_fix_test.go ├── testdata │ └── locks.json ├── http_error_logger.go ├── seedlist_test.go ├── gatherer_wrapper.go ├── utils_test.go ├── shards_collector_test.go ├── profile_status_collector_test.go ├── pbm_collector_test.go ├── base_collector.go ├── encryption_info_test.go ├── dbstats_collector_test.go ├── replset_config_collector_test.go ├── replset_status_collector_test.go ├── currentop_collector_test.go ├── seedlist.go ├── replset_status_collector.go ├── general_collector.go ├── feature_compatibility_version_collector.go ├── replset_config_collector.go ├── top_collector_test.go ├── profile_status_collector.go ├── feature_compatibility_version_collector_test.go ├── dbstats_collector.go ├── general_collector_test.go ├── topology_info_test.go ├── multi_target_test.go ├── collstats_collector.go ├── indexstats_collector.go ├── currentop_collector.go ├── secondary_lag_test.go ├── top_collector.go ├── server.go ├── topology_info.go ├── collstats_collector_test.go ├── metrics_test.go ├── indexstats_collector_test.go └── common_test.go ├── .golangci.yml ├── .goreleaser.yml ├── CHANGELOG ├── Makefile ├── CONTRIBUTING.md └── docs └── development-guide.md /VERSION: -------------------------------------------------------------------------------- 1 | v0.47.1 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @percona/pmm-review-exporters 2 | -------------------------------------------------------------------------------- /test-setup/secret/mongodb_secrets.txt: -------------------------------------------------------------------------------- 1 | zN94kcTrN2CC/X1L9a54/VebKctZnJ/QIO3JdzUWKdY= 2 | -------------------------------------------------------------------------------- /test-setup/pbm/config/pbm.yaml: -------------------------------------------------------------------------------- 1 | storage: 2 | type: filesystem 3 | filesystem: 4 | path: /opt/backups/pbm 5 | -------------------------------------------------------------------------------- /test-setup/kerberos.dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | RUN apk add --no-cache bash krb5 krb5-server krb5-pkinit 3 | EXPOSE 88/udp 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dist 2 | .env 3 | 4 | .vscode/ 5 | .idea 6 | 7 | bin 8 | build 9 | dist 10 | 11 | cover.out 12 | mongodb_exporter 13 | .DS_Store 14 | 15 | -------------------------------------------------------------------------------- /test-setup/scripts/init-psmdb-kerberos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker exec --user root psmdb-kerberos bash -c 'chown mongodb:root /krb5/mongodb.keytab' 4 | docker exec psmdb-kerberos bash -c '/scripts/setup-krb5-mongo.sh' 5 | -------------------------------------------------------------------------------- /.scripts/default/mongodb_exporter.example: -------------------------------------------------------------------------------- 1 | # Full list of options you can see https://github.com/percona/mongodb_exporter 2 | 3 | OPTIONS="--mongodb.uri=mongodb://user:pass@127.0.0.1:27017/admin?ssl=true \ 4 | --web.listen-address=:9216" 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine AS builder 2 | RUN apk add --no-cache ca-certificates 3 | 4 | FROM scratch AS final 5 | USER 65535:65535 6 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 7 | COPY ./mongodb_exporter / 8 | EXPOSE 9216 9 | ENTRYPOINT ["/mongodb_exporter"] -------------------------------------------------------------------------------- /test-setup/scripts/run-mongodb-encrypted.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # set proper permissions for secret file, otherwise mongodb won't start 4 | chmod 600 /secret/mongodb_secrets.txt 5 | mongod --port 27017 --oplogSize 16 --bind_ip_all --enableEncryption --encryptionKeyFile /secret/mongodb_secrets.txt 6 | -------------------------------------------------------------------------------- /.scripts/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | echo "Creating user and group..." 6 | 7 | useradd --system \ 8 | --no-create-home \ 9 | --shell /sbin/nologin \ 10 | --comment "MongoDB Exporter" \ 11 | mongodb_exporter 12 | 13 | systemctl daemon-reload > dev/null || exit $? 14 | 15 | exit 0 16 | -------------------------------------------------------------------------------- /test-setup/mongodb-auth.dockerfile: -------------------------------------------------------------------------------- 1 | ARG TEST_MONGODB_IMAGE=mongo:6.0 2 | FROM ${TEST_MONGODB_IMAGE} 3 | USER root 4 | COPY test-setup/secret/keyfile /opt/keyfile 5 | RUN chown mongodb /opt/keyfile && chmod 400 /opt/keyfile && mkdir -p /home/mongodb/ && chown mongodb /home/mongodb 6 | RUN mkdir /opt/backups && touch /opt/backups/.gitkeep && chown mongodb /opt/backups 7 | USER mongodb 8 | -------------------------------------------------------------------------------- /.scripts/systemd/mongodb_exporter.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Prometheus MongoDB Exporter 3 | Documentation=https://github.com/percona/mongodb_exporter 4 | After=network.target 5 | 6 | [Service] 7 | Type=simple 8 | 9 | User=mongodb_exporter 10 | Group=nogroup 11 | 12 | EnvironmentFile=-/etc/default/mongodb_exporter 13 | ExecStart=/usr/bin/mongodb_exporter $OPTIONS 14 | SyslogIdentifier=mongodb_exporter 15 | 16 | Restart=always 17 | 18 | [Install] 19 | WantedBy=multi-user.target 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "gomod" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | - package-ecosystem: "gomod" 9 | directory: "/tools" 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "docker" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | - package-ecosystem: "github-actions" 17 | directory: "/" 18 | schedule: 19 | interval: "weekly" 20 | -------------------------------------------------------------------------------- /test-setup/scripts/init-pbm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker exec -it --user root pbm-mongo-2-1 bash -c "chown -R mongodb /opt/backups" 3 | 4 | # PBM config fails if replica sets are not completely up, so give enough time for both replica sets and pbm agents to be up. 5 | sleep 20 6 | 7 | docker exec pbm-mongo-2-1 bash -c "pbm config --file /etc/config/pbm.yaml" 8 | 9 | # Wait until agents are restarted after config has been updated 10 | sleep 5 11 | 12 | docker exec pbm-mongo-2-1 bash -c "pbm backup" 13 | 14 | # Wait for backup to complete 15 | sleep 3 16 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * MON-FRI' # 1:30 AM every weekday 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v10 11 | with: 12 | stale-issue-message: 'This issue has been marked as stale because it has been open for 120 days without activity. Please remove the stale label or add a comment; otherwise, it will be closed in 7 days.' 13 | days-before-stale: 120 14 | days-before-close: 7 15 | exempt-issue-labels: bug 16 | -------------------------------------------------------------------------------- /.golangci-required.yml: -------------------------------------------------------------------------------- 1 | --- 2 | linters-settings: 3 | # prevent import of "errors" instead of "github.com/pkg/errors" 4 | depguard: 5 | rules: 6 | # Name of a rule. 7 | main: 8 | # Packages that are not allowed where the value is a suggestion. 9 | deny: 10 | - pkg: "errors" 11 | desc: Should be replaced by github.com/pkg/errors package 12 | 13 | # The most valuable linters; they are required to pass for PR to be merged. 14 | linters: 15 | disable-all: true 16 | enable: 17 | - depguard 18 | - goimports 19 | - ineffassign 20 | - govet 21 | - staticcheck 22 | 23 | issues: 24 | exclude-use-default: false 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: ["community", "enhancement", "triage"] 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | If the improvement is related to PMM, please open [PMM project](https://jira.percona.com/projects/PMM/issues) feature instead of GH feature request. 13 | 14 | **Describe the solution you'd like** 15 | A clear and concise description of what you expect to happen. 16 | 17 | **Describe alternatives you've considered** 18 | A clear and concise description of any alternative solutions or features you've considered. 19 | 20 | **Additional context** 21 | Add any other context or screenshots related to the feature request here. 22 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | [PMM-XXXX](https://jira.percona.com/browse/PMM-XXXX) (optional, if ticket reported) 2 | 3 | - [ ] Links to related pull requests (optional). 4 | 5 | --- 6 | 7 | Below we provide a basic checklist of things that would make it a good PR: 8 | - Make sure to sign the CLA (Contributor License Agreement). 9 | - Make sure all tests pass. 10 | - Keep current with the target branch and fix conflicts if necessary. 11 | - Update jira ticket description if necessary. 12 | - Attach screenshots and/or console output to the jira ticket to confirm new behavior, if applicable. 13 | - Leave notes to the reviewers if you need to focus their attention on something specific. 14 | 15 | Once all checks pass and the code is ready for review, please add `pmm-review-exporters` team as the reviewer. That would assign people from the review team automatically. Report any issues on our [Forum](https://forums.percona.com). 16 | -------------------------------------------------------------------------------- /internal/proto/proto.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2023 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package proto 17 | 18 | type MasterDoc struct { 19 | SetName interface{} `bson:"setName"` 20 | Hosts interface{} `bson:"hosts"` 21 | Msg string `bson:"msg"` 22 | ArbiterOnly bool `bson:"arbiterOnly"` 23 | } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve the exporter 4 | title: '' 5 | labels: ["community", "bug", "triage"] 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | If the issue relates to PMM, please open [JIRA PMM](https://jira.percona.com/projects/PMM/issues) issue instead of GH issue. 13 | 14 | **To Reproduce** 15 | Steps to reproduce the behavior: 16 | 1. what parameters are being passed to `mongodb_exporter` 17 | 2. describe steps to reproduce the issue 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Logs** 23 | Please provide logs relevant to the issue 24 | 25 | **Environment** 26 | - OS, 27 | - environment (docker, k8s, etc) 28 | - MongoDB version 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /test-setup/scripts/setup-krb5-server.sh: -------------------------------------------------------------------------------- 1 | #! /env/sh 2 | 3 | mongohost=`getent hosts ${MONGO_HOST} | awk '{ print $1 }'` 4 | kerberos_host=`getent hosts ${KERBEROS_HOST} | awk '{ print $1 }'` 5 | 6 | cat > /krb5/krb5.conf </dev/null; do 10 | if [ $attempts -eq $max_attempts ]; then 11 | echo "Failed to check MongoDB status after $max_attempts attempts" 12 | exit 1 13 | fi 14 | printf '.' 15 | sleep 1 16 | attempts=$((attempts+1)) 17 | done 18 | 19 | echo "Started.." 20 | 21 | # create role with anyAction on all resources (needed to allow exporter run execute commands) 22 | # create mongodb user using the same username as the kerberos principal 23 | mongosh --host 127.0.0.1:27017 -u "$username" -p "$password" --eval 'db.getSiblingDB("admin").createRole({role: "anyAction", privileges: [{ resource: { anyResource: true }, actions: [ "anyAction" ] }], roles: [] });' 24 | mongosh --host 127.0.0.1:27017 -u "$username" -p "$password" --eval 'db.getSiblingDB("$external").createUser({user: "pmm-test@PERCONATEST.COM", roles: [{role: "anyAction", db: "admin"}]});' 25 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2022 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | //go:build tools 17 | // +build tools 18 | 19 | package tools 20 | 21 | import ( 22 | _ "github.com/daixiang0/gci" 23 | _ "github.com/golangci/golangci-lint/cmd/golangci-lint" 24 | _ "github.com/reviewdog/reviewdog/cmd/reviewdog" 25 | _ "mvdan.cc/gofumpt" 26 | ) 27 | 28 | // tools 29 | //go:generate go build -o ../bin/gci github.com/daixiang0/gci 30 | //go:generate go build -o ../bin/gofumpt mvdan.cc/gofumpt 31 | //go:generate go build -o ../bin/golangci-lint github.com/golangci/golangci-lint/cmd/golangci-lint 32 | //go:generate go build -o ../bin/reviewdog github.com/reviewdog/reviewdog/cmd/reviewdog 33 | -------------------------------------------------------------------------------- /internal/tu/docker_inspect_test.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2022 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package tu 17 | 18 | import ( 19 | "testing" 20 | 21 | "github.com/stretchr/testify/assert" 22 | ) 23 | 24 | func TestInspectContainer(t *testing.T) { 25 | tests := []struct { 26 | containerName string 27 | wantPort string 28 | }{ 29 | { 30 | containerName: "mongos", 31 | wantPort: "17000", 32 | }, 33 | { 34 | containerName: "standalone", 35 | wantPort: "27017", 36 | }, 37 | } 38 | 39 | for _, tc := range tests { 40 | di, err := InspectContainer(tc.containerName) 41 | assert.NoError(t, err) 42 | 43 | ns := di[0].NetworkSettings.Ports["27017/tcp"][0].HostPort 44 | assert.Equal(t, ns, tc.wantPort) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /exporter/debug.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2022 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package exporter 17 | 18 | import ( 19 | "context" 20 | "encoding/json" 21 | "fmt" 22 | "log/slog" 23 | "os" 24 | ) 25 | 26 | func debugResult(log *slog.Logger, m interface{}) { 27 | if !log.Enabled(context.TODO(), slog.LevelDebug) { 28 | return 29 | } 30 | 31 | debugStr, err := json.MarshalIndent(m, "", " ") 32 | if err != nil { 33 | log.Error("cannot marshal struct for debug", "error", err) 34 | return 35 | } 36 | 37 | // don't use the passed-in logger because: 38 | // 1. It will escape new lines and " making it harder to read and to use 39 | // 2. It will add timestamp 40 | // 3. This way is easier to copy/paste to put the info in a ticket 41 | fmt.Fprintln(os.Stderr, string(debugStr)) 42 | } 43 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | linters-settings: 3 | # prevent import of "errors" instead of "github.com/pkg/errors" 4 | depguard: 5 | rules: 6 | # Name of a rule. 7 | main: 8 | # Packages that are not allowed where the value is a suggestion. 9 | deny: 10 | - pkg: "errors" 11 | desc: Should be replaced by github.com/pkg/errors package 12 | 13 | lll: 14 | line-length: 140 15 | tab-width: 4 16 | 17 | unused: 18 | check-exported: false 19 | 20 | unparam: 21 | check-exported: true 22 | 23 | goimports: 24 | # put imports beginning with prefix after 3rd-party packages; 25 | # it's a comma-separated list of prefixes 26 | local-prefixes: github.com/percona/mongodb_exporter 27 | 28 | linters: 29 | enable-all: true 30 | disable: 31 | - lll 32 | - unused 33 | - testpackage 34 | - wsl 35 | - exhaustruct 36 | - varnamelen 37 | - gochecknoglobals # we need globals for better performance, we use them for mapping, and we don't mutate them, so it's safe. 38 | - maligned #deprecated 39 | - scopelint #deprecated 40 | - golint #deprecated 41 | - interfacer #deprecated 42 | issues: 43 | exclude-use-default: false 44 | exclude: 45 | # gas: Duplicated errcheck checks 46 | - 'G104: Errors unhandled' 47 | exclude-rules: 48 | - path: _test\.go 49 | linters: 50 | - unused 51 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | schedule: 5 | # run every Sunday 6 | - cron: '0 13 * * 0' 7 | push: 8 | branches: 9 | - main 10 | - release-0.1x 11 | tags: 12 | - v[0-9]+.[0-9]+.[0-9]+* 13 | pull_request: 14 | 15 | jobs: 16 | lint: 17 | name: Lint Check 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 24 | 25 | - name: Set up Go 26 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 27 | with: 28 | go-version-file: ${{ github.workspace }}/go.mod 29 | 30 | - name: Initialize dependencies and linters 31 | run: make init 32 | 33 | - name: Diff 34 | run: | 35 | make format 36 | git diff --exit-code 37 | - name: Run checks/linters 38 | run: | 39 | # use GITHUB_TOKEN because only it has access to GitHub Checks API 40 | bin/golangci-lint run -c=.golangci-required.yml --out-format=line-number | env REVIEWDOG_GITHUB_API_TOKEN=${{ secrets.GITHUB_TOKEN }} bin/reviewdog -f=golangci-lint -level=error -reporter=github-pr-check 41 | bin/golangci-lint run -c=.golangci.yml --out-format=line-number | env REVIEWDOG_GITHUB_API_TOKEN=${{ secrets.GITHUB_TOKEN }} bin/reviewdog -f=golangci-lint -level=error -reporter=github-pr-review 42 | make check-license 43 | -------------------------------------------------------------------------------- /exporter/exporter_metrics.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2022 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package exporter 17 | 18 | import ( 19 | "time" 20 | 21 | "github.com/prometheus/client_golang/prometheus" 22 | ) 23 | 24 | // measureCollectTime measures time taken for scrape by collector 25 | func measureCollectTime(ch chan<- prometheus.Metric, exporter, collector string) func() { 26 | startTime := time.Now() 27 | timeToCollectDesc := prometheus.NewDesc( 28 | "collector_scrape_time_ms", 29 | "Time taken for scrape by collector", 30 | []string{"exporter"}, 31 | prometheus.Labels{"collector": collector}, // to have ID calculated correctly 32 | ) 33 | 34 | return func() { 35 | scrapeTime := time.Since(startTime) 36 | scrapeMetric := prometheus.MustNewConstMetric( 37 | timeToCollectDesc, 38 | prometheus.GaugeValue, 39 | float64(scrapeTime.Milliseconds()), 40 | exporter) 41 | ch <- scrapeMetric 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - release-0.1x 8 | tags: 9 | - v[0-9]+.[0-9]+.[0-9]+* 10 | pull_request: 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | build: 17 | name: Build 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 24 | with: 25 | fetch-depth: 0 26 | 27 | - name: Set up Go 28 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 29 | with: 30 | go-version-file: ${{ github.workspace }}/go.mod 31 | 32 | - name: Set up QEMU 33 | uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 34 | 35 | - name: Set up Docker Buildx 36 | id: buildx 37 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 38 | 39 | - name: Dry-run GoReleaser 40 | run: | 41 | make release-dry-run 42 | 43 | - name: Run debug commands on failure 44 | if: ${{ failure() }} 45 | run: | 46 | echo "--- Environment variables ---" 47 | env | sort 48 | echo "--- GO Environment ---" 49 | go env | sort 50 | echo "--- Git status ---" 51 | git status 52 | echo "--- Docker logs ---" 53 | docker compose logs 54 | echo "--- Docker ps ---" 55 | docker compose ps -a 56 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | name: Scorecard 2 | on: 3 | # To guarantee Maintained check is occasionally updated. See 4 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 5 | schedule: 6 | - cron: "24 3 * * 1" 7 | push: 8 | branches: 9 | - main 10 | 11 | # Declare default permissions as read only. 12 | permissions: read-all 13 | 14 | jobs: 15 | analysis: 16 | name: Analysis 17 | runs-on: ubuntu-latest 18 | permissions: 19 | # Needed to upload the results to code-scanning dashboard. 20 | security-events: write 21 | # Needed to publish results and get a badge (see publish_results below). 22 | id-token: write 23 | 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 27 | with: 28 | persist-credentials: false 29 | 30 | - name: Run analysis 31 | uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 32 | with: 33 | results_file: results.sarif 34 | results_format: sarif 35 | publish_results: true 36 | 37 | - name: Upload results 38 | uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v3.pre.node20 39 | with: 40 | name: SARIF file 41 | path: results.sarif 42 | retention-days: 5 43 | 44 | # Upload the results to GitHub's code scanning dashboard (optional). 45 | - name: "Upload to code-scanning" 46 | uses: github/codeql-action/upload-sarif@1b168cd39490f61582a9beae412bb7057a6b2c4e # v3.29.5 47 | with: 48 | sarif_file: results.sarif -------------------------------------------------------------------------------- /exporter/debug_test.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2022 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package exporter 17 | 18 | import ( 19 | "io" 20 | "os" 21 | "testing" 22 | 23 | "github.com/prometheus/common/promslog" 24 | "github.com/stretchr/testify/assert" 25 | "github.com/stretchr/testify/require" 26 | "go.mongodb.org/mongo-driver/bson" 27 | ) 28 | 29 | func TestDebug(t *testing.T) { 30 | logLevel := promslog.NewLevel() 31 | err := logLevel.Set("debug") 32 | require.NoError(t, err) 33 | 34 | olderr := os.Stderr 35 | r, w, _ := os.Pipe() 36 | 37 | os.Stderr = w 38 | defer func() { 39 | os.Stderr = olderr 40 | _ = logLevel.Set("error") 41 | }() 42 | 43 | log := promslog.New(&promslog.Config{ 44 | Level: logLevel, 45 | Writer: w, 46 | }) 47 | 48 | m := bson.M{ 49 | "f1": 1, 50 | "f2": "v2", 51 | "f3": bson.M{ 52 | "f4": 4, 53 | }, 54 | } 55 | want := `{ 56 | "f1": 1, 57 | "f2": "v2", 58 | "f3": { 59 | "f4": 4 60 | } 61 | }` + "\n" 62 | 63 | debugResult(log.With("component", "test"), m) 64 | assert.NoError(t, w.Close()) 65 | out, _ := io.ReadAll(r) 66 | 67 | assert.Equal(t, want, string(out)) 68 | } 69 | -------------------------------------------------------------------------------- /exporter/dsn_fix/dsn_fix.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2017 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package dsn_fix 17 | 18 | import ( 19 | "net/url" 20 | 21 | "github.com/AlekSi/pointer" 22 | "go.mongodb.org/mongo-driver/mongo/options" 23 | ) 24 | 25 | // ClientOptionsForDSN applies URI to Client. 26 | func ClientOptionsForDSN(dsn string) (*options.ClientOptions, error) { 27 | clientOptions := options.Client().ApplyURI(dsn) 28 | if e := clientOptions.Validate(); e != nil { 29 | return nil, e 30 | } 31 | 32 | // Workaround for PMM-9320 33 | // if username or password is set, need to replace it with correctly parsed credentials. 34 | parsedDsn, err := url.Parse(dsn) 35 | if err != nil { 36 | // for non-URI, do nothing (PMM-10265) 37 | return clientOptions, nil 38 | } 39 | username := parsedDsn.User.Username() 40 | password, _ := parsedDsn.User.Password() 41 | if username != "" || password != "" { 42 | clientOptions.Auth.Username = username 43 | clientOptions.Auth.Password = password 44 | // set this flag to connect to arbiter when there authentication is enabled 45 | clientOptions.AuthenticateToAnything = pointer.ToBool(true) //nolint:staticcheck 46 | } 47 | 48 | return clientOptions, nil 49 | } 50 | -------------------------------------------------------------------------------- /exporter/testdata/locks.json: -------------------------------------------------------------------------------- 1 | { 2 | "serverStatus": { 3 | "locks": { 4 | "Collection": { 5 | "acquireCount": { 6 | "R": 2, 7 | "W": 1099, 8 | "r": 130495, 9 | "w": 13402 10 | } 11 | }, 12 | "Database": { 13 | "acquireCount": { 14 | "W": 1643, 15 | "r": 740504, 16 | "w": 16049 17 | }, 18 | "acquireWaitCount": { 19 | "W": 2, 20 | "r": 2, 21 | "w": 2 22 | }, 23 | "timeAcquiringMicros": { 24 | "W": 21319, 25 | "r": 73, 26 | "w": 311 27 | } 28 | }, 29 | "Global": { 30 | "acquireCount": { 31 | "R": 1, 32 | "W": 5, 33 | "r": 973318, 34 | "w": 28198 35 | }, 36 | "acquireWaitCount": { 37 | "W": 1, 38 | "w": 1 39 | }, 40 | "timeAcquiringMicros": { 41 | "W": 53, 42 | "w": 34117 43 | } 44 | }, 45 | "Mutex": { 46 | "acquireCount": { 47 | "W": 3080, 48 | "r": 576555 49 | } 50 | }, 51 | "ParallelBatchWriterMode": { 52 | "acquireCount": { 53 | "r": 168547 54 | } 55 | }, 56 | "ReplicationStateTransition": { 57 | "acquireCount": { 58 | "W": 2, 59 | "w": 1001570 60 | }, 61 | "acquireWaitCount": { 62 | "W": 2, 63 | "w": 1 64 | }, 65 | "timeAcquiringMicros": { 66 | "W": 24, 67 | "w": 217 68 | } 69 | }, 70 | "oplog": { 71 | "acquireCount": { 72 | "W": 1, 73 | "r": 610644, 74 | "w": 515 75 | } 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /exporter/http_error_logger.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2025 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package exporter 17 | 18 | import ( 19 | "fmt" 20 | "log/slog" 21 | 22 | "github.com/pkg/errors" 23 | "github.com/prometheus/client_golang/prometheus" 24 | ) 25 | 26 | // httpErrorLogger is a wrapper around slog.Logger that can log promhttp errors (by implementing a Println method). 27 | type httpErrorLogger struct { 28 | logger *slog.Logger 29 | } 30 | 31 | func newHTTPErrorLogger(logger *slog.Logger) *httpErrorLogger { 32 | return &httpErrorLogger{logger: logger} 33 | } 34 | 35 | // Println implements the Println method for httpErrorHandler. 36 | func (h *httpErrorLogger) Println(v ...any) { 37 | // promhttp calls the Println() method as follows: 38 | // logger.Println(message, err) i.e., v[0] is the message (a string) and v[1] is the error (which might be prometheus.MultiError) 39 | if len(v) == 2 { 40 | msg, mok := v[0].(string) 41 | err, eok := v[1].(error) 42 | if mok && eok { 43 | multiErr := prometheus.MultiError{} 44 | if errors.As(err, &multiErr) { 45 | errCount := len(multiErr) 46 | for i, err := range multiErr { 47 | h.logger.Error(msg, "error", err, "total_errors", errCount, "error_no", i) 48 | } 49 | } 50 | } 51 | } 52 | // fallback 53 | h.logger.Error(fmt.Sprint(v...)) 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | # run when new tag is pushed 6 | tags: 7 | - v* 8 | # manually trigger the release 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | goreleaser: 16 | permissions: 17 | contents: write # for goreleaser/goreleaser-action to create a GitHub release 18 | packages: write 19 | attestations: write 20 | id-token: write 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 25 | with: 26 | fetch-depth: 0 27 | 28 | - name: Set up Go 29 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 30 | with: 31 | go-version-file: ${{ github.workspace }}/go.mod 32 | 33 | - name: Login to Docker Hub 34 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 35 | with: 36 | username: ${{ secrets.DOCKERHUB_USERNAME }} 37 | password: ${{ secrets.DOCKERHUB_TOKEN }} 38 | 39 | - name: Login to GitHub Container Registry 40 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 41 | with: 42 | registry: ghcr.io 43 | username: ${{ github.actor }} 44 | password: ${{ secrets.GITHUB_TOKEN }} 45 | 46 | - name: Set up QEMU 47 | uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 48 | 49 | - name: Set up Docker Buildx 50 | id: buildx 51 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 52 | 53 | - name: Run GoReleaser 54 | uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 55 | with: 56 | version: "~> v2" 57 | args: release --clean 58 | env: 59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | -------------------------------------------------------------------------------- /exporter/seedlist_test.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2017 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package exporter 17 | 18 | import ( 19 | "net" 20 | "testing" 21 | 22 | "github.com/foxcpp/go-mockdns" 23 | "github.com/prometheus/common/promslog" 24 | "github.com/stretchr/testify/assert" 25 | 26 | "github.com/percona/mongodb_exporter/internal/tu" 27 | ) 28 | 29 | func TestGetSeedListFromSRV(t *testing.T) { 30 | // Can't run in parallel because it patches the net.DefaultResolver 31 | 32 | log := promslog.New(&promslog.Config{}) 33 | srv := tu.SetupFakeResolver() 34 | 35 | defer func(t *testing.T) { 36 | t.Helper() 37 | err := srv.Close() 38 | assert.NoError(t, err) 39 | }(t) 40 | defer mockdns.UnpatchNet(net.DefaultResolver) 41 | 42 | tests := map[string]string{ 43 | "mongodb+srv://server.example.com": "mongodb://mongo1.example.com:17001,mongo2.example.com:17002,mongo3.example.com:17003/?authSource=admin", 44 | "mongodb+srv://user:pass@server.example.com?replicaSet=rs0&authSource=db0": "mongodb://user:pass@mongo1.example.com:17001,mongo2.example.com:17002,mongo3.example.com:17003/?authSource=db0&replicaSet=rs0", 45 | "mongodb+srv://unexistent.com": "mongodb+srv://unexistent.com", 46 | } 47 | 48 | for uri, expected := range tests { 49 | actual := GetSeedListFromSRV(uri, log) 50 | assert.Equal(t, expected, actual) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /exporter/gatherer_wrapper.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2017 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package exporter 17 | 18 | import ( 19 | "github.com/pkg/errors" 20 | "github.com/prometheus/client_golang/prometheus" 21 | io_prometheus_client "github.com/prometheus/client_model/go" 22 | ) 23 | 24 | // GathererWrapped is a wrapper for prometheus.Gatherer that adds labels to all metrics. 25 | type GathererWrapped struct { 26 | originalGatherer prometheus.Gatherer 27 | labels prometheus.Labels 28 | } 29 | 30 | // NewGathererWrapper creates a new GathererWrapped with the given Gatherer and additional labels. 31 | func NewGathererWrapper(gs prometheus.Gatherer, labels prometheus.Labels) *GathererWrapped { 32 | return &GathererWrapped{ 33 | originalGatherer: gs, 34 | labels: labels, 35 | } 36 | } 37 | 38 | // Gather implements prometheus.Gatherer interface. 39 | func (g *GathererWrapped) Gather() ([]*io_prometheus_client.MetricFamily, error) { 40 | metrics, err := g.originalGatherer.Gather() 41 | if err != nil { 42 | return nil, errors.Wrap(err, "failed to gather metrics") 43 | } 44 | 45 | for _, metric := range metrics { 46 | for _, m := range metric.GetMetric() { 47 | for k, v := range g.labels { 48 | v := v 49 | k := k 50 | m.Label = append(m.Label, &io_prometheus_client.LabelPair{ 51 | Name: &k, 52 | Value: &v, 53 | }) 54 | } 55 | } 56 | } 57 | 58 | return metrics, nil 59 | } 60 | -------------------------------------------------------------------------------- /exporter/utils_test.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2022 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package exporter 17 | 18 | import ( 19 | "strings" 20 | 21 | "github.com/percona/exporter_shared/helpers" 22 | ) 23 | 24 | func filterMetrics(metrics []*helpers.Metric, filters []string) []*helpers.Metric { 25 | res := make([]*helpers.Metric, 0, len(metrics)) 26 | 27 | for _, m := range metrics { 28 | m.Value = 0 29 | for _, filterName := range filters { 30 | if m.Name == filterName { 31 | res = append(res, m) 32 | 33 | break 34 | } 35 | } 36 | } 37 | 38 | return res 39 | } 40 | 41 | func getMetricNames(lines []string) map[string]bool { 42 | names := map[string]bool{} 43 | 44 | for _, line := range lines { 45 | if strings.HasPrefix(line, "# TYPE ") { 46 | m := strings.Split(line, " ") 47 | names[m[2]] = true 48 | } 49 | } 50 | 51 | return names 52 | } 53 | 54 | func filterMetricsWithLabels(metrics []*helpers.Metric, filters []string, labels map[string]string) []*helpers.Metric { 55 | res := make([]*helpers.Metric, 0, len(metrics)) 56 | for _, m := range metrics { 57 | for _, filterName := range filters { 58 | if m.Name == filterName { 59 | validMetric := true 60 | for labelKey, labelValue := range labels { 61 | if m.Labels[labelKey] != labelValue { 62 | validMetric = false 63 | 64 | break 65 | } 66 | } 67 | if validMetric { 68 | res = append(res, m) 69 | } 70 | 71 | break 72 | } 73 | } 74 | } 75 | return res 76 | } 77 | -------------------------------------------------------------------------------- /test-setup/scripts/setup-shard.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # `mongosh` is used starting from MongoDB 5.x 3 | MONGODB_CLIENT="mongosh --quiet" 4 | PARSED=(${VERSION//:/ }) 5 | MONGODB_VERSION=${PARSED[1]} 6 | MONGODB_VENDOR=${PARSED[0]} 7 | if [ "`echo ${MONGODB_VERSION} | cut -c 1`" = "4" ]; then 8 | MONGODB_CLIENT="mongo" 9 | fi 10 | if [ "`echo ${MONGODB_VERSION} | cut -c 1`" = "5" ] && [ ${MONGODB_VENDOR} == "percona/percona-server-mongodb" ]; then 11 | MONGODB_CLIENT="mongo" 12 | fi 13 | echo "MongoDB vendor, client and version: ${MONGODB_VENDOR} ${MONGODB_CLIENT} ${MONGODB_VERSION}" 14 | 15 | mongodb1=`getent hosts ${MONGOS} | awk '{ print $1 }'` 16 | 17 | mongodb11=`getent hosts ${MONGO11} | awk '{ print $1 }'` 18 | mongodb12=`getent hosts ${MONGO12} | awk '{ print $1 }'` 19 | mongodb13=`getent hosts ${MONGO13} | awk '{ print $1 }'` 20 | 21 | mongodb21=`getent hosts ${MONGO21} | awk '{ print $1 }'` 22 | mongodb22=`getent hosts ${MONGO22} | awk '{ print $1 }'` 23 | mongodb23=`getent hosts ${MONGO23} | awk '{ print $1 }'` 24 | 25 | mongodb31=`getent hosts ${MONGO31} | awk '{ print $1 }'` 26 | mongodb32=`getent hosts ${MONGO32} | awk '{ print $1 }'` 27 | mongodb33=`getent hosts ${MONGO33} | awk '{ print $1 }'` 28 | 29 | port=${PORT:-27017} 30 | 31 | echo "Waiting for startup.." 32 | until ${MONGODB_CLIENT} --host ${mongodb1}:${port} --eval 'quit(db.runCommand({ ping: 1 }).ok ? 0 : 2)' &>/dev/null; do 33 | printf '.' 34 | sleep 1 35 | done 36 | 37 | echo "Started.." 38 | 39 | echo setup-shard.sh time now: `date +"%T" ` 40 | echo "Configuring sharding.." 41 | echo "${RS1}/${mongodb11}:${PORT1},${mongodb12}:${PORT2},${mongodb13}:${PORT3}" 42 | echo "${RS2}/${mongodb21}:${PORT1},${mongodb22}:${PORT2},${mongodb23}:${PORT3}" 43 | 44 | ${MONGODB_CLIENT} --host ${mongodb1}:${port} <", 59 | Host: "localhost", 60 | Path: "/db", 61 | User: url.UserPassword("user", "pass+"), 62 | }).String(), 63 | error: "error parsing uri: scheme must be \"mongodb\" or \"mongodb+srv\"", 64 | expectedUser: "user", 65 | expectedPassword: "pass+", 66 | }, 67 | } 68 | for _, tt := range tests { 69 | t.Run(tt.name, func(t *testing.T) { 70 | got, err := ClientOptionsForDSN(tt.dsn) 71 | if tt.error != "" { 72 | assert.Equal(t, err.Error(), tt.error) 73 | } else { 74 | assert.Empty(t, err) 75 | assert.Equal(t, got.Auth.Username, tt.expectedUser) 76 | assert.Equal(t, got.Auth.Password, tt.expectedPassword) 77 | } 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /exporter/pbm_collector_test.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2024 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package exporter 17 | 18 | import ( 19 | "context" 20 | "strings" 21 | "testing" 22 | "time" 23 | 24 | "github.com/prometheus/client_golang/prometheus/testutil" 25 | "github.com/prometheus/common/promslog" 26 | "github.com/stretchr/testify/assert" 27 | "github.com/stretchr/testify/require" 28 | 29 | "github.com/percona/mongodb_exporter/internal/tu" 30 | ) 31 | 32 | //nolint:paralleltest 33 | func TestPBMCollector(t *testing.T) { 34 | t.Parallel() 35 | 36 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 37 | defer cancel() 38 | 39 | port, err := tu.PortForContainer("mongo-2-1") 40 | require.NoError(t, err) 41 | client := tu.TestClient(ctx, port, t) 42 | mongoURI := "mongodb://admin:admin@127.0.0.1:17006/?connectTimeoutMS=1000&directConnection=true&serverSelectionTimeoutMS=1000" //nolint:gosec 43 | 44 | c := newPbmCollector(ctx, client, mongoURI, promslog.New(&promslog.Config{})) 45 | 46 | t.Run("pbm configured metric", func(t *testing.T) { 47 | filter := []string{ 48 | "mongodb_pbm_cluster_backup_configured", 49 | } 50 | expected := strings.NewReader(` 51 | # HELP mongodb_pbm_cluster_backup_configured PBM backups are configured for the cluster 52 | # TYPE mongodb_pbm_cluster_backup_configured gauge 53 | mongodb_pbm_cluster_backup_configured 1` + "\n") 54 | err = testutil.CollectAndCompare(c, expected, filter...) 55 | assert.NoError(t, err) 56 | }) 57 | 58 | t.Run("pbm agent status metric", func(t *testing.T) { 59 | filter := []string{ 60 | "mongodb_pbm_agent_status", 61 | } 62 | expectedLength := 4 // we expect 4 metrics for each member of the RS (1 primary, 2 secondaries, 1 arbiter). 63 | count := testutil.CollectAndCount(c, filter...) 64 | assert.Equal(t, expectedLength, count, "PBM metrics are missing") 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /exporter/base_collector.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2022 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package exporter 17 | 18 | import ( 19 | "context" 20 | "log/slog" 21 | "sync" 22 | 23 | "github.com/prometheus/client_golang/prometheus" 24 | "go.mongodb.org/mongo-driver/mongo" 25 | ) 26 | 27 | type baseCollector struct { 28 | client *mongo.Client 29 | logger *slog.Logger 30 | 31 | lock sync.Mutex 32 | metricsCache []prometheus.Metric 33 | } 34 | 35 | // newBaseCollector creates a skeletal collector, which is used to create other collectors. 36 | func newBaseCollector(client *mongo.Client, logger *slog.Logger) *baseCollector { 37 | return &baseCollector{ 38 | client: client, 39 | logger: logger, 40 | } 41 | } 42 | 43 | func (d *baseCollector) Describe(ctx context.Context, ch chan<- *prometheus.Desc, collect func(mCh chan<- prometheus.Metric)) { 44 | select { 45 | case <-ctx.Done(): 46 | // don't interrupt, let mongodb_up metric to be registered if on timeout we still don't have client connected 47 | if d.client != nil { 48 | return 49 | } 50 | default: 51 | } 52 | 53 | d.lock.Lock() 54 | defer d.lock.Unlock() 55 | 56 | d.metricsCache = make([]prometheus.Metric, 0, defaultCacheSize) 57 | 58 | // This is a copy/paste of prometheus.DescribeByCollect(d, ch) with the aggreated functionality 59 | // to populate the metrics cache. Since on each scrape Prometheus will call Describe and inmediatelly 60 | // after it will call Collect, it is safe to populate the cache here. 61 | metrics := make(chan prometheus.Metric) 62 | go func() { 63 | collect(metrics) 64 | close(metrics) 65 | }() 66 | 67 | for m := range metrics { 68 | d.metricsCache = append(d.metricsCache, m) // populate the cache 69 | ch <- m.Desc() 70 | } 71 | } 72 | 73 | func (d *baseCollector) Collect(ch chan<- prometheus.Metric) { 74 | d.lock.Lock() 75 | defer d.lock.Unlock() 76 | 77 | for _, metric := range d.metricsCache { 78 | ch <- metric 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /exporter/encryption_info_test.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2017 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package exporter 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "strings" 22 | "testing" 23 | "time" 24 | 25 | "github.com/prometheus/client_golang/prometheus/testutil" 26 | "github.com/prometheus/common/promslog" 27 | "github.com/stretchr/testify/assert" 28 | "github.com/stretchr/testify/require" 29 | 30 | "github.com/percona/mongodb_exporter/internal/tu" 31 | ) 32 | 33 | func TestGetEncryptionInfo(t *testing.T) { 34 | t.Parallel() 35 | version, vendor := getMongoDBVersionInfo(t, "standalone-encrypted") 36 | if vendor != "Percona" { 37 | t.Skip("Test is only for Percona MongoDB as upstream MongoDB does not support encryption") 38 | } 39 | 40 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 41 | defer cancel() 42 | 43 | client := tu.TestClient(ctx, tu.MongoDBStandAloneEncryptedPort, t) 44 | t.Cleanup(func() { 45 | err := client.Disconnect(ctx) 46 | assert.NoError(t, err) 47 | }) 48 | logger := promslog.NewNopLogger() 49 | 50 | ti := labelsGetterMock{} 51 | 52 | dbBuildInfo, err := retrieveMongoDBBuildInfo(ctx, client, logger.With("component", "test")) 53 | require.NoError(t, err) 54 | 55 | c := newDiagnosticDataCollector(ctx, client, logger, true, ti, dbBuildInfo) 56 | 57 | // The last \n at the end of this string is important 58 | expected := strings.NewReader(fmt.Sprintf(` 59 | # HELP mongodb_security_encryption_enabled Shows that encryption is enabled 60 | # TYPE mongodb_security_encryption_enabled gauge 61 | mongodb_security_encryption_enabled{type="localKeyFile"} 1 62 | # HELP mongodb_version_info The server version 63 | # TYPE mongodb_version_info gauge 64 | mongodb_version_info{edition="Community",mongodb="%s",vendor="%s"} 1`, version, vendor) + "\n") 65 | 66 | filter := []string{ 67 | "mongodb_security_encryption_enabled", 68 | "mongodb_version_info", 69 | } 70 | 71 | err = testutil.CollectAndCompare(c, expected, filter...) 72 | assert.NoError(t, err) 73 | } 74 | -------------------------------------------------------------------------------- /exporter/dbstats_collector_test.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2017 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package exporter 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "strings" 22 | "testing" 23 | "time" 24 | 25 | "github.com/prometheus/client_golang/prometheus/testutil" 26 | "github.com/prometheus/common/promslog" 27 | "github.com/stretchr/testify/assert" 28 | "go.mongodb.org/mongo-driver/bson" 29 | 30 | "github.com/percona/mongodb_exporter/internal/tu" 31 | ) 32 | 33 | const ( 34 | dbName = "testdb" 35 | ) 36 | 37 | func TestDBStatsCollector(t *testing.T) { 38 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 39 | defer cancel() 40 | 41 | client := tu.DefaultTestClient(ctx, t) 42 | 43 | database := client.Database(dbName) 44 | database.Drop(ctx) //nolint 45 | 46 | defer func() { 47 | err := database.Drop(ctx) 48 | assert.NoError(t, err) 49 | }() 50 | 51 | for i := 0; i < 3; i++ { 52 | coll := fmt.Sprintf("testcol_%02d", i) 53 | for j := 0; j < 10; j++ { 54 | _, err := database.Collection(coll).InsertOne(ctx, bson.M{"f1": j, "f2": "2"}) 55 | assert.NoError(t, err) 56 | } 57 | } 58 | 59 | ti := labelsGetterMock{} 60 | 61 | logger := promslog.New(&promslog.Config{}) 62 | c := newDBStatsCollector(ctx, client, logger, false, ti, []string{dbName}, false) 63 | expected := strings.NewReader(` 64 | # HELP mongodb_dbstats_collections dbstats.collections 65 | # TYPE mongodb_dbstats_collections untyped 66 | mongodb_dbstats_collections{database="testdb"} 3 67 | # HELP mongodb_dbstats_indexes dbstats.indexes 68 | # TYPE mongodb_dbstats_indexes untyped 69 | mongodb_dbstats_indexes{database="testdb"} 3 70 | # HELP mongodb_dbstats_objects dbstats.objects 71 | # TYPE mongodb_dbstats_objects untyped 72 | mongodb_dbstats_objects{database="testdb"} 30` + "\n") 73 | 74 | // Only look at metrics created by our activity 75 | filters := []string{ 76 | "mongodb_dbstats_collections", 77 | "mongodb_dbstats_indexes", 78 | "mongodb_dbstats_objects", 79 | } 80 | err := testutil.CollectAndCompare(c, expected, filters...) 81 | assert.NoError(t, err) 82 | } 83 | -------------------------------------------------------------------------------- /exporter/replset_config_collector_test.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2017 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package exporter 17 | 18 | import ( 19 | "context" 20 | "strings" 21 | "testing" 22 | "time" 23 | 24 | "github.com/prometheus/client_golang/prometheus/testutil" 25 | "github.com/prometheus/common/promslog" 26 | "github.com/stretchr/testify/assert" 27 | 28 | "github.com/percona/mongodb_exporter/internal/tu" 29 | ) 30 | 31 | func TestReplsetConfigCollector(t *testing.T) { 32 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 33 | defer cancel() 34 | 35 | client := tu.DefaultTestClient(ctx, t) 36 | 37 | ti := labelsGetterMock{} 38 | 39 | c := newReplicationSetConfigCollector(ctx, client, promslog.New(&promslog.Config{}), false, ti) 40 | 41 | // The last \n at the end of this string is important 42 | expected := strings.NewReader(` 43 | # HELP mongodb_rs_cfg_protocolVersion rs_cfg.protocolVersion 44 | # TYPE mongodb_rs_cfg_protocolVersion untyped 45 | mongodb_rs_cfg_protocolVersion 1` + "\n") 46 | // Filter metrics for 2 reasons: 47 | // 1. The result is huge 48 | // 2. We need to check against know values. Don't use metrics that return counters like uptime 49 | // or counters like the number of transactions because they won't return a known value to compare 50 | filter := []string{ 51 | "mongodb_rs_cfg_protocolVersion", 52 | } 53 | err := testutil.CollectAndCompare(c, expected, filter...) 54 | assert.NoError(t, err) 55 | } 56 | 57 | func TestReplsetConfigCollectorNoSharding(t *testing.T) { 58 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 59 | defer cancel() 60 | 61 | client := tu.TestClient(ctx, tu.MongoDBStandAlonePort, t) 62 | 63 | ti := labelsGetterMock{} 64 | 65 | c := newReplicationSetConfigCollector(ctx, client, promslog.New(&promslog.Config{}), false, ti) 66 | 67 | // Replication set metrics should not be generated for unsharded server 68 | count := testutil.CollectAndCount(c) 69 | 70 | metaMetricCount := 1 71 | assert.Equal(t, metaMetricCount, count, "Mismatch in metric count for collector run on unsharded server") 72 | } 73 | -------------------------------------------------------------------------------- /exporter/replset_status_collector_test.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2017 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package exporter 17 | 18 | import ( 19 | "context" 20 | "strings" 21 | "testing" 22 | "time" 23 | 24 | "github.com/prometheus/client_golang/prometheus/testutil" 25 | "github.com/prometheus/common/promslog" 26 | "github.com/stretchr/testify/assert" 27 | 28 | "github.com/percona/mongodb_exporter/internal/tu" 29 | ) 30 | 31 | func TestReplsetStatusCollector(t *testing.T) { 32 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 33 | defer cancel() 34 | 35 | client := tu.DefaultTestClient(ctx, t) 36 | 37 | ti := labelsGetterMock{} 38 | 39 | c := newReplicationSetStatusCollector(ctx, client, promslog.New(&promslog.Config{}), false, ti) 40 | 41 | // The last \n at the end of this string is important 42 | expected := strings.NewReader(` 43 | # HELP mongodb_myState myState 44 | # TYPE mongodb_myState untyped 45 | mongodb_myState 1 46 | # HELP mongodb_ok ok 47 | # TYPE mongodb_ok untyped 48 | mongodb_ok 1` + "\n") 49 | // Filter metrics for 2 reasons: 50 | // 1. The result is huge 51 | // 2. We need to check against know values. Don't use metrics that return counters like uptime 52 | // or counters like the number of transactions because they won't return a known value to compare 53 | filter := []string{ 54 | "mongodb_myState", 55 | "mongodb_ok", 56 | } 57 | err := testutil.CollectAndCompare(c, expected, filter...) 58 | assert.NoError(t, err) 59 | } 60 | 61 | func TestReplsetStatusCollectorNoSharding(t *testing.T) { 62 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 63 | defer cancel() 64 | 65 | client := tu.TestClient(ctx, tu.MongoDBStandAlonePort, t) 66 | 67 | ti := labelsGetterMock{} 68 | 69 | c := newReplicationSetStatusCollector(ctx, client, promslog.New(&promslog.Config{}), false, ti) 70 | 71 | // Replication set metrics should not be generated for unsharded server 72 | count := testutil.CollectAndCount(c) 73 | 74 | metaMetricCount := 1 75 | assert.Equal(t, metaMetricCount, count, "Mismatch in metric count for collector run on unsharded server") 76 | } 77 | -------------------------------------------------------------------------------- /.github/workflows/version-check.yml: -------------------------------------------------------------------------------- 1 | name: Version Check 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | push: 7 | branches: [ main ] 8 | 9 | jobs: 10 | version-check: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Get version from VERSION file 20 | id: file-version 21 | run: | 22 | if [ -f "VERSION" ]; then 23 | FILE_VERSION=$(cat VERSION | tr -d '\n\r ') 24 | echo "version=$FILE_VERSION" >> $GITHUB_OUTPUT 25 | echo "VERSION file contains: $FILE_VERSION" 26 | else 27 | echo "VERSION file not found!" 28 | exit 1 29 | fi 30 | 31 | - name: Get version from git tag 32 | id: git-version 33 | run: | 34 | GIT_VERSION=$(git describe --tags --abbrev=0 --always) 35 | echo "version=$GIT_VERSION" >> $GITHUB_OUTPUT 36 | echo "Git tag version: $GIT_VERSION" 37 | 38 | - name: Install semver comparison tool 39 | run: | 40 | npm install -g semver 41 | 42 | - name: Compare versions 43 | run: | 44 | FILE_VERSION="${{ steps.file-version.outputs.version }}" 45 | GIT_VERSION="${{ steps.git-version.outputs.version }}" 46 | 47 | echo "Comparing versions:" 48 | echo " VERSION file: $FILE_VERSION" 49 | echo " Git tag: $GIT_VERSION" 50 | 51 | # Remove 'v' prefix if present for comparison 52 | FILE_VERSION_CLEAN=$(echo "$FILE_VERSION" | sed 's/^v//') 53 | GIT_VERSION_CLEAN=$(echo "$GIT_VERSION" | sed 's/^v//') 54 | 55 | # Check if versions are valid semver 56 | if ! semver "$FILE_VERSION_CLEAN" >/dev/null 2>&1; then 57 | echo "ERROR: VERSION file contains invalid semantic version: $FILE_VERSION" 58 | exit 1 59 | fi 60 | 61 | if ! semver "$GIT_VERSION_CLEAN" >/dev/null 2>&1; then 62 | echo "ERROR: Git tag contains invalid semantic version: $GIT_VERSION" 63 | exit 1 64 | fi 65 | 66 | # Compare versions: -1 if first < second, 0 if equal, 1 if first > second 67 | if [ ! $(semver -r ">=$GIT_VERSION_CLEAN" "$FILE_VERSION_CLEAN") ]; then 68 | echo "VERSION file ($FILE_VERSION) is behind git tag ($GIT_VERSION)" 69 | echo "The VERSION file must be ahead of or equal to the latest git tag." 70 | exit 1 71 | else 72 | echo "VERSION file ($FILE_VERSION) is ahead of or equal to git tag ($GIT_VERSION)" 73 | fi 74 | -------------------------------------------------------------------------------- /exporter/currentop_collector_test.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2017 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package exporter 17 | 18 | import ( 19 | "context" 20 | "sync" 21 | "testing" 22 | "time" 23 | 24 | "github.com/prometheus/client_golang/prometheus/testutil" 25 | "github.com/prometheus/common/promslog" 26 | "github.com/stretchr/testify/assert" 27 | "go.mongodb.org/mongo-driver/bson" 28 | 29 | "github.com/percona/mongodb_exporter/internal/tu" 30 | ) 31 | 32 | func TestCurrentopCollector(t *testing.T) { 33 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 34 | defer cancel() 35 | 36 | var wg sync.WaitGroup 37 | 38 | client := tu.DefaultTestClient(ctx, t) 39 | 40 | database := client.Database("testdb") 41 | _ = database.Drop(ctx) 42 | 43 | defer func() { 44 | err := database.Drop(ctx) 45 | assert.NoError(t, err) 46 | }() 47 | ch := make(chan struct{}) 48 | wg.Add(1) 49 | go func() { 50 | defer wg.Done() 51 | coll := "testcol_01" 52 | for j := 0; j < 100; j++ { //nolint:intrange // false positive 53 | _, err := database.Collection(coll).InsertOne(ctx, bson.M{"f1": j, "f2": "2"}) 54 | assert.NoError(t, err) 55 | } 56 | ch <- struct{}{} 57 | _, _ = database.Collection(coll).Find(ctx, bson.M{"$where": "function() {return sleep(100)}"}) 58 | }() 59 | 60 | ti := labelsGetterMock{} 61 | st := "0s" 62 | 63 | c := newCurrentopCollector(ctx, client, promslog.New(&promslog.Config{}), false, ti, st) 64 | 65 | // Filter metrics by reason: 66 | // 1. The result will be different on different hardware 67 | // 2. Can't check labels like 'decs' and 'opid' because they don't return a known value for comparison 68 | // It looks like: 69 | // # HELP mongodb_currentop_query_uptime currentop_query. 70 | // # TYPE mongodb_currentop_query_uptime untyped 71 | // mongodb_currentop_query_uptime{collection="testcol_00",database="testdb",decs="conn6365",ns="testdb.testcol_00",op="insert",opid="448307"} 2524 72 | 73 | filter := []string{ 74 | "mongodb_currentop_query_uptime", 75 | } 76 | 77 | <-ch 78 | 79 | time.Sleep(1 * time.Second) 80 | 81 | count := testutil.CollectAndCount(c, filter...) 82 | assert.True(t, count > 0) 83 | wg.Wait() 84 | } 85 | -------------------------------------------------------------------------------- /exporter/seedlist.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2017 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package exporter 17 | 18 | import ( 19 | "log" 20 | "log/slog" 21 | "net" 22 | "net/url" 23 | "strconv" 24 | "strings" 25 | ) 26 | 27 | // GetSeedListFromSRV converts mongodb+srv URI to flat connection string. 28 | func GetSeedListFromSRV(uri string, logger *slog.Logger) string { //nolint:cyclop 29 | uriParsed, err := url.Parse(uri) 30 | if err != nil { 31 | log.Fatalf("Failed to parse URI %s: %v", uri, err) 32 | } 33 | 34 | cname, srvRecords, err := net.LookupSRV("mongodb", "tcp", uriParsed.Hostname()) 35 | if err != nil { 36 | logger.Error("Failed to lookup SRV records", "uri", uri, "error", err) 37 | return uri 38 | } 39 | 40 | if len(srvRecords) == 0 { 41 | logger.Error("No SRV records found", "uri", uri) 42 | return uri 43 | } 44 | 45 | queryString := uriParsed.RawQuery 46 | 47 | txtRecords, err := net.LookupTXT(uriParsed.Hostname()) 48 | if err != nil { 49 | logger.Error("Failed to lookup TXT records", "cname", cname, "error", err) 50 | } 51 | if len(txtRecords) > 1 { 52 | logger.Error("Multiple TXT records were found and none will be applied", "cname", cname) 53 | } 54 | if len(txtRecords) == 1 { 55 | // We take connection parameters from the TXT record 56 | uriParams, err := url.ParseQuery(txtRecords[0]) 57 | if err != nil { 58 | logger.Error("Failed to parse TXT record", "txt_record", txtRecords[0], "error", err) 59 | } else { 60 | // Override connection parameters with ones from URI query string 61 | for p, v := range uriParsed.Query() { 62 | uriParams[p] = v 63 | } 64 | queryString = uriParams.Encode() 65 | } 66 | } 67 | 68 | // Build final connection URI 69 | servers := make([]string, len(srvRecords)) 70 | for i, srv := range srvRecords { 71 | servers[i] = net.JoinHostPort(strings.TrimSuffix(srv.Target, "."), strconv.FormatUint(uint64(srv.Port), 10)) 72 | } 73 | uri = "mongodb://" 74 | if uriParsed.User != nil { 75 | uri += uriParsed.User.String() + "@" 76 | } 77 | uri += strings.Join(servers, ",") 78 | if uriParsed.Path != "" { 79 | uri += uriParsed.Path 80 | } else { 81 | uri += "/" 82 | } 83 | if queryString != "" { 84 | uri += "?" + queryString 85 | } 86 | 87 | return uri 88 | } 89 | -------------------------------------------------------------------------------- /exporter/replset_status_collector.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2017 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package exporter 17 | 18 | import ( 19 | "context" 20 | "log/slog" 21 | 22 | "github.com/prometheus/client_golang/prometheus" 23 | "go.mongodb.org/mongo-driver/bson" 24 | "go.mongodb.org/mongo-driver/mongo" 25 | ) 26 | 27 | const ( 28 | replicationNotEnabled = 76 29 | replicationNotYetInitialized = 94 30 | ) 31 | 32 | type replSetGetStatusCollector struct { 33 | ctx context.Context 34 | base *baseCollector 35 | 36 | compatibleMode bool 37 | topologyInfo labelsGetter 38 | } 39 | 40 | // newReplicationSetStatusCollector creates a collector for statistics on replication set. 41 | func newReplicationSetStatusCollector(ctx context.Context, client *mongo.Client, logger *slog.Logger, compatible bool, topology labelsGetter) *replSetGetStatusCollector { 42 | return &replSetGetStatusCollector{ 43 | ctx: ctx, 44 | base: newBaseCollector(client, logger.With("collector", "replset_status")), 45 | 46 | compatibleMode: compatible, 47 | topologyInfo: topology, 48 | } 49 | } 50 | 51 | func (d *replSetGetStatusCollector) Describe(ch chan<- *prometheus.Desc) { 52 | d.base.Describe(d.ctx, ch, d.collect) 53 | } 54 | 55 | func (d *replSetGetStatusCollector) Collect(ch chan<- prometheus.Metric) { 56 | d.base.Collect(ch) 57 | } 58 | 59 | func (d *replSetGetStatusCollector) collect(ch chan<- prometheus.Metric) { 60 | defer measureCollectTime(ch, "mongodb", "replset_status")() 61 | 62 | logger := d.base.logger 63 | client := d.base.client 64 | 65 | cmd := bson.D{{Key: "replSetGetStatus", Value: "1"}} 66 | res := client.Database("admin").RunCommand(d.ctx, cmd) 67 | 68 | var m bson.M 69 | 70 | if err := res.Decode(&m); err != nil { 71 | if e, ok := err.(mongo.CommandError); ok { 72 | if e.Code == replicationNotYetInitialized || e.Code == replicationNotEnabled { 73 | return 74 | } 75 | } 76 | logger.Error("cannot get replSetGetStatus", "error", err) 77 | 78 | return 79 | } 80 | 81 | logger.Debug("replSetGetStatus result:") 82 | debugResult(logger, m) 83 | 84 | for _, metric := range makeMetrics("", m, d.topologyInfo.baseLabels(), d.compatibleMode) { 85 | ch <- metric 86 | } 87 | } 88 | 89 | var _ prometheus.Collector = (*replSetGetStatusCollector)(nil) 90 | -------------------------------------------------------------------------------- /exporter/general_collector.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2017 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package exporter 17 | 18 | import ( 19 | "context" 20 | "log/slog" 21 | 22 | "github.com/prometheus/client_golang/prometheus" 23 | "go.mongodb.org/mongo-driver/mongo" 24 | "go.mongodb.org/mongo-driver/mongo/readpref" 25 | ) 26 | 27 | // This collector is always enabled and collects general MongoDB connectivity status. 28 | type generalCollector struct { 29 | ctx context.Context 30 | base *baseCollector 31 | nodeType mongoDBNodeType 32 | } 33 | 34 | // newGeneralCollector creates a collector for MongoDB connectivity status. 35 | func newGeneralCollector(ctx context.Context, client *mongo.Client, nodeType mongoDBNodeType, logger *slog.Logger) *generalCollector { 36 | return &generalCollector{ 37 | ctx: ctx, 38 | nodeType: nodeType, 39 | base: newBaseCollector(client, logger.With("collector", "general")), 40 | } 41 | } 42 | 43 | func (d *generalCollector) Describe(ch chan<- *prometheus.Desc) { 44 | d.base.Describe(d.ctx, ch, d.collect) 45 | } 46 | 47 | func (d *generalCollector) Collect(ch chan<- prometheus.Metric) { 48 | d.base.Collect(ch) 49 | } 50 | 51 | func (d *generalCollector) collect(ch chan<- prometheus.Metric) { 52 | defer measureCollectTime(ch, "mongodb", "general")() 53 | ch <- mongodbUpMetric(d.ctx, d.base.client, d.nodeType, d.base.logger) 54 | } 55 | 56 | func mongodbUpMetric(ctx context.Context, client *mongo.Client, nodeType mongoDBNodeType, log *slog.Logger) prometheus.Metric { //nolint:ireturn 57 | var value float64 58 | var clusterRole mongoDBNodeType 59 | 60 | if client != nil { 61 | if err := client.Ping(ctx, readpref.PrimaryPreferred()); err == nil { 62 | value = 1 63 | } else { 64 | log.Error("error while checking mongodb connection, mongo_up will be set to 0", "error", err.Error()) 65 | } 66 | switch nodeType { //nolint:exhaustive 67 | case typeShardServer: 68 | clusterRole = typeMongod 69 | default: 70 | clusterRole = nodeType 71 | } 72 | } 73 | 74 | labels := map[string]string{"cluster_role": string(clusterRole)} 75 | d := prometheus.NewDesc("mongodb_up", "Whether MongoDB is up.", nil, labels) 76 | 77 | return prometheus.MustNewConstMetric(d, prometheus.GaugeValue, value) 78 | } 79 | 80 | var _ prometheus.Collector = (*generalCollector)(nil) 81 | -------------------------------------------------------------------------------- /exporter/feature_compatibility_version_collector.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2017 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package exporter 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "log/slog" 22 | "strconv" 23 | 24 | "github.com/prometheus/client_golang/prometheus" 25 | "go.mongodb.org/mongo-driver/bson" 26 | "go.mongodb.org/mongo-driver/mongo" 27 | ) 28 | 29 | type featureCompatibilityCollector struct { 30 | ctx context.Context 31 | base *baseCollector 32 | } 33 | 34 | // newProfileCollector creates a collector for being processed queries. 35 | func newFeatureCompatibilityCollector(ctx context.Context, client *mongo.Client, logger *slog.Logger) *featureCompatibilityCollector { 36 | return &featureCompatibilityCollector{ 37 | ctx: ctx, 38 | base: newBaseCollector(client, logger.With("collector", "featureCompatibility")), 39 | } 40 | } 41 | 42 | func (d *featureCompatibilityCollector) Describe(ch chan<- *prometheus.Desc) { 43 | d.base.Describe(d.ctx, ch, d.collect) 44 | } 45 | 46 | func (d *featureCompatibilityCollector) Collect(ch chan<- prometheus.Metric) { 47 | d.base.Collect(ch) 48 | } 49 | 50 | func (d *featureCompatibilityCollector) collect(ch chan<- prometheus.Metric) { 51 | defer measureCollectTime(ch, "mongodb", "fcv")() 52 | 53 | cmd := bson.D{{Key: "getParameter", Value: 1}, {Key: "featureCompatibilityVersion", Value: 1}} 54 | client := d.base.client 55 | if client == nil { 56 | return 57 | } 58 | res := client.Database("admin").RunCommand(d.ctx, cmd) 59 | 60 | m := make(map[string]interface{}) 61 | if err := res.Decode(&m); err != nil { 62 | d.base.logger.Error("Failed to decode featureCompatibilityVersion", "error", err) 63 | ch <- prometheus.NewInvalidMetric(prometheus.NewInvalidDesc(err), err) 64 | return 65 | } 66 | 67 | rawValue := walkTo(m, []string{"featureCompatibilityVersion", "version"}) 68 | if rawValue != nil { 69 | versionString := fmt.Sprintf("%v", rawValue) 70 | version, err := strconv.ParseFloat(versionString, 64) 71 | if err != nil { 72 | d.base.logger.Error("Failed to parse featureCompatibilityVersion", "error", err) 73 | ch <- prometheus.NewInvalidMetric(prometheus.NewInvalidDesc(err), err) 74 | return 75 | } 76 | 77 | d := prometheus.NewDesc("mongodb_fcv_feature_compatibility_version", "Feature compatibility version", []string{"version"}, map[string]string{}) 78 | ch <- prometheus.MustNewConstMetric(d, prometheus.GaugeValue, version, versionString) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /exporter/replset_config_collector.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2017 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package exporter 17 | 18 | import ( 19 | "context" 20 | "log/slog" 21 | 22 | "github.com/pkg/errors" 23 | "github.com/prometheus/client_golang/prometheus" 24 | "go.mongodb.org/mongo-driver/bson" 25 | "go.mongodb.org/mongo-driver/mongo" 26 | ) 27 | 28 | type replSetGetConfigCollector struct { 29 | ctx context.Context 30 | base *baseCollector 31 | 32 | compatibleMode bool 33 | topologyInfo labelsGetter 34 | } 35 | 36 | // newReplicationSetConfigCollector creates a collector for configuration of replication set. 37 | func newReplicationSetConfigCollector(ctx context.Context, client *mongo.Client, logger *slog.Logger, compatible bool, topology labelsGetter) *replSetGetConfigCollector { 38 | return &replSetGetConfigCollector{ 39 | ctx: ctx, 40 | base: newBaseCollector(client, logger.With("collector", "replset_config")), 41 | 42 | compatibleMode: compatible, 43 | topologyInfo: topology, 44 | } 45 | } 46 | 47 | func (d *replSetGetConfigCollector) Describe(ch chan<- *prometheus.Desc) { 48 | d.base.Describe(d.ctx, ch, d.collect) 49 | } 50 | 51 | func (d *replSetGetConfigCollector) Collect(ch chan<- prometheus.Metric) { 52 | d.base.Collect(ch) 53 | } 54 | 55 | func (d *replSetGetConfigCollector) collect(ch chan<- prometheus.Metric) { 56 | defer measureCollectTime(ch, "mongodb", "replset_config")() 57 | 58 | logger := d.base.logger 59 | client := d.base.client 60 | 61 | cmd := bson.D{{Key: "replSetGetConfig", Value: "1"}} 62 | res := client.Database("admin").RunCommand(d.ctx, cmd) 63 | 64 | var m bson.M 65 | 66 | if err := res.Decode(&m); err != nil { 67 | if e, ok := err.(mongo.CommandError); ok { //nolint // https://github.com/percona/mongodb_exporter/pull/295#issuecomment-922874632 68 | if e.Code == replicationNotYetInitialized || e.Code == replicationNotEnabled { 69 | return 70 | } 71 | } 72 | logger.Error("cannot get replSetGetConfig", "error", err) 73 | 74 | return 75 | } 76 | 77 | config, ok := m["config"].(bson.M) 78 | if !ok { 79 | err := errors.Wrapf(errUnexpectedDataType, "%T for data field", m["config"]) 80 | logger.Error("cannot decode getDiagnosticData", "error", err) 81 | 82 | return 83 | } 84 | m = config 85 | 86 | logger.Debug("replSetGetConfig result:") 87 | debugResult(logger, m) 88 | 89 | for _, metric := range makeMetrics("rs_cfg", m, d.topologyInfo.baseLabels(), d.compatibleMode) { 90 | ch <- metric 91 | } 92 | } 93 | 94 | var _ prometheus.Collector = (*replSetGetConfigCollector)(nil) 95 | -------------------------------------------------------------------------------- /exporter/top_collector_test.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2017 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package exporter 17 | 18 | import ( 19 | "context" 20 | "testing" 21 | "time" 22 | 23 | "github.com/prometheus/client_golang/prometheus/testutil" 24 | "github.com/prometheus/common/promslog" 25 | "github.com/stretchr/testify/assert" 26 | 27 | "github.com/percona/mongodb_exporter/internal/tu" 28 | ) 29 | 30 | func TestTopCollector(t *testing.T) { 31 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 32 | defer cancel() 33 | 34 | client := tu.DefaultTestClient(ctx, t) 35 | 36 | ti := labelsGetterMock{} 37 | 38 | c := newTopCollector(ctx, client, promslog.New(&promslog.Config{}), ti) 39 | 40 | // Filter metrics for 2 reasons: 41 | // 1. The result is huge 42 | // 2. We need to check against know values. Don't use metrics that return counters like uptime 43 | // or counters like the number of transactions because they won't return a known value to compare 44 | //filter := []string{ 45 | // "mongodb_top_update_count", 46 | //} 47 | var filter []string 48 | count := testutil.CollectAndCount(c, filter...) 49 | 50 | /* 51 | The number of metrics is not a constant. It depends on the number of collections in the db. 52 | It looks like: 53 | 54 | # HELP mongodb_top_update_count top.update. 55 | # TYPE mongodb_top_update_count untyped 56 | mongodb_top_update_count{namespace="admin.system.roles"} 0 57 | mongodb_top_update_count{namespace="admin.system.version"} 3 58 | mongodb_top_update_count{namespace="config.cache.chunks.config.system.sessions"} 0 59 | mongodb_top_update_count{namespace="config.cache.collections"} 1540 60 | mongodb_top_update_count{namespace="config.image_collection"} 0 61 | mongodb_top_update_count{namespace="config.system.sessions"} 12 62 | mongodb_top_update_count{namespace="config.transaction_coordinators"} 0 63 | mongodb_top_update_count{namespace="config.transactions"} 0 64 | mongodb_top_update_count{namespace="local.oplog.rs"} 0 65 | mongodb_top_update_count{namespace="local.replset.election"} 0 66 | mongodb_top_update_count{namespace="local.replset.minvalid"} 0 67 | mongodb_top_update_count{namespace="local.replset.oplogTruncateAfterPoint"} 0 68 | mongodb_top_update_count{namespace="local.startup_log"} 0 69 | mongodb_top_update_count{namespace="local.system.replset"} 0 70 | mongodb_top_update_count{namespace="local.system.rollback.id"} 0 71 | 72 | */ 73 | assert.True(t, count > 0) 74 | } 75 | -------------------------------------------------------------------------------- /exporter/profile_status_collector.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2017 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package exporter 17 | 18 | import ( 19 | "context" 20 | "log/slog" 21 | "time" 22 | 23 | "github.com/prometheus/client_golang/prometheus" 24 | "go.mongodb.org/mongo-driver/bson" 25 | "go.mongodb.org/mongo-driver/bson/primitive" 26 | "go.mongodb.org/mongo-driver/mongo" 27 | ) 28 | 29 | type profileCollector struct { 30 | ctx context.Context 31 | base *baseCollector 32 | compatibleMode bool 33 | topologyInfo labelsGetter 34 | profiletimets int 35 | } 36 | 37 | // newProfileCollector creates a collector for being processed queries. 38 | func newProfileCollector(ctx context.Context, client *mongo.Client, logger *slog.Logger, 39 | compatible bool, topology labelsGetter, profileTimeTS int, 40 | ) *profileCollector { 41 | return &profileCollector{ 42 | ctx: ctx, 43 | base: newBaseCollector(client, logger.With("collector", "profile")), 44 | compatibleMode: compatible, 45 | topologyInfo: topology, 46 | profiletimets: profileTimeTS, 47 | } 48 | } 49 | 50 | func (d *profileCollector) Describe(ch chan<- *prometheus.Desc) { 51 | d.base.Describe(d.ctx, ch, d.collect) 52 | } 53 | 54 | func (d *profileCollector) Collect(ch chan<- prometheus.Metric) { 55 | d.base.Collect(ch) 56 | } 57 | 58 | func (d *profileCollector) collect(ch chan<- prometheus.Metric) { 59 | defer measureCollectTime(ch, "mongodb", "profile")() 60 | 61 | logger := d.base.logger 62 | client := d.base.client 63 | timeScrape := d.profiletimets 64 | 65 | databases, err := databases(d.ctx, client, nil, nil) 66 | if err != nil { 67 | logger.Warn("cannot get databases", "error", err) 68 | return 69 | } 70 | 71 | // Now time + '--collector.profile-time-ts' 72 | ts := primitive.NewDateTimeFromTime(time.Now().Add(-time.Duration(time.Second * time.Duration(timeScrape)))) 73 | 74 | labels := d.topologyInfo.baseLabels() 75 | 76 | // Get all slow queries from all databases 77 | cmd := bson.M{"ts": bson.M{"$gte": ts}} 78 | for _, db := range databases { 79 | res, err := client.Database(db).Collection("system.profile").CountDocuments(d.ctx, cmd) 80 | if err != nil { 81 | logger.Warn("cannot get profile count for database", "database", db, "error", err) 82 | break 83 | } 84 | labels["database"] = db 85 | 86 | m := primitive.M{"count": res} 87 | 88 | logger.Debug("profile response from MongoDB:") 89 | debugResult(logger, primitive.M{db: m}) 90 | 91 | for _, metric := range makeMetrics("profile_slow_query", m, labels, d.compatibleMode) { 92 | ch <- metric 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /internal/util/util.go: -------------------------------------------------------------------------------- 1 | // mongodb_exporter 2 | // Copyright (C) 2023 Percona LLC 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package util 17 | 18 | import ( 19 | "context" 20 | 21 | "go.mongodb.org/mongo-driver/bson" 22 | "go.mongodb.org/mongo-driver/mongo" 23 | "go.mongodb.org/mongo-driver/x/mongo/driver/topology" 24 | 25 | "github.com/percona/mongodb_exporter/internal/proto" 26 | ) 27 | 28 | const ( 29 | ErrNotYetInitialized = int32(94) 30 | ErrNoReplicationEnabled = int32(76) 31 | ErrNotPrimaryOrSecondary = int32(13436) 32 | ) 33 | 34 | // MyState returns the replica set and the instance's state if available. 35 | func MyState(ctx context.Context, client *mongo.Client) (string, int, error) { 36 | var status proto.ReplicaSetStatus 37 | 38 | err := client.Database("admin").RunCommand(ctx, bson.M{"replSetGetStatus": 1}).Decode(&status) 39 | if err != nil { 40 | return "", 0, err 41 | } 42 | 43 | return status.Set, int(status.MyState), nil 44 | } 45 | 46 | // MyRole returns the role of the mongo instance. 47 | func MyRole(ctx context.Context, client *mongo.Client) (*proto.HelloResponse, error) { 48 | var role proto.HelloResponse 49 | err := client.Database("admin").RunCommand(ctx, bson.M{"isMaster": 1}).Decode(&role) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return &role, nil 55 | } 56 | 57 | func ReplicasetConfig(ctx context.Context, client *mongo.Client) (*proto.ReplicasetConfig, error) { 58 | var rs proto.ReplicasetConfig 59 | if err := client.Database("admin").RunCommand(ctx, bson.M{"replSetGetConfig": 1}).Decode(&rs); err != nil { 60 | return nil, err 61 | } 62 | 63 | return &rs, nil 64 | } 65 | 66 | func IsReplicationNotEnabledError(err mongo.CommandError) bool { 67 | return err.Code == ErrNotYetInitialized || err.Code == ErrNoReplicationEnabled || 68 | err.Code == ErrNotPrimaryOrSecondary 69 | } 70 | 71 | func ClusterID(ctx context.Context, client *mongo.Client) (string, error) { 72 | var cv proto.ConfigVersion 73 | if err := client.Database("config").Collection("version").FindOne(ctx, bson.M{}).Decode(&cv); err == nil { 74 | return cv.ClusterID.Hex(), nil 75 | } 76 | 77 | var si proto.ShardIdentity 78 | 79 | filter := bson.M{"_id": "shardIdentity"} 80 | 81 | if err := client.Database("admin").Collection("system.version").FindOne(ctx, filter).Decode(&si); err == nil { 82 | return si.ClusterID.Hex(), nil 83 | } 84 | 85 | rc, err := ReplicasetConfig(ctx, client) 86 | if err != nil { 87 | if e, ok := err.(mongo.CommandError); ok && IsReplicationNotEnabledError(e) { 88 | return "", nil 89 | } 90 | if _, ok := err.(topology.ServerSelectionError); ok { 91 | return "", nil 92 | } 93 | return "", err 94 | } 95 | 96 | return rc.Config.Settings.ReplicaSetID.Hex(), nil 97 | } 98 | -------------------------------------------------------------------------------- /test-setup/scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # `mongosh` is used starting from MongoDB 5.x 3 | MONGODB_CLIENT="mongosh --quiet" 4 | PARSED=(${VERSION//:/ }) 5 | MONGODB_VERSION=${PARSED[1]} 6 | MONGODB_VENDOR=${PARSED[0]} 7 | 8 | if [ "`echo ${MONGODB_VERSION} | cut -c 1`" = "4" ]; then 9 | MONGODB_CLIENT="mongo" 10 | fi 11 | if [ "`echo ${MONGODB_VERSION} | cut -c 1`" = "5" ] && [ ${MONGODB_VENDOR} == "percona/percona-server-mongodb" ]; then 12 | MONGODB_CLIENT="mongo" 13 | fi 14 | echo "MongoDB vendor, client and version: ${MONGODB_VENDOR} ${MONGODB_CLIENT} ${MONGODB_VERSION}" 15 | 16 | mongodb1=`getent hosts ${MONGO1} | awk '{ print $1 }'` 17 | mongodb2=`getent hosts ${MONGO2} | awk '{ print $1 }'` 18 | mongodb3=`getent hosts ${MONGO3} | awk '{ print $1 }'` 19 | arbiter=`getent hosts ${ARBITER} | awk '{ print $1 }'` 20 | 21 | username=${MONGO_INITDB_ROOT_USERNAME} 22 | password=${MONGO_INITDB_ROOT_PASSWORD} 23 | backups_dir=${PBM_BACKUPS_DIR} 24 | 25 | port=${PORT:-27017} 26 | 27 | echo "Waiting for startup.." 28 | until ${MONGODB_CLIENT} --host ${mongodb1}:${port} --eval 'quit(db.runCommand({ ping: 1 }).ok ? 0 : 2)' &>/dev/null; do 29 | printf '.' 30 | sleep 1 31 | done 32 | 33 | echo "Started.." 34 | 35 | echo setup.sh time now: `date +"%T" ` 36 | 37 | 38 | function cnf_servers() { 39 | echo "setup cnf servers on ${MONGO1}(${mongodb1}:${port})" 40 | ${MONGODB_CLIENT} --host ${mongodb1}:${port} <