259 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			259 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			Python
		
	
	
# -*- coding: utf-8 -*-
 | 
						|
# Copyright 2015-2019 Prometheus Python Client Developers
 | 
						|
# Copyright 2019 Matrix.org Foundation C.I.C.
 | 
						|
#
 | 
						|
# Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
# you may not use this file except in compliance with the License.
 | 
						|
# You may obtain a copy of the License at
 | 
						|
#
 | 
						|
#     http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
#
 | 
						|
# Unless required by applicable law or agreed to in writing, software
 | 
						|
# distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
# See the License for the specific language governing permissions and
 | 
						|
# limitations under the License.
 | 
						|
 | 
						|
"""
 | 
						|
This code is based off `prometheus_client/exposition.py` from version 0.7.1.
 | 
						|
 | 
						|
Due to the renaming of metrics in prometheus_client 0.4.0, this customised
 | 
						|
vendoring of the code will emit both the old versions that Synapse dashboards
 | 
						|
expect, and the newer "best practice" version of the up-to-date official client.
 | 
						|
"""
 | 
						|
 | 
						|
import math
 | 
						|
import threading
 | 
						|
from collections import namedtuple
 | 
						|
from http.server import BaseHTTPRequestHandler, HTTPServer
 | 
						|
from socketserver import ThreadingMixIn
 | 
						|
from urllib.parse import parse_qs, urlparse
 | 
						|
 | 
						|
from prometheus_client import REGISTRY
 | 
						|
 | 
						|
from twisted.web.resource import Resource
 | 
						|
 | 
						|
try:
 | 
						|
    from prometheus_client.samples import Sample
 | 
						|
except ImportError:
 | 
						|
    Sample = namedtuple("Sample", ["name", "labels", "value", "timestamp", "exemplar"])
 | 
						|
 | 
						|
 | 
						|
CONTENT_TYPE_LATEST = str("text/plain; version=0.0.4; charset=utf-8")
 | 
						|
 | 
						|
 | 
						|
INF = float("inf")
 | 
						|
MINUS_INF = float("-inf")
 | 
						|
 | 
						|
 | 
						|
def floatToGoString(d):
 | 
						|
    d = float(d)
 | 
						|
    if d == INF:
 | 
						|
        return "+Inf"
 | 
						|
    elif d == MINUS_INF:
 | 
						|
        return "-Inf"
 | 
						|
    elif math.isnan(d):
 | 
						|
        return "NaN"
 | 
						|
    else:
 | 
						|
        s = repr(d)
 | 
						|
        dot = s.find(".")
 | 
						|
        # Go switches to exponents sooner than Python.
 | 
						|
        # We only need to care about positive values for le/quantile.
 | 
						|
        if d > 0 and dot > 6:
 | 
						|
            mantissa = "{0}.{1}{2}".format(s[0], s[1:dot], s[dot + 1 :]).rstrip("0.")
 | 
						|
            return "{0}e+0{1}".format(mantissa, dot - 1)
 | 
						|
        return s
 | 
						|
 | 
						|
 | 
						|
