Skip to content

Commit 74336a1

Browse files
committed
Allow to fetch DB stats from _all_dbs.
closes #15
1 parent 207b39c commit 74336a1

File tree

6 files changed

+146
-29
lines changed

6 files changed

+146
-29
lines changed

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,17 @@ credentials, e.g. like this:
2323

2424
docker run -p 9984:9984 gesellix/couchdb-prometheus-exporter -couchdb.uri=http://couchdb:5984 -couchdb.username=root -couchdb.password=a-secret
2525

26-
If you need database disk usage stats, simply add a comma separated list of database names like this:
26+
## Database disk usage stats
27+
28+
If you need database disk usage stats, add a comma separated list of database names like this:
2729

2830
docker run -p 9984:9984 gesellix/couchdb-prometheus-exporter -couchdb.uri=http://couchdb:5984 -databases=db-1,db-2 -couchdb.username=root -couchdb.password=a-secret
2931

32+
Or, if you want to get stats for every database, please use `_all_dbs` as database name:
33+
34+
docker run -p 9984:9984 gesellix/couchdb-prometheus-exporter -couchdb.uri=http://couchdb:5984 -databases=_all_dbs -couchdb.username=root -couchdb.password=a-secret
35+
36+
3037
## Monitoring CouchDB with Prometheus, Grafana and Docker
3138

