From 6e1e16a7ac98d7a37073c7b0ddd50136f0068993 Mon Sep 17 00:00:00 2001 From: Tim Small Date: Wed, 23 Nov 2016 18:12:01 +0000 Subject: [PATCH 01/29] Just check for a url scheme to allow users to provide a handler. --- prometheus_client/exposition.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index 13a91e01..0fa39a81 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -165,7 +165,8 @@ def delete_from_gateway(gateway, job, grouping_key=None, timeout=None): def _use_gateway(method, gateway, job, registry, grouping_key, timeout): - if not (gateway.startswith('http://') or gateway.startswith('https://')): + gateway_url = urlparse(gateway) + if not gateway_url.scheme: gateway = 'http://{0}'.format(gateway) url = '{0}/metrics/job/{1}'.format(gateway, quote_plus(job)) From 87369398bc1ccb7b22970b8ade92465a39850b5c Mon Sep 17 00:00:00 2001 From: Tim Small Date: Wed, 23 Nov 2016 18:16:27 +0000 Subject: [PATCH 02/29] Allow a handler to be passed in to carry out a custom request. Allow a custom handler to be provided, so that the caller can provide code which carried out basic auth, https client certificate validation or other arbitrary schemes and access methods such as using different types of proxy. --- prometheus_client/exposition.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index 0fa39a81..8eb20724 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -111,7 +111,7 @@ def write_to_textfile(path, registry): os.rename(tmppath, path) -def push_to_gateway(gateway, job, registry, grouping_key=None, timeout=None): +def push_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, handler=None): '''Push metrics to the given pushgateway. `gateway` the url for your push gateway. Either of the form @@ -126,10 +126,10 @@ def push_to_gateway(gateway, job, registry, grouping_key=None, timeout=None): 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=None): '''PushAdd metrics to the given pushgateway. `gateway` the url for your push gateway. Either of the form @@ -144,10 +144,10 @@ def pushadd_to_gateway(gateway, job, registry, grouping_key=None, timeout=None): 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=None): '''Delete metrics from the given pushgateway. `gateway` the url for your push gateway. Either of the form @@ -161,10 +161,10 @@ def delete_from_gateway(gateway, job, grouping_key=None, timeout=None): 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): +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) @@ -182,10 +182,14 @@ def _use_gateway(method, gateway, job, registry, grouping_key, timeout): 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)) + if handler is None: + resp = build_opener(handler).open(request, timeout=timeout) + if resp.code >= 400: + raise IOError("error talking to pushgateway: {0} {1}".format( + resp.code, resp.msg)) + else: + handler(url=url, method=lambda: method, timeout=timeout, + headers=[('Content-Type', CONTENT_TYPE_LATEST)], content=data) def instance_ip_grouping_key(): '''Grouping key with instance set to the IP Address of this host.''' From 7b0677c02e486bc1a23521d3ee425c5540c0a916 Mon Sep 17 00:00:00 2001 From: Tim Small Date: Fri, 25 Nov 2016 17:45:38 +0000 Subject: [PATCH 03/29] Document new handler parameter to pushgateway function Provide pydoc for the new handler function to the various pushgateway functions, provide parameter descriptions in the documentatation for the push_to_gateway function to avoid excessive copy-and-paste. --- prometheus_client/exposition.py | 36 +++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index 8eb20724..e965f9d5 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -123,6 +123,30 @@ def push_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, han 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.''' @@ -141,6 +165,12 @@ 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.''' @@ -158,6 +188,12 @@ def delete_from_gateway(gateway, job, grouping_key=None, timeout=None, handler=N 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.''' From c37a15c21904334aabc260da6efa3c4f7c927048 Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Sun, 15 Jan 2017 13:51:14 +0700 Subject: [PATCH 04/29] Separate default handler code for re-use and add test case for custom handler (refs #120) --- prometheus_client/exposition.py | 17 +++++++---------- prometheus_client/handlers/__init__.py | 0 prometheus_client/handlers/base.py | 18 ++++++++++++++++++ tests/test_exposition.py | 13 ++++++++++++- 4 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 prometheus_client/handlers/__init__.py create mode 100644 prometheus_client/handlers/base.py diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index 47f4d9f4..438f30eb 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -10,6 +10,9 @@ from wsgiref.simple_server import make_server from . import core + +from handlers.base import handler as default_handler + try: from BaseHTTPServer import BaseHTTPRequestHandler from BaseHTTPServer import HTTPServer @@ -222,17 +225,11 @@ def _use_gateway(method, gateway, job, registry, grouping_key, timeout, handler) 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 + headers=[('Content-Type', CONTENT_TYPE_LATEST)] if handler is None: - resp = build_opener(handler).open(request, timeout=timeout) - if resp.code >= 400: - raise IOError("error talking to pushgateway: {0} {1}".format( - resp.code, resp.msg)) - else: - handler(url=url, method=lambda: method, timeout=timeout, - headers=[('Content-Type', CONTENT_TYPE_LATEST)], content=data) + handler = default_handler + 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.''' diff --git a/prometheus_client/handlers/__init__.py b/prometheus_client/handlers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/prometheus_client/handlers/base.py b/prometheus_client/handlers/base.py new file mode 100644 index 00000000..b91a1722 --- /dev/null +++ b/prometheus_client/handlers/base.py @@ -0,0 +1,18 @@ +#!/usr/bin/python + +try: + from urllib2 import build_opener, Request, HTTPHandler +except ImportError: + # Python 3 + from urllib.request import build_opener, Request, HTTPHandler + +def handler(url, method, timeout, headers, data): + '''Default handler that implements HTTP/HTTPS connections.''' + 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)) diff --git a/tests/test_exposition.py b/tests/test_exposition.py index fa5cfdb0..0a5c6df1 100644 --- a/tests/test_exposition.py +++ b/tests/test_exposition.py @@ -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.handlers.base import handler as default_handler try: from BaseHTTPServer import BaseHTTPRequestHandler @@ -22,7 +23,6 @@ from http.server import BaseHTTPRequestHandler from http.server import HTTPServer - class TestGenerateText(unittest.TestCase): def setUp(self): self.registry = CollectorRegistry() @@ -165,6 +165,17 @@ 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']) + 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') + @unittest.skipIf( sys.platform == "darwin", "instance_ip_grouping_key() does not work on macOS." From 5914671137716052ac740dd55f174575c1a623aa Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Sun, 15 Jan 2017 15:26:20 +0700 Subject: [PATCH 05/29] Fix relative import error. --- prometheus_client/exposition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index 438f30eb..2d58eae6 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -11,7 +11,7 @@ from . import core -from handlers.base import handler as default_handler +from .handlers.base import handler as default_handler try: from BaseHTTPServer import BaseHTTPRequestHandler From 0c1220aae6e95e6ec8d93f0605819aa4a29959ad Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Sun, 15 Jan 2017 15:40:48 +0700 Subject: [PATCH 06/29] Add 'handlers' package to distribution. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 99081ef4..5aa9c57c 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ license = "Apache Software License 2.0", keywords = "prometheus monitoring instrumentation client", url = "https://github.com/prometheus/client_python", - packages=['prometheus_client', 'prometheus_client.bridge', 'prometheus_client.twisted'], + packages=['prometheus_client', 'prometheus_client.bridge', 'prometheus_client.twisted', 'prometheus_client.handlers'], extras_requires={ 'twisted': ['twisted'], }, From d8a1694f42f8140ecad80b835208218744da9020 Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Sun, 15 Jan 2017 16:46:24 +0700 Subject: [PATCH 07/29] Add 'handler_args' to pass extra informatoin to handler. --- prometheus_client/exposition.py | 19 +++++++++++-------- prometheus_client/handlers/base.py | 2 +- prometheus_client/handlers/basic_auth.py | 16 ++++++++++++++++ 3 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 prometheus_client/handlers/basic_auth.py diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index 2d58eae6..47e84fbb 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -121,7 +121,7 @@ def write_to_textfile(path, registry): os.rename(tmppath, path) -def push_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, handler=None): +def push_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, handler=None, handler_args=None): '''Push metrics to the given pushgateway. `gateway` the url for your push gateway. Either of the form @@ -157,13 +157,14 @@ def push_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, han failure. 'content' is the data which should be used to form the HTTP Message Body. + `handler_args` is an optional dict of extra arguments to provide 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, handler) + _use_gateway('PUT', gateway, job, registry, grouping_key, timeout, handler, handler_args) -def pushadd_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, handler=None): +def pushadd_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, handler=None, handler_args=None): '''PushAdd metrics to the given pushgateway. `gateway` the url for your push gateway. Either of the form @@ -184,10 +185,10 @@ def pushadd_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, 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, handler) + _use_gateway('POST', gateway, job, registry, grouping_key, timeout, handler, handler_args) -def delete_from_gateway(gateway, job, grouping_key=None, timeout=None, handler=None): +def delete_from_gateway(gateway, job, grouping_key=None, timeout=None, handler=None, handler_args=None): '''Delete metrics from the given pushgateway. `gateway` the url for your push gateway. Either of the form @@ -207,10 +208,10 @@ def delete_from_gateway(gateway, job, grouping_key=None, timeout=None, handler=N 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, handler) + _use_gateway('DELETE', gateway, job, None, grouping_key, timeout, handler, handler_args) -def _use_gateway(method, gateway, job, registry, grouping_key, timeout, handler): +def _use_gateway(method, gateway, job, registry, grouping_key, timeout, handler, handler_args): gateway_url = urlparse(gateway) if not gateway_url.scheme: gateway = 'http://{0}'.format(gateway) @@ -228,8 +229,10 @@ def _use_gateway(method, gateway, job, registry, grouping_key, timeout, handler) headers=[('Content-Type', CONTENT_TYPE_LATEST)] if handler is None: handler = default_handler + if handler_args is None: + handler_args = dict() handler(url=url, method=method, timeout=timeout, - headers=headers, data=data) + headers=headers, data=data, **handler_args) def instance_ip_grouping_key(): '''Grouping key with instance set to the IP Address of this host.''' diff --git a/prometheus_client/handlers/base.py b/prometheus_client/handlers/base.py index b91a1722..d2319136 100644 --- a/prometheus_client/handlers/base.py +++ b/prometheus_client/handlers/base.py @@ -6,7 +6,7 @@ # Python 3 from urllib.request import build_opener, Request, HTTPHandler -def handler(url, method, timeout, headers, data): +def handler(url, method, timeout, headers, data, **kwargs): '''Default handler that implements HTTP/HTTPS connections.''' request = Request(url, data=data) request.get_method = lambda: method diff --git a/prometheus_client/handlers/basic_auth.py b/prometheus_client/handlers/basic_auth.py new file mode 100644 index 00000000..463568ff --- /dev/null +++ b/prometheus_client/handlers/basic_auth.py @@ -0,0 +1,16 @@ +#!/usr/bin/python + +import os, base64 + +from prometheus_client.handlers.base import handler as default_handler + +def handler(url, method, timeout, headers, data, **kwargs): + '''Handler that implements HTTP Basic Auth by setting auth headers if + 'username' passed as keyword argument.''' + if 'username' in kwargs: + username = kwargs['username'] + password = kwargs['password'] + auth_value = "{0}:{1}".format(username, password) + auth_header = "Basic {0}".format(base64.b64encode(bytes(auth_value))) + headers.append(['Authorization', auth_header]) + default_handler(url, method, timeout, headers, data) From 51b1ab02b48a2efa5d73eba1bc7332a8b160f84b Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Tue, 17 Jan 2017 13:03:39 +0700 Subject: [PATCH 08/29] Remove unnecessary imports. --- prometheus_client/exposition.py | 2 -- prometheus_client/handlers/basic_auth.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index 47e84fbb..af8624e3 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -16,7 +16,6 @@ try: from BaseHTTPServer import BaseHTTPRequestHandler from BaseHTTPServer import HTTPServer - from urllib2 import build_opener, Request, HTTPHandler from urllib import quote_plus from urlparse import parse_qs, urlparse except ImportError: @@ -24,7 +23,6 @@ unicode = str from http.server import BaseHTTPRequestHandler from http.server import HTTPServer - from urllib.request import build_opener, Request, HTTPHandler from urllib.parse import quote_plus, parse_qs, urlparse diff --git a/prometheus_client/handlers/basic_auth.py b/prometheus_client/handlers/basic_auth.py index 463568ff..077232db 100644 --- a/prometheus_client/handlers/basic_auth.py +++ b/prometheus_client/handlers/basic_auth.py @@ -1,6 +1,6 @@ #!/usr/bin/python -import os, base64 +import base64 from prometheus_client.handlers.base import handler as default_handler From e3071c4cf859180a47f3f489ab852a81fbb8857e Mon Sep 17 00:00:00 2001 From: Tim Small Date: Wed, 23 Nov 2016 18:12:01 +0000 Subject: [PATCH 09/29] Just check for a url scheme to allow users to provide a handler. --- prometheus_client/exposition.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index 9ef4d259..d6a3d0c3 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -172,7 +172,8 @@ def delete_from_gateway(gateway, job, grouping_key=None, timeout=None): def _use_gateway(method, gateway, job, registry, grouping_key, timeout): - if not (gateway.startswith('http://') or gateway.startswith('https://')): + gateway_url = urlparse(gateway) + if not gateway_url.scheme: gateway = 'http://{0}'.format(gateway) url = '{0}/metrics/job/{1}'.format(gateway, quote_plus(job)) From c3de82b60c11520a2768b6df9508cd2910814785 Mon Sep 17 00:00:00 2001 From: Tim Small Date: Wed, 23 Nov 2016 18:16:27 +0000 Subject: [PATCH 10/29] Allow a handler to be passed in to carry out a custom request. Allow a custom handler to be provided, so that the caller can provide code which carried out basic auth, https client certificate validation or other arbitrary schemes and access methods such as using different types of proxy. --- prometheus_client/exposition.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index d6a3d0c3..2dbfaf0b 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -118,7 +118,7 @@ def write_to_textfile(path, registry): os.rename(tmppath, path) -def push_to_gateway(gateway, job, registry, grouping_key=None, timeout=None): +def push_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, handler=None): '''Push metrics to the given pushgateway. `gateway` the url for your push gateway. Either of the form @@ -133,10 +133,10 @@ def push_to_gateway(gateway, job, registry, grouping_key=None, timeout=None): 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=None): '''PushAdd metrics to the given pushgateway. `gateway` the url for your push gateway. Either of the form @@ -151,10 +151,10 @@ def pushadd_to_gateway(gateway, job, registry, grouping_key=None, timeout=None): 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=None): '''Delete metrics from the given pushgateway. `gateway` the url for your push gateway. Either of the form @@ -168,10 +168,10 @@ def delete_from_gateway(gateway, job, grouping_key=None, timeout=None): 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): +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) @@ -189,10 +189,14 @@ def _use_gateway(method, gateway, job, registry, grouping_key, timeout): 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)) + if handler is None: + resp = build_opener(handler).open(request, timeout=timeout) + if resp.code >= 400: + raise IOError("error talking to pushgateway: {0} {1}".format( + resp.code, resp.msg)) + else: + handler(url=url, method=lambda: method, timeout=timeout, + headers=[('Content-Type', CONTENT_TYPE_LATEST)], content=data) def instance_ip_grouping_key(): '''Grouping key with instance set to the IP Address of this host.''' From 568f6cab05b68033c376dba07c31a4dcb369a0e0 Mon Sep 17 00:00:00 2001 From: Tim Small Date: Fri, 25 Nov 2016 17:45:38 +0000 Subject: [PATCH 11/29] Document new handler parameter to pushgateway function Provide pydoc for the new handler function to the various pushgateway functions, provide parameter descriptions in the documentatation for the push_to_gateway function to avoid excessive copy-and-paste. --- prometheus_client/exposition.py | 36 +++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index 2dbfaf0b..47f4d9f4 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -130,6 +130,30 @@ def push_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, han 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.''' @@ -148,6 +172,12 @@ 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.''' @@ -165,6 +195,12 @@ def delete_from_gateway(gateway, job, grouping_key=None, timeout=None, handler=N 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.''' From 814e24d562edb301567098c8be058e33bbd5e6c9 Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Sun, 15 Jan 2017 13:51:14 +0700 Subject: [PATCH 12/29] Separate default handler code for re-use and add test case for custom handler (refs #120) --- prometheus_client/exposition.py | 17 +++++++---------- prometheus_client/handlers/__init__.py | 0 prometheus_client/handlers/base.py | 18 ++++++++++++++++++ tests/test_exposition.py | 13 ++++++++++++- 4 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 prometheus_client/handlers/__init__.py create mode 100644 prometheus_client/handlers/base.py diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index 47f4d9f4..438f30eb 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -10,6 +10,9 @@ from wsgiref.simple_server import make_server from . import core + +from handlers.base import handler as default_handler + try: from BaseHTTPServer import BaseHTTPRequestHandler from BaseHTTPServer import HTTPServer @@ -222,17 +225,11 @@ def _use_gateway(method, gateway, job, registry, grouping_key, timeout, handler) 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 + headers=[('Content-Type', CONTENT_TYPE_LATEST)] if handler is None: - resp = build_opener(handler).open(request, timeout=timeout) - if resp.code >= 400: - raise IOError("error talking to pushgateway: {0} {1}".format( - resp.code, resp.msg)) - else: - handler(url=url, method=lambda: method, timeout=timeout, - headers=[('Content-Type', CONTENT_TYPE_LATEST)], content=data) + handler = default_handler + 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.''' diff --git a/prometheus_client/handlers/__init__.py b/prometheus_client/handlers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/prometheus_client/handlers/base.py b/prometheus_client/handlers/base.py new file mode 100644 index 00000000..b91a1722 --- /dev/null +++ b/prometheus_client/handlers/base.py @@ -0,0 +1,18 @@ +#!/usr/bin/python + +try: + from urllib2 import build_opener, Request, HTTPHandler +except ImportError: + # Python 3 + from urllib.request import build_opener, Request, HTTPHandler + +def handler(url, method, timeout, headers, data): + '''Default handler that implements HTTP/HTTPS connections.''' + 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)) diff --git a/tests/test_exposition.py b/tests/test_exposition.py index fa5cfdb0..0a5c6df1 100644 --- a/tests/test_exposition.py +++ b/tests/test_exposition.py @@ -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.handlers.base import handler as default_handler try: from BaseHTTPServer import BaseHTTPRequestHandler @@ -22,7 +23,6 @@ from http.server import BaseHTTPRequestHandler from http.server import HTTPServer - class TestGenerateText(unittest.TestCase): def setUp(self): self.registry = CollectorRegistry() @@ -165,6 +165,17 @@ 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']) + 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') + @unittest.skipIf( sys.platform == "darwin", "instance_ip_grouping_key() does not work on macOS." From 9139e82b2aae5d6e87f43943a9fd3bdf459c41e0 Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Sun, 15 Jan 2017 15:26:20 +0700 Subject: [PATCH 13/29] Fix relative import error. --- prometheus_client/exposition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index 438f30eb..2d58eae6 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -11,7 +11,7 @@ from . import core -from handlers.base import handler as default_handler +from .handlers.base import handler as default_handler try: from BaseHTTPServer import BaseHTTPRequestHandler From ea1e3e77d962f0a0026a3b1741db118a0d4864dd Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Sun, 15 Jan 2017 15:40:48 +0700 Subject: [PATCH 14/29] Add 'handlers' package to distribution. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ac41ede7..a58c5e28 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ license = "Apache Software License 2.0", keywords = "prometheus monitoring instrumentation client", url = "https://github.com/prometheus/client_python", - packages=['prometheus_client', 'prometheus_client.bridge', 'prometheus_client.twisted'], + packages=['prometheus_client', 'prometheus_client.bridge', 'prometheus_client.twisted', 'prometheus_client.handlers'], extras_require={ 'twisted': ['twisted'], }, From 0ac7442868653bb8e629abeb4534d741ab35e93d Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Sun, 15 Jan 2017 16:46:24 +0700 Subject: [PATCH 15/29] Add 'handler_args' to pass extra informatoin to handler. --- prometheus_client/exposition.py | 19 +++++++++++-------- prometheus_client/handlers/base.py | 2 +- prometheus_client/handlers/basic_auth.py | 16 ++++++++++++++++ 3 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 prometheus_client/handlers/basic_auth.py diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index 2d58eae6..47e84fbb 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -121,7 +121,7 @@ def write_to_textfile(path, registry): os.rename(tmppath, path) -def push_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, handler=None): +def push_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, handler=None, handler_args=None): '''Push metrics to the given pushgateway. `gateway` the url for your push gateway. Either of the form @@ -157,13 +157,14 @@ def push_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, han failure. 'content' is the data which should be used to form the HTTP Message Body. + `handler_args` is an optional dict of extra arguments to provide 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, handler) + _use_gateway('PUT', gateway, job, registry, grouping_key, timeout, handler, handler_args) -def pushadd_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, handler=None): +def pushadd_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, handler=None, handler_args=None): '''PushAdd metrics to the given pushgateway. `gateway` the url for your push gateway. Either of the form @@ -184,10 +185,10 @@ def pushadd_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, 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, handler) + _use_gateway('POST', gateway, job, registry, grouping_key, timeout, handler, handler_args) -def delete_from_gateway(gateway, job, grouping_key=None, timeout=None, handler=None): +def delete_from_gateway(gateway, job, grouping_key=None, timeout=None, handler=None, handler_args=None): '''Delete metrics from the given pushgateway. `gateway` the url for your push gateway. Either of the form @@ -207,10 +208,10 @@ def delete_from_gateway(gateway, job, grouping_key=None, timeout=None, handler=N 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, handler) + _use_gateway('DELETE', gateway, job, None, grouping_key, timeout, handler, handler_args) -def _use_gateway(method, gateway, job, registry, grouping_key, timeout, handler): +def _use_gateway(method, gateway, job, registry, grouping_key, timeout, handler, handler_args): gateway_url = urlparse(gateway) if not gateway_url.scheme: gateway = 'http://{0}'.format(gateway) @@ -228,8 +229,10 @@ def _use_gateway(method, gateway, job, registry, grouping_key, timeout, handler) headers=[('Content-Type', CONTENT_TYPE_LATEST)] if handler is None: handler = default_handler + if handler_args is None: + handler_args = dict() handler(url=url, method=method, timeout=timeout, - headers=headers, data=data) + headers=headers, data=data, **handler_args) def instance_ip_grouping_key(): '''Grouping key with instance set to the IP Address of this host.''' diff --git a/prometheus_client/handlers/base.py b/prometheus_client/handlers/base.py index b91a1722..d2319136 100644 --- a/prometheus_client/handlers/base.py +++ b/prometheus_client/handlers/base.py @@ -6,7 +6,7 @@ # Python 3 from urllib.request import build_opener, Request, HTTPHandler -def handler(url, method, timeout, headers, data): +def handler(url, method, timeout, headers, data, **kwargs): '''Default handler that implements HTTP/HTTPS connections.''' request = Request(url, data=data) request.get_method = lambda: method diff --git a/prometheus_client/handlers/basic_auth.py b/prometheus_client/handlers/basic_auth.py new file mode 100644 index 00000000..463568ff --- /dev/null +++ b/prometheus_client/handlers/basic_auth.py @@ -0,0 +1,16 @@ +#!/usr/bin/python + +import os, base64 + +from prometheus_client.handlers.base import handler as default_handler + +def handler(url, method, timeout, headers, data, **kwargs): + '''Handler that implements HTTP Basic Auth by setting auth headers if + 'username' passed as keyword argument.''' + if 'username' in kwargs: + username = kwargs['username'] + password = kwargs['password'] + auth_value = "{0}:{1}".format(username, password) + auth_header = "Basic {0}".format(base64.b64encode(bytes(auth_value))) + headers.append(['Authorization', auth_header]) + default_handler(url, method, timeout, headers, data) From d3f5a58c83d4f2cf0d6ac3ab8ba17314d85003b5 Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Tue, 17 Jan 2017 13:03:39 +0700 Subject: [PATCH 16/29] Remove unnecessary imports. --- prometheus_client/exposition.py | 2 -- prometheus_client/handlers/basic_auth.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index 47e84fbb..af8624e3 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -16,7 +16,6 @@ try: from BaseHTTPServer import BaseHTTPRequestHandler from BaseHTTPServer import HTTPServer - from urllib2 import build_opener, Request, HTTPHandler from urllib import quote_plus from urlparse import parse_qs, urlparse except ImportError: @@ -24,7 +23,6 @@ unicode = str from http.server import BaseHTTPRequestHandler from http.server import HTTPServer - from urllib.request import build_opener, Request, HTTPHandler from urllib.parse import quote_plus, parse_qs, urlparse diff --git a/prometheus_client/handlers/basic_auth.py b/prometheus_client/handlers/basic_auth.py index 463568ff..077232db 100644 --- a/prometheus_client/handlers/basic_auth.py +++ b/prometheus_client/handlers/basic_auth.py @@ -1,6 +1,6 @@ #!/usr/bin/python -import os, base64 +import base64 from prometheus_client.handlers.base import handler as default_handler From 44df258b203ecf3eae46c09e1264f0c062754fd7 Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Wed, 25 Jan 2017 11:58:12 +0700 Subject: [PATCH 17/29] Use closure to pass args to handlers (refs #60). --- README.md | 18 ++++++++++++++++++ prometheus_client/exposition.py | 19 ++++++++----------- prometheus_client/handlers/base.py | 23 +++++++++++++---------- prometheus_client/handlers/basic_auth.py | 21 +++++++++++---------- tests/test_exposition.py | 17 +++++++++++++++-- 5 files changed, 65 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 8aa1d326..18f25884 100644 --- a/README.md +++ b/README.md @@ -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 +import os + +def my_auth_handler(url, method, timeout, headers, data): + username = os.environ['PUSHGW_USERNAME'] + password = os.environ['PUSHGW_PASSWORD'] + return basic_auth_handler(url, method, timeout, headers, data, username, password) +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 diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index af8624e3..dec3640c 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -119,7 +119,7 @@ def write_to_textfile(path, registry): os.rename(tmppath, path) -def push_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, handler=None, handler_args=None): +def push_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, handler=None): '''Push metrics to the given pushgateway. `gateway` the url for your push gateway. Either of the form @@ -155,14 +155,13 @@ def push_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, han failure. 'content' is the data which should be used to form the HTTP Message Body. - `handler_args` is an optional dict of extra arguments to provide 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, handler, handler_args) + _use_gateway('PUT', gateway, job, registry, grouping_key, timeout, handler) -def pushadd_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, handler=None, handler_args=None): +def pushadd_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, handler=None): '''PushAdd metrics to the given pushgateway. `gateway` the url for your push gateway. Either of the form @@ -183,10 +182,10 @@ def pushadd_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, 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, handler, handler_args) + _use_gateway('POST', gateway, job, registry, grouping_key, timeout, handler) -def delete_from_gateway(gateway, job, grouping_key=None, timeout=None, handler=None, handler_args=None): +def delete_from_gateway(gateway, job, grouping_key=None, timeout=None, handler=None): '''Delete metrics from the given pushgateway. `gateway` the url for your push gateway. Either of the form @@ -206,10 +205,10 @@ def delete_from_gateway(gateway, job, grouping_key=None, timeout=None, handler=N 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, handler, handler_args) + _use_gateway('DELETE', gateway, job, None, grouping_key, timeout, handler) -def _use_gateway(method, gateway, job, registry, grouping_key, timeout, handler, handler_args): +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) @@ -227,10 +226,8 @@ def _use_gateway(method, gateway, job, registry, grouping_key, timeout, handler, headers=[('Content-Type', CONTENT_TYPE_LATEST)] if handler is None: handler = default_handler - if handler_args is None: - handler_args = dict() handler(url=url, method=method, timeout=timeout, - headers=headers, data=data, **handler_args) + headers=headers, data=data)() def instance_ip_grouping_key(): '''Grouping key with instance set to the IP Address of this host.''' diff --git a/prometheus_client/handlers/base.py b/prometheus_client/handlers/base.py index d2319136..d026590b 100644 --- a/prometheus_client/handlers/base.py +++ b/prometheus_client/handlers/base.py @@ -6,13 +6,16 @@ # Python 3 from urllib.request import build_opener, Request, HTTPHandler -def handler(url, method, timeout, headers, data, **kwargs): - '''Default handler that implements HTTP/HTTPS connections.''' - 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)) +def handler(url, method, timeout, headers, data): + def handle(): + '''Default handler that implements HTTP/HTTPS connections.''' + 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 diff --git a/prometheus_client/handlers/basic_auth.py b/prometheus_client/handlers/basic_auth.py index 077232db..319d0350 100644 --- a/prometheus_client/handlers/basic_auth.py +++ b/prometheus_client/handlers/basic_auth.py @@ -4,13 +4,14 @@ from prometheus_client.handlers.base import handler as default_handler -def handler(url, method, timeout, headers, data, **kwargs): - '''Handler that implements HTTP Basic Auth by setting auth headers if - 'username' passed as keyword argument.''' - if 'username' in kwargs: - username = kwargs['username'] - password = kwargs['password'] - auth_value = "{0}:{1}".format(username, password) - auth_header = "Basic {0}".format(base64.b64encode(bytes(auth_value))) - headers.append(['Authorization', auth_header]) - default_handler(url, method, timeout, headers, data) +def handler(url, method, timeout, headers, data, username = None, password = None): + def handle(): + '''Handler that implements HTTP Basic Auth by setting auth headers if + 'username' and 'password' arguments are supplied and not None.''' + if username is not None and password is not None: + auth_value = "{0}:{1}".format(username, password) + auth_header = "Basic {0}".format(base64.b64encode(bytes(auth_value))) + headers.append(['Authorization', auth_header]) + default_handler(url, method, timeout, headers, data)() + + return handle diff --git a/tests/test_exposition.py b/tests/test_exposition.py index 0a5c6df1..eef77c44 100644 --- a/tests/test_exposition.py +++ b/tests/test_exposition.py @@ -14,6 +14,7 @@ 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.handlers.base import handler as default_handler +from prometheus_client.handlers.basic_auth import handler as basic_auth_handler try: from BaseHTTPServer import BaseHTTPRequestHandler @@ -99,7 +100,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() @@ -168,7 +172,7 @@ def test_delete_with_groupingkey(self): def test_push_with_handler(self): def my_test_handler(url, method, timeout, headers, data): headers.append(['X-Test-Header', 'foobar']) - default_handler(url, method, timeout, headers, data) + 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') @@ -176,6 +180,15 @@ def my_test_handler(url, method, timeout, headers, data): 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." From 46a9e92f91c9266e6a733ea1602eb3d4324e196d Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Wed, 25 Jan 2017 12:00:14 +0700 Subject: [PATCH 18/29] Add missing import to auth example. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 18f25884..f443cafa 100644 --- a/README.md +++ b/README.md @@ -328,6 +328,7 @@ you can use a special handler to set the Authorization header. ```python from prometheus_client import CollectorRegistry, Gauge, push_to_gateway +from prometheus_client.handlers.basic_auth import handler as basic_auth_handler import os def my_auth_handler(url, method, timeout, headers, data): From eb9500b61ab3722470ec644cefe0b309121a3abe Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Wed, 25 Jan 2017 16:52:05 +0700 Subject: [PATCH 19/29] Simplify example by hardcoding username/password. --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f443cafa..dbccac0e 100644 --- a/README.md +++ b/README.md @@ -329,11 +329,10 @@ you can use a special handler to set the Authorization header. ```python from prometheus_client import CollectorRegistry, Gauge, push_to_gateway from prometheus_client.handlers.basic_auth import handler as basic_auth_handler -import os def my_auth_handler(url, method, timeout, headers, data): - username = os.environ['PUSHGW_USERNAME'] - password = os.environ['PUSHGW_PASSWORD'] + username = 'foobar' + password = 'secret123' return basic_auth_handler(url, method, timeout, headers, data, username, password) registry = CollectorRegistry() g = Gauge('job_last_success_unixtime', 'Last time a batch job successfully finished', registry=registry) From 7f863dd4fdd492608cbc0096f5fedb9b2e15fd0c Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Wed, 25 Jan 2017 17:02:34 +0700 Subject: [PATCH 20/29] Fix for docstring comment. --- prometheus_client/handlers/basic_auth.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/prometheus_client/handlers/basic_auth.py b/prometheus_client/handlers/basic_auth.py index 319d0350..8d24c107 100644 --- a/prometheus_client/handlers/basic_auth.py +++ b/prometheus_client/handlers/basic_auth.py @@ -6,8 +6,9 @@ def handler(url, method, timeout, headers, data, username = None, password = None): def handle(): - '''Handler that implements HTTP Basic Auth by setting auth headers if - 'username' and 'password' arguments are supplied and not None.''' + '''Handler that implements HTTP Basic Auth. + Sets auth headers using supplied 'username' and 'password', if set. + ''' if username is not None and password is not None: auth_value = "{0}:{1}".format(username, password) auth_header = "Basic {0}".format(base64.b64encode(bytes(auth_value))) From 0bebb0abfa608a6b4cefd8e3c4a79ccb1b72e475 Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Wed, 25 Jan 2017 21:44:23 +0700 Subject: [PATCH 21/29] Move base handler back into exposition. --- prometheus_client/exposition.py | 28 +++++++++++++++++------- prometheus_client/handlers/base.py | 21 ------------------ prometheus_client/handlers/basic_auth.py | 2 +- tests/test_exposition.py | 2 +- 4 files changed, 22 insertions(+), 31 deletions(-) delete mode 100644 prometheus_client/handlers/base.py diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index dec3640c..04105967 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -10,12 +10,10 @@ from wsgiref.simple_server import make_server from . import core - -from .handlers.base import handler as default_handler - try: from BaseHTTPServer import BaseHTTPRequestHandler from BaseHTTPServer import HTTPServer + from urllib2 import build_opener, Request, HTTPHandler from urllib import quote_plus from urlparse import parse_qs, urlparse except ImportError: @@ -23,6 +21,7 @@ unicode = str from http.server import BaseHTTPRequestHandler from http.server import HTTPServer + from urllib.request import build_opener, Request, HTTPHandler from urllib.parse import quote_plus, parse_qs, urlparse @@ -119,7 +118,22 @@ def write_to_textfile(path, registry): os.rename(tmppath, path) -def push_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, handler=None): +def default_handler(url, method, timeout, headers, data): + def handle(): + '''Default handler that implements HTTP/HTTPS connections.''' + 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 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 @@ -161,7 +175,7 @@ def push_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, han _use_gateway('PUT', gateway, job, registry, grouping_key, timeout, handler) -def pushadd_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, handler=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 @@ -185,7 +199,7 @@ def pushadd_to_gateway(gateway, job, registry, grouping_key=None, timeout=None, _use_gateway('POST', gateway, job, registry, grouping_key, timeout, handler) -def delete_from_gateway(gateway, job, grouping_key=None, timeout=None, handler=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 @@ -224,8 +238,6 @@ def _use_gateway(method, gateway, job, registry, grouping_key, timeout, handler) for k, v in sorted(grouping_key.items())]) headers=[('Content-Type', CONTENT_TYPE_LATEST)] - if handler is None: - handler = default_handler handler(url=url, method=method, timeout=timeout, headers=headers, data=data)() diff --git a/prometheus_client/handlers/base.py b/prometheus_client/handlers/base.py deleted file mode 100644 index d026590b..00000000 --- a/prometheus_client/handlers/base.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/python - -try: - from urllib2 import build_opener, Request, HTTPHandler -except ImportError: - # Python 3 - from urllib.request import build_opener, Request, HTTPHandler - -def handler(url, method, timeout, headers, data): - def handle(): - '''Default handler that implements HTTP/HTTPS connections.''' - 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 diff --git a/prometheus_client/handlers/basic_auth.py b/prometheus_client/handlers/basic_auth.py index 8d24c107..19558cd3 100644 --- a/prometheus_client/handlers/basic_auth.py +++ b/prometheus_client/handlers/basic_auth.py @@ -2,7 +2,7 @@ import base64 -from prometheus_client.handlers.base import handler as default_handler +from prometheus_client.exposition import default_handler def handler(url, method, timeout, headers, data, username = None, password = None): def handle(): diff --git a/tests/test_exposition.py b/tests/test_exposition.py index eef77c44..a1ce9a7a 100644 --- a/tests/test_exposition.py +++ b/tests/test_exposition.py @@ -13,7 +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.handlers.base import handler as default_handler +from prometheus_client.exposition import default_handler from prometheus_client.handlers.basic_auth import handler as basic_auth_handler try: From 7e4592591729e66b1d8f317027d627e9a873e48f Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Thu, 26 Jan 2017 08:40:45 +0700 Subject: [PATCH 22/29] Move basic auth handler into exposition. Update docstring. --- README.md | 2 +- prometheus_client/exposition.py | 22 +++++++++++++++++++++- prometheus_client/handlers/__init__.py | 0 prometheus_client/handlers/basic_auth.py | 18 ------------------ setup.py | 2 +- tests/test_exposition.py | 3 +-- 6 files changed, 24 insertions(+), 23 deletions(-) delete mode 100644 prometheus_client/handlers/__init__.py delete mode 100644 prometheus_client/handlers/basic_auth.py diff --git a/README.md b/README.md index dbccac0e..0e730682 100644 --- a/README.md +++ b/README.md @@ -328,7 +328,7 @@ you can use a special handler to set the Authorization header. ```python from prometheus_client import CollectorRegistry, Gauge, push_to_gateway -from prometheus_client.handlers.basic_auth import handler as basic_auth_handler +from prometheus_client.exposition import basic_auth_handler def my_auth_handler(url, method, timeout, headers, data): username = 'foobar' diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index 04105967..2dee5594 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -8,6 +8,7 @@ import threading from contextlib import closing from wsgiref.simple_server import make_server +import base64 from . import core try: @@ -119,8 +120,10 @@ def write_to_textfile(path, registry): def default_handler(url, method, timeout, headers, data): + '''Default handler that implements HTTP/HTTPS connections. + + Used by the push_to_gateway functions. Can be re-used by other handlers.''' def handle(): - '''Default handler that implements HTTP/HTTPS connections.''' request = Request(url, data=data) request.get_method = lambda: method for k, v in headers: @@ -133,6 +136,23 @@ def handle(): 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: + auth_value = "{0}:{1}".format(username, password) + auth_header = "Basic {0}".format(base64.b64encode(bytes(auth_value))) + 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. diff --git a/prometheus_client/handlers/__init__.py b/prometheus_client/handlers/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/prometheus_client/handlers/basic_auth.py b/prometheus_client/handlers/basic_auth.py deleted file mode 100644 index 19558cd3..00000000 --- a/prometheus_client/handlers/basic_auth.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/python - -import base64 - -from prometheus_client.exposition import default_handler - -def handler(url, method, timeout, headers, data, username = None, password = None): - def handle(): - '''Handler that implements HTTP Basic Auth. - Sets auth headers using supplied 'username' and 'password', if set. - ''' - if username is not None and password is not None: - auth_value = "{0}:{1}".format(username, password) - auth_header = "Basic {0}".format(base64.b64encode(bytes(auth_value))) - headers.append(['Authorization', auth_header]) - default_handler(url, method, timeout, headers, data)() - - return handle diff --git a/setup.py b/setup.py index a58c5e28..ac41ede7 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ license = "Apache Software License 2.0", keywords = "prometheus monitoring instrumentation client", url = "https://github.com/prometheus/client_python", - packages=['prometheus_client', 'prometheus_client.bridge', 'prometheus_client.twisted', 'prometheus_client.handlers'], + packages=['prometheus_client', 'prometheus_client.bridge', 'prometheus_client.twisted'], extras_require={ 'twisted': ['twisted'], }, diff --git a/tests/test_exposition.py b/tests/test_exposition.py index a1ce9a7a..1ba4b77f 100644 --- a/tests/test_exposition.py +++ b/tests/test_exposition.py @@ -13,8 +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 -from prometheus_client.handlers.basic_auth import handler as basic_auth_handler +from prometheus_client.exposition import default_handler, basic_auth_handler try: from BaseHTTPServer import BaseHTTPRequestHandler From daa40fd9851806508616daac9288df450fdd88f0 Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Sun, 29 Jan 2017 12:56:20 +0700 Subject: [PATCH 23/29] Fix for 'TypeError: string argument without an encoding' (regressing in python3). --- prometheus_client/exposition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index 2dee5594..f3c4e9e4 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -146,7 +146,7 @@ def handle(): ''' if username is not None and password is not None: auth_value = "{0}:{1}".format(username, password) - auth_header = "Basic {0}".format(base64.b64encode(bytes(auth_value))) + auth_header = "Basic {0}".format(base64.b64encode(bytes(auth_value, 'utf8'))) headers.append(['Authorization', auth_header]) default_handler(url, method, timeout, headers, data)() From 1ba7c2e59b3b1fe508b110549c803c931e1d79dc Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Sun, 29 Jan 2017 13:02:58 +0700 Subject: [PATCH 24/29] Fix for 'TypeError: string argument without an encoding' (regressing in python3). --- prometheus_client/exposition.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index f3c4e9e4..ad2ff604 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -146,7 +146,8 @@ def handle(): ''' if username is not None and password is not None: auth_value = "{0}:{1}".format(username, password) - auth_header = "Basic {0}".format(base64.b64encode(bytes(auth_value, 'utf8'))) + 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)() From 8f7511ac7ea4f3907c2cdadaba17f54b4450765f Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Sun, 29 Jan 2017 13:17:29 +0700 Subject: [PATCH 25/29] Fix for 'TypeError: string argument without an encoding' (regressing in python3). --- prometheus_client/exposition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index ad2ff604..fcb2db48 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -145,8 +145,8 @@ def handle(): '''Handler that implements HTTP Basic Auth. ''' if username is not None and password is not None: - auth_value = "{0}:{1}".format(username, password) - auth_token = base64.b64encode(auth_value) + auth_value = bytes('{0}:{1}'.format(username, password), 'utf8') + auth_token = str(base64.b64encode(auth_value), 'utf8') auth_header = "Basic {0}".format(auth_token) headers.append(['Authorization', auth_header]) default_handler(url, method, timeout, headers, data)() From 444a748068113607561c7267e6e6156f7615bc7e Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Sun, 29 Jan 2017 13:26:24 +0700 Subject: [PATCH 26/29] Resort to determining python version to work around base64 byte encoding issue. --- prometheus_client/exposition.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index fcb2db48..26ac0c8b 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -9,6 +9,7 @@ from contextlib import closing from wsgiref.simple_server import make_server import base64 +import sys from . import core try: @@ -145,8 +146,12 @@ def handle(): '''Handler that implements HTTP Basic Auth. ''' if username is not None and password is not None: - auth_value = bytes('{0}:{1}'.format(username, password), 'utf8') - auth_token = str(base64.b64encode(auth_value), 'utf8') + 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)() From 62e8012bd64224b805e9a586742dc2dd4d4cc8c5 Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Sun, 29 Jan 2017 13:35:08 +0700 Subject: [PATCH 27/29] Potential workaround for py26 test failures. --- tests/test_exposition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_exposition.py b/tests/test_exposition.py index 1ba4b77f..4a75657d 100644 --- a/tests/test_exposition.py +++ b/tests/test_exposition.py @@ -111,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://{0}'.format(':'.join([str(x) for x in httpd.server_address])) class TestServer(threading.Thread): def run(self): httpd.handle_request() From b084d647544c1b715c7a0cd6552bb4c1fe97c0be Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Sun, 29 Jan 2017 13:38:15 +0700 Subject: [PATCH 28/29] Revert "Potential workaround for py26 test failures." This reverts commit 62e8012bd64224b805e9a586742dc2dd4d4cc8c5. --- tests/test_exposition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_exposition.py b/tests/test_exposition.py index 4a75657d..1ba4b77f 100644 --- a/tests/test_exposition.py +++ b/tests/test_exposition.py @@ -111,7 +111,7 @@ def do_PUT(self): do_DELETE = do_PUT httpd = HTTPServer(('localhost', 0), TestHandler) - self.address = 'http://{0}'.format(':'.join([str(x) for x in httpd.server_address])) + self.address = ':'.join([str(x) for x in httpd.server_address]) class TestServer(threading.Thread): def run(self): httpd.handle_request() From a7afc96d7afe7c5f5934f52fedd8d5afbf86f3f1 Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Tue, 31 Jan 2017 09:19:21 +0700 Subject: [PATCH 29/29] Another potential workaround for py26 test failures. --- tests/test_exposition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_exposition.py b/tests/test_exposition.py index 1ba4b77f..3b1fb16f 100644 --- a/tests/test_exposition.py +++ b/tests/test_exposition.py @@ -111,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()