New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Mongo dbstats #3228
Mongo dbstats #3228
Changes from 3 commits
9274966
f28f347
c811c3e
590b9e2
b6f8da3
74f9ee0
dd895b5
2c92245
5a9ae7f
8e1934c
1f420d7
65d8d89
a4a6e75
3c6d97c
aafaafe
663b4da
99bc01f
82be711
98b86a0
f207fac
2b3f230
abf8973
5bde2ee
976294a
7b0a38f
827348c
2de87a8
54009f4
594085b
48f9a53
757dbd0
06b7220
e4b627a
28e51fd
1c80f5e
a8450fb
35ac943
a9dd25f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
{ | ||
"@timestamp":"2016-05-23T08:05:34.853Z", | ||
"beat":{ | ||
"hostname":"beathost", | ||
"name":"beathost" | ||
}, | ||
"metricset":{ | ||
"host":"localhost", | ||
"module":"mysql", | ||
"name":"status", | ||
"rtt":44269 | ||
}, | ||
"mongodb":{ | ||
"dbstats":{ | ||
"example": "dbstats" | ||
} | ||
}, | ||
"type":"metricsets" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
=== mongodb dbstats MetricSet | ||
|
||
This is the dbstats metricset of the module mongodb. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
- name: dbstats | ||
type: group | ||
description: > | ||
dbstats provides an overview of a particular mongo database. This document | ||
is most concerned with data volumes of a database. | ||
fields: | ||
- name: avg_object_size | ||
type: long | ||
|
||
- name: collections | ||
type: integer | ||
|
||
- name: data_size | ||
type: long | ||
|
||
- name: db | ||
type: keyword | ||
|
||
- name: file_size | ||
type: long | ||
|
||
- name: index_size | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is probably |
||
type: long | ||
|
||
- name: indexes | ||
type: long | ||
|
||
- name: num_extents | ||
type: long | ||
|
||
- name: objects | ||
type: long | ||
|
||
- name: storage_size | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is the unit for all the |
||
type: long |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package dbstats | ||
|
||
import ( | ||
s "github.com/elastic/beats/metricbeat/schema" | ||
c "github.com/elastic/beats/metricbeat/schema/mapstriface" | ||
) | ||
|
||
var schema = s.Schema{ | ||
"db": c.Str("db"), | ||
"collection": c.Int("collections"), | ||
"objects": c.Int("objects"), | ||
"avg_object_size": c.Int("avgObjectSize"), | ||
"data_size": c.Int("dataSize"), | ||
"storage_size": c.Int("storageSize"), | ||
"num_extents": c.Int("numExtents"), | ||
"indexes": c.Int("indexes"), | ||
"index_size": c.Int("indexSize"), | ||
"file_size": c.Int("fileSize"), | ||
} | ||
|
||
var eventMapping = schema.Apply |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package dbstats | ||
|
||
import ( | ||
"errors" | ||
|
||
"github.com/beats/libbeat/logp" | ||
"github.com/elastic/beats/libbeat/common" | ||
"github.com/elastic/beats/metricbeat/mb" | ||
"gopkg.in/mgo.v2" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor detail: We normally put this in between the general and the beats imports, so it is:
|
||
) | ||
|
||
// init registers the MetricSet with the central registry. | ||
// The New method will be called after the setup of the module and before starting to fetch data | ||
func init() { | ||
if err := mb.Registry.AddMetricSet("mongodb", "dbstats", New); err != nil { | ||
panic(err) | ||
} | ||
} | ||
|
||
// 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 { | ||
mb.BaseMetricSet | ||
dialInfo *mgo.DialInfo | ||
} | ||
|
||
// New create 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) { | ||
dialInfo, err := mgo.ParseURL(base.HostData().URI) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add an experimental flag here? We normally introduce new metricsets as experimental. This allows us to first get some real word feedback for it and still change the data structure if needed without having to wait for a major release. Have a look here: https://github.com/elastic/beats/blob/master/metricbeat/module/haproxy/stat/stat.go#L34 |
||
if err != nil { | ||
return nil, err | ||
} | ||
dialInfo.Timeout = base.Module().Config().Timeout | ||
|
||
return &MetricSet{ | ||
BaseMetricSet: base, | ||
dialInfo: dialInfo, | ||
}, nil | ||
} | ||
|
||
// Fetch methods implements the data gathering and data conversion to the right format | ||
// It returns the event which is then forward to the output. In case of an error, a | ||
// descriptive error must be returned. | ||
func (m *MetricSet) Fetch() ([]common.MapStr, error) { | ||
|
||
// establish connection to mongo | ||
session, err := mgo.DialWithInfo(m.dialInfo) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This follows the pattern used by the According to the [docs],(https://www.elastic.co/guide/en/beats/metricbeat/current/metricset-details.html#_timeout_connections_to_services) connections should be established in the I'm going to begin implementing a pattern where the mongo session is persisted in each Lastly, based on this implementation I'm more confident that the mongodb module is not connecting directly to the individual instances, but would instead connect to the cluster. This means that any cluster member may answer the query and result in data inconsistencies. This issue is a bit more involved, and I don't know enough about the platform to be sure, so I may need to jump on a Zoom call or something to talk further. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 on persisting the connection. It is probably best to extract the connection setup logic to the module level and just use it as a function. About connecting to single instances or the cluster can be come tricky. We had a similar issue with Kafka. Our recommendation is to install metricbeat on each edge node (some exceptions exist here), as it gives you also additional metrics about the system etc. I'm don't know the details of the mongodb clustering but for Kafka we solved it the following way (generic description):
This requires that a metricbeat instance can detect if it is a master or not. Also I assume this must be dynamic, as master can change over time. I assume dbStats response is the same for each node, so only the data from master should be fetched. Happy to jump on a zoom call to go into more details. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Connecting to single instancesTricky indeed. Fortunately, mgo (the mongo client) has a I create the individual connections in the "controller" here I've begun implementing something similar for the mongodb module, minus node discovery. This is implemented at the module level to share logic and prevent redundant connections. Detecting master/primaryEasily implemented with mgo. You can specify read preference to always read from primary. If the primary changes, your reads will automatically be directed to the new primary. dbStats response the sameTechnically dbStats response (and serverStatus) are not the same for each node. Sometimes the differences are trivial, other times they're not. I'll cover the cases where they're not the same: In the case of a replica set, the state of the secondaries lag behind the primary. Thus, seeing the stats for each node helps monitor replication lag and success/failure. This is especially helpful in the case of multi-datacenter replication, where larger discrepancies are usually present. In the case of a sharded cluster, the complexity is compounded because nodes contain different databases and collections, and a shard can also be a replica set! =o ConclusionGiven these considerations, I'm still having a hard time understanding what the best implementation for metricbeat would be. Given the current implementation - the node that is read from is implicit and can change from period to period. For example, you might connect to mongo on If we enforce that metricbeat only establishes a direct connection to a single mongo instance on localhost, then all of this complexity goes away and output is very predictable. Yay! But, it limits the flexibility of the system. If we allow the agent to connect to mongo as a cluster, and permit configuration of multiple hosts, then I think there's a lot of different directions we could take. Definitely a Zoom call will be helpful. I'm sure you can teach me a lot more about how metricbeat works under the hood, so I can understand the best direction. I don't have your email, but can you contact me at sccrespo@gmail.com and we'll set up a time? Perhaps we could talk sometime Thursday afternoon (your time). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @scottcrespo Sorry, I was mostly offline the last two days, so only saw the update now. I always try to go with the simplest solution first, which I agree is the one only connecting to localhost. Because of docker I would probably rephrase this to: The mongodb module gets only the stats from the node it connects to, as it does not necessarly always have to be localhost on docker. Could you update the PR so that it always only connects to the defined host (and not the cluster)? About zoom call: Lets see how far we can take it here in the PR as I'm rarely only in the next days because of holidays. But I'm happy to answer any questions about metricbeat in more detail here if needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No problem! I've been on vacation as well, but I have a few days to work on beats before taking some additional time off. I'm in the process of updating the mongodb module to establish direct connections to the hosts listed in the config, and to persist those connections between calls. I should be able to update this PR within a few days! |
||
if err != nil { | ||
return nil, err | ||
} | ||
defer session.Close() | ||
|
||
session.SetMode(mgo.Monotonic, true) | ||
|
||
// Get the list of databases database names, which we'll use to call db.stats() on each | ||
dbNames, err := session.DatabaseNames() | ||
if err != nil { | ||
logp.Err("Error retrieving database names from Mongo instance") | ||
return []common.MapStr{}, err | ||
} | ||
|
||
// events is the list of events collected from each of the databases. | ||
events := []common.MapStr{} | ||
|
||
// for each database, call db.stats() and append to events | ||
for _, dbName := range dbNames { | ||
db := session.DB(dbName) | ||
|
||
result := map[string]interface{}{} | ||
|
||
err := db.Run("dbStats", &result) | ||
if err != nil { | ||
logp.Err("Failed to retrieve stats for db %s", dbName) | ||
continue | ||
} | ||
events = append(events, eventMapping(result)) | ||
} | ||
|
||
// if we failed to collect on any databases, return an error | ||
if len(events) == 0 { | ||
err = errors.New("Failed to fetch stats for all databases in mongo instance") | ||
return []common.MapStr{}, err | ||
} | ||
|
||
return events, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package dbstats | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/beats/metricbeat/module/mongodb" | ||
mbtest "github.com/elastic/beats/metricbeat/mb/testing" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestFetch(t *testing.T) { | ||
f := mbtest.NewEventsFetcher(t, getConfig()) | ||
events, err := f.Fetch() | ||
if !assert.NoError(t, err) { | ||
t.FailNow() | ||
} | ||
|
||
for _, event := range events { | ||
t.Logf("%s/%s event: %+v", f.Module().Name(), f.Name(), event) | ||
|
||
// Check a few event Fields | ||
db := event["db"].(string) | ||
assert.NotEqual(t, db, "") | ||
|
||
dataSize := event["data_size"].(int64) | ||
assert.True(t, dataSize > 0) | ||
|
||
collections := event["collections"].(int64) | ||
assert.True(t, collections > 0) | ||
} | ||
} | ||
|
||
func TestData(t *testing.T) { | ||
f := mbtest.NewEventsFetcher(t, getConfig()) | ||
err := mbtest.WriteEvents(f, t) | ||
if err != nil { | ||
t.Fatal("write", err) | ||
} | ||
} | ||
|
||
func getConfig() map[string]interface{} { | ||
return map[string]interface{}{ | ||
"module": "mongodb", | ||
"metricsets": []string{"dbstats"}, | ||
"hosts": []string{mongodb.GetEnvHost() + ":" + mongodb.GetEnvPort()}, | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An example doc should be generated here with
make generate-json
. But that can still be done after merging the PR.