Skip to content

Commit 71df2cb

Browse files
author
Remi Hakim
committed
Merge pull request #700 from majorleaguesoccer/couchbase
Updated Couchbase check and corresponding unit test
2 parents 78a39aa + 4ace199 commit 71df2cb

File tree

3 files changed

+186
-0
lines changed

3 files changed

+186
-0
lines changed

checks.d/couchbase.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import urllib2
2+
import re
3+
from util import json, headers
4+
5+
from checks import AgentCheck
6+
7+
#Constants
8+
COUCHBASE_STATS_PATH = '/pools/nodes'
9+
10+
class Couchbase(AgentCheck):
11+
"""Extracts stats from Couchbase via its REST API
12+
http://docs.couchbase.com/couchbase-manual-2.0/#using-the-rest-api
13+
"""
14+
def _create_metric(self, data, tags=None):
15+
storage_totals = data['stats']['storageTotals']
16+
for key, storage_type in storage_totals.items():
17+
for metric, val in storage_type.items():
18+
if val is not None:
19+
cleaned_metric_name = self.camel_case_to_joined_lower(metric)
20+
full_metric_name = '.'.join(['couchbase', key, cleaned_metric_name])
21+
self.gauge(full_metric_name, val, tags=tags)
22+
# self.log.debug('found metric %s with value %s' % (metric_name, val))
23+
24+
for bucket_name, bucket_stats in data['buckets'].items():
25+
for metric, val in bucket_stats.items():
26+
if val is not None:
27+
cleaned_metric_name = self.camel_case_to_joined_lower(metric)
28+
full_metric_name = '.'.join(['couchbase', 'by_bucket', cleaned_metric_name])
29+
metric_tags = list(tags)
30+
metric_tags.append('bucket:%s' % bucket_name)
31+
self.gauge(full_metric_name, val[0], tags=metric_tags, device_name=bucket_name)
32+
# self.log.debug('found metric %s with value %s' % (metric_name, val[0]))
33+
34+
for node_name, node_stats in data['nodes'].items():
35+
for metric, val in node_stats['interestingStats'].items():
36+
if val is not None:
37+
cleaned_metric_name = self.camel_case_to_joined_lower(metric)
38+
full_metric_name = '.'.join(['couchbase', 'by_node', cleaned_metric_name])
39+
metric_tags = list(tags)
40+
metric_tags.append('node:%s' % node_name)
41+
self.gauge(full_metric_name, val, tags=metric_tags, device_name=node_name)
42+
# self.log.debug('found metric %s with value %s' % (metric_name, val))
43+
44+
45+
def _get_stats(self, url):
46+
"Hit a given URL and return the parsed json"
47+
self.log.debug('Fetching Couchbase stats at url: %s' % url)
48+
req = urllib2.Request(url, None, headers(self.agentConfig))
49+
50+
# Do the request, log any errors
51+
request = urllib2.urlopen(req)
52+
response = request.read()
53+
return json.loads(response)
54+
55+
def check(self, instance):
56+
server = instance.get('server', None)
57+
if server is None:
58+
return False
59+
data = self.get_data(server)
60+
self._create_metric(data, tags=['instance:%s' % server])
61+
62+
def get_data(self, server):
63+
# The dictionary to be returned.
64+
couchbase = {'stats': None,
65+
'buckets': {},
66+
'nodes': {}
67+
}
68+
69+
# build couchbase stats entry point
70+
url = '%s%s' % (server, COUCHBASE_STATS_PATH)
71+
overall_stats = self._get_stats(url)
72+
73+
# No overall stats? bail out now
74+
if overall_stats is None:
75+
raise Exception("No data returned from couchbase endpoint: %s" % url)
76+
77+
couchbase['stats'] = overall_stats
78+
79+
nodes = overall_stats['nodes']
80+
81+
# Next, get all the nodes
82+
if nodes is not None:
83+
for node in nodes:
84+
couchbase['nodes'][node['hostname']] = node
85+
86+
# Next, get all buckets .
87+
endpoint = overall_stats['buckets']['uri']
88+
89+
url = '%s%s' % (server, endpoint)
90+
buckets = self._get_stats(url)
91+
92+
if buckets is not None:
93+
for bucket in buckets:
94+
bucket_name = bucket['name']
95+
96+
# We have to manually build the URI for the stats bucket, as this is not auto discoverable
97+
url = '%s/pools/nodes/buckets/%s/stats' % (server, bucket_name)
98+
bucket_stats = self._get_stats(url)
99+
bucket_samples = bucket_stats['op']['samples']
100+
if bucket_samples is not None:
101+
couchbase['buckets'][bucket['name']] = bucket_samples
102+
103+
return couchbase
104+
105+
# Takes a camelCased variable and returns a joined_lower equivalent.
106+
# Returns input if non-camelCase variable is detected.
107+
def camel_case_to_joined_lower(self, variable):
108+
# replace non-word with _
109+
converted_variable = re.sub('\W+', '_', variable)
110+
111+
# insert _ in front of capital letters and lowercase the string
112+
converted_variable = re.sub('([A-Z])', '_\g<1>', converted_variable).lower()
113+
114+
# remove duplicate _
115+
converted_variable = re.sub('_+', '_', converted_variable)
116+
117+
# handle special case of starting/ending underscores
118+
converted_variable = re.sub('^_|_$', '', converted_variable)
119+
120+
return converted_variable
121+

