|
52 | 52 | from packaging.version import parse as parse_version |
53 | 53 | from prometheus_client import ( |
54 | 54 | CollectorRegistry, |
55 | | - Histogram, |
56 | 55 | Metric, |
57 | 56 | generate_latest, |
| 57 | + values, |
58 | 58 | ) |
| 59 | +from prometheus_client.context_managers import Timer |
59 | 60 | from prometheus_client.core import ( |
60 | 61 | REGISTRY, |
61 | 62 | GaugeHistogramMetricFamily, |
62 | 63 | GaugeMetricFamily, |
63 | 64 | Sample, |
64 | 65 | ) |
65 | 66 | from prometheus_client.metrics import _get_use_created |
| 67 | +from prometheus_client.samples import Exemplar |
| 68 | +from prometheus_client.utils import INF, floatToGoString |
66 | 69 | from prometheus_client.values import ValueClass |
67 | 70 | from typing_extensions import Dict, Self |
68 | 71 |
|
@@ -526,7 +529,7 @@ def __init__( |
526 | 529 | self._gauge_value: float = 0 |
527 | 530 |
|
528 | 531 | if not self._labelvalues and self._registry: |
529 | | - # TODO: look into what to do here |
| 532 | + # TODO: look into what to do here, and maybe move it to the wrapperbase? |
530 | 533 | self._registry.register(self) # type: ignore |
531 | 534 |
|
532 | 535 | def set(self, value: float) -> None: |
@@ -596,6 +599,143 @@ def _metric_init(self) -> None: |
596 | 599 | ) |
597 | 600 |
|
598 | 601 |
|
| 602 | +class SynapseHistogram(SynapseMetricWrapperBase): |
| 603 | + _type = "histogram" |
| 604 | + _reserved_labelnames = ["le"] |
| 605 | + DEFAULT_BUCKETS = ( |
| 606 | + 0.005, |
| 607 | + 0.01, |
| 608 | + 0.025, |
| 609 | + 0.05, |
| 610 | + 0.075, |
| 611 | + 0.1, |
| 612 | + 0.25, |
| 613 | + 0.5, |
| 614 | + 0.75, |
| 615 | + 1.0, |
| 616 | + 2.5, |
| 617 | + 5.0, |
| 618 | + 7.5, |
| 619 | + 10.0, |
| 620 | + INF, |
| 621 | + ) |
| 622 | + |
| 623 | + def __init__( |
| 624 | + self, |
| 625 | + name: str, |
| 626 | + documentation: str, |
| 627 | + labelnames: Iterable[str] = (), |
| 628 | + namespace: str = "", |
| 629 | + subsystem: str = "", |
| 630 | + unit: str = "", |
| 631 | + registry: Optional[CollectorRegistry] = REGISTRY, |
| 632 | + _labelvalues: Optional[Sequence[str]] = None, |
| 633 | + buckets: Sequence[float] = DEFAULT_BUCKETS, |
| 634 | + ): |
| 635 | + self._prepare_buckets(buckets) |
| 636 | + super().__init__( |
| 637 | + name=name, |
| 638 | + documentation=documentation, |
| 639 | + labelnames=labelnames, |
| 640 | + namespace=namespace, |
| 641 | + subsystem=subsystem, |
| 642 | + unit=unit, |
| 643 | + registry=registry, |
| 644 | + _labelvalues=_labelvalues, |
| 645 | + ) |
| 646 | + self._histogram = meter.create_histogram( |
| 647 | + self._name, |
| 648 | + unit=self._unit, |
| 649 | + description=self._documentation, |
| 650 | + explicit_bucket_boundaries_advisory=buckets, |
| 651 | + ) |
| 652 | + self._kwargs["buckets"] = buckets |
| 653 | + |
| 654 | + def _prepare_buckets(self, source_buckets: Sequence[Union[float, str]]) -> None: |
| 655 | + buckets = [float(b) for b in source_buckets] |
| 656 | + if buckets != sorted(buckets): |
| 657 | + # This is probably an error on the part of the user, |
| 658 | + # so raise rather than sorting for them. |
| 659 | + raise ValueError("Buckets not in sorted order") |
| 660 | + if buckets and buckets[-1] != INF: |
| 661 | + buckets.append(INF) |
| 662 | + if len(buckets) < 2: |
| 663 | + raise ValueError("Must have at least two buckets") |
| 664 | + self._upper_bounds = buckets |
| 665 | + |
| 666 | + def _metric_init(self) -> None: |
| 667 | + self._buckets: list[values.ValueClass] = [] |
| 668 | + self._created = time() |
| 669 | + bucket_labelnames = self._labelnames + ("le",) |
| 670 | + self._sum = values.ValueClass( |
| 671 | + self._type, |
| 672 | + self._name, |
| 673 | + self._name + "_sum", |
| 674 | + self._labelnames, |
| 675 | + self._labelvalues, |
| 676 | + self._documentation, |
| 677 | + ) |
| 678 | + for b in self._upper_bounds: |
| 679 | + self._buckets.append( |
| 680 | + values.ValueClass( |
| 681 | + self._type, |
| 682 | + self._name, |
| 683 | + self._name + "_bucket", |
| 684 | + bucket_labelnames, |
| 685 | + self._labelvalues + (floatToGoString(b),), |
| 686 | + self._documentation, |
| 687 | + ) |
| 688 | + ) |
| 689 | + |
| 690 | + def observe(self, amount: float, exemplar: Optional[Dict[str, str]] = None) -> None: |
| 691 | + """Observe the given amount. |
| 692 | +
|
| 693 | + The amount is usually positive or zero. Negative values are |
| 694 | + accepted but prevent current versions of Prometheus from |
| 695 | + properly detecting counter resets in the sum of |
| 696 | + observations. See |
| 697 | + https://prometheus.io/docs/practices/histograms/#count-and-sum-of-observations |
| 698 | + for details. |
| 699 | + """ |
| 700 | + self._raise_if_not_observable() |
| 701 | + self._sum.inc(amount) |
| 702 | + for i, bound in enumerate(self._upper_bounds): |
| 703 | + if amount <= bound: |
| 704 | + self._buckets[i].inc(1) |
| 705 | + if exemplar: |
| 706 | + # _validate_exemplar(exemplar) |
| 707 | + self._buckets[i].set_exemplar(Exemplar(exemplar, amount, time())) |
| 708 | + break |
| 709 | + |
| 710 | + def time(self) -> Timer: |
| 711 | + """Time a block of code or function, and observe the duration in seconds. |
| 712 | +
|
| 713 | + Can be used as a function decorator or context manager. |
| 714 | + """ |
| 715 | + return Timer(self, "observe") |
| 716 | + |
| 717 | + def _child_samples(self) -> Iterable[Sample]: |
| 718 | + samples = [] |
| 719 | + acc = 0.0 |
| 720 | + for i, bound in enumerate(self._upper_bounds): |
| 721 | + acc += self._buckets[i].get() |
| 722 | + samples.append( |
| 723 | + Sample( |
| 724 | + "_bucket", |
| 725 | + {"le": floatToGoString(bound)}, |
| 726 | + acc, |
| 727 | + None, |
| 728 | + self._buckets[i].get_exemplar(), |
| 729 | + ) |
| 730 | + ) |
| 731 | + samples.append(Sample("_count", {}, acc, None, None)) |
| 732 | + if self._upper_bounds[0] >= 0: |
| 733 | + samples.append(Sample("_sum", {}, self._sum.get(), None, None)) |
| 734 | + if _get_use_created(): |
| 735 | + samples.append(Sample("_created", {}, self._created, None, None)) |
| 736 | + return tuple(samples) |
| 737 | + |
| 738 | + |
599 | 739 | @attr.s(slots=True, hash=True, auto_attribs=True, kw_only=True) |
600 | 740 | class LaterGauge(Collector): |
601 | 741 | """A Gauge which periodically calls a user-provided callback to produce metrics.""" |
@@ -1091,7 +1231,7 @@ def collect(self) -> Iterable[Metric]: |
1091 | 1231 | "synapse_event_processing_lag", "", labelnames=["name", SERVER_NAME_LABEL] |
1092 | 1232 | ) |
1093 | 1233 |
|
1094 | | -event_processing_lag_by_event = Histogram( |
| 1234 | +event_processing_lag_by_event = SynapseHistogram( |
1095 | 1235 | "synapse_event_processing_lag_by_event", |
1096 | 1236 | "Time between an event being persisted and it being queued up to be sent to the relevant remote servers", |
1097 | 1237 | labelnames=["name", SERVER_NAME_LABEL], |
@@ -1119,7 +1259,7 @@ def collect(self) -> Iterable[Metric]: |
1119 | 1259 | ) |
1120 | 1260 |
|
1121 | 1261 | # 3PID send info |
1122 | | -threepid_send_requests = Histogram( |
| 1262 | +threepid_send_requests = SynapseHistogram( |
1123 | 1263 | "synapse_threepid_send_requests_with_tries", |
1124 | 1264 | documentation="Number of requests for a 3pid token by try count. Note if" |
1125 | 1265 | " there is a request with try count of 4, then there would have been one" |
@@ -1219,4 +1359,5 @@ def render_GET(self, request: Request) -> bytes: |
1219 | 1359 | "install_gc_manager", |
1220 | 1360 | "SynapseCounter", |
1221 | 1361 | "SynapseGauge", |
| 1362 | + "SynapseHistogram", |
1222 | 1363 | ] |
0 commit comments