3239
For a step-by-step guide, see [Monitoring CouchDB with Prometheus, Grafana and Docker](https://medium.com/@redgeoff/monitoring-couchdb-with-prometheus-grafana-and-docker-4693bc8408f0)

couchdb-exporter_test.go

Lines changed: 99 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020

2121
type Handler func(w http.ResponseWriter, r *http.Request)
2222

23-
func BasicAuth(basicAuth lib.BasicAuth, pass Handler) Handler {
23+
func BasicAuthHandler(basicAuth lib.BasicAuth, pass Handler) Handler {
2424

2525
validate := func(basicAuth lib.BasicAuth, username, password string) bool {
2626
if username == basicAuth.Username && password == basicAuth.Password {
@@ -88,7 +88,7 @@ func couchdbResponse(t *testing.T, versionSuffix string) Handler {
8888

8989
func performCouchdbStatsTest(t *testing.T, couchdbVersion string, expectedMetricsCount int, expectedGetRequestCount float64, expectedDiskSize float64) {
9090
basicAuth := lib.BasicAuth{Username: "username", Password: "password"}
91-
handler := http.HandlerFunc(BasicAuth(basicAuth, couchdbResponse(t, couchdbVersion)))
91+
handler := http.HandlerFunc(BasicAuthHandler(basicAuth, couchdbResponse(t, couchdbVersion)))
9292
server := httptest.NewServer(handler)
9393

9494
e := lib.NewExporter(server.URL, basicAuth, []string{"example", "another-example"}, true)
@@ -109,12 +109,18 @@ func performCouchdbStatsTest(t *testing.T, couchdbVersion string, expectedMetric
109109
t.Errorf("got more metrics (%d) as expected (%d)", metricsCount, expectedMetricsCount)
110110
}
111111

112-
actualGetRequestCount := testutil.GetGaugeValue(metricFamilies, "couchdb_httpd_request_methods", "method", "GET")
112+
actualGetRequestCount, err := testutil.GetGaugeValue(metricFamilies, "couchdb_httpd_request_methods", "method", "GET")
113+
if err != nil {
114+
t.Error(err)
115+
}
113116
if expectedGetRequestCount != actualGetRequestCount {
114117
t.Errorf("expected %f GET requests, but got %f instead", expectedGetRequestCount, actualGetRequestCount)
115118
}
116119

117-
actualDiskSize := testutil.GetGaugeValue(metricFamilies, "couchdb_database_disk_size", "db_name", "example")
120+
actualDiskSize, err := testutil.GetGaugeValue(metricFamilies, "couchdb_database_disk_size", "db_name", "example")
121+
if err != nil {
122+
t.Error(err)
123+
}
118124
if expectedDiskSize != actualDiskSize {
119125
t.Errorf("expected %f disk size, but got %f instead", expectedDiskSize, actualDiskSize)
120126
}
@@ -139,9 +145,18 @@ func TestCouchdbStatsV1Integration(t *testing.T) {
139145
t.Error(err)
140146
}
141147
dbUrl := fmt.Sprintf("http://%s", dbAddress)
148+
basicAuth := lib.BasicAuth{Username: "root", Password: "a-secret"}
149+
150+
client := lib.NewCouchdbClient(dbUrl, basicAuth, true)
151+
databases := []string{"v1_testdb1", "v1_testdb2"}
152+
for _, db := range databases {
153+
_, err = client.Request("PUT", fmt.Sprintf("%s/%s", client.BaseUri, db), nil)
154+
if err != nil {
155+
t.Error(err)
156+
}
157+
}
142158

143159
t.Run("node_up", func(t *testing.T) {
144-
basicAuth := lib.BasicAuth{Username: "root", Password: "a-secret"}
145160
e := lib.NewExporter(dbUrl, basicAuth, []string{}, true)
146161

147162
ch := make(chan prometheus.Metric)
@@ -153,19 +168,52 @@ func TestCouchdbStatsV1Integration(t *testing.T) {
153168
metricFamilies := testutil.CollectMetrics(ch, false)
154169

155170
nodeName := "master"
156-
actualNodeUp := testutil.GetGaugeValue(metricFamilies, "couchdb_httpd_node_up", "node_name", nodeName)
171+
actualNodeUp, err := testutil.GetGaugeValue(metricFamilies, "couchdb_httpd_node_up", "node_name", nodeName)
172+
if err != nil {
173+
t.Error(err)
174+
}
157175
if actualNodeUp != 1 {
158176
t.Errorf("Expected node '%s' at '%s' to be available.", nodeName, dbUrl)
159177
}
160178
})
179+
180+
t.Run("_all_dbs", func(t *testing.T) {
181+
e := lib.NewExporter(dbUrl, basicAuth, []string{"_all_dbs"}, true)
182+
183+
ch := make(chan prometheus.Metric)
184+
go func() {
185+
defer close(ch)
186+
e.Collect(ch)
187+
}()
188+
189+
metricFamilies := testutil.CollectMetrics(ch, false)
190+
191+
for _, db := range databases {
192+
databaseDataSize, err := testutil.GetGaugeValue(metricFamilies, "couchdb_database_data_size", "db_name", db)
193+
if err != nil {
194+
log.Println(err)
195+
t.Errorf("Expected stats to be collected for database '%s'.", db)
196+
}
197+
if databaseDataSize != 0 {
198+
t.Errorf("Expected database data size to be 0 for database '%s'.", db)
199+
}
200+
}
201+
})
202+
203+
for _, db := range databases {
204+
_, err = client.Request("DELETE", fmt.Sprintf("%s/%s", client.BaseUri, db), nil)
205+
if err != nil {
206+
t.Error(err)
207+
}
208+
}
161209
}
162210

163-
func membership(t *testing.T, basicAuth lib.BasicAuth) (func(address string) (bool, error)) {
211+
func awaitMembership(t *testing.T, basicAuth lib.BasicAuth) (func(address string) (bool, error)) {
164212
time.Sleep(5 * time.Second)
165213

166214
return func(address string) (bool, error) {
167215
dbUrl := fmt.Sprintf("http://%s", address)
168-
c := lib.NewCouchdbClient(dbUrl, basicAuth, []string{}, true)
216+
c := lib.NewCouchdbClient(dbUrl, basicAuth, true)
169217
nodeNames, err := c.GetNodeNames()
170218
if err != nil {
171219
if err, ok := err.(net.Error); ok && (err.Timeout() || err.Temporary()) {
@@ -198,11 +246,20 @@ func TestCouchdbStatsV2Integration(t *testing.T) {
198246
dbUrl := fmt.Sprintf("http://%s", dbAddress)
199247
basicAuth := lib.BasicAuth{Username: "root", Password: "a-secret"}
200248

201-
err = cluster_config.AwaitNodes([]string{dbAddress}, membership(t, basicAuth))
249+
err = cluster_config.AwaitNodes([]string{dbAddress}, awaitMembership(t, basicAuth))
202250
if err != nil {
203251
t.Error(err)
204252
}
205253

254+
client := lib.NewCouchdbClient(dbUrl, basicAuth, true)
255+
databases := []string{"v2_testdb1", "v2_testdb2"}
256+
for _, db := range databases {
257+
_, err = client.Request("PUT", fmt.Sprintf("%s/%s", client.BaseUri, db), nil)
258+
if err != nil {
259+
t.Error(err)
260+
}
261+
}
262+
206263
t.Run("node_up", func(t *testing.T) {
207264
e := lib.NewExporter(dbUrl, basicAuth, []string{}, true)
208265

@@ -215,12 +272,42 @@ func TestCouchdbStatsV2Integration(t *testing.T) {
215272
metricFamilies := testutil.CollectMetrics(ch, false)
216273

217274
nodeName := "[email protected]"
218-
actualNodeUp := testutil.GetGaugeValue(metricFamilies, "couchdb_httpd_node_up", "node_name", nodeName)
275+
actualNodeUp, err := testutil.GetGaugeValue(metricFamilies, "couchdb_httpd_node_up", "node_name", nodeName)
276+
if err != nil {
277+
t.Error(err)
278+
}
219279
if actualNodeUp != 1 {
220280
t.Errorf("Expected node '%s' at '%s' to be available.", nodeName, dbUrl)
221281
}
222282
})
223283

224-
// <tear-down code>
225-
// (nothing to do)
284+
t.Run("_all_dbs", func(t *testing.T) {
285+
e := lib.NewExporter(dbUrl, basicAuth, []string{"_all_dbs"}, true)
286+
287+
ch := make(chan prometheus.Metric)
288+
go func() {
289+
defer close(ch)
290+
e.Collect(ch)
291+
}()
292+
293+
metricFamilies := testutil.CollectMetrics(ch, false)
294+
295+
for _, db := range databases {
296+
databaseDataSize, err := testutil.GetGaugeValue(metricFamilies, "couchdb_database_data_size", "db_name", db)
297+
if err != nil {
298+
log.Println(err)
299+
t.Errorf("Expected stats to be collected for database '%s'.", db)
300+
}
301+
if databaseDataSize != 0 {
302+
t.Errorf("Expected database data size to be 0 for database '%s'.", db)
303+
}
304+
}
305+
})
306+
307+
for _, db := range databases {
308+
_, err = client.Request("DELETE", fmt.Sprintf("%s/%s", client.BaseUri, db), nil)
309+
if err != nil {
310+
t.Error(err)
311+
}
312+
}
226313
}

lib/collector.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,23 +55,35 @@ func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
5555
e.activeTasksReplication.Describe(ch)
5656
}
5757

58+
func (e *Exporter) getMonitoredDatabaseNames(candidates []string) ([]string, error) {
59+
if len(candidates) == 1 && candidates[0] == "_all_dbs" {
60+
return e.client.getDatabaseList()
61+
}
62+
return candidates, nil
63+
}
64+
5865
func (e *Exporter) collect(ch chan<- prometheus.Metric) error {
66+
e.up.Set(0)
5967
sendStatus := func() {
6068
ch <- e.up
6169
}
6270
defer sendStatus()
6371

64-
e.up.Set(0)
65-
stats, err := e.client.getStats()
72+
var databases, err = e.getMonitoredDatabaseNames(e.databases)
73+
if err != nil {
74+
return err
75+
}
76+
77+
stats, err := e.client.getStats(databases)
6678
if err != nil {
6779
return fmt.Errorf("error collecting couchdb stats: %v", err)
6880
}
6981
e.up.Set(1)
7082

7183
if stats.ApiVersion == "2" {
72-
e.collectV2(stats, exposedHttpStatusCodes, e.databases)
84+
e.collectV2(stats, exposedHttpStatusCodes, databases)
7385
} else {
74-
e.collectV1(stats, exposedHttpStatusCodes, e.databases)
86+
e.collectV1(stats, exposedHttpStatusCodes, databases)
7587
}
7688

7789
e.nodeUp.Collect(ch)

lib/couchdb-client.go

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ type BasicAuth struct {
2020
type CouchdbClient struct {
2121
BaseUri string
2222
basicAuth BasicAuth
23-
databases []string
2423
client *http.Client
2524
}
2625

@@ -128,7 +127,7 @@ func (c *CouchdbClient) getStatsByNodeName(urisByNodeName map[string]string) (ma
128127
return statsByNodeName, nil
129128
}
130129

131-
func (c *CouchdbClient) getStats() (Stats, error) {
130+
func (c *CouchdbClient) getStats(databases []string) (Stats, error) {
132131
isCouchDbV2, err := c.isCouchDbV2()
133132
if err != nil {
134133
return Stats{}, err
@@ -142,7 +141,7 @@ func (c *CouchdbClient) getStats() (Stats, error) {
142141
if err != nil {
143142
return Stats{}, err
144143
}
145-
databaseStats, err := c.getDatabasesStatsByNodeName(urisByNode)
144+
databaseStats, err := c.getDatabasesStatsByNodeName(databases)
146145
if err != nil {
147146
return Stats{}, err
148147
}
@@ -163,7 +162,7 @@ func (c *CouchdbClient) getStats() (Stats, error) {
163162
if err != nil {
164163
return Stats{}, err
165164
}
166-
databaseStats, err := c.getDatabasesStatsByNodeName(urisByNode)
165+
databaseStats, err := c.getDatabasesStatsByNodeName(databases)
167166
if err != nil {
168167
return Stats{}, err
169168
}
@@ -179,9 +178,9 @@ func (c *CouchdbClient) getStats() (Stats, error) {
179178
}
180179
}
181180

182-
func (c *CouchdbClient) getDatabasesStatsByNodeName(urisByNodeName map[string]string) (map[string]DatabaseStats, error) {
181+
func (c *CouchdbClient) getDatabasesStatsByNodeName(databases []string) (map[string]DatabaseStats, error) {
183182
dbStatsByDbName := make(map[string]DatabaseStats)
184-
for _, dbName := range c.databases {
183+
for _, dbName := range databases {
185184
data, err := c.Request("GET", fmt.Sprintf("%s/%s", c.BaseUri, dbName), nil)
186185
if err != nil {
187186
return nil, fmt.Errorf("error reading database '%s' stats: %v", dbName, err)
@@ -218,6 +217,19 @@ func (c *CouchdbClient) getActiveTasks() (ActiveTasksResponse, error) {
218217
return activeTasks, nil
219218
}
220219

220+
func (c *CouchdbClient) getDatabaseList() ([]string, error) {
221+
data, err := c.Request("GET", fmt.Sprintf("%s/_all_dbs", c.BaseUri), nil)
222+
if err != nil {
223+
return nil, err
224+
}
225+
var dbs []string
226+
err = json.Unmarshal(data, &dbs)
227+
if err != nil {
228+
return nil, err
229+
}
230+
return dbs, nil
231+
}
232+
221233
func (c *CouchdbClient) Request(method string, uri string, body io.Reader) (respData []byte, err error) {
222234
req, err := http.NewRequest(method, uri, body)
223235
if err != nil {
@@ -249,7 +261,7 @@ func (c *CouchdbClient) Request(method string, uri string, body io.Reader) (resp
249261
return respData, nil
250262
}
251263

252-
func NewCouchdbClient(uri string, basicAuth BasicAuth, databases []string, insecure bool) *CouchdbClient {
264+
func NewCouchdbClient(uri string, basicAuth BasicAuth, insecure bool) *CouchdbClient {
253265
httpClient := &http.Client{
254266
Transport: &http.Transport{
255267
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
@@ -259,7 +271,6 @@ func NewCouchdbClient(uri string, basicAuth BasicAuth, databases []string, insec
259271
return &CouchdbClient{
260272
BaseUri: uri,
261273
basicAuth: basicAuth,
262-
databases: databases,
263274
client: httpClient,
264275
}
265276
}

lib/exporter.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ type Exporter struct {
4949
func NewExporter(uri string, basicAuth BasicAuth, databases []string, insecure bool) *Exporter {
5050

5151
return &Exporter{
52-
client: NewCouchdbClient(uri, basicAuth, databases, insecure),
52+
client: NewCouchdbClient(uri, basicAuth, insecure),
5353
databases: databases,
5454

5555
up: prometheus.NewGauge(

testutil/metric-reader.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,20 @@ func CollectMetrics(ch chan prometheus.Metric, debugMetrics bool) map[string]*dt
4040
return metricFamiliesByName
4141
}
4242

43-
func GetGaugeValue(metricFamilies map[string]*dto.MetricFamily, metricDesc string, labelName string, labelValue string) float64 {
43+
func GetGaugeValue(metricFamilies map[string]*dto.MetricFamily, metricDesc string, labelName string, labelValue string) (float64, error) {
4444
//func getGaugeValue(metrics []*dto.Metric, metricDesc string, labelName string, labelValue string) float64 {
4545
for desc, metrics := range metricFamilies {
4646
if metricDesc == "" || desc == metricDesc {
4747
for _, metric := range metrics.Metric {
4848
for _, label := range metric.Label {
4949
if *label.Name == labelName && *label.Value == labelValue {
50-
return *metric.Gauge.Value
50+
return *metric.Gauge.Value, nil
5151
}
5252
}
5353
}
5454
}
5555
}
56-
return 0
56+
return 0, fmt.Errorf("no gauge found")
5757
}
5858

5959
func CountMetrics(metricFamilies map[string]*dto.MetricFamily) int {

0 commit comments

Comments
 (0)