conf.d/couchbase.yaml.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
init_config:
2+
3+
instances:
4+
# - server: http://localhost:8091

tests/test_couchbase.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import unittest
2+
from tests.common import load_check
3+
4+
class CouchbaseTestCase(unittest.TestCase):
5+
6+
def setUp(self):
7+
self.config = {
8+
'instances': [{
9+
'server': 'http://localhost:8091',
10+
}]
11+
}
12+
self.agentConfig = {
13+
'version': '0.1',
14+
'api_key': 'toto'
15+
}
16+
self.check = load_check('couchbase', self.config, self.agentConfig)
17+
18+
def test_camel_case_to_joined_lower(self):
19+
test_pairs = {
20+
'camelCase' : 'camel_case',
21+
'FirstCapital' : 'first_capital',
22+
'joined_lower' : 'joined_lower',
23+
'joined_Upper1' : 'joined_upper1',
24+
'Joined_upper2' : 'joined_upper2',
25+
'Joined_Upper3' : 'joined_upper3',
26+
'_leading_Underscore' : 'leading_underscore',
27+
'Trailing_Underscore_' : 'trailing_underscore',
28+
'DOubleCAps' : 'd_ouble_c_aps',
29+
'@@@super--$$-Funky__$__$$%' : 'super_funky',
30+
}
31+
32+
for test_input, expected_output in test_pairs.items():
33+
test_output = self.check.camel_case_to_joined_lower(test_input)
34+
self.assertEqual(test_output, expected_output,
35+
'Input was %s, expected output was %s, actual output was %s' % (test_input, expected_output, test_output))
36+
37+
def test_metrics_casing(self):
38+
self.check.check(self.config['instances'][0])
39+
40+
metrics = self.check.get_metrics()
41+
42+
camel_cased_metrics = [u'couchbase.hdd.used_by_data',
43+
u'couchbase.ram.used_by_data',
44+
u'couchbase.ram.quota_total',
45+
u'couchbase.ram.quota_used',
46+
]
47+
48+
found_metrics = [k[0] for k in metrics if k[0] in camel_cased_metrics]
49+
self.assertEqual(found_metrics.sort(), camel_cased_metrics.sort())
50+
51+
def test_metrics(self):
52+
self.check.check(self.config['instances'][0])
53+
54+
metrics = self.check.get_metrics()
55+
56+
self.assertTrue(type(metrics) == type([]), metrics)
57+
self.assertTrue(len(metrics) > 3)
58+
self.assertTrue(len([k for k in metrics if "instance:http://localhost:8091" in k[3]['tags']]) > 3)
59+
60+
self.assertTrue(len([k for k in metrics if -1 != k[0].find('by_node')]) > 1, 'Unable to fund any per node metrics')
61+
self.assertTrue(len([k for k in metrics if -1 != k[0].find('by_bucket')]) > 1, 'Unable to fund any per node metrics')

0 commit comments

Comments
 (0)