Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support HTTPS/TLS. #946

Merged
merged 9 commits into from Nov 6, 2023
42 changes: 39 additions & 3 deletions README.md
Expand Up @@ -252,6 +252,20 @@ h = Histogram('request_latency_seconds', 'Description of histogram')
h.observe(4.7, {'trace_id': 'abc123'})
```

Exemplars are only rendered in the OpenMetrics exposition format. If using the
HTTP server or apps in this library, content negotiation can be used to specify
OpenMetrics (which is done by default in Prometheus). Otherwise it will be
necessary to use `generate_latest` from
`prometheus_client.openmetrics.exposition` to view exemplars.

To view exemplars in Prometheus it is also necessary to enable the the
exemplar-storage feature flag:
```
--enable-feature=exemplar-storage
```
Additional information is available in [the Prometheus
documentation](https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage).

### Disabling `_created` metrics

By default counters, histograms, and summaries export an additional series
Expand All @@ -274,8 +288,8 @@ ProcessCollector(namespace='mydaemon', pid=lambda: open('/var/run/daemon.pid').r
### Platform Collector

The client also automatically exports some metadata about Python. If using Jython,
metadata about the JVM in use is also included. This information is available as
labels on the `python_info` metric. The value of the metric is 1, since it is the
metadata about the JVM in use is also included. This information is available as
labels on the `python_info` metric. The value of the metric is 1, since it is the
labels that carry information.

### Disabling Default Collector metrics
Expand Down Expand Up @@ -313,6 +327,28 @@ To add Prometheus exposition to an existing HTTP server, see the `MetricsHandler
which provides a `BaseHTTPRequestHandler`. It also serves as a simple example of how
to write a custom endpoint.

By default, the prometheus client will accept only HTTP requests from Prometheus.
To enable HTTPS, `certfile` and `keyfile` need to be provided. The certificate is
presented to Prometheus as a server certificate during the TLS handshake, and
the private key in the key file must belong to the public key in the certificate.

When HTTPS is enabled, by default mutual TLS (mTLS) is enforced (i.e. Prometheus is
csmarchbanks marked this conversation as resolved.
Show resolved Hide resolved
required to present a client certificate during TLS handshake) and the client certificate
including its hostname is validated against the CA certificate chain.
`insecure_skip_verify=True` can be used to disable mTLS.

`cafile` can be used to specify a certificate file containing a CA certificate chain that
is used to validate the client certificate. `capath` can be used to specify a
certificate directory containing a CA certificate chain that is used to validate the
client certificate. If neither of them is provided, a default CA certificate chain
is used (see Python [ssl.SSLContext.load_default_certs()](https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_default_certs))

karezachen marked this conversation as resolved.
Show resolved Hide resolved
```
from prometheus_client import start_http_server

start_http_server(8000, certfile="server_certificate.pem", keyfile="private_key.pem")
karezachen marked this conversation as resolved.
Show resolved Hide resolved
```

#### Twisted

To use prometheus with [twisted](https://twistedmatrix.com/), there is `MetricsResource` which exposes metrics as a twisted resource.
Expand Down Expand Up @@ -379,7 +415,7 @@ Such an application can be useful when integrating Prometheus metrics with ASGI
apps.

By default, the WSGI application will respect `Accept-Encoding:gzip` headers used by Prometheus
and compress the response if such a header is present. This behaviour can be disabled by passing
and compress the response if such a header is present. This behaviour can be disabled by passing
`disable_compression=True` when creating the app, like this:

```python
Expand Down
58 changes: 56 additions & 2 deletions prometheus_client/exposition.py
Expand Up @@ -159,7 +159,58 @@ def _get_best_family(address, port):
return family, sockaddr[0]


def start_wsgi_server(port: int, addr: str = '0.0.0.0', registry: CollectorRegistry = REGISTRY) -> None:
def _get_ssl_ctx(
certfile: str,
keyfile: str,
protocol: int,
cafile: Optional[str] = None,
capath: Optional[str] = None,
insecure_skip_verify: bool = False,
) -> ssl.SSLContext:
"""Load context supports SSL."""
ssl_cxt = ssl.SSLContext(protocol=protocol)

if cafile is not None or capath is not None:
try:
ssl_cxt.load_verify_locations(cafile, capath)
except IOError as exc:
exc_type = type(exc)
msg = str(exc)
raise exc_type(f"Cannot load CA certificate chain from file "
f"{cafile!r} or directory {capath!r}: {msg}")
else:
try:
ssl_cxt.load_default_certs(purpose=ssl.Purpose.CLIENT_AUTH)
except IOError as exc:
exc_type = type(exc)
msg = str(exc)
raise exc_type(f"Cannot load default CA certificate chain: {msg}")

if not insecure_skip_verify:
ssl_cxt.verify_mode = ssl.CERT_REQUIRED

try:
ssl_cxt.load_cert_chain(certfile=certfile, keyfile=keyfile)
except IOError as exc:
exc_type = type(exc)
msg = str(exc)
raise exc_type(f"Cannot load server certificate file {certfile!r} or "
f"its private key file {keyfile!r}: {msg}")

return ssl_cxt


def start_wsgi_server(
port: int,
addr: str = '0.0.0.0',
registry: CollectorRegistry = REGISTRY,
karezachen marked this conversation as resolved.
Show resolved Hide resolved
certfile: Optional[str] = None,
keyfile: Optional[str] = None,
cafile: Optional[str] = None,
karezachen marked this conversation as resolved.
Show resolved Hide resolved
capath: Optional[str] = None,
karezachen marked this conversation as resolved.
Show resolved Hide resolved
protocol: int = ssl.PROTOCOL_TLS_SERVER,
insecure_skip_verify: bool = False,
) -> None:
"""Starts a WSGI server for prometheus metrics as a daemon thread."""

class TmpServer(ThreadingWSGIServer):
Expand All @@ -168,6 +219,9 @@ class TmpServer(ThreadingWSGIServer):
TmpServer.address_family, addr = _get_best_family(addr, port)
app = make_wsgi_app(registry)
httpd = make_server(addr, port, app, TmpServer, handler_class=_SilentHandler)
if certfile and keyfile:
context = _get_ssl_ctx(certfile, keyfile, protocol, cafile, capath, insecure_skip_verify)
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
t = threading.Thread(target=httpd.serve_forever)
t.daemon = True
t.start()
Expand Down Expand Up @@ -407,7 +461,7 @@ def tls_auth_handler(
The default protocol (ssl.PROTOCOL_TLS_CLIENT) will also enable
ssl.CERT_REQUIRED and SSLContext.check_hostname by default. This can be
disabled by setting insecure_skip_verify to True.

Both this handler and the TLS feature on pushgateay are experimental."""
context = ssl.SSLContext(protocol=protocol)
if cafile is not None:
Expand Down