Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6e1e16a
Just check for a url scheme to allow users to provide a handler.
tim-seoss Nov 23, 2016
8736939
Allow a handler to be passed in to carry out a custom request.
tim-seoss Nov 23, 2016
7b0677c
Document new handler parameter to pushgateway function
tim-seoss Nov 25, 2016
0be1ec5
Merge branch 'master' of https://github.com/prometheus/client_python
tim-seoss Nov 25, 2016
c37a15c
Separate default handler code for re-use and add test case for custom…
rossigee Jan 15, 2017
5914671
Fix relative import error.
rossigee Jan 15, 2017
0c1220a
Add 'handlers' package to distribution.
rossigee Jan 15, 2017
d8a1694
Add 'handler_args' to pass extra informatoin to handler.
rossigee Jan 15, 2017
a67bf46
Merge branch 'master' of https://github.com/prometheus/client_python
rossigee Jan 17, 2017
51b1ab0
Remove unnecessary imports.
rossigee Jan 17, 2017
e3071c4
Just check for a url scheme to allow users to provide a handler.
tim-seoss Nov 23, 2016
c3de82b
Allow a handler to be passed in to carry out a custom request.
tim-seoss Nov 23, 2016
568f6ca
Document new handler parameter to pushgateway function
tim-seoss Nov 25, 2016
814e24d
Separate default handler code for re-use and add test case for custom…
rossigee Jan 15, 2017
9139e82
Fix relative import error.
rossigee Jan 15, 2017
ea1e3e7
Add 'handlers' package to distribution.
rossigee Jan 15, 2017
0ac7442
Add 'handler_args' to pass extra informatoin to handler.
rossigee Jan 15, 2017
d3f5a58
Remove unnecessary imports.
rossigee Jan 17, 2017
8adfe8a
Merge branch 'master' of https://github.com/rossigee/client_python
rossigee Jan 25, 2017
44df258
Use closure to pass args to handlers (refs #60).
rossigee Jan 25, 2017
46a9e92
Add missing import to auth example.
rossigee Jan 25, 2017
eb9500b
Simplify example by hardcoding username/password.
rossigee Jan 25, 2017
7f863dd
Fix for docstring comment.
rossigee Jan 25, 2017
0bebb0a
Move base handler back into exposition.
rossigee Jan 25, 2017
7e45925
Move basic auth handler into exposition. Update docstring.
rossigee Jan 26, 2017
daa40fd
Fix for 'TypeError: string argument without an encoding' (regressing …
rossigee Jan 29, 2017
1ba7c2e
Fix for 'TypeError: string argument without an encoding' (regressing …
rossigee Jan 29, 2017
8f7511a
Fix for 'TypeError: string argument without an encoding' (regressing …
rossigee Jan 29, 2017
444a748
Resort to determining python version to work around base64 byte encod…
rossigee Jan 29, 2017
62e8012
Potential workaround for py26 test failures.
rossigee Jan 29, 2017
b084d64
Revert "Potential workaround for py26 test failures."
rossigee Jan 29, 2017
a7afc96
Another potential workaround for py26 test failures.
rossigee Jan 31, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,24 @@ for more information.
`instance_ip_grouping_key` returns a grouping key with the instance label set
to the host's IP address.

### Handlers for authentication

If the push gateway you are connecting to is protected with HTTP Basic Auth,
you can use a special handler to set the Authorization header.

```python
from prometheus_client import CollectorRegistry, Gauge, push_to_gateway
from prometheus_client.exposition import basic_auth_handler

def my_auth_handler(url, method, timeout, headers, data):
username = 'foobar'
password = 'secret123'
return basic_auth_handler(url, method, timeout, headers, data, username, password)
Copy link
Contributor

@brian-brazil brian-brazil Jan 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd hardcode the user/pw here to keep it simpler.

If you use an env example, some users will be confused and think it only works if it comes from the env.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example updated.

registry = CollectorRegistry()
g = Gauge('job_last_success_unixtime', 'Last time a batch job successfully finished', registry=registry)
g.set_to_current_time()
push_to_gateway('localhost:9091', job='batchA', registry=registry, handler=my_auth_handler)
```

## Bridges

Expand Down
104 changes: 89 additions & 15 deletions prometheus_client/exposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import threading
from contextlib import closing
from wsgiref.simple_server import make_server
import base64
import sys

from . import core
try:
Expand Down Expand Up @@ -118,7 +120,46 @@ def write_to_textfile(path, registry):
os.rename(tmppath, path)


def push_to_gateway(gateway, job, registry, grouping_key=None, timeout=None):
def default_handler(url, method, timeout, headers, data):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a docstring to indicate this is for use with the pgw functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

'''Default handler that implements HTTP/HTTPS connections.

Used by the push_to_gateway functions. Can be re-used by other handlers.'''
def handle():
request = Request(url, data=data)
request.get_method = lambda: method
for k, v in headers:
request.add_header(k, v)
resp = build_opener(HTTPHandler).open(request, timeout=timeout)
if resp.code >= 400:
raise IOError("error talking to pushgateway: {0} {1}".format(
resp.code, resp.msg))

return handle


def basic_auth_handler(url, method, timeout, headers, data, username=None, password=None):
'''Handler that implements HTTP/HTTPS connections with Basic Auth.

Sets auth headers using supplied 'username' and 'password', if set.
Used by the push_to_gateway functions. Can be re-used by other handlers.'''
def handle():
'''Handler that implements HTTP Basic Auth.
'''
if username is not None and password is not None:
if sys.version_info >= (3,0):
auth_value = bytes('{0}:{1}'.format(username, password), 'utf8')
auth_token = str(base64.b64encode(auth_value), 'utf8')
else:
auth_value = '{0}:{1}'.format(username, password)
auth_token = base64.b64encode(auth_value)
auth_header = "Basic {0}".format(auth_token)
headers.append(['Authorization', auth_header])
default_handler(url, method, timeout, headers, data)()

return handle


def push_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, handler=default_handler):
'''Push metrics to the given pushgateway.

`gateway` the url for your push gateway. Either of the form
Expand All @@ -130,13 +171,37 @@ def push_to_gateway(gateway, job, registry, grouping_key=None, timeout=None):
Defaults to None
`timeout` is how long push will attempt to connect before giving up.
Defaults to None
`handler` is an optional function which can be provided to perform
requests to the 'gateway'.
Defaults to None, in which case an http or https request
will be carried out by a default handler.
If not None, the argument must be a function which accepts
the following arguments:
url, method, timeout, headers, and content
May be used to implement additional functionality not
supported by the built-in default handler (such as SSL
client certicates, and HTTP authentication mechanisms).
'url' is the URL for the request, the 'gateway' argument
described earlier will form the basis of this URL.
'method' is the HTTP method which should be used when
carrying out the request.
'timeout' requests not successfully completed after this
many seconds should be aborted. If timeout is None, then
the handler should not set a timeout.
'headers' is a list of ("header-name","header-value") tuples
which must be passed to the pushgateway in the form of HTTP
request headers.
The function should raise an exception (e.g. IOError) on
failure.
'content' is the data which should be used to form the HTTP
Message Body.

This overwrites all metrics with the same job and grouping_key.
This uses the PUT HTTP method.'''
_use_gateway('PUT', gateway, job, registry, grouping_key, timeout)
_use_gateway('PUT', gateway, job, registry, grouping_key, timeout, handler)


def pushadd_to_gateway(gateway, job, registry, grouping_key=None, timeout=None):
def pushadd_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, handler=default_handler):
'''PushAdd metrics to the given pushgateway.

`gateway` the url for your push gateway. Either of the form
Expand All @@ -148,13 +213,19 @@ def pushadd_to_gateway(gateway, job, registry, grouping_key=None, timeout=None):
Defaults to None
`timeout` is how long push will attempt to connect before giving up.
Defaults to None
`handler` is an optional function which can be provided to perform
requests to the 'gateway'.
Defaults to None, in which case an http or https request
will be carried out by a default handler.
See the 'prometheus_client.push_to_gateway' documentation
for implementation requirements.

This replaces metrics with the same name, job and grouping_key.
This uses the POST HTTP method.'''
_use_gateway('POST', gateway, job, registry, grouping_key, timeout)
_use_gateway('POST', gateway, job, registry, grouping_key, timeout, handler)


def delete_from_gateway(gateway, job, grouping_key=None, timeout=None):
def delete_from_gateway(gateway, job, grouping_key=None, timeout=None, handler=default_handler):
'''Delete metrics from the given pushgateway.

`gateway` the url for your push gateway. Either of the form
Expand All @@ -165,14 +236,21 @@ def delete_from_gateway(gateway, job, grouping_key=None, timeout=None):
Defaults to None
`timeout` is how long delete will attempt to connect before giving up.
Defaults to None
`handler` is an optional function which can be provided to perform
requests to the 'gateway'.
Defaults to None, in which case an http or https request
will be carried out by a default handler.
See the 'prometheus_client.push_to_gateway' documentation
for implementation requirements.

This deletes metrics with the given job and grouping_key.
This uses the DELETE HTTP method.'''
_use_gateway('DELETE', gateway, job, None, grouping_key, timeout)
_use_gateway('DELETE', gateway, job, None, grouping_key, timeout, handler)


def _use_gateway(method, gateway, job, registry, grouping_key, timeout):
if not (gateway.startswith('http://') or gateway.startswith('https://')):
def _use_gateway(method, gateway, job, registry, grouping_key, timeout, handler):
gateway_url = urlparse(gateway)
if not gateway_url.scheme:
gateway = 'http://{0}'.format(gateway)
url = '{0}/metrics/job/{1}'.format(gateway, quote_plus(job))

Expand All @@ -185,13 +263,9 @@ def _use_gateway(method, gateway, job, registry, grouping_key, timeout):
url = url + ''.join(['/{0}/{1}'.format(quote_plus(str(k)), quote_plus(str(v)))
for k, v in sorted(grouping_key.items())])

request = Request(url, data=data)
request.add_header('Content-Type', CONTENT_TYPE_LATEST)
request.get_method = lambda: method
resp = build_opener(HTTPHandler).open(request, timeout=timeout)
if resp.code >= 400:
raise IOError("error talking to pushgateway: {0} {1}".format(
resp.code, resp.msg))
headers=[('Content-Type', CONTENT_TYPE_LATEST)]
handler(url=url, method=method, timeout=timeout,
headers=headers, data=data)()

def instance_ip_grouping_key():
'''Grouping key with instance set to the IP Address of this host.'''
Expand Down
29 changes: 26 additions & 3 deletions tests/test_exposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from prometheus_client import CollectorRegistry, generate_latest
from prometheus_client import push_to_gateway, pushadd_to_gateway, delete_from_gateway
from prometheus_client import CONTENT_TYPE_LATEST, instance_ip_grouping_key
from prometheus_client.exposition import default_handler, basic_auth_handler

try:
from BaseHTTPServer import BaseHTTPRequestHandler
Expand All @@ -22,7 +23,6 @@
from http.server import BaseHTTPRequestHandler
from http.server import HTTPServer


class TestGenerateText(unittest.TestCase):
def setUp(self):
self.registry = CollectorRegistry()
Expand Down Expand Up @@ -99,7 +99,10 @@ def setUp(self):
self.requests = requests = []
class TestHandler(BaseHTTPRequestHandler):
def do_PUT(self):
self.send_response(201)
if 'with_basic_auth' in self.requestline and self.headers['authorization'] != 'Basic Zm9vOmJhcg==':
self.send_response(401)
else:
self.send_response(201)
length = int(self.headers['content-length'])
requests.append((self, self.rfile.read(length)))
self.end_headers()
Expand All @@ -108,7 +111,7 @@ def do_PUT(self):
do_DELETE = do_PUT

httpd = HTTPServer(('localhost', 0), TestHandler)
self.address = ':'.join([str(x) for x in httpd.server_address])
self.address = 'http://localhost:{0}'.format(httpd.server_address[1])
class TestServer(threading.Thread):
def run(self):
httpd.handle_request()
Expand Down Expand Up @@ -165,6 +168,26 @@ def test_delete_with_groupingkey(self):
self.assertEqual(self.requests[0][0].headers.get('content-type'), CONTENT_TYPE_LATEST)
self.assertEqual(self.requests[0][1], b'')

def test_push_with_handler(self):
def my_test_handler(url, method, timeout, headers, data):
headers.append(['X-Test-Header', 'foobar'])
return default_handler(url, method, timeout, headers, data)
push_to_gateway(self.address, "my_job", self.registry, handler=my_test_handler)
self.assertEqual(self.requests[0][0].command, 'PUT')
self.assertEqual(self.requests[0][0].path, '/metrics/job/my_job')
self.assertEqual(self.requests[0][0].headers.get('content-type'), CONTENT_TYPE_LATEST)
self.assertEqual(self.requests[0][0].headers.get('x-test-header'), 'foobar')
self.assertEqual(self.requests[0][1], b'# HELP g help\n# TYPE g gauge\ng 0.0\n')

def test_push_with_basic_auth_handler(self):
def my_auth_handler(url, method, timeout, headers, data):
return basic_auth_handler(url, method, timeout, headers, data, "foo", "bar")
push_to_gateway(self.address, "my_job_with_basic_auth", self.registry, handler=my_auth_handler)
self.assertEqual(self.requests[0][0].command, 'PUT')
self.assertEqual(self.requests[0][0].path, '/metrics/job/my_job_with_basic_auth')
self.assertEqual(self.requests[0][0].headers.get('content-type'), CONTENT_TYPE_LATEST)
self.assertEqual(self.requests[0][1], b'# HELP g help\n# TYPE g gauge\ng 0.0\n')

@unittest.skipIf(
sys.platform == "darwin",
"instance_ip_grouping_key() does not work on macOS."
Expand Down