8000 Add support for gzip content-encoding (#776) · chnacib/client_python@3c91b3f · GitHub
[go: up one dir, main page]

Skip to content
8000

Commit 3c91b3f

Browse files
authored
Add support for gzip content-encoding (prometheus#776)
* Add support for gzip Content-Encoding Signed-off-by: ivan-valkov <iv.v.valkov@gmail.com> * remove the case insensitive check for accept header Signed-off-by: ivan-valkov <iv.v.valkov@gmail.com> * Add option to disable compression Signed-off-by: ivan-valkov <iv.v.valkov@gmail.com> * sMake disable_compression an argument instead of env var Signed-off-by: ivan-valkov <iv.v.valkov@gmail.com> * Update readme Signed-off-by: ivan-valkov <iv.v.valkov@gmail.com> * Fix linters Signed-off-by: ivan-valkov <iv.v.valkov@gmail.com>
1 parent c044b88 commit 3c91b3f

File tree

5 files changed

+215
-90
lines changed

5 files changed

+215
-90
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,14 @@ from prometheus_client import start_wsgi_server
338338
start_wsgi_server(8000)
339339
```
340340

341+
By default, the WSGI application will respect `Accept-Encoding:gzip` headers used by Prometheus
342+
and compress the response if such a header is present. This behaviour can be disabled by passing
343+
`disable_compression=True` when creating the app, like this:
344+
345+
```python
346+
app = make_wsgi_app(disable_compression=True)
347+
```
348+
341349
#### ASGI
342350

343351
To use Prometheus with [ASGI](http://asgi.readthedocs.org/en/latest/), there is
@@ -351,6 +359,14 @@ app = make_asgi_app()
351359
Such an application can be useful when integrating Prometheus metrics with ASGI
352360
apps.
353361

362+
By default, the WSGI application will respect `Accept-Encoding:gzip` headers used by Prometheus
363+
and compress the response if such a header is present. This behaviour can be disabled by passing
364+
`disable_compression=True` when creating the app, like this:
365+
366+
```python
367+
app = make_asgi_app(disable_compression=True)
368+
```
369+
354370
#### Flask
355371

356372
To use Prometheus with [Flask](http://flask.pocoo.org/) we need to serve metrics through a Prometheus WSGI application. This can be achieved using [Flask's application dispatching](http://flask.pocoo.org/docs/latest/patterns/appdispatch/). Below is a working example.

prometheus_client/asgi.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from .registry import CollectorRegistry, REGISTRY
66

77

8-
def make_asgi_app(registry: CollectorRegistry = REGISTRY) -> Callable:
8+
def make_asgi_app(registry: CollectorRegistry = REGISTRY, disable_compression: bool = False) -> Callable:
99
"""Create a ASGI app which serves the metrics from a registry."""
1010

1111
async def prometheus_app(scope, receive, send):
@@ -14,20 +14,25 @@ async def prometheus_app(scope, receive, send):
1414
params = parse_qs(scope.get('query_string', b''))
1515
accept_header = "Accept: " + ",".join([
1616
value.decode("utf8") for (name, value) in scope.get('headers')
17-
if name.decode("utf8") == 'accept'
17+
if name.decode("utf8").lower() == 'accept'
18+
])
19+
accept_encoding_header = ",".join([
20+
value.decode("utf8") for (name, value) in scope.get('headers')
21+
if name.decode("utf8").lower() == 'accept-encoding'
1822
])
1923
# Bake output
20-
status, header, output = _bake_output(registry, accept_header, params)
24+
status, headers, output = _bake_output(registry, accept_header, accept_encoding_header, params, disable_compression)
25+
formatted_headers = []
26+
for header in headers:
27+
formatted_headers.append(tuple(x.encode('utf8') for x in header))
2128
# Return output
2229
payload = await receive()
2330
if payload.get("type") == "http.request":
2431
await send(
2532
{
2633
"type": "http.response.start",
2734
"status": int(status.split(' ')[0]),
28-
"headers": [
29-
tuple(x.encode('utf8') for x in header)
30-
]
35+
"headers": formatted_headers,
3136
}
3237
)
3338
await send({"type": "http.response.body", "body": output})

prometheus_client/exposition.py

Lines changed: 78 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import base64
22
from contextlib import closing
3+
import gzip
34
from http.server import BaseHTTPRequestHandler
45
import os
56
import socket
@@ -93,32 +94,39 @@ def redirect_request(self, req, fp, code, msg, headers, newurl):
9394
return new_request
9495

9596

96-
def _bake_output(registry, accept_header, params):
97+
def _bake_output(registry, accept_header, accept_encoding_header, params, disable_compression):
9798
"""Bake output for metrics output."""
98-
encoder, content_type = choose_encoder(accept_header)
99+
# Choose the correct plain text format of the output.
100+
formatter, content_type = choose_formatter(accept_header)
99101
if 'name[]' in params:
100102
registry = registry.restricted_registry(params['name[]'])
101-
output = encoder(registry)
102-
return '200 OK', ('Content-Type', content_type), output
103+
output = formatter(registry)
104+
headers = [('Content-Type', content_type)]
105+
# If gzip encoding required, gzip the output.
106+
if not disable_compression and gzip_accepted(accept_encoding_header):
107+
output = gzip.compress(output)
108+
headers.append(('Content-Encoding', 'gzip'))
109+
return '200 OK', headers, output
103110

104111

105-
def make_wsgi_app(registry: CollectorRegistry = REGISTRY) -> Callable:
112+
def make_wsgi_app(registry: CollectorRegistry = REGISTRY, disable_compression: bool = False) -> Callable:
106113
"""Create a WSGI app which serves the metrics from a registry."""
107114

108115
def prometheus_app(environ, start_response):
109116
# Prepare parameters
110117
accept_header = environ.get('HTTP_ACCEPT')
118+
accept_encoding_header = environ.get('HTTP_ACCEPT_ENCODING')
111119
params = parse_qs(environ.get('QUERY_STRING', ''))
112120
if environ['PATH_INFO'] == '/favicon.ico':
113121
# Serve empty response for browsers
114122
status = '200 OK'
115-
header = ('', '')
123+
headers = [('', '')]
116124
output = b''
117125
else:
118126
# Bake output
119-
status, header, output = _bake_output(registry, accept_header, params)
127+
status, headers, output = _bake_output(registry, accept_header, accept_encoding_header, params, disable_compression)
120128
# Return output
121-
start_response(status, [header])
129+
start_response(status, headers)
122130
return [output]
123131

124132
return prometheus_app
@@ -152,8 +160,10 @@ def _get_best_family(address, port):
152160

153161
def start_wsgi_server(port: int, addr: str = '0.0.0.0', registry: CollectorRegistry = REGISTRY) -> None:
154162
"""Starts a WSGI server for prometheus metrics as a daemon thread."""
163+
155164
class TmpServer(ThreadingWSGIServer):
156165
"""Copy of ThreadingWSGIServer to update address_family locally"""
166+
157167
TmpServer.address_family, addr = _get_best_family(addr, port)
158168
app = make_wsgi_app(registry)
159169
httpd = make_server(addr, port, app, TmpServer, handler_class=_SilentHandler)
@@ -227,7 +237,7 @@ def sample_line(line):
227237
return ''.join(output).encode('utf-8')
228238

229239

230-
def choose_encoder(accept_header: str) -> Tuple[Callable[[CollectorRegistry], bytes], str]:
240+
def choose_formatter(accept_header: str) -> Tuple[Callable[[CollectorRegistry], bytes], str]:
231241
accept_header = accept_header or ''
232242
for accepted in accept_header.split(','):
233243
if accepted.split(';')[0].strip() == 'application/openmetrics-text':
@@ -236,6 +246,14 @@ def choose_encoder(accept_header: str) -> Tuple[Callable[[CollectorRegistry], by
236246
return generate_latest, CONTENT_TYPE_LATEST
237247

238248

249+
def gzip_accepted(accept_encoding_header: str) -> bool:
250+
accept_encoding_header = accept_encoding_header or ''
251+
for accepted in accept_encoding_header.split(','):
252+
if accepted.split(';')[0].strip().lower() == 'gzip':
253+
return True
254+
return False
255+
256+
239257
class MetricsHandler(BaseHTTPRequestHandler):
240258
"""HTTP handler that gives metrics from ``REGISTRY``."""
241259
registry: CollectorRegistry = REGISTRY
@@ -244,12 +262,14 @@ def do_GET(self) -> None:
244262
# Prepare parameters
245263
registry = self.registry
246264
accept_header = self.headers.get('Accept')
265+
accept_encoding_header = self.headers.get('Accept-Encoding')
247266
params = parse_qs(urlparse(self.path).query)
248267
# Bake output
249-
status, header, output = _bake_output(registry, accept_header, params)
268+
status, headers, output = _bake_output(registry, accept_header, accept_encoding_header, params, False)
250269
# Return output
251270
self.send_response(int(status.split(' ')[0]))
252-
self.send_header(*header)
271+
for header in headers:
272+
self.send_header(*header)
253273
self.end_headers()
254274
self.wfile.write(output)
255275

@@ -289,14 +309,13 @@ def write_to_textfile(path: str, registry: CollectorRegistry) -> None:
289309

290310

291311
def _make_handler(
292-
url: str,
293-
method: str,
294-
timeout: Optional[float],
295-
headers: Sequence[Tuple[str, str]],
296-
data: bytes,
297-
base_handler: type,
312+
url: str,
313+
method: str,
314+
timeout: Optional[float],
315+
headers: Sequence[Tuple[str, str]],
316+
data: bytes,
317+
base_handler: type,
298318
) -> Callable[[], None]:
299-
300319
def handle() -> None:
301320
request = Request(url, data=data)
302321
request.get_method = lambda: method # type: ignore
@@ -310,11 +329,11 @@ def handle() -> None:
310329

311330

312331
def default_handler(
313-
url: str,
314-
method: str,
315-
timeout: Optional[float],
316-
headers: List[Tuple[str, str]],
317-
data: bytes,
332+
url: str,
333+
method: str,
334+
timeout: Optional[float],
335+
headers: List[Tuple[str, str]],
336+
data: bytes,
318337
) -> Callable[[], None]:
319338
"""Default handler that implements HTTP/HTTPS connections.
320339
@@ -324,11 +343,11 @@ def default_handler(
324343

325344

326345
def passthrough_redirect_handler(
327-
url: str,
328-
method: str,
329-
timeout: Optional[float],
330-
headers: List[Tuple[str, str]],
331-
data: bytes,
346+
url: str,
347+
method: str,
348+
timeout: Optional[float],
349+
headers: List[Tuple[str, str]],
350+
data: bytes,
332351
) -> Callable[[], None]:
333352
"""
334353
Handler that automatically trusts redirect responses for all HTTP methods.
@@ -344,13 +363,13 @@ def passthrough_redirect_handler(
344363

345364

346365
def basic_auth_handler(
347-
url: str,
348-
method: str,
349-
timeout: Optional[float],
350-
headers: List[Tuple[str, str]],
351-
data: bytes,
352-
username: str = None,
353-
password: str = None,
366+
url: str,
367+
method: str,
368+
timeout: Optional[float],
369+
headers: List[Tuple[str, str]],
370+
data: bytes,
371+
username: str = None,
372+
password: str = None,
354373
) -> Callable[[], None]:
355374
"""Handler that implements HTTP/HTTPS connections with Basic Auth.
356375
@@ -371,12 +390,12 @@ def handle():
371390

372391

373392
def push_to_gateway(
374-
gateway: str,
375-
job: str,
376-
registry: CollectorRegistry,
377-
grouping_key: Optional[Dict[str, Any]] = None,
378-
timeout: Optional[float] = 30,
379-
handler: Callable = default_handler,
393+
gateway: str,
394+
job: str,
395+
registry: CollectorRegistry,
396+
grouping_key: Optional[Dict[str, Any]] = None,
397+
timeout: Optional[float] = 30,
398+
handler: Callable = default_handler,
380399
) -> None:
381400
"""Push metrics to the given pushgateway.
382401
@@ -420,12 +439,12 @@ def push_to_gateway(
420439

421440

422441
def pushadd_to_gateway(
423-
gateway: str,
424-
job: str,
425-
registry: Optional[CollectorRegistry],
426-
grouping_key: Optional[Dict[str, Any]] = None,
427-
timeout: Optional[float] = 30,
428-
handler: Callable = default_handler,
442+
gateway: str,
443+
job: str,
444+
registry: Optional[CollectorRegistry],
445+
grouping_key: Optional[Dict[str, Any]] = None,
446+
timeout: Optional[float] = 30,
447+
handler: Callable = default_handler,
429448
) -> None:
430449
"""PushAdd metrics to the given pushgateway.
431450
@@ -451,11 +470,11 @@ def pushadd_to_gateway(
451470

452471

453472
def delete_from_gateway(
454-
gateway: str,
455-
job: str,
456-
grouping_key: Optional[Dict[str, Any]] = None,
457-
timeout: Optional[float] = 30,
458-
handler: Callable = default_handler,
473+
gateway: str,
474+
job: str,
475+
grouping_key: Optional[Dict[str, Any]] = None,
476+
timeout: Optional[float] = 30,
477+
handler: Callable = default_handler,
459478
) -> None:
460479
"""Delete metrics from the given pushgateway.
461480
@@ -480,13 +499,13 @@ def delete_from_gateway(
480499

481500

482501
def _use_gateway(
483-
method: str,
484-
gateway: str,
485-
job: str,
486-
registry: Optional[CollectorRegistry],
487-
grouping_key: Optional[Dict[str, Any]],
488-
timeout: Optional[float],
489-
handler: Callable,
502+
method: str,
503+
gateway: str,
504+
job: str,
505+
registry: Optional[CollectorRegistry],
506+
grouping_key: Optional[Dict[str, Any]],
507+
timeout: Optional[float],
508+
handler: Callable,
490509
) -> None:
491510
gateway_url = urlparse(gateway)
492511
# See https://bugs.python.org/issue27657 for details on urlparse in py>=3.7.6.

0 commit comments

Comments
 (0)
0