def sample_line(line, name):
 | 
						|
    if line.labels:
 | 
						|
        labelstr = "{{{0}}}".format(
 | 
						|
            ",".join(
 | 
						|
                [
 | 
						|
                    '{0}="{1}"'.format(
 | 
						|
                        k,
 | 
						|
                        v.replace("\\", r"\\").replace("\n", r"\n").replace('"', r"\""),
 | 
						|
                    )
 | 
						|
                    for k, v in sorted(line.labels.items())
 | 
						|
                ]
 | 
						|
            )
 | 
						|
        )
 | 
						|
    else:
 | 
						|
        labelstr = ""
 | 
						|
    timestamp = ""
 | 
						|
    if line.timestamp is not None:
 | 
						|
        # Convert to milliseconds.
 | 
						|
        timestamp = " {0:d}".format(int(float(line.timestamp) * 1000))
 | 
						|
    return "{0}{1} {2}{3}\n".format(
 | 
						|
        name, labelstr, floatToGoString(line.value), timestamp
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
def nameify_sample(sample):
 | 
						|
    """
 | 
						|
    If we get a prometheus_client<0.4.0 sample as a tuple, transform it into a
 | 
						|
    namedtuple which has the names we expect.
 | 
						|
    """
 | 
						|
    if not isinstance(sample, Sample):
 | 
						|
        sample = Sample(*sample, None, None)
 | 
						|
 | 
						|
    return sample
 | 
						|
 | 
						|
 | 
						|
def generate_latest(registry, emit_help=False):
 | 
						|
    output = []
 | 
						|
 | 
						|
    for metric in registry.collect():
 | 
						|
 | 
						|
        if metric.name.startswith("__unused"):
 | 
						|
            continue
 | 
						|
 | 
						|
        if not metric.samples:
 | 
						|
            # No samples, don't bother.
 | 
						|
            continue
 | 
						|
 | 
						|
        mname = metric.name
 | 
						|
        mnewname = metric.name
 | 
						|
        mtype = metric.type
 | 
						|
 | 
						|
        # OpenMetrics -> Prometheus
 | 
						|
        if mtype == "counter":
 | 
						|
            mnewname = mnewname + "_total"
 | 
						|
        elif mtype == "info":
 | 
						|
            mtype = "gauge"
 | 
						|
            mnewname = mnewname + "_info"
 | 
						|
        elif mtype == "stateset":
 | 
						|
            mtype = "gauge"
 | 
						|
        elif mtype == "gaugehistogram":
 | 
						|
            mtype = "histogram"
 | 
						|
        elif mtype == "unknown":
 | 
						|
            mtype = "untyped"
 | 
						|
 | 
						|
        # Output in the old format for compatibility.
 | 
						|
        if emit_help:
 | 
						|
            output.append(
 | 
						|
                "# HELP {0} {1}\n".format(
 | 
						|
                    mname,
 | 
						|
                    metric.documentation.replace("\\", r"\\").replace("\n", r"\n"),
 | 
						|
                )
 | 
						|
            )
 | 
						|
        output.append("# TYPE {0} {1}\n".format(mname, mtype))
 | 
						|
        for sample in map(nameify_sample, metric.samples):
 | 
						|
            # Get rid of the OpenMetrics specific samples
 | 
						|
            for suffix in ["_created", "_gsum", "_gcount"]:
 | 
						|
                if sample.name.endswith(suffix):
 | 
						|
                    break
 | 
						|
            else:
 | 
						|
                newname = sample.name.replace(mnewname, mname)
 | 
						|
                if ":" in newname and newname.endswith("_total"):
 | 
						|
                    newname = newname[: -len("_total")]
 | 
						|
                output.append(sample_line(sample, newname))
 | 
						|
 | 
						|
        # Get rid of the weird colon things while we're at it
 | 
						|
        if mtype == "counter":
 | 
						|
            mnewname = mnewname.replace(":total", "")
 | 
						|
        mnewname = mnewname.replace(":", "_")
 | 
						|
 | 
						|
        if mname == mnewname:
 | 
						|
            continue
 | 
						|
 | 
						|
        # Also output in the new format, if it's different.
 | 
						|
        if emit_help:
 | 
						|
            output.append(
 | 
						|
                "# HELP {0} {1}\n".format(
 | 
						|
                    mnewname,
 | 
						|
                    metric.documentation.replace("\\", r"\\").replace("\n", r"\n"),
 | 
						|
                )
 | 
						|
            )
 | 
						|
        output.append("# TYPE {0} {1}\n".format(mnewname, mtype))
 | 
						|
        for sample in map(nameify_sample, metric.samples):
 | 
						|
            # Get rid of the OpenMetrics specific samples
 | 
						|
            for suffix in ["_created", "_gsum", "_gcount"]:
 | 
						|
                if sample.name.endswith(suffix):
 | 
						|
                    break
 | 
						|
            else:
 | 
						|
                output.append(
 | 
						|
                    sample_line(
 | 
						|
                        sample, sample.name.replace(":total", "").replace(":", "_")
 | 
						|
                    )
 | 
						|
                )
 | 
						|
 | 
						|
    return "".join(output).encode("utf-8")
 | 
						|
 | 
						|
 | 
						|
class MetricsHandler(BaseHTTPRequestHandler):
 | 
						|
    """HTTP handler that gives metrics from ``REGISTRY``."""
 | 
						|
 | 
						|
    registry = REGISTRY
 | 
						|
 | 
						|
    def do_GET(self):
 | 
						|
        registry = self.registry
 | 
						|
        params = parse_qs(urlparse(self.path).query)
 | 
						|
 | 
						|
        if "help" in params:
 | 
						|
            emit_help = True
 | 
						|
        else:
 | 
						|
            emit_help = False
 | 
						|
 | 
						|
        try:
 | 
						|
            output = generate_latest(registry, emit_help=emit_help)
 | 
						|
        except Exception:
 | 
						|
            self.send_error(500, "error generating metric output")
 | 
						|
            raise
 | 
						|
        self.send_response(200)
 | 
						|
        self.send_header("Content-Type", CONTENT_TYPE_LATEST)
 | 
						|
        self.end_headers()
 | 
						|
        self.wfile.write(output)
 | 
						|
 | 
						|
    def log_message(self, format, *args):
 | 
						|
        """Log nothing."""
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def factory(cls, registry):
 | 
						|
        """Returns a dynamic MetricsHandler class tied
 | 
						|
           to the passed registry.
 | 
						|
        """
 | 
						|
        # This implementation relies on MetricsHandler.registry
 | 
						|
        #  (defined above and defaulted to REGISTRY).
 | 
						|
 | 
						|
        # As we have unicode_literals, we need to create a str()
 | 
						|
        #  object for type().
 | 
						|
        cls_name = str(cls.__name__)
 | 
						|
        MyMetricsHandler = type(cls_name, (cls, object), {"registry": registry})
 | 
						|
        return MyMetricsHandler
 | 
						|
 | 
						|
 | 
						|
class _ThreadingSimpleServer(ThreadingMixIn, HTTPServer):
 | 
						|
    """Thread per request HTTP server."""
 | 
						|
 | 
						|
    # Make worker threads "fire and forget". Beginning with Python 3.7 this
 | 
						|
    # prevents a memory leak because ``ThreadingMixIn`` starts to gather all
 | 
						|
    # non-daemon threads in a list in order to join on them at server close.
 | 
						|
    # Enabling daemon threads virtually makes ``_ThreadingSimpleServer`` the
 | 
						|
    # same as Python 3.7's ``ThreadingHTTPServer``.
 | 
						|
    daemon_threads = True
 | 
						|
 | 
						|
 | 
						|
def start_http_server(port, addr="", registry=REGISTRY):
 | 
						|
    """Starts an HTTP server for prometheus metrics as a daemon thread"""
 | 
						|
    CustomMetricsHandler = MetricsHandler.factory(registry)
 | 
						|
    httpd = _ThreadingSimpleServer((addr, port), CustomMetricsHandler)
 | 
						|
    t = threading.Thread(target=httpd.serve_forever)
 | 
						|
    t.daemon = True
 | 
						|
    t.start()
 | 
						|
 | 
						|
 | 
						|
class MetricsResource(Resource):
 | 
						|
    """
 | 
						|
    Twisted ``Resource`` that serves prometheus metrics.
 | 
						|
    """
 | 
						|
 | 
						|
    isLeaf = True
 | 
						|
 | 
						|
    def __init__(self, registry=REGISTRY):
 | 
						|
        self.registry = registry
 | 
						|
 | 
						|
    def render_GET(self, request):
 | 
						|
        request.setHeader(b"Content-Type", CONTENT_TYPE_LATEST.encode("ascii"))
 | 
						|
        return generate_latest(self.registry)
 |