diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index e9afc9f97e8..d5bf9d8b1b4 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -77,6 +77,7 @@ https://github.com/elastic/beats/compare/v6.4.0...6.x[Check the HEAD diff] *Metricbeat* +- Add `replstatus` metricset to MongoDB module {pull}7604[7604] - Add experimental socket summary metricset to system module {pull}6782[6782] - Move common kafka fields (broker, topic and partition.id) to the module level to facilitate events correlation {pull}7767[7767] - Add fields for memory fragmentation, memory allocator stats, copy on write, master-slave status, and active defragmentation to `info` metricset of Redis module. {pull}7695[7695] diff --git a/metricbeat/docker-compose.yml b/metricbeat/docker-compose.yml index 0ddc7bf6cd1..1087b50a267 100644 --- a/metricbeat/docker-compose.yml +++ b/metricbeat/docker-compose.yml @@ -134,6 +134,7 @@ services: mongodb: build: ./module/mongodb/_meta + command: mongod --replSet beats munin: build: ./module/munin/_meta diff --git a/metricbeat/docs/fields.asciidoc b/metricbeat/docs/fields.asciidoc index ac425777cee..8c4e3483bbf 100644 --- a/metricbeat/docs/fields.asciidoc +++ b/metricbeat/docs/fields.asciidoc @@ -11276,6 +11276,381 @@ type: long The number of times the background process removes documents from collections with a ttl index. +-- + +[float] +== replstatus fields + +replstatus provides an overview of replica set status. + + + +[float] +== oplog fields + +oplog provides an overview of replication oplog status, which is retrieved from db.getReplicationInfo(). + + + +*`mongodb.replstatus.oplog.size.allocated`*:: ++ +-- +type: long + +format: bytes + +The total amount of space used by the replstatus in bytes. + + +-- + +*`mongodb.replstatus.oplog.size.used`*:: ++ +-- +type: long + +format: bytes + +total amount of space allocated to the replstatus in bytes. + + +-- + +*`mongodb.replstatus.oplog.first.timestamp`*:: ++ +-- +type: long + +Timestamp of the first (i.e. earliest) operation in the replstatus + + +-- + +*`mongodb.replstatus.oplog.last.timestamp`*:: ++ +-- +type: long + +Timestamp of the last (i.e. latest) operation in the replstatus + + +-- + +*`mongodb.replstatus.oplog.window`*:: ++ +-- +type: long + +The difference between the first and last operation in the replstatus. + + +-- + +*`mongodb.replstatus.set_name`*:: ++ +-- +type: keyword + +The name of the replica set. + + +-- + +*`mongodb.replstatus.server_date`*:: ++ +-- +type: date + +Reflects the current time according to the server that processed the replSetGetStatus command. + + +-- + + +*`mongodb.replstatus.optimes.last_committed`*:: ++ +-- +type: long + +Information, from the viewpoint of this member, regarding the most recent operation that has been written to a majority of replica set members. + + +-- + +*`mongodb.replstatus.optimes.applied`*:: ++ +-- +type: long + +Information, from the viewpoint of this member, regarding the most recent operation that has been applied to this member of the replica set. + + +-- + +*`mongodb.replstatus.optimes.durable`*:: ++ +-- +type: long + +Information, from the viewpoint of this member, regarding the most recent operation that has been written to the journal of this member of the replica set. + + +-- + +[float] +== lag fields + +Delay between a write operation on the primary and its copy to a secondary + + + +*`mongodb.replstatus.lag.max`*:: ++ +-- +type: long + +format: duration + +Difference between optime of primary and slowest secondary + + +-- + +*`mongodb.replstatus.lag.min`*:: ++ +-- +type: long + +format: duration + +Difference between optime of primary and fastest secondary + + +-- + +[float] +== headroom fields + +Difference between the primary's oplog window and the replication lag of the secondary + + + +*`mongodb.replstatus.headroom.max`*:: ++ +-- +type: long + +format: duration + +Difference between primary's oplog window and the replication lag of the fastest secondary + + +-- + +*`mongodb.replstatus.headroom.min`*:: ++ +-- +type: long + +format: duration + +Difference between primary's oplog window and the replication lag of the slowest secondary + + +-- + +[float] +== members fields + +Provides information about members of replica set grouped by their state + + + +*`mongodb.replstatus.members.primary.host`*:: ++ +-- +type: keyword + +Host address of the primary + + +-- + +*`mongodb.replstatus.members.primary.optime`*:: ++ +-- +type: keyword + +Optime of primary + + +-- + +*`mongodb.replstatus.members.secondary.hosts`*:: ++ +-- +type: array + +List of secondary hosts + + +-- + +*`mongodb.replstatus.members.secondary.optimes`*:: ++ +-- +type: keyword + +Optimes of secondaries + + +-- + +*`mongodb.replstatus.members.secondary.count`*:: ++ +-- +type: long + +-- + +*`mongodb.replstatus.members.recovering.hosts`*:: ++ +-- +type: array + +List of recovering members hosts + + +-- + +*`mongodb.replstatus.members.recovering.count`*:: ++ +-- +type: long + +Count of members in the `recovering` state + + +-- + +*`mongodb.replstatus.members.unknown.hosts`*:: ++ +-- +type: array + +List of members' hosts in the `unknown` state + + +-- + +*`mongodb.replstatus.members.unknown.count`*:: ++ +-- +type: long + +Count of members with `unknown` state + + +-- + +*`mongodb.replstatus.members.startup2.hosts`*:: ++ +-- +type: array + +List of initializing members hosts + + +-- + +*`mongodb.replstatus.members.startup2.count`*:: ++ +-- +type: long + +Count of members in the `startup2` state + + +-- + +*`mongodb.replstatus.members.arbiter.hosts`*:: ++ +-- +type: array + +List of arbiters hosts + + +-- + +*`mongodb.replstatus.members.arbiter.count`*:: ++ +-- +type: long + +Count of arbiters + + +-- + +*`mongodb.replstatus.members.down.hosts`*:: ++ +-- +type: array + +List of `down` members hosts + + +-- + +*`mongodb.replstatus.members.down.count`*:: ++ +-- +type: long + +Count of `down` members + + +-- + +*`mongodb.replstatus.members.rollback.hosts`*:: ++ +-- +type: array + +List of members in the `rollback` state + + +-- + +*`mongodb.replstatus.members.rollback.count`*:: ++ +-- +type: long + +Count of members in the `rollback` state + + +-- + +*`mongodb.replstatus.members.unhealthy.hosts`*:: ++ +-- +type: array + +List of members' hosts with healthy = false + + +-- + +*`mongodb.replstatus.members.unhealthy.count`*:: ++ +-- +type: long + +Count of unhealthy members + + -- [float] diff --git a/metricbeat/docs/modules/mongodb.asciidoc b/metricbeat/docs/modules/mongodb.asciidoc index 7ed549a7f38..a7fcfbdfc95 100644 --- a/metricbeat/docs/modules/mongodb.asciidoc +++ b/metricbeat/docs/modules/mongodb.asciidoc @@ -66,7 +66,7 @@ in <>. Here is an example configuration: ---- metricbeat.modules: - module: mongodb - metricsets: ["dbstats", "status", "collstats", "metrics"] + metricsets: ["dbstats", "status", "collstats", "metrics", "replstatus"] period: 10s enabled: true @@ -112,6 +112,8 @@ The following metricsets are available: * <> +* <> + * <> include::mongodb/collstats.asciidoc[] @@ -120,5 +122,7 @@ include::mongodb/dbstats.asciidoc[] include::mongodb/metrics.asciidoc[] +include::mongodb/replstatus.asciidoc[] + include::mongodb/status.asciidoc[] diff --git a/metricbeat/docs/modules/mongodb/replstatus.asciidoc b/metricbeat/docs/modules/mongodb/replstatus.asciidoc new file mode 100644 index 00000000000..15258578fd2 --- /dev/null +++ b/metricbeat/docs/modules/mongodb/replstatus.asciidoc @@ -0,0 +1,23 @@ +//// +This file is generated! See scripts/docs_collector.py +//// + +[[metricbeat-metricset-mongodb-replstatus]] +=== MongoDB replstatus metricset + +experimental[] + +include::../../../module/mongodb/replstatus/_meta/docs.asciidoc[] + + +==== Fields + +For a description of each field in the metricset, see the +<> section. + +Here is an example document generated by this metricset: + +[source,json] +---- +include::../../../module/mongodb/replstatus/_meta/data.json[] +---- diff --git a/metricbeat/docs/modules_list.asciidoc b/metricbeat/docs/modules_list.asciidoc index c3884bab9d3..b9273300f5e 100644 --- a/metricbeat/docs/modules_list.asciidoc +++ b/metricbeat/docs/modules_list.asciidoc @@ -89,9 +89,10 @@ This file is generated! See scripts/docs_collector.py |<> beta[] |image:./images/icon-no.png[No prebuilt dashboards] | .1+| .1+| |<> beta[] |<> |image:./images/icon-yes.png[Prebuilt dashboards are available] | -.4+| .4+| |<> +.5+| .5+| |<> |<> |<> experimental[] +|<> experimental[] |<> |<> beta[] |image:./images/icon-no.png[No prebuilt dashboards] | .1+| .1+| |<> beta[] diff --git a/metricbeat/include/list.go b/metricbeat/include/list.go index 917d8588d98..b5032aae0e0 100644 --- a/metricbeat/include/list.go +++ b/metricbeat/include/list.go @@ -114,6 +114,7 @@ import ( _ "github.com/elastic/beats/metricbeat/module/mongodb/collstats" _ "github.com/elastic/beats/metricbeat/module/mongodb/dbstats" _ "github.com/elastic/beats/metricbeat/module/mongodb/metrics" + _ "github.com/elastic/beats/metricbeat/module/mongodb/replstatus" _ "github.com/elastic/beats/metricbeat/module/mongodb/status" _ "github.com/elastic/beats/metricbeat/module/munin" _ "github.com/elastic/beats/metricbeat/module/munin/node" diff --git a/metricbeat/metricbeat.reference.yml b/metricbeat/metricbeat.reference.yml index 649b7bcf0a6..51342480ebe 100644 --- a/metricbeat/metricbeat.reference.yml +++ b/metricbeat/metricbeat.reference.yml @@ -444,7 +444,7 @@ metricbeat.modules: #------------------------------- MongoDB Module ------------------------------ - module: mongodb - metricsets: ["dbstats", "status", "collstats", "metrics"] + metricsets: ["dbstats", "status", "collstats", "metrics", "replstatus"] period: 10s enabled: true diff --git a/metricbeat/module/mongodb/_meta/config.reference.yml b/metricbeat/module/mongodb/_meta/config.reference.yml index 2aa1e6322e3..91928047af2 100644 --- a/metricbeat/module/mongodb/_meta/config.reference.yml +++ b/metricbeat/module/mongodb/_meta/config.reference.yml @@ -1,5 +1,5 @@ - module: mongodb - metricsets: ["dbstats", "status", "collstats", "metrics"] + metricsets: ["dbstats", "status", "collstats", "metrics", "replstatus"] period: 10s enabled: true diff --git a/metricbeat/module/mongodb/_meta/config.yml b/metricbeat/module/mongodb/_meta/config.yml index 5f9c0aac612..971de03d949 100644 --- a/metricbeat/module/mongodb/_meta/config.yml +++ b/metricbeat/module/mongodb/_meta/config.yml @@ -4,6 +4,7 @@ # - status # - collstats # - metrics + # - replstatus period: 10s # The hosts must be passed as MongoDB URLs in the format: diff --git a/metricbeat/module/mongodb/fields.go b/metricbeat/module/mongodb/fields.go index d51ee0dc225..02c6060f3c8 100644 --- a/metricbeat/module/mongodb/fields.go +++ b/metricbeat/module/mongodb/fields.go @@ -31,5 +31,5 @@ func init() { // Asset returns asset data func Asset() string { - return "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" + return "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" } diff --git a/metricbeat/module/mongodb/replstatus/_meta/data.json b/metricbeat/module/mongodb/replstatus/_meta/data.json new file mode 100644 index 00000000000..3aa60d3933b --- /dev/null +++ b/metricbeat/module/mongodb/replstatus/_meta/data.json @@ -0,0 +1,90 @@ +{ + "@timestamp": "2016-05-23T08:05:34.853Z", + "beat": { + "hostname": "host.example.com", + "name": "host.example.com" + }, + "metricset":{ + "host":"s1.example.com:27017,s2.example.com:27017,s3.example.com:27017", + "module":"mongodb", + "name":"replstatus", + "rtt": 44269 + }, + "mongodb": { + "replstatus": { + "oplog": { + "window": 13881222, + "size": { + "allocated": 2348399104, + "used": 2369966391 + }, + "first": { + "timestamp": 1520110569 + }, + "last": { + "timestamp": 1533991791 + } + }, + "set_name": "beats", + "server_date": "2018-08-11T12:49:52.984Z", + "optimes": { + "last_committed": 1533991791, + "applied": 1533991791, + "durable": 1533991791 + }, + "lag": { + "max": 10, + "min": 10 + }, + "headroom": { + "max": 13881212, + "min": 13881212 + }, + "members": { + "arbiter": { + "hosts": [ + "s1.example.com:27017" + ], + "count": 1 + }, + "rollback": { + "hosts": [], + "count": 0 + }, + "unhealthy": { + "count": 0, + "hosts": [] + }, + "secondary": { + "count": 1, + "optimes": [ + 1533991781 + ], + "hosts": [ + "s2.example.com:27017" + ] + }, + "recovering": { + "count": 0, + "hosts": [] + }, + "unknown": { + "hosts": [], + "count": 0 + }, + "startup2": { + "hosts": [], + "count": 0 + }, + "down": { + "hosts": [], + "count": 0 + }, + "primary": { + "optime": 1533991791, + "host": "s3.example.com:27017" + } + } + } + } +} diff --git a/metricbeat/module/mongodb/replstatus/_meta/docs.asciidoc b/metricbeat/module/mongodb/replstatus/_meta/docs.asciidoc new file mode 100644 index 00000000000..3df608460f7 --- /dev/null +++ b/metricbeat/module/mongodb/replstatus/_meta/docs.asciidoc @@ -0,0 +1 @@ +This is the `replstatus` metricset of the module mongodb. diff --git a/metricbeat/module/mongodb/replstatus/_meta/fields.yml b/metricbeat/module/mongodb/replstatus/_meta/fields.yml new file mode 100644 index 00000000000..4eb25f56823 --- /dev/null +++ b/metricbeat/module/mongodb/replstatus/_meta/fields.yml @@ -0,0 +1,175 @@ +- name: replstatus + type: group + description: > + replstatus provides an overview of replica set status. + fields: + - name: oplog + type: group + description: > + oplog provides an overview of replication oplog status, which is retrieved from db.getReplicationInfo(). + fields: + - name: size.allocated + type: long + format: bytes + description: > + The total amount of space used by the replstatus in bytes. + - name: size.used + type: long + format: bytes + description: > + total amount of space allocated to the replstatus in bytes. + + - name: first.timestamp + type: long + description: > + Timestamp of the first (i.e. earliest) operation in the replstatus + + - name: last.timestamp + type: long + description: > + Timestamp of the last (i.e. latest) operation in the replstatus + + - name: window + type: long + description: > + The difference between the first and last operation in the replstatus. + + - name: set_name + type: keyword + description: > + The name of the replica set. + + - name: server_date + type: date + description: > + Reflects the current time according to the server that processed the replSetGetStatus command. + + - name: optimes + type: group + fields: + - name: last_committed + type: long + description: > + Information, from the viewpoint of this member, regarding the most recent operation that has been written to a majority of replica set members. + - name: applied + type: long + description: > + Information, from the viewpoint of this member, regarding the most recent operation that has been applied to this member of the replica set. + - name: durable + type: long + description: > + Information, from the viewpoint of this member, regarding the most recent operation that has been written to the journal of this member of the replica set. + + - name: lag + type: group + description: > + Delay between a write operation on the primary and its copy to a secondary + fields: + - name: max + type: long + format: duration + description: > + Difference between optime of primary and slowest secondary + - name: min + type: long + format: duration + description: > + Difference between optime of primary and fastest secondary + + - name: headroom + type: group + description: > + Difference between the primary's oplog window and the replication lag of the secondary + fields: + - name: max + type: long + format: duration + description: > + Difference between primary's oplog window and the replication lag of the fastest secondary + - name: min + type: long + format: duration + description: > + Difference between primary's oplog window and the replication lag of the slowest secondary + + - name: members + type: group + description: > + Provides information about members of replica set grouped by their state + fields: + - name: primary.host + type: keyword + description: > + Host address of the primary + - name: primary.optime + type: keyword + description: > + Optime of primary + - name: secondary.hosts + type: array + description: > + List of secondary hosts + - name: secondary.optimes + type: keyword + description: > + Optimes of secondaries + - name: secondary.count + type: long + descriprtion: > + Count of secondaries + - name: recovering.hosts + type: array + description: > + List of recovering members hosts + - name: recovering.count + type: long + description: > + Count of members in the `recovering` state + - name: unknown.hosts + type: array + description: > + List of members' hosts in the `unknown` state + - name: unknown.count + type: long + description: > + Count of members with `unknown` state + - name: startup2.hosts + type: array + description: > + List of initializing members hosts + - name: startup2.count + type: long + description: > + Count of members in the `startup2` state + - name: arbiter.hosts + type: array + description: > + List of arbiters hosts + - name: arbiter.count + type: long + description: > + Count of arbiters + - name: down.hosts + type: array + description: > + List of `down` members hosts + - name: down.count + type: long + description: > + Count of `down` members + - name: rollback.hosts + type: array + description: > + List of members in the `rollback` state + - name: rollback.count + type: long + description: > + Count of members in the `rollback` state + - name: unhealthy.hosts + type: array + description: > + List of members' hosts with healthy = false + - name: unhealthy.count + type: long + description: > + Count of unhealthy members diff --git a/metricbeat/module/mongodb/replstatus/data.go b/metricbeat/module/mongodb/replstatus/data.go new file mode 100644 index 00000000000..cf2539c1eec --- /dev/null +++ b/metricbeat/module/mongodb/replstatus/data.go @@ -0,0 +1,124 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package replstatus + +import ( + "github.com/elastic/beats/libbeat/common" +) + +func eventMapping(oplogInfo oplogInfo, replStatus MongoReplStatus) common.MapStr { + var result common.MapStr = make(common.MapStr) + + result["oplog"] = common.MapStr{ + "size": common.MapStr{ + "allocated": oplogInfo.allocated, + "used": oplogInfo.used, + }, + "first": common.MapStr{ + "timestamp": oplogInfo.firstTs, + }, + "last": common.MapStr{ + "timestamp": oplogInfo.lastTs, + }, + "window": oplogInfo.diff, + } + result["set_name"] = replStatus.Set + result["server_date"] = replStatus.Date + result["optimes"] = common.MapStr{ + "last_committed": replStatus.OpTimes.LastCommitted.getTimeStamp(), + "applied": replStatus.OpTimes.Applied.getTimeStamp(), + "durable": replStatus.OpTimes.Durable.getTimeStamp(), + } + + // find lag and headroom + minLag, maxLag, lagIsOk := findLag(replStatus.Members) + if lagIsOk { + result["lag"] = common.MapStr{ + "max": maxLag, + "min": minLag, + } + + result["headroom"] = common.MapStr{ + "max": oplogInfo.diff - minLag, + "min": oplogInfo.diff - maxLag, + } + } else { + result["lag"] = common.MapStr{ + "max": nil, + "min": nil, + } + + result["headroom"] = common.MapStr{ + "max": nil, + "min": nil, + } + } + + var ( + secondaryHosts = findHostsByState(replStatus.Members, SECONDARY) + recoveringHosts = findHostsByState(replStatus.Members, RECOVERING) + unknownHosts = findHostsByState(replStatus.Members, UNKNOWN) + startup2Hosts = findHostsByState(replStatus.Members, STARTUP2) + arbiterHosts = findHostsByState(replStatus.Members, ARBITER) + downHosts = findHostsByState(replStatus.Members, DOWN) + rollbackHosts = findHostsByState(replStatus.Members, ROLLBACK) + unhealthyHosts = findUnhealthyHosts(replStatus.Members) + ) + + result["members"] = common.MapStr{ + "primary": common.MapStr{ + "host": findHostsByState(replStatus.Members, PRIMARY)[0], + "optime": findOptimesByState(replStatus.Members, PRIMARY)[0], + }, + "secondary": common.MapStr{ + "hosts": secondaryHosts, + "count": len(secondaryHosts), + "optimes": findOptimesByState(replStatus.Members, SECONDARY), + }, + "recovering": common.MapStr{ + "hosts": recoveringHosts, + "count": len(recoveringHosts), + }, + "unknown": common.MapStr{ + "hosts": unknownHosts, + "count": len(unknownHosts), + }, + "startup2": common.MapStr{ + "hosts": startup2Hosts, + "count": len(startup2Hosts), + }, + "arbiter": common.MapStr{ + "hosts": arbiterHosts, + "count": len(arbiterHosts), + }, + "down": common.MapStr{ + "hosts": downHosts, + "count": len(downHosts), + }, + "rollback": common.MapStr{ + "hosts": rollbackHosts, + "count": len(rollbackHosts), + }, + "unhealthy": common.MapStr{ + "hosts": unhealthyHosts, + "count": len(unhealthyHosts), + }, + } + + return result +} diff --git a/metricbeat/module/mongodb/replstatus/info.go b/metricbeat/module/mongodb/replstatus/info.go new file mode 100644 index 00000000000..f775044f9f6 --- /dev/null +++ b/metricbeat/module/mongodb/replstatus/info.go @@ -0,0 +1,106 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package replstatus + +import ( + "errors" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" + + "github.com/elastic/beats/libbeat/logp" +) + +type oplogInfo struct { + allocated int64 + used float64 + firstTs int64 + lastTs int64 + diff int64 +} + +// CollSize contains data about collection size +type CollSize struct { + MaxSize int64 `bson:"maxSize"` // Shows the maximum size of the collection. + Size float64 `bson:"size"` // The total size in memory of all records in a collection. +} + +const oplogCol = "oplog.rs" + +func getReplicationInfo(mongoSession *mgo.Session) (*oplogInfo, error) { + // get oplog.rs collection + db := mongoSession.DB("local") + if collections, err := db.CollectionNames(); err != nil || !contains(collections, oplogCol) { + if err == nil { + err = errors.New("collection oplog.rs was not found") + } + + logp.Err(err.Error()) + return nil, err + } + collection := db.C(oplogCol) + + // get oplog size + var oplogSize CollSize + if err := db.Run(bson.D{{Name: "collStats", Value: oplogCol}}, &oplogSize); err != nil { + return nil, err + } + + // get first and last items in the oplog + firstTs, err := getOpTimestamp(collection, "$natural") + if err != nil { + return nil, err + } + + lastTs, err := getOpTimestamp(collection, "-$natural") + if err != nil { + return nil, err + } + + diff := lastTs - firstTs + + return &oplogInfo{ + allocated: oplogSize.MaxSize, + used: oplogSize.Size, + firstTs: firstTs, + lastTs: lastTs, + diff: diff, + }, nil +} + +func getOpTimestamp(collection *mgo.Collection, sort string) (int64, error) { + iter := collection.Find(nil).Sort(sort).Iter() + + var opTime OpTime + if !iter.Next(&opTime) { + err := errors.New("objects not found in local.oplog.rs -- Is this a new and empty db instance?") + logp.Err(err.Error()) + return 0, err + } + + return opTime.getTimeStamp(), nil +} + +func contains(s []string, x string) bool { + for _, n := range s { + if x == n { + return true + } + } + return false +} diff --git a/metricbeat/module/mongodb/replstatus/replstatus.go b/metricbeat/module/mongodb/replstatus/replstatus.go new file mode 100644 index 00000000000..41406d2b64e --- /dev/null +++ b/metricbeat/module/mongodb/replstatus/replstatus.go @@ -0,0 +1,84 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package replstatus + +import ( + mgo "gopkg.in/mgo.v2" + + "github.com/elastic/beats/libbeat/common" + "github.com/elastic/beats/libbeat/common/cfgwarn" + "github.com/elastic/beats/libbeat/logp" + "github.com/elastic/beats/metricbeat/mb" + "github.com/elastic/beats/metricbeat/module/mongodb" +) + +var debugf = logp.MakeDebug("mongodb.replstatus") + +func init() { + mb.Registry.MustAddMetricSet("mongodb", "replstatus", New, + mb.WithHostParser(mongodb.ParseURL)) +} + +// MetricSet type defines all fields of the MetricSet +// As a minimum it must inherit the mb.BaseMetricSet fields, but can be extended with +// additional entries. These variables can be used to persist data or configuration between +// multiple fetch calls. +type MetricSet struct { + *mongodb.MetricSet +} + +// New creates a new instance of the MetricSet +// Part of new is also setting up the configuration by processing additional +// configuration entries if needed. +func New(base mb.BaseMetricSet) (mb.MetricSet, error) { + cfgwarn.Experimental("The mongodb replstatus metricset is experimental.") + + ms, err := mongodb.NewMetricSet(base) + if err != nil { + return nil, err + } + return &MetricSet{ms}, nil +} + +// Fetch methods implements the data gathering and data conversion to the right +// format. It publishes the event which is then forwarded to the output. In case +// of an error set the Error field of mb.Event or simply call report.Error(). +func (m *MetricSet) Fetch() (common.MapStr, error) { + // instantiate direct connections to each of the configured Mongo hosts + mongoSession, err := mongodb.NewDirectSession(m.DialInfo) + if err != nil { + return nil, err + } + defer mongoSession.Close() + + mongoSession.SetMode(mgo.Strong, true) + + oplogInfo, err := getReplicationInfo(mongoSession) + if err != nil { + return nil, err + } + + replStatus, err := getReplicationStatus(mongoSession) + if err != nil { + return nil, err + } + + event := eventMapping(*oplogInfo, *replStatus) + + return event, nil +} diff --git a/metricbeat/module/mongodb/replstatus/replstatus_integration_test.go b/metricbeat/module/mongodb/replstatus/replstatus_integration_test.go new file mode 100644 index 00000000000..40e60bca51b --- /dev/null +++ b/metricbeat/module/mongodb/replstatus/replstatus_integration_test.go @@ -0,0 +1,140 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// +build integration + +package replstatus + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" + + "github.com/elastic/beats/libbeat/common" + "github.com/elastic/beats/libbeat/tests/compose" + mbtest "github.com/elastic/beats/metricbeat/mb/testing" + "github.com/elastic/beats/metricbeat/module/mongodb" +) + +func TestFetch(t *testing.T) { + compose.EnsureUp(t, "mongodb") + + err := initiateReplicaSet(t) + if !assert.NoError(t, err) { + t.FailNow() + } + + f := mbtest.NewEventFetcher(t, getConfig()) + event, err := f.Fetch() + if !assert.NoError(t, err) { + t.FailNow() + } + + t.Logf("%s/%s event: %+v", f.Module().Name(), f.Name(), event) + + // Check event fields + oplog := event["oplog"].(common.MapStr) + allocated := oplog["size"].(common.MapStr)["allocated"].(int64) + assert.True(t, allocated >= 0) + + used := oplog["size"].(common.MapStr)["used"].(float64) + assert.True(t, used > 0) + + firstTs := oplog["first"].(common.MapStr)["timestamp"].(int64) + assert.True(t, firstTs >= 0) + + window := oplog["window"].(int64) + assert.True(t, window >= 0) + + members := event["members"].(common.MapStr) + primary := members["primary"].(common.MapStr) + assert.NotEmpty(t, primary["host"].(string)) + assert.True(t, primary["optime"].(int64) > 0) + + set := event["set_name"].(string) + assert.Equal(t, set, "beats") +} + +func TestData(t *testing.T) { + compose.EnsureUp(t, "mongodb") + + f := mbtest.NewEventFetcher(t, getConfig()) + err := mbtest.WriteEvent(f, t) + if err != nil { + t.Fatal("write", err) + } +} + +func getConfig() map[string]interface{} { + return map[string]interface{}{ + "module": "mongodb", + "metricsets": []string{"replstatus"}, + "hosts": []string{mongodb.GetEnvHost() + ":" + mongodb.GetEnvPort()}, + } +} + +func initiateReplicaSet(t *testing.T) error { + url := getConfig()["hosts"].([]string)[0] + + dialInfo, err := mgo.ParseURL(url) + if err != nil { + return err + } + dialInfo.Direct = true + + mongoSession, err := mongodb.NewDirectSession(dialInfo) + if err != nil { + return err + } + defer mongoSession.Close() + + // get oplog.rs collection + db := mongoSession.DB("admin") + config := ReplicaConfig{"beats", []Host{{0, url}}} + var initiateResult map[string]interface{} + if err := db.Run(bson.M{"replSetInitiate": config}, &initiateResult); err != nil { + if err.Error() != "already initialized" { + return err + } + } + + var status map[string]interface{} + for { + db.Run(bson.M{"replSetGetStatus": 1}, &status) + myState, ok := status["myState"].(int) + t.Logf("Mongodb state is %d", myState) + if ok && myState == 1 { + time.Sleep(5 * time.Second) // hack, wait more for replica set to become stable + break + } + } + + return nil +} + +type ReplicaConfig struct { + id string `bson:_id` + members []Host `bson:hosts` +} + +type Host struct { + id int `bson:_id` + host string `bson:host` +} diff --git a/metricbeat/module/mongodb/replstatus/status.go b/metricbeat/module/mongodb/replstatus/status.go new file mode 100644 index 00000000000..e7e7de688a0 --- /dev/null +++ b/metricbeat/module/mongodb/replstatus/status.go @@ -0,0 +1,167 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package replstatus + +import ( + "time" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// MongoReplStatus cointains the status of the replica set from the point of view of the server that processed the command. +type MongoReplStatus struct { + Ok bool `bson:"ok"` + Set string `bson:"set"` + Date time.Time `bson:"date"` + Members []Member `bson:"members"` + MyState int `bson:"myState"` + Term int `bson:"term"` + HeartbeatIntervalMillis int `bson:"heartbeatIntervalMillis"` + OpTimes struct { + LastCommitted OpTime `bson:"lastCommittedOpTime"` + Applied OpTime `bson:"appliedOpTime"` + Durable OpTime `bson:"durableOpTime"` + } `bson:"optimes"` +} + +// Member provides information about a member in the replica set. +type Member struct { + Health bool `bson:"health"` + Name string `bson:"name"` + State int `bson:"state"` + StateStr string `bson:"stateStr"` + Uptime int `bson:"uptime"` + OpTime OpTime `bson:"optime"` + OpTimeDate time.Time `bson:"optimeDate"` + ElectionTime int64 `bson:"electionTime"` + ElectionDate time.Time `bson:"electaionDate"` + ConfigVersion int `bson:"configVersion"` + Self bool `bson:"self"` +} + +// OpTime holds information regarding the operation from the operation log +type OpTime struct { + Ts int64 `bson:"ts"` // The timestamp of the last operation applied to this member of the replica set + T int `bson:"t"` // The term in which the last applied operation was originally generated on the primary. +} + +// MemberState shows the state of a member in the replica set +type MemberState int + +const ( + // STARTUP state + STARTUP MemberState = 0 + // PRIMARY state + PRIMARY MemberState = 1 + // SECONDARY state + SECONDARY MemberState = 2 + // RECOVERING state + RECOVERING MemberState = 3 + // STARTUP2 state + STARTUP2 MemberState = 5 + // UNKNOWN state + UNKNOWN MemberState = 6 + // ARBITER state + ARBITER MemberState = 7 + // DOWN state + DOWN MemberState = 8 + // ROLLBACK state + ROLLBACK MemberState = 9 + // REMOVED state + REMOVED MemberState = 10 +) + +func (optime *OpTime) getTimeStamp() int64 { + return optime.Ts >> 32 +} + +func getReplicationStatus(mongoSession *mgo.Session) (*MongoReplStatus, error) { + db := mongoSession.DB("admin") + + var replStatus MongoReplStatus + if err := db.Run(bson.M{"replSetGetStatus": 1}, &replStatus); err != nil { + return nil, err + } + + return &replStatus, nil +} + +func findUnhealthyHosts(members []Member) []string { + var hosts []string + + for _, member := range members { + if member.Health == false { + hosts = append(hosts, member.Name) + } + } + + return hosts +} + +func findHostsByState(members []Member, state MemberState) []string { + var hosts []string + + for _, member := range members { + memberState := MemberState(member.State) + if memberState == state { + hosts = append(hosts, member.Name) + } + } + + return hosts +} + +func findLag(members []Member) (minLag int64, maxLag int64, hasSecondary bool) { + var minOptime, maxOptime, primaryOptime int64 = 1<<63 - 1, 0, 0 + hasSecondary = false + + for _, member := range members { + memberState := MemberState(member.State) + if memberState == SECONDARY { + hasSecondary = true + + if minOptime > member.OpTime.getTimeStamp() { + minOptime = member.OpTime.getTimeStamp() + } + + if member.OpTime.getTimeStamp() > maxOptime { + maxOptime = member.OpTime.getTimeStamp() + } + } else if memberState == PRIMARY { + primaryOptime = member.OpTime.getTimeStamp() + } + } + + minLag = primaryOptime - maxOptime + maxLag = primaryOptime - minOptime + return minLag, maxLag, hasSecondary +} + +func findOptimesByState(members []Member, state MemberState) []int64 { + var optimes []int64 + + for _, member := range members { + memberState := MemberState(member.State) + if memberState == state { + optimes = append(optimes, member.OpTime.getTimeStamp()) + } + } + + return optimes +} diff --git a/metricbeat/modules.d/mongodb.yml.disabled b/metricbeat/modules.d/mongodb.yml.disabled index 59734e329a7..0b84b2ba788 100644 --- a/metricbeat/modules.d/mongodb.yml.disabled +++ b/metricbeat/modules.d/mongodb.yml.disabled @@ -7,6 +7,7 @@ # - status # - collstats # - metrics + # - replstatus period: 10s # The hosts must be passed as MongoDB URLs in the format: