Skip to content

Commit ae44fe0

Browse files
committed
Add a Graphtie Bridge.
1 parent 53948f3 commit ae44fe0

File tree

5 files changed

+156
-2
lines changed

5 files changed

+156
-2
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
build
2+
dist
3+
*.egg-info
4+
*.pyc
5+
*.swp

README.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ ProcessCollector(namespace='mydaemon', pid=lambda: open('/var/run/daemon.pid').r
208208

209209
There are several options for exporting metrics.
210210

211-
## HTTP
211+
### HTTP
212212

213213
Metrics are usually exposed over HTTP, to be read by the Prometheus server.
214214

@@ -226,7 +226,7 @@ To add Prometheus exposition to an existing HTTP server, see the `MetricsServlet
226226
which provides a `BaseHTTPRequestHandler`. It also serves as a simple example of how
227227
to write a custom endpoint.
228228

229-
## Node exporter textfile collector
229+
### Node exporter textfile collector
230230

231231
The [textfile collector](https://github.com/prometheus/node_exporter#textfile-collector)
232232
allows machine-level statistics to be exported out via the Node exporter.
@@ -245,3 +245,22 @@ write_to_textfile('/configured/textfile/path/raid.prom', registry)
245245

246246
A separate registry is used, as the default registry may contain other metrics
247247
such as those from the Process Collector.
248+
249+
## Bridges
250+
251+
It is also possible to expose metrics to systems other than Prometheus.
252+
This allows you to take advantage of Prometheus instrumentation even
253+
if you are not quite ready to fully transition to Prometheus yet.
254+
255+
### Graphite
256+
257+
Metrics are pushed over TCP in the Graphite plaintext format.
258+
259+
```python
260+
from prometheus_client.bridge.graphite import GraphiteBridge
261+
gb = GraphiteBridge(('graphite.your.org', 2003))
262+
# Push once.
263+
gb.push()
264+
# Push every 10 seconds in a daemon thread.
265+
gb.start(10.0)
266+
```

prometheus_client/bridge/__init__.py

Whitespace-only changes.

prometheus_client/bridge/graphite.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/usr/bin/python
2+
from __future__ import unicode_literals
3+
4+
import re
5+
import socket
6+
import time
7+
import threading
8+
9+
from .. import core
10+
11+
# Roughly, have to keep to what works as a file name.
12+
# We also remove periods, so labels can be distinguished.
13+
_INVALID_GRAPHITE_CHARS = re.compile(r"[^a-zA-Z0-9_-]")
14+
15+
def _sanitize(s):
16+
return _INVALID_GRAPHITE_CHARS.sub('_', s)
17+
18+
19+
class _RegularPush(threading.Thread):
20+
21+
def __init__(self, pusher, interval):
22+
super(_RegularPush, self).__init__()
23+
self._pusher = pusher
24+
self._interval = interval
25+
26+
def run(self):
27+
wait_until = time.time()
28+
while True:
29+
while True:
30+
now = time.time()
31+
if now >= wait_until:
32+
# May need to skip some pushes.
33+
while wait_until < now:
34+
wait_until += self._interval
35+
break
36+
# time.sleep can return early.
37+
time.sleep(wait_until - now)
38+
self._pusher.push()
39+
40+
41+
class GraphiteBridge(object):
42+
def __init__(self, address, registry=core.REGISTRY, timeout_seconds=30, _time=time):
43+
self._address = address
44+
self._registry = registry
45+
self._timeout = timeout_seconds
46+
self._time = _time
47+
48+
def push(self):
49+
now = int(self._time.time())
50+
output = []
51+
for metric in self._registry.collect():
52+
for name, labels, value in metric._samples:
53+
if labels:
54+
labelstr = '.' + '.'.join(
55+
['{0}.{1}'.format(
56+
_sanitize(k), _sanitize(v))
57+
for k, v in sorted(labels.items())])
58+
else:
59+
labelstr = ''
60+
output.append('{0}{1} {2} {3}\n'.format(
61+
_sanitize(name), labelstr, float(value), now))
62+
63+
conn = socket.create_connection(self._address, self._timeout)
64+
conn.sendall(''.join(output).encode('ascii'))
65+
conn.close()
66+
67+
def start(self, interval=60.0):
68+
t = _RegularPush(self, interval)
69+
t.daemon = True
70+
t.start()
71+
72+

tests/graphite_bridge.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import unittest
2+
import threading
3+
try:
4+
import SocketServer
5+
except ImportError:
6+
import socketserver as SocketServer
7+
8+
from prometheus_client import Counter, CollectorRegistry
9+
from prometheus_client.bridge.graphite import GraphiteBridge
10+
11+
class FakeTime(object):
12+
def time(self):
13+
return 1434898897.5
14+
15+
class TestGraphiteBridge(unittest.TestCase):
16+
def setUp(self):
17+
self.registry = CollectorRegistry()
18+
19+
self.data = ''
20+
class TCPHandler(SocketServer.BaseRequestHandler):
21+
def handle(s):
22+
self.data = s.request.recv(1024)
23+
server = SocketServer.TCPServer(('', 0), TCPHandler)
24+
class ServingThread(threading.Thread):
25+
def run(self):
26+
server.handle_request()
27+
server.socket.close()
28+
self.t = ServingThread()
29+
self.t.start()
30+
31+
self.gb = GraphiteBridge(server.server_address, self.registry, _time=FakeTime())
32+
33+
def test_nolabels(self):
34+
counter = Counter('c', 'help', registry=self.registry)
35+
counter.inc()
36+
37+
self.gb.push()
38+
self.t.join()
39+
40+
self.assertEqual(b'c 1.0 1434898897\n', self.data)
41+
42+
def test_labels(self):
43+
labels = Counter('labels', 'help', ['a', 'b'], registry=self.registry)
44+
labels.labels('c', 'd').inc()
45+
46+
self.gb.push()
47+
self.t.join()
48+
49+
self.assertEqual(b'labels.a.c.b.d 1.0 1434898897\n', self.data)
50+
51+
def test_sanitizing(self):
52+
labels = Counter('labels', 'help', ['a'], registry=self.registry)
53+
labels.labels('c.:8').inc()
54+
55+
self.gb.push()
56+
self.t.join()
57+
58+
self.assertEqual(b'labels.a.c__8 1.0 1434898897\n', self.data)

0 commit comments

Comments
 (0)