Merge pull request #92 from ysyneu/main

Add MongoDB metrics collector
This commit is contained in:
ulricqin 2022-07-13 15:03:28 +08:00 committed by GitHub
commit 745e7a94a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 5061 additions and 5 deletions

View File

@ -23,6 +23,7 @@ import (
_ "flashcat.cloud/categraf/inputs/linux_sysctl_fs"
_ "flashcat.cloud/categraf/inputs/logstash"
_ "flashcat.cloud/categraf/inputs/mem"
_ "flashcat.cloud/categraf/inputs/mongodb"
_ "flashcat.cloud/categraf/inputs/mysql"
_ "flashcat.cloud/categraf/inputs/net"
_ "flashcat.cloud/categraf/inputs/net_response"

View File

@ -0,0 +1,56 @@
[[instances]]
# log level, enum: panic, fatal, error, warn, warning, info, debug, trace, defaults to info.
log_level = "info"
# append some const labels to metrics
# NOTICE! the instance label is required for dashboards
labels = { instance="mongo-cluster-01" }
# mongodb dsn, see https://www.mongodb.com/docs/manual/reference/connection-string/
mongodb_uri = "mongodb://127.0.0.1:27017"
# if you don't specify the username or password in the mongodb_uri, you can set here.
# This will overwrite the dsn, it would be helpful when special characters existing in the username or password and you don't want to encode them.
# NOTICE! this user must be granted enough rights to query needed stats, see ../inputs/mongodb/README.md
username = "username@Bj"
password = "password@Bj"
# if set to true, use the direct connection way
# direct_connect = true
# collect all means you collect all the metrics, if set, all below enable_xxx flags in this section will be ignored
collect_all = true
# if set to true, collect databases metrics
# enable_db_stats = true
# if set to true, collect getDiagnosticData metrics
# enable_diagnostic_data = true
# if set to true, collect replSetGetStatus metrics
# enable_replicaset_status = true
# if set to true, collect top metrics by admin command
# enable_top_metrics = true
# if set to true, collect index metrics. You should specify one of the coll_stats_namespaces and the discovering_mode flags.
# enable_index_stats = true
# if set to true, collect collections metrics. You should specify one of the coll_stats_namespaces and the discovering_mode flags.
# enable_coll_stats = true
# Only get stats for the collections matching this list of namespaces. if none set, discovering_mode will be enabled.
# Example: db1.col1,db.col1
# coll_stats_namespaces = []
# Only get stats for index with the collections matching this list of namespaces.
# Example: db1.col1,db.col1
# index_stats_collections = []
# if set to true, replace -1 to DESC for label key_name of the descending_index metrics
# enable_override_descending_index = true
# which exposes metrics with 0.1x compatible metric names has been implemented which simplifies migration from the old version to the current version.
# compatible_mode = true
# [[instances]]
# # interval = global.interval * interval_times
# interval_times = 1
# log_level = "error"
# append some labels to metrics
# labels = { instance="mongo-cluster-02" }
# mongodb_uri = "mongodb://username:password@127.0.0.1:27017"
# collect_all = true
# compatible_mode = true

11
go.mod
View File

@ -3,6 +3,7 @@ module flashcat.cloud/categraf
go 1.18
require (
github.com/AlekSi/pointer v1.2.0
github.com/ClickHouse/clickhouse-go/v2 v2.0.15
github.com/Shopify/sarama v1.34.1
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137
@ -42,15 +43,19 @@ require (
github.com/open-telemetry/opentelemetry-collector-contrib/receiver/kafkareceiver v0.54.0
github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinreceiver v0.54.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/percona/percona-toolkit v0.0.0-20211210121818-b2860eee3152
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.12.2
github.com/prometheus/client_model v0.2.0
github.com/prometheus/common v0.35.0
github.com/prometheus/prometheus v0.36.2
github.com/shirou/gopsutil/v3 v3.22.5
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.4
github.com/toolkits/pkg v1.3.0
github.com/ulricqin/gosnmp v0.0.1
github.com/xdg/scram v1.0.5
go.mongodb.org/mongo-driver v1.9.1
go.opentelemetry.io/collector v0.54.0
go.opentelemetry.io/otel/metric v0.30.0
go.opentelemetry.io/otel/trace v1.7.0
@ -206,7 +211,6 @@ require (
github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/alertmanager v0.24.0 // indirect
@ -219,9 +223,9 @@ require (
github.com/rs/cors v1.8.2 // indirect
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.4.0 // indirect
@ -240,8 +244,8 @@ require (
github.com/xdg-go/scram v1.1.1 // indirect
github.com/xdg-go/stringprep v1.0.3 // indirect
github.com/xdg/stringprep v1.0.3 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
go.mongodb.org/mongo-driver v1.8.3 // indirect
go.opencensus.io v0.23.0 // indirect
go.opentelemetry.io/collector/pdata v0.54.0 // indirect
go.opentelemetry.io/collector/semconv v0.54.0 // indirect
@ -269,6 +273,7 @@ require (
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gotest.tools/v3 v3.2.0 // indirect

11
go.sum
View File

@ -57,6 +57,8 @@ code.cloudfoundry.org/bytefmt v0.0.0-20190710193110-1eb035ffe2b6/go.mod h1:wN/zk
contrib.go.opencensus.io/exporter/prometheus v0.4.1 h1:oObVeKo2NxpdF/fIfrPsNj6K0Prg0R0mHM+uANlYMiM=
contrib.go.opencensus.io/exporter/prometheus v0.4.1/go.mod h1:t9wvfitlUjGXG2IXAZsuFq26mDGid/JwCEXp+gTG/9U=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
github.com/Azure/azure-sdk-for-go v65.0.0+incompatible h1:HzKLt3kIwMm4KeJYTdx9EbjRYTySD/t8i1Ee/W5EGXw=
github.com/Azure/azure-sdk-for-go v65.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
@ -954,6 +956,8 @@ github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhEC
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.0.0-beta.8 h1:dy81yyLYJDwMTifq24Oi/IslOslRrDSb3jwDggjz3Z0=
github.com/pelletier/go-toml/v2 v2.0.0-beta.8/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/percona/percona-toolkit v0.0.0-20211210121818-b2860eee3152 h1:3AOGevjw3JfLPqzos6VnF2L9T0UosjHu4IO8+Ogk08w=
github.com/percona/percona-toolkit v0.0.0-20211210121818-b2860eee3152/go.mod h1:CCa6vyT51VeEG5KcJ2smk4/HyxX6Aunt8RxfnwlC85A=
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pierrec/cmdflag v0.0.2/go.mod h1:a3zKGZ3cdQUfxjd0RGMLZr8xI3nvpJOB+m6o/1X5BmU=
@ -1038,6 +1042,7 @@ github.com/schollz/progressbar/v2 v2.13.2/go.mod h1:6YZjqdthH6SCZKv2rqGryrxPtfmR
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shirou/gopsutil v2.19.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/gopsutil/v3 v3.22.5 h1:atX36I/IXgFiB81687vSiBI5zrMsxcIBkP9cQMJQoJA=
github.com/shirou/gopsutil/v3 v3.22.5/go.mod h1:so9G9VzeHt/hsd0YwqprnjHnfARAUktauykSbr+y2gA=
@ -1141,6 +1146,7 @@ github.com/xdg/stringprep v1.0.3 h1:cmL5Enob4W83ti/ZHuZLuKD/xqJfus4fVPwE+/BDm+4=
github.com/xdg/stringprep v1.0.3/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -1158,8 +1164,9 @@ go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsX
go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0=
go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg=
go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng=
go.mongodb.org/mongo-driver v1.8.3 h1:TDKlTkGDKm9kkJVUOAXDK5/fkqKHJVwYQSpoRfB43R4=
go.mongodb.org/mongo-driver v1.8.3/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY=
go.mongodb.org/mongo-driver v1.9.1 h1:m078y9v7sBItkt1aaoe2YlvWEXcD263e1a4E1fBrJ1c=
go.mongodb.org/mongo-driver v1.9.1/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@ -1787,6 +1794,8 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4=
gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=

View File

@ -16,7 +16,7 @@ const capMetricChan = 1000
var parser = new(pp.Parser)
func Collect(e prometheus.Collector, slist *list.SafeList) error {
func Collect(e prometheus.Collector, slist *list.SafeList, constLabels ...map[string]string) error {
if e == nil {
return errors.New("exporter must not be nil")
}
@ -54,6 +54,12 @@ func Collect(e prometheus.Collector, slist *list.SafeList) error {
labels[*kv.Name] = *kv.Value
}
for _, kvs := range constLabels {
for k, v := range kvs {
labels[k] = v
}
}
switch {
case dtoMetric.Counter != nil:
_ = slist.PushFront(types.NewSample(desc.Name(), *dtoMetric.Counter.Value, labels))

26
inputs/mongodb/README.md Normal file
View File

@ -0,0 +1,26 @@
# mongodb
mongodb 监控采集插件由mongodb-exporterhttps://github.com/percona/mongodb_exporter封装而来。
## Configuration
- 配置文件,[参考示例](../../conf/input.mongodb/mongodb.toml)
- 配置权限,至少授予以下权限给配置文件中用于连接 MongoDB 的 user 才能收集指标:
```
{
"role":"clusterMonitor",
"db":"admin"
},
{
"role":"read",
"db":"local"
}
```
更详细的权限配置请参考[官方文档](https://www.mongodb.com/docs/manual/reference/built-in-roles/#mongodb-authrole-clusterMonitor)
## 监控大盘和告警规则
同级目录下的 dashboard.json、alerts.json 可以直接导入夜莺使用。

282
inputs/mongodb/alerts.json Normal file
View File

@ -0,0 +1,282 @@
[
{
"name": "Mongo出现Assert错误",
"note": "",
"prod": "",
"algorithm": "",
"algo_params": null,
"delay": 0,
"severity": 3,
"disabled": 0,
"prom_for_duration": 1800,
"prom_ql": "rate(mongodb_ss_asserts{assert_type=~\"regular|message\"}[5m]) > 0",
"prom_eval_interval": 15,
"enable_stime": "00:00",
"enable_etime": "23:59",
"enable_days_of_week": [
"1",
"2",
"3",
"4",
"5",
"6",
"0"
],
"enable_in_bg": 0,
"notify_recovered": 1,
"notify_channels": [],
"notify_repeat_step": 60,
"notify_max_number": 0,
"recover_duration": 0,
"callbacks": [],
"runbook_url": "",
"append_tags": [
"alertname=MongoAssertsDetected"
]
},
{
"name": "Mongo出现游标超时",
"note": "",
"prod": "",
"algorithm": "",
"algo_params": null,
"delay": 0,
"severity": 2,
"disabled": 0,
"prom_for_duration": 1800,
"prom_ql": "rate(mongodb_ss_metrics_cursor_timedOut[5m]) > 0",
"prom_eval_interval": 15,
"enable_stime": "00:00",
"enable_etime": "23:59",
"enable_days_of_week": [
"1",
"2",
"3",
"4",
"5",
"6",
"0"
],
"enable_in_bg": 0,
"notify_recovered": 1,
"notify_channels": [],
"notify_repeat_step": 60,
"notify_max_number": 0,
"recover_duration": 0,
"callbacks": [],
"runbook_url": "",
"append_tags": [
"alertname=MongoRecurrentCursorTimeout"
]
},
{
"name": "Mongo出现页错误中断",
"note": "",
"prod": "",
"algorithm": "",
"algo_params": null,
"delay": 0,
"severity": 2,
"disabled": 0,
"prom_for_duration": 1800,
"prom_ql": "rate(mongodb_ss_extra_info_page_faults[5m]) > 0",
"prom_eval_interval": 15,
"enable_stime": "00:00",
"enable_etime": "23:59",
"enable_days_of_week": [
"1",
"2",
"3",
"4",
"5",
"6",
"0"
],
"enable_in_bg": 0,
"notify_recovered": 1,
"notify_channels": [],
"notify_repeat_step": 60,
"notify_max_number": 0,
"recover_duration": 0,
"callbacks": [],
"runbook_url": "",
"append_tags": [
"alertname=MongoRecurrentMemoryPageFaults"
]
},
{
"name": "Mongo刚刚有重启请注意",
"note": "",
"prod": "",
"algorithm": "",
"algo_params": null,
"delay": 0,
"severity": 3,
"disabled": 0,
"prom_for_duration": 0,
"prom_ql": "mongodb_ss_uptime < 60",
"prom_eval_interval": 15,
"enable_stime": "00:00",
"enable_etime": "23:59",
"enable_days_of_week": [
"1",
"2",
"3",
"4",
"5",
"6",
"0"
],
"enable_in_bg": 0,
"notify_recovered": 1,
"notify_channels": [],
"notify_repeat_step": 60,
"notify_max_number": 0,
"recover_duration": 0,
"callbacks": [],
"runbook_url": "",
"append_tags": [
"alertname=MongoRestarted"
]
},
{
"name": "Mongo副本集主从延迟超过30s",
"note": "",
"prod": "",
"algorithm": "",
"algo_params": null,
"delay": 0,
"severity": 1,
"disabled": 0,
"prom_for_duration": 60,
"prom_ql": "mongodb_mongod_replset_member_replication_lag > 30",
"prom_eval_interval": 15,
"enable_stime": "00:00",
"enable_etime": "23:59",
"enable_days_of_week": [
"1",
"2",
"3",
"4",
"5",
"6",
"0"
],
"enable_in_bg": 0,
"notify_recovered": 1,
"notify_channels": [],
"notify_repeat_step": 60,
"notify_max_number": 0,
"recover_duration": 0,
"callbacks": [],
"runbook_url": "",
"append_tags": [
"alertname=MongoSlaveReplicationLag(>30s)"
]
},
{
"name": "Mongo实例挂了",
"note": "",
"prod": "",
"algorithm": "",
"algo_params": null,
"delay": 0,
"severity": 1,
"disabled": 0,
"prom_for_duration": 0,
"prom_ql": "mongodb_up < 1",
"prom_eval_interval": 15,
"enable_stime": "00:00",
"enable_etime": "23:59",
"enable_days_of_week": [
"1",
"2",
"3",
"4",
"5",
"6",
"0"
],
"enable_in_bg": 0,
"notify_recovered": 1,
"notify_channels": [],
"notify_repeat_step": 60,
"notify_max_number": 0,
"recover_duration": 0,
"callbacks": [],
"runbook_url": "",
"append_tags": [
"alertname=MongoServerDown"
]
},
{
"name": "Mongo操作平均耗时超过250秒",
"note": "",
"prod": "",
"algorithm": "",
"algo_params": null,
"delay": 0,
"severity": 2,
"disabled": 0,
"prom_for_duration": 600,
"prom_ql": "rate(mongodb_ss_opLatencies_latency[5m]) / rate(mongodb_ss_opLatencies_ops[5m]) > 250000",
"prom_eval_interval": 15,
"enable_stime": "00:00",
"enable_etime": "23:59",
"enable_days_of_week": [
"1",
"2",
"3",
"4",
"5",
"6",
"0"
],
"enable_in_bg": 0,
"notify_recovered": 1,
"notify_channels": [],
"notify_repeat_step": 60,
"notify_max_number": 0,
"recover_duration": 0,
"callbacks": [],
"runbook_url": "",
"append_tags": [
"alertname=MongoOperationHighLatency"
]
},
{
"name": "Mongo连接数已超过80%",
"note": "",
"prod": "",
"algorithm": "",
"algo_params": null,
"delay": 0,
"severity": 2,
"disabled": 0,
"prom_for_duration": 120,
"prom_ql": "avg by (instance) (mongodb_ss_connections{conn_type=\"current\"}) / avg by (instance) (mongodb_ss_connections{conn_type=\"available\"}) * 100 > 80",
"prom_eval_interval": 15,
"enable_stime": "00:00",
"enable_etime": "23:59",
"enable_days_of_week": [
"1",
"2",
"3",
"4",
"5",
"6",
"0"
],
"enable_in_bg": 0,
"notify_recovered": 1,
"notify_channels": [],
"notify_repeat_step": 60,
"notify_max_number": 0,
"recover_duration": 0,
"callbacks": [],
"runbook_url": "",
"append_tags": [
"alertname=MongoTooManyConnections(>80%)"
]
}
]

View File

@ -0,0 +1,936 @@
{
"name": "MongoDB Overview - 模板",
"tags": "Prometheus MongoDB",
"configs": {
"var": [
{
"name": "instance",
"definition": "label_values(mongodb_up,instance)"
}
],
"panels": [
{
"id": "dd7882d6-9502-4a76-845a-efdbcdb25466",
"type": "row",
"name": "Basic Info",
"layout": {
"h": 1,
"w": 24,
"x": 0,
"y": 0,
"i": "dd7882d6-9502-4a76-845a-efdbcdb25466",
"isResizable": false
}
},
{
"targets": [
{
"refId": "A",
"expr": "mongodb_up{instance=\"$instance\"}",
"time": {
"num": 1,
"unit": "hour",
"description": "小时"
}
}
],
"name": "Up",
"description": "实例数",
"custom": {
"textMode": "value",
"colorMode": "value",
"calc": "lastNotNull",
"colSpan": 1,
"textSize": {}
},
"options": {
"valueMappings": [
{
"type": "range",
"match": {
"from": 1
},
"result": {
"color": "#53b503"
}
},
{
"type": "range",
"match": {
"special": null,
"from": 0,
"to": 1
},
"result": {
"color": "#e70d0d"
}
}
],
"standardOptions": {}
},
"version": "2.0.0",
"type": "stat",
"layout": {
"h": 7,
"w": 6,
"x": 0,
"y": 1,
"i": "8ab8a2a8-9545-4e58-b9ba-34d68408fdda",
"isResizable": true
},
"id": "8ab8a2a8-9545-4e58-b9ba-34d68408fdda"
},
{
"targets": [
{
"refId": "A",
"expr": "mongodb_ss_uptime{instance='$instance'}",
"time": {
"num": 1,
"unit": "hour",
"description": "小时"
}
}
],
"name": "Uptime",
"description": "启用时长",
"custom": {
"textMode": "value",
"colorMode": "value",
"calc": "lastNotNull",
"colSpan": 1,
"textSize": {
"title": null
}
},
"options": {
"valueMappings": [
{
"type": "range",
"match": {
"to": 1800
},
"result": {
"color": "#ec7718"
}
},
{
"type": "range",
"match": {
"from": 1800
},
"result": {
"color": "#53b503"
}
}
],
"standardOptions": {
"util": "humantimeSeconds"
}
},
"version": "2.0.0",
"type": "stat",
"layout": {
"h": 7,
"w": 6,
"x": 6,
"y": 1,
"i": "7ac43abc-bb8f-4af1-9c62-f24e9c467390",
"isResizable": true
},
"id": "7ac43abc-bb8f-4af1-9c62-f24e9c467390"
},
{
"targets": [
{
"refId": "A",
"expr": "mongodb_ss_mem_resident{instance='$instance'} * 1024 * 1024",
"legend": "resident"
},
{
"expr": "mongodb_ss_mem_virtual{instance='$instance'} * 1024 * 1024",
"refId": "B",
"legend": "virtual"
}
],
"name": "Memory",
"description": "内存占用MiB",
"options": {
"tooltip": {
"mode": "all",
"sort": "desc"
},
"legend": {
"displayMode": "hidden"
},
"standardOptions": {
"util": "bytesIEC"
},
"thresholds": {}
},
"custom": {
"drawStyle": "lines",
"lineInterpolation": "smooth",
"fillOpacity": 0.5,
"stack": "off"
},
"version": "2.0.0",
"type": "timeseries",
"layout": {
"h": 7,
"w": 6,
"x": 12,
"y": 1,
"i": "0bffd1e3-ca5b-46a8-b7ba-c04c4db74b62",
"isResizable": true
},
"id": "0bffd1e3-ca5b-46a8-b7ba-c04c4db74b62"
},
{
"targets": [
{
"refId": "A",
"expr": "rate(mongodb_ss_extra_info_page_faults{instance=\"$instance\"}[5m])",
"legend": "total"
}
],
"name": "Page Faults",
"description": "页缺失中断次数 Page faults indicate that requests are processed from disk either because an index is missing or there is not enough memory for the data set. Consider increasing memory or sharding out.",
"options": {
"tooltip": {
"mode": "all",
"sort": "desc"
},
"legend": {
"displayMode": "hidden"
},
"standardOptions": {
"util": "none",
"decimals": null
},
"thresholds": {}
},
"custom": {
"drawStyle": "lines",
"lineInterpolation": "smooth",
"fillOpacity": 0.5,
"stack": "off"
},
"version": "2.0.0",
"type": "timeseries",
"layout": {
"h": 7,
"w": 6,
"x": 18,
"y": 1,
"i": "7d00bb33-1018-4af1-a498-17cd8b517f2f",
"isResizable": true
},
"id": "7d00bb33-1018-4af1-a498-17cd8b517f2f"
},
{
"targets": [
{
"refId": "A",
"expr": "rate(mongodb_ss_network_bytesOut{instance='$instance'}[5m])",
"legend": "bytesOut"
},
{
"expr": "rate(mongodb_ss_network_bytesIn{instance='$instance'}[5m])",
"refId": "B",
"legend": "bytesIn"
}
],
"name": "Network I/O",
"description": "网络流量byte",
"options": {
"tooltip": {
"mode": "all",
"sort": "none"
},
"legend": {
"displayMode": "hidden"
},
"standardOptions": {
"util": "bytesSI"
},
"thresholds": {}
},
"custom": {
"drawStyle": "lines",
"lineInterpolation": "smooth",
"fillOpacity": 0.5,
"stack": "off"
},
"version": "2.0.0",
"type": "timeseries",
"layout": {
"h": 7,
"w": 6,
"x": 0,
"y": 8,
"i": "3efb0ec7-7ebf-4d0c-b6a0-b498ada6b4a5",
"isResizable": true
},
"id": "3efb0ec7-7ebf-4d0c-b6a0-b498ada6b4a5"
},
{
"targets": [
{
"refId": "A",
"expr": "mongodb_ss_connections{instance=\"$instance\", conn_type=\"current\"}",
"legend": "current"
}
],
"name": "Connections",
"description": "连接数 Keep in mind the hard limit on the maximum number of connections set by your distribution.",
"options": {
"tooltip": {
"mode": "all",
"sort": "none"
},
"legend": {
"displayMode": "hidden"
},
"standardOptions": {},
"thresholds": {}
},
"custom": {
"drawStyle": "lines",
"lineInterpolation": "smooth",
"fillOpacity": 0.5,
"stack": "off"
},
"version": "2.0.0",
"type": "timeseries",
"layout": {
"h": 7,
"w": 6,
"x": 6,
"y": 8,
"i": "f55e9333-9e0d-4eb0-82e1-0adc38e34316",
"isResizable": true
},
"id": "f55e9333-9e0d-4eb0-82e1-0adc38e34316"
},
{
"targets": [
{
"refId": "A",
"expr": "rate(mongodb_ss_asserts{instance=\"$instance\"}[5m])",
"legend": "{{assert_type}}"
}
],
"name": "Assert Events",
"description": "断言错误次数 Asserts are not important by themselves, but you can correlate spikes with other graphs.",
"options": {
"tooltip": {
"mode": "all",
"sort": "none"
},
"legend": {
"displayMode": "hidden"
},
"standardOptions": {},
"thresholds": {}
},
"custom": {
"drawStyle": "lines",
"lineInterpolation": "smooth",
"fillOpacity": 0.5,
"stack": "off"
},
"version": "2.0.0",
"type": "timeseries",
"layout": {
"h": 7,
"w": 6,
"x": 12,
"y": 8,
"i": "14b7339c-07bd-4798-b127-7db7117bc664",
"isResizable": true
},
"id": "14b7339c-07bd-4798-b127-7db7117bc664"
},
{
"targets": [
{
"refId": "A",
"expr": "mongodb_ss_globalLock_currentQueue{instance=\"$instance\"}",
"legend": "{{count_type}}"
}
],
"name": "Lock Queue",
"description": "等待获取锁操作数量 Any number of queued operations for long periods of time is an indication of possible issues. Find the cause and fix it before requests get stuck in the queue.",
"options": {
"tooltip": {
"mode": "all",
"sort": "none"
},
"legend": {
"displayMode": "hidden"
},
"standardOptions": {},
"thresholds": {}
},
"custom": {
"drawStyle": "lines",
"lineInterpolation": "smooth",
"fillOpacity": 0.5,
"stack": "off"
},
"version": "2.0.0",
"type": "timeseries",
"layout": {
"h": 7,
"w": 6,
"x": 18,
"y": 8,
"i": "5dc13288-5642-494f-b60b-d12ee568d5bd",
"isResizable": true
},
"id": "5dc13288-5642-494f-b60b-d12ee568d5bd"
},
{
"id": "39584c81-90e3-4a89-982a-4881429e5091",
"type": "row",
"name": "Operation Info",
"layout": {
"h": 1,
"w": 24,
"x": 0,
"y": 15,
"i": "39584c81-90e3-4a89-982a-4881429e5091",
"isResizable": false
}
},
{
"targets": [
{
"refId": "A",
"expr": "rate(mongodb_ss_opcounters{instance=\"$instance\", type!=\"command\"}[5m])",
"legend": "{{legacy_op_type}}"
},
{
"expr": "rate(mongodb_ss_opcountersRepl{instance=\"$instance\", type!~\"(command|query|getmore)\"}[5m]) ",
"refId": "B",
"legend": "repl_{{legacy_op_type}}"
},
{
"expr": "rate(mongodb_ss_metrics_ttl_deletedDocuments{instance=\"$instance\"}[5m]) ",
"refId": "C",
"legend": "ttl_delete"
}
],
"name": "Command Operations",
"description": "接收请求数 Shows how many times a command is executed per second on average during the selected interval.",
"options": {
"tooltip": {
"mode": "all",
"sort": "desc"
},
"legend": {
"displayMode": "hidden"
},
"standardOptions": {},
"thresholds": {}
},
"custom": {
"drawStyle": "lines",
"lineInterpolation": "smooth",
"fillOpacity": 0.5,
"stack": "off"
},
"version": "2.0.0",
"type": "timeseries",
"layout": {
"h": 7,
"w": 12,
"x": 0,
"y": 16,
"i": "cbce4c2b-c215-4aab-a093-7ae95472ef1f",
"isResizable": true
},
"id": "cbce4c2b-c215-4aab-a093-7ae95472ef1f"
},
{
"targets": [
{
"refId": "A",
"expr": "rate(mongodb_ss_metrics_document{instance=\"$instance\"}[5m])",
"legend": "{{doc_op_type}}"
}
],
"name": "Document Operations",
"description": "文档操作数 When used in combination with 'Command Operations', this graph can help identify write amplification. For example, when one insert or update command actually inserts or updates hundreds, thousands, or even millions of documents.",
"options": {
"tooltip": {
"mode": "all",
"sort": "none"
},
"legend": {
"displayMode": "hidden"
},
"standardOptions": {},
"thresholds": {}
},
"custom": {
"drawStyle": "lines",
"lineInterpolation": "smooth",
"fillOpacity": 0.5,
"stack": "off"
},
"version": "2.0.0",
"type": "timeseries",
"layout": {
"h": 7,
"w": 12,
"x": 12,
"y": 16,
"i": "e4db591b-b434-4687-b7f0-a467ef5f6cc1",
"isResizable": true
},
"id": "e4db591b-b434-4687-b7f0-a467ef5f6cc1"
},
{
"targets": [
{
"refId": "A",
"expr": "rate(mongodb_ss_opLatencies_latency{instance='$instance'}[5m]) / rate(mongodb_ss_opLatencies_ops{instance='$instance'}[5m]) / 1000",
"legend": "{{op_type}}"
}
],
"name": "Response Time",
"description": "操作详情耗时(毫秒)",
"options": {
"tooltip": {
"mode": "all",
"sort": "none"
},
"legend": {
"displayMode": "hidden"
},
"standardOptions": {
"util": "milliseconds"
},
"thresholds": {}
},
"custom": {
"drawStyle": "lines",
"lineInterpolation": "smooth",
"fillOpacity": 0.5,
"stack": "off"
},
"version": "2.0.0",
"type": "timeseries",
"layout": {
"h": 7,
"w": 8,
"x": 0,
"y": 23,
"i": "32ca2e82-a2ad-4095-a352-9cd985b5dddc",
"isResizable": true
},
"id": "32ca2e82-a2ad-4095-a352-9cd985b5dddc"
},
{
"targets": [
{
"refId": "A",
"expr": "sum(increase(mongodb_ss_metrics_queryExecutor_scannedObjects{instance=\"$instance\"}[5m])) / sum(increase(mongodb_ss_metrics_document{instance=\"$instance\", doc_op_type=\"returned\"}[5m]))",
"legend": "Document"
},
{
"expr": "sum(increase(mongodb_ss_metrics_queryExecutor_scanned{instance=\"$instance\"}[5m])) / sum(increase(mongodb_ss_metrics_document{instance=\"$instance\", doc_op_type=\"returned\"}[5m]))",
"refId": "B",
"legend": "Index"
}
],
"name": "Query Efficiency",
"description": "查询效率",
"options": {
"tooltip": {
"mode": "all",
"sort": "none"
},
"legend": {
"displayMode": "hidden"
},
"standardOptions": {
"util": "percentUnit"
},
"thresholds": {}
},
"custom": {
"drawStyle": "lines",
"lineInterpolation": "smooth",
"fillOpacity": 0.5,
"stack": "off"
},
"version": "2.0.0",
"type": "timeseries",
"layout": {
"h": 7,
"w": 8,
"x": 8,
"y": 23,
"i": "98e954e5-943b-46ee-ad47-bb41c408be36",
"isResizable": true
},
"id": "98e954e5-943b-46ee-ad47-bb41c408be36"
},
{
"targets": [
{
"refId": "A",
"expr": "mongodb_ss_metrics_cursor_open{instance=\"$instance\"}",
"legend": "{{csr_type}}"
}
],
"name": "Cursors",
"description": "游标数量 Helps identify why connections are increasing. Shows active cursors compared to cursors being automatically killed after 10 minutes due to an application not closing the connection.",
"options": {
"tooltip": {
"mode": "all",
"sort": "none"
},
"legend": {
"displayMode": "hidden"
},
"standardOptions": {},
"thresholds": {}
},
"custom": {
"drawStyle": "lines",
"lineInterpolation": "smooth",
"fillOpacity": 0.5,
"stack": "off"
},
"version": "2.0.0",
"type": "timeseries",
"layout": {
"h": 7,
"w": 8,
"x": 16,
"y": 23,
"i": "bc58e613-7770-42b8-86e2-3248cfe8e28e",
"isResizable": true
},
"id": "bc58e613-7770-42b8-86e2-3248cfe8e28e"
},
{
"id": "96898c4d-9df7-43dd-886e-66640bacb9f2",
"type": "row",
"name": "Cache Info",
"layout": {
"h": 1,
"w": 24,
"x": 0,
"y": 30,
"i": "96898c4d-9df7-43dd-886e-66640bacb9f2",
"isResizable": false
}
},
{
"targets": [
{
"refId": "A",
"expr": "mongodb_ss_wt_cache_bytes_currently_in_the_cache{instance='$instance'}",
"legend": "currently_in_the_cache"
},
{
"expr": "mongodb_ss_wt_cache_bytes_dirty_in_the_cache_cumulative{instance='$instance'}",
"refId": "E",
"legend": "dirty_in_the_cache_cumulative"
},
{
"expr": "mongodb_ss_wt_cache_bytes_allocated_for_updates{instance='$instance'}",
"refId": "B",
"legend": "allocated_for_updates"
},
{
"expr": "mongodb_ss_wt_cache_bytes_belonging_to_the_history_store_table_in_the_cache{instance='$instance'}",
"refId": "D",
"legend": "belonging_to_the_history_store_table_in_the_cache"
},
{
"expr": "mongodb_ss_wt_cache_bytes_belonging_to_page_images_in_the_cache{instance='$instance'}",
"refId": "C",
"legend": "belonging_to_page_images_in_the_cache"
},
{
"expr": "mongodb_ss_wt_cache_bytes_not_belonging_to_page_images_in_the_cache{instance='$instance'}",
"refId": "F",
"legend": "not_belonging_to_page_images_in_the_cache"
}
],
"name": "Cache Size",
"description": "缓存大小byte",
"options": {
"tooltip": {
"mode": "all",
"sort": "none"
},
"legend": {
"displayMode": "hidden"
},
"standardOptions": {
"util": "bytesIEC"
},
"thresholds": {}
},
"custom": {
"drawStyle": "lines",
"lineInterpolation": "smooth",
"fillOpacity": 0.5,
"stack": "off"
},
"version": "2.0.0",
"type": "timeseries",
"layout": {
"h": 7,
"w": 6,
"x": 0,
"y": 31,
"i": "074ec28a-6760-480f-8ae3-9b7a6ed37e45",
"isResizable": true
},
"id": "074ec28a-6760-480f-8ae3-9b7a6ed37e45"
},
{
"targets": [
{
"refId": "A",
"expr": "rate(mongodb_ss_wt_cache_bytes_read_into_cache{instance='$instance'}[5m])",
"legend": "read_into_cache"
},
{
"expr": "rate(mongodb_ss_wt_cache_bytes_written_from_cache{instance='$instance'}[5m])",
"refId": "B",
"legend": "written_from_cache"
}
],
"name": "Cache I/O",
"description": "写入或读取的缓存数据大小byte",
"options": {
"tooltip": {
"mode": "all",
"sort": "none"
},
"legend": {
"displayMode": "hidden"
},
"standardOptions": {
"util": "bytesSI"
},
"thresholds": {}
},
"custom": {
"drawStyle": "lines",
"lineInterpolation": "smooth",
"fillOpacity": 0.5,
"stack": "off"
},
"version": "2.0.0",
"type": "timeseries",
"layout": {
"h": 7,
"w": 6,
"x": 6,
"y": 31,
"i": "67b2e881-9949-4b04-a654-4e78c9e773af",
"isResizable": true
},
"id": "67b2e881-9949-4b04-a654-4e78c9e773af"
},
{
"targets": [
{
"refId": "A",
"expr": "100 * sum(mongodb_ss_wt_cache_tracked_dirty_pages_in_the_cache{instance='$instance'}) / sum(mongodb_ss_wt_cache_pages_read_into_cache{instance='$instance'} + mongodb_ss_wt_cache_pages_written_from_cache{instance='$instance'})",
"legend": "dirty rate"
}
],
"name": "Cache Dirty Pages Rate",
"description": "缓存脏页占比",
"options": {
"tooltip": {
"mode": "all",
"sort": "none"
},
"legend": {
"displayMode": "hidden"
},
"standardOptions": {
"util": "percent"
},
"thresholds": {}
},
"custom": {
"drawStyle": "lines",
"lineInterpolation": "smooth",
"fillOpacity": 0.5,
"stack": "off"
},
"version": "2.0.0",
"type": "timeseries",
"layout": {
"h": 7,
"w": 6,
"x": 12,
"y": 31,
"i": "3a8b5e4e-510e-4302-a1b0-f124277b2621",
"isResizable": true
},
"id": "3a8b5e4e-510e-4302-a1b0-f124277b2621"
},
{
"targets": [
{
"refId": "A",
"expr": "rate(mongodb_mongod_wiredtiger_cache_evicted_total{instance='$instance'}[5m])",
"legend": "evicted pages"
}
],
"name": "Cache Evicted Pages",
"description": "缓存剔除页数量",
"options": {
"tooltip": {
"mode": "all",
"sort": "none"
},
"legend": {
"displayMode": "hidden"
},
"standardOptions": {},
"thresholds": {}
},
"custom": {
"drawStyle": "lines",
"lineInterpolation": "smooth",
"fillOpacity": 0.5,
"stack": "off"
},
"version": "2.0.0",
"type": "timeseries",
"layout": {
"h": 7,
"w": 6,
"x": 18,
"y": 31,
"i": "8a61ef96-f386-4c58-b574-8cb2d968a8bb",
"isResizable": true
},
"id": "8a61ef96-f386-4c58-b574-8cb2d968a8bb"
},
{
"id": "f72662cc-92dd-4112-a5ca-987d7c82766d",
"type": "row",
"name": "ReplSet Info",
"layout": {
"h": 1,
"w": 24,
"x": 0,
"y": 38,
"i": "f72662cc-92dd-4112-a5ca-987d7c82766d",
"isResizable": false
}
},
{
"targets": [
{
"refId": "A",
"expr": "time() - max(mongodb_rs_members_electionDate)/1000"
}
],
"name": "Replset Election",
"description": "副本集选主时间",
"custom": {
"textMode": "value",
"colorMode": "value",
"calc": "lastNotNull",
"colSpan": 1,
"textSize": {}
},
"options": {
"valueMappings": [
{
"type": "range",
"match": {
"to": 1800
},
"result": {
"color": "#f24526"
}
},
{
"type": "range",
"match": {
"from": 1800
},
"result": {
"color": "#53b503"
}
}
],
"standardOptions": {
"util": "seconds",
"decimals": 1
}
},
"version": "2.0.0",
"type": "stat",
"layout": {
"h": 7,
"w": 12,
"x": 0,
"y": 39,
"i": "84df3245-a7fb-4aed-9879-adabbe60abec",
"isResizable": true
},
"id": "84df3245-a7fb-4aed-9879-adabbe60abec"
},
{
"targets": [
{
"refId": "A",
"expr": "mongodb_mongod_replset_member_replication_lag{instance=\"$instance\"}",
"legend": "{{name}}"
}
],
"name": "Replset Lag Seconds",
"description": "副本集成员主从同步延迟",
"options": {
"tooltip": {
"mode": "all",
"sort": "none"
},
"legend": {
"displayMode": "hidden"
},
"standardOptions": {
"util": "seconds"
},
"thresholds": {}
},
"custom": {
"drawStyle": "lines",
"lineInterpolation": "smooth",
"fillOpacity": 0.5,
"stack": "off"
},
"version": "2.0.0",
"type": "timeseries",
"layout": {
"h": 7,
"w": 12,
"x": 12,
"y": 39,
"i": "33453bc0-6b09-4299-8d3c-bd955d72ffee",
"isResizable": true
},
"id": "33453bc0-6b09-4299-8d3c-bd955d72ffee"
}
]
}
}

View File

@ -0,0 +1,84 @@
// mongodb_exporter
// Copyright (C) 2022 Percona LLC
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package exporter
import (
"context"
"sync"
"github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/mongo"
)
type baseCollector struct {
client *mongo.Client
logger *logrus.Logger
lock sync.Mutex
metricsCache []prometheus.Metric
}
// newBaseCollector creates a skeletal collector, which is used to create other collectors.
func newBaseCollector(client *mongo.Client, logger *logrus.Logger) *baseCollector {
return &baseCollector{
client: client,
logger: logger,
}
}
func (d *baseCollector) Describe(ctx context.Context, ch chan<- *prometheus.Desc, collect func(mCh chan<- prometheus.Metric)) {
select {
case <-ctx.Done():
return
default:
}
d.lock.Lock()
defer d.lock.Unlock()
d.metricsCache = make([]prometheus.Metric, 0, defaultCacheSize)
// This is a copy/paste of prometheus.DescribeByCollect(d, ch) with the aggreated functionality
// to populate the metrics cache. Since on each scrape Prometheus will call Describe and inmediatelly
// after it will call Collect, it is safe to populate the cache here.
metrics := make(chan prometheus.Metric)
go func() {
collect(metrics)
close(metrics)
}()
for m := range metrics {
d.metricsCache = append(d.metricsCache, m) // populate the cache
ch <- m.Desc()
}
}
func (d *baseCollector) Collect(ch chan<- prometheus.Metric, collect func(mCh chan<- prometheus.Metric)) {
d.lock.Lock()
defer d.lock.Unlock()
if len(d.metricsCache) > 0 {
for _, metric := range d.metricsCache {
ch <- metric
}
return
}
collect(ch)
}

View File

@ -0,0 +1,148 @@
// mongodb_exporter
// Copyright (C) 2017 Percona LLC
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package exporter
import (
"context"
"strings"
"github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
type collstatsCollector struct {
ctx context.Context
base *baseCollector
compatibleMode bool
discoveringMode bool
topologyInfo labelsGetter
collections []string
}
// newCollectionStatsCollector creates a collector for statistics about collections.
func newCollectionStatsCollector(ctx context.Context, client *mongo.Client, logger *logrus.Logger, compatible, discovery bool, topology labelsGetter, collections []string) *collstatsCollector {
return &collstatsCollector{
ctx: ctx,
base: newBaseCollector(client, logger),
compatibleMode: compatible,
discoveringMode: discovery,
topologyInfo: topology,
collections: collections,
}
}
func (d *collstatsCollector) Describe(ch chan<- *prometheus.Desc) {
d.base.Describe(d.ctx, ch, d.collect)
}
func (d *collstatsCollector) Collect(ch chan<- prometheus.Metric) {
d.base.Collect(ch, d.collect)
}
func (d *collstatsCollector) collect(ch chan<- prometheus.Metric) {
collections := d.collections
client := d.base.client
logger := d.base.logger
if d.discoveringMode {
namespaces, err := listAllCollections(d.ctx, client, d.collections, systemDBs)
if err != nil {
logger.Errorf("cannot auto discover databases and collections: %s", err.Error())
return
}
collections = fromMapToSlice(namespaces)
}
for _, dbCollection := range collections {
parts := strings.Split(dbCollection, ".")
if len(parts) < 2 { //nolint:gomnd
continue
}
database := parts[0]
collection := strings.Join(parts[1:], ".") // support collections having a .
aggregation := bson.D{
{
Key: "$collStats", Value: bson.M{
// TODO: PMM-9568 : Add support to handle histogram metrics
"latencyStats": bson.M{"histograms": false},
"storageStats": bson.M{"scale": 1},
},
},
}
project := bson.D{
{
Key: "$project", Value: bson.M{
"storageStats.wiredTiger": 0,
"storageStats.indexDetails": 0,
},
},
}
cursor, err := client.Database(database).Collection(collection).Aggregate(d.ctx, mongo.Pipeline{aggregation, project})
if err != nil {
logger.Errorf("cannot get $collstats cursor for collection %s.%s: %s", database, collection, err)
continue
}
var stats []bson.M
if err = cursor.All(d.ctx, &stats); err != nil {
logger.Errorf("cannot get $collstats for collection %s.%s: %s", database, collection, err)
continue
}
logger.Debugf("$collStats metrics for %s.%s", database, collection)
debugResult(logger, stats)
prefix := "collstats"
labels := d.topologyInfo.baseLabels()
labels["database"] = database
labels["collection"] = collection
for _, metrics := range stats {
for _, metric := range makeMetrics(prefix, metrics, labels, d.compatibleMode) {
ch <- metric
}
}
}
}
func fromMapToSlice(databases map[string][]string) []string {
var collections []string
for db, cols := range databases {
for _, value := range cols {
collections = append(collections, db+"."+value)
}
}
return collections
}
var _ prometheus.Collector = (*collstatsCollector)(nil)

View File

@ -0,0 +1,230 @@
// mongodb_exporter
// Copyright (C) 2022 Percona LLC
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package exporter
import (
"context"
"sort"
"strings"
"github.com/AlekSi/pointer"
"github.com/pkg/errors"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
var systemDBs = []string{"admin", "config", "local"} //nolint:gochecknoglobals
func listCollections(ctx context.Context, client *mongo.Client, database string, filterInNamespaces []string) ([]string, error) {
filter := bson.D{} // Default=empty -> list all collections
// if there is a filter with the list of collections we want, create a filter like
// $or: {
// {"$regex": "collection1"},
// {"$regex": "collection2"},
// }
if len(filterInNamespaces) > 0 {
matchExpressions := []bson.D{}
for _, namespace := range filterInNamespaces {
parts := strings.Split(namespace, ".") // db.collection.name.with.dots
if len(parts) > 1 {
// The part before the first dot is the database name.
// The rest is the collection name and it can have dots. We need to rebuild it.
collection := strings.Join(parts[1:], ".")
matchExpressions = append(matchExpressions,
bson.D{{Key: "name", Value: primitive.Regex{Pattern: collection, Options: "i"}}})
}
}
if len(matchExpressions) > 0 {
filter = bson.D{{Key: "$or", Value: matchExpressions}}
}
}
collections, err := client.Database(database).ListCollectionNames(ctx, filter)
if err != nil {
return nil, errors.Wrap(err, "cannot get the list of collections for discovery")
}
return collections, nil
}
// databases returns the list of databases matching the filters.
// - filterInNamespaces: Include only the database names matching the any of the regular expressions in this list.
//
// Case will be ignored because the function will automatically add the ignore case
// flag to the regular expression.
//
// - exclude: List of databases to be excluded. Useful to ignore system databases.
func databases(ctx context.Context, client *mongo.Client, filterInNamespaces []string, exclude []string) ([]string, error) {
opts := &options.ListDatabasesOptions{NameOnly: pointer.ToBool(true), AuthorizedDatabases: pointer.ToBool(true)}
filter := bson.D{}
if excludeFilter := makeExcludeFilter(exclude); excludeFilter != nil {
filter = append(filter, *excludeFilter)
}
if namespacesFilter := makeDBsFilter(filterInNamespaces); namespacesFilter != nil {
filter = append(filter, *namespacesFilter)
}
dbNames, err := client.ListDatabaseNames(ctx, filter, opts)
if err != nil {
return nil, errors.Wrap(err, "cannot get the database names list")
}
return dbNames, nil
}
func makeExcludeFilter(exclude []string) *primitive.E {
filterExpressions := []bson.D{}
for _, dbname := range exclude {
filterExpressions = append(filterExpressions,
bson.D{{Key: "name", Value: bson.D{{Key: "$ne", Value: dbname}}}},
)
}
if len(filterExpressions) == 0 {
return nil
}
return &primitive.E{Key: "$and", Value: filterExpressions}
}
func makeDBsFilter(filterInNamespaces []string) *primitive.E {
filterExpressions := []bson.D{}
nss := removeEmptyStrings(filterInNamespaces)
for _, namespace := range nss {
parts := strings.Split(namespace, ".")
filterExpressions = append(filterExpressions,
bson.D{{Key: "name", Value: bson.D{{Key: "$eq", Value: parts[0]}}}},
)
}
if len(filterExpressions) == 0 {
return nil
}
return &primitive.E{Key: "$or", Value: filterExpressions}
}
func removeEmptyStrings(items []string) []string {
cleanList := []string{}
for _, item := range items {
if item == "" {
continue
}
cleanList = append(cleanList, item)
}
return cleanList
}
func unique(slice []string) []string {
keys := make(map[string]bool)
list := []string{}
for _, entry := range slice {
if _, ok := keys[entry]; !ok {
keys[entry] = true
list = append(list, entry)
}
}
return list
}
func listAllCollections(ctx context.Context, client *mongo.Client, filterInNamespaces []string, excludeDBs []string) (map[string][]string, error) {
namespaces := make(map[string][]string)
dbs, err := databases(ctx, client, filterInNamespaces, excludeDBs)
if err != nil {
return nil, errors.Wrap(err, "cannot make the list of databases to list all collections")
}
filterNS := removeEmptyStrings(filterInNamespaces)
// If there are no specified namespaces to search for collections, it means all dbs should be included.
if len(filterNS) == 0 {
filterNS = append(filterNS, dbs...)
}
for _, db := range dbs {
for _, namespace := range filterNS {
parts := strings.Split(namespace, ".")
dbname := strings.TrimSpace(parts[0])
if dbname == "" || dbname != db {
continue
}
colls, err := listCollections(ctx, client, db, []string{namespace})
if err != nil {
return nil, errors.Wrapf(err, "cannot list the collections for %q", db)
}
if _, ok := namespaces[db]; !ok {
namespaces[db] = []string{}
}
namespaces[db] = append(namespaces[db], colls...)
}
}
// Make it testable.
for db, colls := range namespaces {
uc := unique(colls)
sort.Strings(uc)
namespaces[db] = uc
}
return namespaces, nil
}
func nonSystemCollectionsCount(ctx context.Context, client *mongo.Client, includeNamespaces []string, filterInCollections []string) (int, error) {
databases, err := databases(ctx, client, includeNamespaces, systemDBs)
if err != nil {
return 0, errors.Wrap(err, "cannot retrieve the collection names for count collections")
}
var count int
for _, dbname := range databases {
colls, err := listCollections(ctx, client, dbname, filterInCollections)
if err != nil {
return 0, errors.Wrap(err, "cannot get collections count")
}
count += len(colls)
}
return count, nil
}
func splitNamespace(ns string) (database, collection string) {
parts := strings.Split(ns, ".")
if len(parts) < 2 { // there is no collection?
return parts[0], ""
}
return parts[0], strings.Join(parts[1:], ".")
}

View File

@ -0,0 +1,101 @@
// mongodb_exporter
// Copyright (C) 2017 Percona LLC
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package exporter
import (
"context"
"github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
type dbstatsCollector struct {
ctx context.Context
base *baseCollector
compatibleMode bool
topologyInfo labelsGetter
databaseFilter []string
}
// newDBStatsCollector creates a collector for statistics on database storage.
func newDBStatsCollector(ctx context.Context, client *mongo.Client, logger *logrus.Logger, compatible bool, topology labelsGetter, databaseRegex []string) *dbstatsCollector {
return &dbstatsCollector{
ctx: ctx,
base: newBaseCollector(client, logger),
compatibleMode: compatible,
topologyInfo: topology,
databaseFilter: databaseRegex,
}
}
func (d *dbstatsCollector) Describe(ch chan<- *prometheus.Desc) {
d.base.Describe(d.ctx, ch, d.collect)
}
func (d *dbstatsCollector) Collect(ch chan<- prometheus.Metric) {
d.base.Collect(ch, d.collect)
}
func (d *dbstatsCollector) collect(ch chan<- prometheus.Metric) {
logger := d.base.logger
client := d.base.client
dbNames, err := databases(d.ctx, client, d.databaseFilter, nil)
if err != nil {
logger.Errorf("Failed to get database names: %s", err)
return
}
logger.Debugf("getting stats for databases: %v", dbNames)
for _, db := range dbNames {
var dbStats bson.M
cmd := bson.D{{Key: "dbStats", Value: 1}, {Key: "scale", Value: 1}}
r := client.Database(db).RunCommand(d.ctx, cmd)
err := r.Decode(&dbStats)
if err != nil {
logger.Errorf("Failed to get $dbstats for database %s: %s", db, err)
continue
}
logger.Debugf("$dbStats metrics for %s", db)
debugResult(logger, dbStats)
prefix := "dbstats"
labels := d.topologyInfo.baseLabels()
// Since all dbstats will have the same fields, we need to use a label
// to differentiate metrics between different databases.
labels["database"] = db
newMetrics := makeMetrics(prefix, dbStats, labels, d.compatibleMode)
for _, metric := range newMetrics {
ch <- metric
}
}
}
var _ prometheus.Collector = (*dbstatsCollector)(nil)

View File

@ -0,0 +1,43 @@
// mongodb_exporter
// Copyright (C) 2022 Percona LLC
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package exporter
import (
"encoding/json"
"fmt"
"os"
"github.com/sirupsen/logrus"
)
func debugResult(log *logrus.Logger, m interface{}) {
if !log.IsLevelEnabled(logrus.DebugLevel) {
return
}
debugStr, err := json.MarshalIndent(m, "", " ")
if err != nil {
log.Errorf("cannot marshal struct for debug: %s", err)
return
}
// don't use logrus because:
// 1. It will escape new lines and " making it harder to read and to use
// 2. It will add timestamp
// 3. This way is easier to copy/paste to put the info in a ticket
fmt.Fprintln(os.Stderr, string(debugStr))
}

View File

@ -0,0 +1,112 @@
// mongodb_exporter
// Copyright (C) 2017 Percona LLC
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package exporter
import (
"context"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
type diagnosticDataCollector struct {
ctx context.Context
base *baseCollector
compatibleMode bool
topologyInfo labelsGetter
}
// newDiagnosticDataCollector creates a collector for diagnostic information.
func newDiagnosticDataCollector(ctx context.Context, client *mongo.Client, logger *logrus.Logger, compatible bool, topology labelsGetter) *diagnosticDataCollector {
return &diagnosticDataCollector{
ctx: ctx,
base: newBaseCollector(client, logger),
compatibleMode: compatible,
topologyInfo: topology,
}
}
func (d *diagnosticDataCollector) Describe(ch chan<- *prometheus.Desc) {
d.base.Describe(d.ctx, ch, d.collect)
}
func (d *diagnosticDataCollector) Collect(ch chan<- prometheus.Metric) {
d.base.Collect(ch, d.collect)
}
func (d *diagnosticDataCollector) collect(ch chan<- prometheus.Metric) {
var m bson.M
logger := d.base.logger
client := d.base.client
cmd := bson.D{{Key: "getDiagnosticData", Value: "1"}}
res := client.Database("admin").RunCommand(d.ctx, cmd)
if res.Err() != nil {
if isArbiter, _ := isArbiter(d.ctx, client); isArbiter {
return
}
}
if err := res.Decode(&m); err != nil {
logger.Errorf("cannot run getDiagnosticData: %s", err)
}
if m == nil || m["data"] == nil {
logger.Error("cannot run getDiagnosticData: response is empty")
}
m, ok := m["data"].(bson.M)
if !ok {
err := errors.Wrapf(errUnexpectedDataType, "%T for data field", m["data"])
logger.Errorf("cannot decode getDiagnosticData: %s", err)
}
logger.Debug("getDiagnosticData result")
debugResult(logger, m)
metrics := makeMetrics("", m, d.topologyInfo.baseLabels(), d.compatibleMode)
metrics = append(metrics, locksMetrics(m)...)
metrics = append(metrics, specialMetrics(d.ctx, client, m, logger)...)
if cem, err := cacheEvictedTotalMetric(m); err == nil {
metrics = append(metrics, cem)
}
if d.compatibleMode {
nodeType, err := getNodeType(d.ctx, client)
if err != nil {
logger.WithFields(logrus.Fields{
"component": "diagnosticDataCollector",
}).Errorf("Cannot get node type to check if this is a mongos: %s", err)
} else if nodeType == typeMongos {
metrics = append(metrics, mongosMetrics(d.ctx, client, logger)...)
}
}
for _, metric := range metrics {
ch <- metric
}
}
// check interface.
var _ prometheus.Collector = (*diagnosticDataCollector)(nil)

View File

@ -0,0 +1,269 @@
// mongodb_exporter
// Copyright (C) 2017 Percona LLC
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// Package exporter implements the collectors and metrics handlers.
package exporter
import (
"context"
"fmt"
"sync"
"github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
var _ prometheus.Collector = (*Exporter)(nil)
// Exporter holds Exporter methods and attributes.
type Exporter struct {
client *mongo.Client
clientMu sync.Mutex
logger *logrus.Logger
opts *Opts
lock *sync.Mutex
totalCollectionsCount int
cs []prometheus.Collector
}
// Opts holds new exporter options.
type Opts struct {
URI string
Username string
Password string
// Only get stats for the collections matching this list of namespaces.
// Example: db1.col1,db.col1
CollStatsNamespaces []string
IndexStatsCollections []string
CollStatsLimit int
CompatibleMode bool
DirectConnect bool
DiscoveringMode bool
CollectAll bool
EnableDBStats bool
EnableDiagnosticData bool
EnableReplicasetStatus bool
EnableTopMetrics bool
EnableIndexStats bool
EnableCollStats bool
EnableOverrideDescendingIndex bool
Logger *logrus.Logger
}
var (
errCannotHandleType = fmt.Errorf("don't know how to handle data type")
errUnexpectedDataType = fmt.Errorf("unexpected data type")
)
const (
defaultCacheSize = 1000
)
// New connects to the database and returns a new Exporter instance.
func New(opts *Opts) (*Exporter, error) {
if opts == nil {
opts = new(Opts)
}
if opts.Logger == nil {
opts.Logger = logrus.New()
}
exp := &Exporter{
logger: opts.Logger,
opts: opts,
lock: &sync.Mutex{},
totalCollectionsCount: -1, // Not calculated yet. waiting the db connection.
}
ctx := context.Background()
_, err := exp.getClient(ctx)
if err != nil {
return nil, fmt.Errorf("cannot connect to mongo: %v", err)
}
return exp, exp.initCollectors(ctx, exp.client)
}
func (e *Exporter) Close() {
if e.client != nil {
e.client.Disconnect(context.Background())
}
}
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
wg := new(sync.WaitGroup)
for idx := range e.cs {
wg.Add(1)
go func(i int) {
defer wg.Done()
e.cs[i].Collect(ch)
}(idx)
}
wg.Wait()
}
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
wg := new(sync.WaitGroup)
wg.Add(len(e.cs))
for idx := range e.cs {
go func(i int) {
defer wg.Done()
e.cs[i].Describe(ch)
}(idx)
}
wg.Wait()
}
func (e *Exporter) initCollectors(ctx context.Context, client *mongo.Client) error {
gc := newGeneralCollector(ctx, client, e.opts.Logger)
e.cs = append(e.cs, gc)
// Enable collectors like collstats and indexstats depending on the number of collections
// present in the database.
limitsOk := false
if e.opts.CollStatsLimit <= 0 || // Unlimited
e.getTotalCollectionsCount() <= e.opts.CollStatsLimit {
limitsOk = true
}
if e.opts.CollectAll {
if len(e.opts.CollStatsNamespaces) == 0 {
e.opts.DiscoveringMode = true
}
e.opts.EnableDiagnosticData = true
e.opts.EnableDBStats = true
e.opts.EnableCollStats = true
e.opts.EnableTopMetrics = true
e.opts.EnableReplicasetStatus = true
e.opts.EnableIndexStats = true
}
topologyInfo := newTopologyInfo(ctx, client)
if e.opts.EnableDiagnosticData {
ddc := newDiagnosticDataCollector(ctx, client, e.opts.Logger,
e.opts.CompatibleMode, topologyInfo)
e.cs = append(e.cs, ddc)
}
// If we manually set the collection names we want or auto discovery is set.
if (len(e.opts.CollStatsNamespaces) > 0 || e.opts.DiscoveringMode) && e.opts.EnableCollStats && limitsOk {
cc := newCollectionStatsCollector(ctx, client, e.opts.Logger,
e.opts.CompatibleMode, e.opts.DiscoveringMode,
topologyInfo, e.opts.CollStatsNamespaces)
e.cs = append(e.cs, cc)
}
// If we manually set the collection names we want or auto discovery is set.
if (len(e.opts.IndexStatsCollections) > 0 || e.opts.DiscoveringMode) && e.opts.EnableIndexStats && limitsOk {
ic := newIndexStatsCollector(ctx, client, e.opts.Logger,
e.opts.DiscoveringMode, e.opts.EnableOverrideDescendingIndex,
topologyInfo, e.opts.IndexStatsCollections)
e.cs = append(e.cs, ic)
}
if e.opts.EnableDBStats && limitsOk {
cc := newDBStatsCollector(ctx, client, e.opts.Logger,
e.opts.CompatibleMode, topologyInfo, nil)
e.cs = append(e.cs, cc)
}
nodeType, err := getNodeType(ctx, client)
if err != nil {
return fmt.Errorf("cannot get node type to check if this is a mongos : %s", err)
}
if e.opts.EnableTopMetrics && nodeType != typeMongos && limitsOk {
tc := newTopCollector(ctx, client, e.opts.Logger,
e.opts.CompatibleMode, topologyInfo)
e.cs = append(e.cs, tc)
}
// replSetGetStatus is not supported through mongos.
if e.opts.EnableReplicasetStatus && nodeType != typeMongos {
rsgsc := newReplicationSetStatusCollector(ctx, client, e.opts.Logger,
e.opts.CompatibleMode, topologyInfo)
e.cs = append(e.cs, rsgsc)
}
return nil
}
func (e *Exporter) getTotalCollectionsCount() int {
e.lock.Lock()
defer e.lock.Unlock()
return e.totalCollectionsCount
}
func (e *Exporter) getClient(ctx context.Context) (*mongo.Client, error) {
// Get global client. Maybe it must be initialized first.
// Initialization is retried with every scrape until it succeeds once.
e.clientMu.Lock()
defer e.clientMu.Unlock()
// If client is already initialized, return it.
if e.client != nil {
return e.client, nil
}
client, err := connect(context.Background(), e.opts.URI, e.opts.Username, e.opts.Password, e.opts.DirectConnect)
if err != nil {
return nil, err
}
e.client = client
return client, nil
}
func connect(ctx context.Context, dsn, username, password string, directConnect bool) (*mongo.Client, error) {
opts := options.Client().ApplyURI(dsn)
opts.SetDirect(directConnect)
opts.SetAppName("mongodb_exporter")
if len(username) > 0 || len(password) > 0 {
opts.SetAuth(options.Credential{
Username: username,
Password: password,
})
}
client, err := mongo.Connect(ctx, opts)
if err != nil {
return nil, err
}
if err = client.Ping(ctx, nil); err != nil {
// Ping failed. Close background connections. Error is ignored since the ping error is more relevant.
_ = client.Disconnect(ctx)
return nil, fmt.Errorf("cannot connect to MongoDB: %w", err)
}
return client, nil
}

View File

@ -0,0 +1,71 @@
// mongodb_exporter
// Copyright (C) 2017 Percona LLC
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package exporter
import (
"context"
"github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/readpref"
)
// This collector is always enabled and it is not directly related to any particular MongoDB
// command to gather stats.
type generalCollector struct {
ctx context.Context
base *baseCollector
}
// newGeneralCollector creates a collector for MongoDB connectivity status.
func newGeneralCollector(ctx context.Context, client *mongo.Client, logger *logrus.Logger) *generalCollector {
return &generalCollector{
ctx: ctx,
base: newBaseCollector(client, logger),
}
}
func (d *generalCollector) Describe(ch chan<- *prometheus.Desc) {
d.base.Describe(d.ctx, ch, d.collect)
}
func (d *generalCollector) Collect(ch chan<- prometheus.Metric) {
d.base.Collect(ch, d.collect)
}
func (d *generalCollector) collect(ch chan<- prometheus.Metric) {
ch <- mongodbUpMetric(d.ctx, d.base.client, d.base.logger)
}
func mongodbUpMetric(ctx context.Context, client *mongo.Client, log *logrus.Logger) prometheus.Metric {
var value float64
if client != nil {
if err := client.Ping(ctx, readpref.PrimaryPreferred()); err == nil {
value = 1
} else {
log.Errorf("error while checking mongodb connection: %s. mongo_up is set to 0", err)
}
}
d := prometheus.NewDesc("mongodb_up", "Whether MongoDB is up.", nil, nil)
return prometheus.MustNewConstMetric(d, prometheus.GaugeValue, value)
}
var _ prometheus.Collector = (*generalCollector)(nil)

View File

@ -0,0 +1,160 @@
// mongodb_exporter
// Copyright (C) 2017 Percona LLC
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package exporter
import (
"context"
"fmt"
"strings"
"github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
type indexstatsCollector struct {
ctx context.Context
base *baseCollector
discoveringMode bool
overrideDescendingIndex bool
topologyInfo labelsGetter
collections []string
}
// newIndexStatsCollector creates a collector for statistics on index usage.
func newIndexStatsCollector(ctx context.Context, client *mongo.Client, logger *logrus.Logger, discovery, overrideDescendingIndex bool, topology labelsGetter, collections []string) *indexstatsCollector {
return &indexstatsCollector{
ctx: ctx,
base: newBaseCollector(client, logger),
discoveringMode: discovery,
topologyInfo: topology,
overrideDescendingIndex: overrideDescendingIndex,
collections: collections,
}
}
func (d *indexstatsCollector) Describe(ch chan<- *prometheus.Desc) {
d.base.Describe(d.ctx, ch, d.collect)
}
func (d *indexstatsCollector) Collect(ch chan<- prometheus.Metric) {
d.base.Collect(ch, d.collect)
}
func (d *indexstatsCollector) collect(ch chan<- prometheus.Metric) {
collections := d.collections
logger := d.base.logger
client := d.base.client
if d.discoveringMode {
namespaces, err := listAllCollections(d.ctx, client, d.collections, systemDBs)
if err != nil {
logger.Errorf("cannot auto discover databases and collections")
return
}
collections = fromMapToSlice(namespaces)
}
for _, dbCollection := range collections {
parts := strings.Split(dbCollection, ".")
if len(parts) < 2 { //nolint:gomnd
continue
}
database := parts[0]
collection := strings.Join(parts[1:], ".")
aggregation := bson.D{
{Key: "$indexStats", Value: bson.M{}},
}
cursor, err := client.Database(database).Collection(collection).Aggregate(d.ctx, mongo.Pipeline{aggregation})
if err != nil {
logger.Errorf("cannot get $indexStats cursor for collection %s.%s: %s", database, collection, err)
continue
}
var stats []bson.M
if err = cursor.All(d.ctx, &stats); err != nil {
logger.Errorf("cannot get $indexStats for collection %s.%s: %s", database, collection, err)
continue
}
d.base.logger.Debugf("indexStats for %s.%s", database, collection)
debugResult(d.base.logger, stats)
for _, metric := range stats {
indexName := fmt.Sprintf("%s", metric["name"])
// Override the label name
if d.overrideDescendingIndex {
indexName = strings.ReplaceAll(fmt.Sprintf("%s", metric["name"]), "-1", "DESC")
}
// prefix and labels are needed to avoid duplicated metric names since the metrics are the
// same, for different collections.
prefix := "indexstats"
labels := d.topologyInfo.baseLabels()
labels["database"] = database
labels["collection"] = collection
labels["key_name"] = indexName
metrics := sanitizeMetrics(metric)
for _, metric := range makeMetrics(prefix, metrics, labels, false) {
ch <- metric
}
}
}
}
// According to specs, we should expose only this 2 metrics. 'building' might not exist.
func sanitizeMetrics(m bson.M) bson.M {
ops := float64(0)
if val := walkTo(m, []string{"accesses", "ops"}); val != nil {
if f, err := asFloat64(val); err == nil {
ops = *f
}
}
filteredMetrics := bson.M{
"accesses": bson.M{
"ops": ops,
},
}
if val := walkTo(m, []string{"building"}); val != nil {
if f, err := asFloat64(val); err == nil {
filteredMetrics["building"] = *f
}
}
return filteredMetrics
}
var _ prometheus.Collector = (*indexstatsCollector)(nil)

View File

@ -0,0 +1,430 @@
// mongodb_exporter
// Copyright (C) 2017 Percona LLC
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package exporter
import (
"regexp"
"strings"
"time"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
const (
exporterPrefix = "mongodb_"
)
type rawMetric struct {
// Full Qualified Name
fqName string
// Help string
help string
// Label names
ln []string
// Label values
lv []string
// Metric value as float64
val float64
// Value type
vt prometheus.ValueType
}
//nolint:gochecknoglobals
var (
// Rules to shrink metric names
// Please do not change the definitions order: rules are sorted by precedence.
prefixes = [][]string{
{"serverStatus.wiredTiger.transaction", "ss_wt_txn"},
{"serverStatus.wiredTiger", "ss_wt"},
{"serverStatus", "ss"},
{"replSetGetStatus", "rs"},
{"systemMetrics", "sys"},
{"local.oplog.rs.stats.wiredTiger", "oplog_stats_wt"},
{"local.oplog.rs.stats", "oplog_stats"},
{"collstats_storage.wiredTiger", "collstats_storage_wt"},
{"collstats_storage.indexDetails", "collstats_storage_idx"},
{"collStats.storageStats", "collstats_storage"},
{"collStats.latencyStats", "collstats_latency"},
}
// This map is used to add labels to some specific metrics.
// For example, the fields under the serverStatus.opcounters. structure have this
// signature:
//
// "opcounters": primitive.M{
// "insert": int32(4),
// "query": int32(2118),
// "update": int32(14),
// "delete": int32(22),
// "getmore": int32(9141),
// "command": int32(67923),
// },
//
// Applying the renaming rules, serverStatus will become ss but instead of having metrics
// with the form ss.opcounters.<operation> where operation is each one of the fields inside
// the structure (insert, query, update, etc), those keys will become labels for the same
// metric name. The label name is defined as the value for each metric name in the map and
// the value the label will have is the field name in the structure. Example.
//
// mongodb_ss_opcounters{legacy_op_type="insert"} 4
// mongodb_ss_opcounters{legacy_op_type="query"} 2118
// mongodb_ss_opcounters{legacy_op_type="update"} 14
// mongodb_ss_opcounters{legacy_op_type="delete"} 22
// mongodb_ss_opcounters{legacy_op_type="getmore"} 9141
// mongodb_ss_opcounters{legacy_op_type="command"} 67923
//
nodeToPDMetrics = map[string]string{
"collStats.storageStats.indexDetails.": "index_name",
"globalLock.activeQueue.": "count_type",
"globalLock.locks.": "lock_type",
"serverStatus.asserts.": "assert_type",
"serverStatus.connections.": "conn_type",
"serverStatus.globalLock.currentQueue.": "count_type",
"serverStatus.metrics.commands.": "cmd_name",
"serverStatus.metrics.cursor.open.": "csr_type",
"serverStatus.metrics.document.": "doc_op_type",
"serverStatus.opLatencies.": "op_type",
"serverStatus.opReadConcernCounters.": "concern_type",
"serverStatus.opcounters.": "legacy_op_type",
"serverStatus.opcountersRepl.": "legacy_op_type",
"serverStatus.transactions.commitTypes.": "commit_type",
"serverStatus.wiredTiger.concurrentTransactions.": "txn_rw_type",
"serverStatus.wiredTiger.perf.": "perf_bucket",
"systemMetrics.disks.": "device_name",
}
// Regular expressions used to make the metric name Prometheus-compatible
// This variables are global to compile the regexps only once.
specialCharsRe = regexp.MustCompile(`[^a-zA-Z0-9_]+`)
repeatedUnderscoresRe = regexp.MustCompile(`__+`)
dollarRe = regexp.MustCompile(`\_$`)
)
// prometheusize renames metrics by replacing some prefixes with shorter names
// replace special chars to follow Prometheus metric naming rules and adds the
// exporter name prefix.
func prometheusize(s string) string {
for _, pair := range prefixes {
if strings.HasPrefix(s, pair[0]+".") {
s = pair[1] + strings.TrimPrefix(s, pair[0])
break
}
}
s = specialCharsRe.ReplaceAllString(s, "_")
s = dollarRe.ReplaceAllString(s, "")
s = repeatedUnderscoresRe.ReplaceAllString(s, "_")
s = strings.TrimPrefix(s, "_")
return exporterPrefix + s
}
// nameAndLabel checks if there are predefined metric name and label for that metric or
// the standard metrics name should be used in place.
func nameAndLabel(prefix, name string) (string, string) {
if label, ok := nodeToPDMetrics[prefix]; ok {
return prometheusize(prefix), label
}
return prometheusize(prefix + name), ""
}
// makeRawMetric creates a Prometheus metric based on the parameters we collected by
// traversing the MongoDB structures returned by the collector functions.
func makeRawMetric(prefix, name string, value interface{}, labels map[string]string) (*rawMetric, error) {
f, err := asFloat64(value)
if err != nil {
return nil, err
}
if f == nil {
return nil, nil
}
help := metricHelp(prefix, name)
fqName, label := nameAndLabel(prefix, name)
metricType := prometheus.UntypedValue
if strings.HasSuffix(strings.ToLower(name), "count") {
metricType = prometheus.CounterValue
}
rm := &rawMetric{
fqName: fqName,
help: help,
val: *f,
vt: metricType,
ln: make([]string, 0, len(labels)),
lv: make([]string, 0, len(labels)),
}
// Add original labels to the metric
for k, v := range labels {
rm.ln = append(rm.ln, k)
rm.lv = append(rm.lv, v)
}
// Add predefined label, if any
if label != "" {
rm.ln = append(rm.ln, label)
rm.lv = append(rm.lv, name)
}
return rm, nil
}
func asFloat64(value interface{}) (*float64, error) {
var f float64
switch v := value.(type) {
case bool:
if v {
f = 1
}
case int:
f = float64(v)
case int32:
f = float64(v)
case int64:
f = float64(v)
case float32:
f = float64(v)
case float64:
f = v
case primitive.DateTime:
f = float64(v)
case primitive.A, primitive.ObjectID, primitive.Timestamp, primitive.Binary, string, []uint8, time.Time:
return nil, nil
default:
return nil, errors.Wrapf(errCannotHandleType, "%T", v)
}
return &f, nil
}
func rawToPrometheusMetric(rm *rawMetric) (prometheus.Metric, error) {
d := prometheus.NewDesc(rm.fqName, rm.help, rm.ln, nil)
return prometheus.NewConstMetric(d, rm.vt, rm.val, rm.lv...)
}
// metricHelp builds the metric help.
// It is a very very very simple function, but the idea is if the future we want
// to improve the help somehow, there is only one place to change it for the real
// functions and for all the tests.
// Use only prefix or name but not both because 2 metrics cannot have same name but different help.
// For metrics where we labelize some keys, if we put the real metric name here it will be rejected
// by prometheus. For first level metrics, there is no prefix so we should use the metric name or
// the help would be empty.
func metricHelp(prefix, name string) string {
if prefix != "" {
return prefix
}
return name
}
func makeMetrics(prefix string, m bson.M, labels map[string]string, compatibleMode bool) []prometheus.Metric {
var res []prometheus.Metric
if prefix != "" {
prefix += "."
}
for k, val := range m {
switch v := val.(type) {
case bson.M:
res = append(res, makeMetrics(prefix+k, v, labels, compatibleMode)...)
case map[string]interface{}:
res = append(res, makeMetrics(prefix+k, v, labels, compatibleMode)...)
case primitive.A:
v = []interface{}(v)
res = append(res, processSlice(prefix, k, v, labels, compatibleMode)...)
case []interface{}:
continue
default:
rm, err := makeRawMetric(prefix, k, v, labels)
if err != nil {
invalidMetric := prometheus.NewInvalidMetric(prometheus.NewInvalidDesc(err), err)
res = append(res, invalidMetric)
continue
}
// makeRawMetric returns a nil metric for some data types like strings
// because we cannot extract data from all types
if rm == nil {
continue
}
metrics := []*rawMetric{rm}
if renamedMetrics := metricRenameAndLabel(rm, specialConversions()); renamedMetrics != nil {
metrics = renamedMetrics
}
for _, m := range metrics {
metric, err := rawToPrometheusMetric(m)
if err != nil {
invalidMetric := prometheus.NewInvalidMetric(prometheus.NewInvalidDesc(err), err)
res = append(res, invalidMetric)
continue
}
res = append(res, metric)
if compatibleMode {
res = appendCompatibleMetric(res, m)
}
}
}
}
return res
}
// Extract maps from arrays. Only some structures like replicasets have arrays of members
// and each member is represented by a map[string]interface{}.
func processSlice(prefix, k string, v []interface{}, commonLabels map[string]string, compatibleMode bool) []prometheus.Metric {
metrics := make([]prometheus.Metric, 0)
labels := make(map[string]string)
for name, value := range commonLabels {
labels[name] = value
}
for _, item := range v {
var s map[string]interface{}
switch i := item.(type) {
case map[string]interface{}:
s = i
case primitive.M:
s = map[string]interface{}(i)
default:
continue
}
// use the replicaset or server name as a label
if name, ok := s["name"].(string); ok {
labels["member_idx"] = name
}
if state, ok := s["stateStr"].(string); ok {
labels["member_state"] = state
}
metrics = append(metrics, makeMetrics(prefix+k, s, labels, compatibleMode)...)
}
return metrics
}
type conversion struct {
newName string
oldName string
labelConversions map[string]string // key: current label, value: old exporter (compatible) label
labelValueConversions map[string]string // key: current label, value: old exporter (compatible) label
prefix string
suffixLabel string
suffixMapping map[string]string
}
func metricRenameAndLabel(rm *rawMetric, convs []conversion) []*rawMetric {
// check if the metric exists in the conversions array.
// if it exists, it should be converted.
var result []*rawMetric
for _, cm := range convs {
switch {
case cm.newName != "" && rm.fqName == cm.newName: // first renaming case. See (1)
result = append(result, newToOldMetric(rm, cm))
case cm.prefix != "" && strings.HasPrefix(rm.fqName, cm.prefix): // second renaming case. See (2)
conversionSuffix := strings.TrimPrefix(rm.fqName, cm.prefix)
conversionSuffix = strings.TrimPrefix(conversionSuffix, "_")
// Check that also the suffix matches.
// In the conversion array, there are metrics with the same prefix but the 'old' name varies
// also depending on the metic suffix
if _, ok := cm.suffixMapping[conversionSuffix]; ok {
om := createOldMetricFromNew(rm, cm)
result = append(result, om)
}
}
}
return result
}
// specialConversions returns a list of special conversions we want to implement.
// See: https://jira.percona.com/browse/PMM-6506
func specialConversions() []conversion {
return []conversion{
{
oldName: "mongodb_ss_opLatencies_ops",
prefix: "mongodb_ss_opLatencies",
suffixLabel: "op_type",
suffixMapping: map[string]string{
"commands_ops": "commands",
"reads_ops": "reads",
"transactions_ops": "transactions",
"writes_ops": "writes",
},
},
{
oldName: "mongodb_ss_opLatencies_latency",
prefix: "mongodb_ss_opLatencies",
suffixLabel: "op_type",
suffixMapping: map[string]string{
"commands_latency": "commands",
"reads_latency": "reads",
"transactions_latency": "transactions",
"writes_latency": "writes",
},
},
// mongodb_ss_wt_concurrentTransactions_read_out
// mongodb_ss_wt_concurrentTransactions_write_out
{
oldName: "mongodb_ss_wt_concurrentTransactions_out",
prefix: "mongodb_ss_wt_concurrentTransactions",
suffixLabel: "txn_rw",
suffixMapping: map[string]string{
"read_out": "read",
"write_out": "write",
},
},
// mongodb_ss_wt_concurrentTransactions_read_available
// mongodb_ss_wt_concurrentTransactions_write_available
{
oldName: "mongodb_ss_wt_concurrentTransactions_available",
prefix: "mongodb_ss_wt_concurrentTransactions",
suffixLabel: "txn_rw",
suffixMapping: map[string]string{
"read_available": "read",
"write_available": "write",
},
},
// mongodb_ss_wt_concurrentTransactions_read_totalTickets
// mongodb_ss_wt_concurrentTransactions_write_totalTickets
{
oldName: "mongodb_ss_wt_concurrentTransactions_totalTickets",
prefix: "mongodb_ss_wt_concurrentTransactions",
suffixLabel: "txn_rw",
suffixMapping: map[string]string{
"read_totalTickets": "read",
"write_totalTickets": "write",
},
},
}
}

View File

@ -0,0 +1,89 @@
// mongodb_exporter
// Copyright (C) 2017 Percona LLC
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package exporter
import (
"context"
"github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
const (
replicationNotEnabled = 76
replicationNotYetInitialized = 94
)
type replSetGetStatusCollector struct {
ctx context.Context
base *baseCollector
compatibleMode bool
topologyInfo labelsGetter
}
// newReplicationSetStatusCollector creates a collector for statistics on replication set.
func newReplicationSetStatusCollector(ctx context.Context, client *mongo.Client, logger *logrus.Logger, compatible bool, topology labelsGetter) *replSetGetStatusCollector {
return &replSetGetStatusCollector{
ctx: ctx,
base: newBaseCollector(client, logger),
compatibleMode: compatible,
topologyInfo: topology,
}
}
func (d *replSetGetStatusCollector) Describe(ch chan<- *prometheus.Desc) {
d.base.Describe(d.ctx, ch, d.collect)
}
func (d *replSetGetStatusCollector) Collect(ch chan<- prometheus.Metric) {
d.base.Collect(ch, d.collect)
}
func (d *replSetGetStatusCollector) collect(ch chan<- prometheus.Metric) {
logger := d.base.logger
client := d.base.client
cmd := bson.D{{Key: "replSetGetStatus", Value: "1"}}
res := client.Database("admin").RunCommand(d.ctx, cmd)
var m bson.M
if err := res.Decode(&m); err != nil {
if e, ok := err.(mongo.CommandError); ok {
if e.Code == replicationNotYetInitialized || e.Code == replicationNotEnabled {
return
}
}
logger.Errorf("cannot get replSetGetStatus: %s", err)
return
}
logger.Debug("replSetGetStatus result:")
debugResult(logger, m)
for _, metric := range makeMetrics("", m, d.topologyInfo.baseLabels(), d.compatibleMode) {
ch <- metric
}
}
var _ prometheus.Collector = (*replSetGetStatusCollector)(nil)

View File

@ -0,0 +1,74 @@
// mongodb_exporter
// Copyright (C) 2017 Percona LLC
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package exporter
import (
"context"
"github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
type serverStatusCollector struct {
ctx context.Context
base *baseCollector
compatibleMode bool
topologyInfo labelsGetter
}
// newServerStatusCollector creates a collector for statistics on server status.
func newServerStatusCollector(ctx context.Context, client *mongo.Client, logger *logrus.Logger, compatible bool, topology labelsGetter) *serverStatusCollector {
return &serverStatusCollector{
ctx: ctx,
base: newBaseCollector(client, logger),
compatibleMode: compatible,
topologyInfo: topology,
}
}
func (d *serverStatusCollector) Describe(ch chan<- *prometheus.Desc) {
d.base.Describe(d.ctx, ch, d.collect)
}
func (d *serverStatusCollector) Collect(ch chan<- prometheus.Metric) {
d.base.Collect(ch, d.collect)
}
func (d *serverStatusCollector) collect(ch chan<- prometheus.Metric) {
logger := d.base.logger
client := d.base.client
cmd := bson.D{{Key: "serverStatus", Value: "1"}}
res := client.Database("admin").RunCommand(d.ctx, cmd)
var m bson.M
if err := res.Decode(&m); err != nil {
ch <- prometheus.NewInvalidMetric(prometheus.NewInvalidDesc(err), err)
return
}
logrus.Debug("serverStatus result:")
debugResult(logger, m)
for _, metric := range makeMetrics("", m, d.topologyInfo.baseLabels(), d.compatibleMode) {
ch <- metric
}
}

View File

@ -0,0 +1,157 @@
// mongodb_exporter
// Copyright (C) 2017 Percona LLC
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package exporter
import (
"context"
"fmt"
"github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
type topCollector struct {
ctx context.Context
base *baseCollector
compatibleMode bool
topologyInfo labelsGetter
}
var ErrInvalidOrMissingTotalsEntry = fmt.Errorf("Invalid or misssing totals entry in top results")
func newTopCollector(ctx context.Context, client *mongo.Client, logger *logrus.Logger, compatible bool,
topology labelsGetter,
) *topCollector {
return &topCollector{
ctx: ctx,
base: newBaseCollector(client, logger),
compatibleMode: compatible,
topologyInfo: topology,
}
}
func (d *topCollector) Describe(ch chan<- *prometheus.Desc) {
d.base.Describe(d.ctx, ch, d.collect)
}
func (d *topCollector) Collect(ch chan<- prometheus.Metric) {
d.base.Collect(ch, d.collect)
}
func (d *topCollector) collect(ch chan<- prometheus.Metric) {
logger := d.base.logger
client := d.base.client
cmd := bson.D{{Key: "top", Value: "1"}}
res := client.Database("admin").RunCommand(d.ctx, cmd)
var m primitive.M
if err := res.Decode(&m); err != nil {
ch <- prometheus.NewInvalidMetric(prometheus.NewInvalidDesc(err), err)
return
}
logrus.Debug("top result:")
debugResult(logger, m)
totals, ok := m["totals"].(primitive.M)
if !ok {
ch <- prometheus.NewInvalidMetric(prometheus.NewInvalidDesc(ErrInvalidOrMissingTotalsEntry),
ErrInvalidOrMissingTotalsEntry)
}
/*
The top command will return a structure with a key named totals and it is a map
where the key is the collection namespace and for each collection there are per
collection usage statistics.
Example: rs1:SECONDARY> db.adminCommand({"top": 1});
{
"totals" : {
"note" : "all times in microseconds",
"admin.system.roles" : {
"total" : {
"time" : 41,
"count" : 1
},
"readLock" : {
"time" : 41,
"count" : 1
},
"writeLock" : {
"time" : 0,
"count" : 0
},
"queries" : {
"time" : 41,
"count" : 1
},
"getmore" : {
"time" : 0,
"count" : 0
},
"insert" : {
"time" : 0,
"count" : 0
},
"update" : {
"time" : 0,
"count" : 0
},
"remove" : {
"time" : 0,
"count" : 0
},
"commands" : {
"time" : 0,
"count" : 0
}
},
"admin.system.version" : {
"total" : {
"time" : 63541,
"count" : 218
},
If we pass this structure to the makeMetrics function, we will have metric names with the form of
prefix + namespace + metric like mongodb_top_totals_admin.system.role_readlock_count.
Having the namespace as part of the metric is a Prometheus anti pattern and diffucults grouping
metrics in Grafana. For this reason, we need to manually loop through the metric in the totals key
and pass the namespace as a label to the makeMetrics function.
*/
for namespace, metrics := range totals {
labels := d.topologyInfo.baseLabels()
db, coll := splitNamespace(namespace)
labels["database"] = db
labels["collection"] = coll
mm, ok := metrics.(primitive.M) // ingore entries like -> "note" : "all times in microseconds"
if !ok {
continue
}
for _, metric := range makeMetrics("top", mm, labels, d.compatibleMode) {
ch <- metric
}
}
}

View File

@ -0,0 +1,193 @@
// mongodb_exporter
// Copyright (C) 2017 Percona LLC
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package exporter
import (
"context"
"fmt"
"sync"
"github.com/percona/percona-toolkit/src/go/mongolib/proto"
"github.com/percona/percona-toolkit/src/go/mongolib/util"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
type mongoDBNodeType string
const (
labelClusterRole = "cl_role"
labelClusterID = "cl_id"
labelReplicasetName = "rs_nm"
labelReplicasetState = "rs_state"
typeIsDBGrid = "isdbgrid"
typeMongos mongoDBNodeType = "mongos"
typeMongod mongoDBNodeType = "mongod"
typeShardServer mongoDBNodeType = "shardsvr"
typeOther mongoDBNodeType = ""
)
type labelsGetter interface {
baseLabels() map[string]string
loadLabels(context.Context) error
}
// This is an object to make it posible to easily reload the labels in case of
// disconnection from the db. Just call loadLabels when required.
type topologyInfo struct {
// TODO: with https://jira.percona.com/browse/PMM-6435, replace this client pointer
// by a new connector, able to reconnect if needed. In case of reconnection, we should
// call loadLabels to refresh the labels because they might have changed
client *mongo.Client
rw sync.RWMutex
labels map[string]string
}
// ErrCannotGetTopologyLabels Cannot read topology labels.
var ErrCannotGetTopologyLabels = fmt.Errorf("cannot get topology labels")
func newTopologyInfo(ctx context.Context, client *mongo.Client) *topologyInfo {
ti := &topologyInfo{
client: client,
labels: make(map[string]string),
rw: sync.RWMutex{},
}
err := ti.loadLabels(ctx)
if err != nil {
logrus.Warnf("cannot load topology labels: %s", err)
}
return ti
}
// baseLabels returns a copy of the topology labels because in some collectors like
// collstats collector, we must use these base labels and add the namespace or other labels.
func (t *topologyInfo) baseLabels() map[string]string {
c := map[string]string{}
t.rw.RLock()
for k, v := range t.labels {
c[k] = v
}
t.rw.RUnlock()
return c
}
// TopologyLabels reads several values from MongoDB instance like replicaset name, and other
// topology information and returns a map of labels used to better identify the current monitored instance.
func (t *topologyInfo) loadLabels(ctx context.Context) error {
t.rw.Lock()
defer t.rw.Unlock()
t.labels = make(map[string]string)
role, err := getClusterRole(ctx, t.client)
if err != nil {
return errors.Wrap(err, "cannot get node type for topology info")
}
t.labels[labelClusterRole] = role
// Standalone instances or mongos instances won't have a replicaset name
if rs, err := util.ReplicasetConfig(ctx, t.client); err == nil {
t.labels[labelReplicasetName] = rs.Config.ID
}
isArbiter, err := isArbiter(ctx, t.client)
if err != nil {
return err
}
cid, err := util.ClusterID(ctx, t.client)
if err != nil {
if !isArbiter { // arbiters don't have a cluster ID
return errors.Wrapf(ErrCannotGetTopologyLabels, "error getting cluster ID: %s", err)
}
}
t.labels[labelClusterID] = cid
// Standalone instances or mongos instances won't have a replicaset state
state, err := util.MyState(ctx, t.client)
if err == nil {
t.labels[labelReplicasetState] = fmt.Sprintf("%d", state)
}
return nil
}
func isArbiter(ctx context.Context, client *mongo.Client) (bool, error) {
doc := struct {
ArbiterOnly bool `bson:"arbiterOnly"`
}{}
if err := client.Database("admin").RunCommand(ctx, primitive.M{"isMaster": 1}).Decode(&doc); err != nil {
return false, errors.Wrap(err, "cannot check if the instance is an arbiter")
}
return doc.ArbiterOnly, nil
}
func getNodeType(ctx context.Context, client *mongo.Client) (mongoDBNodeType, error) {
md := proto.MasterDoc{}
if err := client.Database("admin").RunCommand(ctx, primitive.M{"isMaster": 1}).Decode(&md); err != nil {
return "", err
}
if md.SetName != nil || md.Hosts != nil {
return typeShardServer, nil
} else if md.Msg == typeIsDBGrid {
// isdbgrid is always the msg value when calling isMaster on a mongos
// see http://docs.mongodb.org/manual/core/sharded-cluster-query-router/
return typeMongos, nil
}
return typeMongod, nil
}
func getClusterRole(ctx context.Context, client *mongo.Client) (string, error) {
cmdOpts := primitive.M{}
// Not always we can get this info. For example, we cannot get this for hidden hosts so
// if there is an error, just ignore it
res := client.Database("admin").RunCommand(ctx, primitive.D{
{Key: "getCmdLineOpts", Value: 1},
{Key: "recordStats", Value: 1},
})
if res.Err() != nil {
return "", nil
}
if err := res.Decode(&cmdOpts); err != nil {
return "", errors.Wrap(err, "cannot decode getCmdLineOpts response")
}
if walkTo(cmdOpts, []string{"parsed", "sharding", "configDB"}) != nil {
return "mongos", nil
}
clusterRole := ""
if cr := walkTo(cmdOpts, []string{"parsed", "sharding", "clusterRole"}); cr != nil {
clusterRole, _ = cr.(string)
}
return clusterRole, nil
}

File diff suppressed because it is too large Load Diff

175
inputs/mongodb/mongodb.go Normal file
View File

@ -0,0 +1,175 @@
package mongodb
import (
"errors"
"fmt"
"log"
"sync"
"sync/atomic"
"flashcat.cloud/categraf/config"
"flashcat.cloud/categraf/inputs"
"flashcat.cloud/categraf/inputs/mongodb/exporter"
"flashcat.cloud/categraf/types"
"github.com/sirupsen/logrus"
"github.com/toolkits/pkg/container/list"
)
const inputName = "mongodb"
type MongoDB struct {
config.Interval
counter uint64
waitgrp sync.WaitGroup
Instances []*Instance `toml:"instances"`
}
func init() {
inputs.Add(inputName, func() inputs.Input {
return &MongoDB{}
})
}
func (r *MongoDB) Prefix() string {
return ""
}
func (r *MongoDB) Init() error {
if len(r.Instances) == 0 {
return types.ErrInstancesEmpty
}
for i := 0; i < len(r.Instances); i++ {
if err := r.Instances[i].Init(); err != nil {
return err
}
}
return nil
}
func (r *MongoDB) Drop() {
for _, i := range r.Instances {
if i == nil {
continue
}
if i.e != nil {
i.e.Close()
}
}
}
func (r *MongoDB) Gather(slist *list.SafeList) {
atomic.AddUint64(&r.counter, 1)
for i := range r.Instances {
ins := r.Instances[i]
if len(ins.MongodbURI) == 0 {
continue
}
r.waitgrp.Add(1)
go func(slist *list.SafeList, ins *Instance) {
defer r.waitgrp.Done()
if ins.IntervalTimes > 0 {
counter := atomic.LoadUint64(&r.counter)
if counter%uint64(ins.IntervalTimes) != 0 {
return
}
}
ins.gatherOnce(slist)
}(slist, ins)
}
r.waitgrp.Wait()
}
type Instance struct {
Labels map[string]string `toml:"labels"`
IntervalTimes int64 `toml:"interval_times"`
LogLevel string `toml:"log_level"`
// Address (host:port) of MongoDB server.
MongodbURI string `toml:"mongodb_uri,omitempty"`
Username string `toml:"username,omitempty"`
Password string `toml:"password,omitempty"`
CollStatsNamespaces []string `toml:"coll_stats_namespaces,omitempty"`
IndexStatsCollections []string `toml:"index_stats_collections,omitempty"`
CollStatsLimit int `toml:"coll_stats_limit,omitempty"`
CompatibleMode bool `toml:"compatible_mode,omitempty"`
DirectConnect bool `toml:"direct_connect,omitempty"`
DiscoveringMode bool `toml:"discovering_mode,omitempty"`
CollectAll bool `toml:"collect_all,omitempty"`
EnableDBStats bool `toml:"enable_db_stats,omitempty"`
EnableDiagnosticData bool `toml:"enable_diagnostic_data,omitempty"`
EnableReplicasetStatus bool `toml:"enable_replicaset_status,omitempty"`
EnableTopMetrics bool `toml:"enable_top_metrics,omitempty"`
EnableIndexStats bool `toml:"enable_index_stats,omitempty"`
EnableCollStats bool `toml:"enable_coll_stats,omitempty"`
EnableOverrideDescendingIndex bool `toml:"enable_override_descending_index,omitempty"`
e *exporter.Exporter `toml:"-"`
}
func (ins *Instance) Init() error {
if len(ins.MongodbURI) == 0 {
return nil
}
if len(ins.LogLevel) == 0 {
ins.LogLevel = "info"
}
level, err := logrus.ParseLevel(ins.LogLevel)
if err != nil {
return err
}
if ins.Labels == nil {
ins.Labels = make(map[string]string)
}
_, ok := ins.Labels["instance"]
if !ok {
return errors.New("instance must be specified in labels")
}
l := logrus.New()
l.SetLevel(level)
e, err := exporter.New(&exporter.Opts{
URI: string(ins.MongodbURI),
Username: ins.Username,
Password: ins.Password,
CollStatsNamespaces: ins.CollStatsNamespaces,
IndexStatsCollections: ins.IndexStatsCollections,
CollStatsLimit: 0,
CompatibleMode: ins.CompatibleMode,
DirectConnect: ins.DirectConnect,
DiscoveringMode: ins.DiscoveringMode,
CollectAll: ins.CollectAll,
EnableDBStats: ins.EnableDBStats,
EnableDiagnosticData: ins.EnableDiagnosticData,
EnableReplicasetStatus: ins.EnableReplicasetStatus,
EnableTopMetrics: ins.EnableTopMetrics,
EnableIndexStats: ins.EnableIndexStats,
EnableCollStats: ins.EnableCollStats,
EnableOverrideDescendingIndex: ins.EnableOverrideDescendingIndex,
Logger: l,
})
if err != nil {
return fmt.Errorf("could not instantiate mongodb lag exporter: %w", err)
}
ins.e = e
return nil
}
func (ins *Instance) gatherOnce(slist *list.SafeList) {
err := inputs.Collect(ins.e, slist, ins.Labels)
if err != nil {
log.Println("E! failed to collect metrics:", err)
}
}