|
@@ -0,0 +1,258 @@
|
|
|
+# -*- 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)
|