Skip to content
Merged
Changes from 9 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4df927b
Add in base linting for metrics
MadLittleMods Jul 23, 2025
cec25dd
Support `labelnames` argument being a Tuple expression
MadLittleMods Jul 23, 2025
d62d564
Make label name arg position/name generic
MadLittleMods Jul 24, 2025
325caec
Update wording
MadLittleMods Jul 24, 2025
53bfbc6
Explain why `prometheus_client.metrics_core.Metric` isn't included here
MadLittleMods Jul 24, 2025
395b33b
Disambiguate
MadLittleMods Jul 24, 2025
7818657
Remove debug log
MadLittleMods Jul 24, 2025
6bb5fef
Fix lints in in lint
MadLittleMods Jul 24, 2025
b073bb1
Merge branch 'develop' into madlittlemods/18592-lint-metric
MadLittleMods Jul 25, 2025
6758742
Add cross-check for `prometheus_metric_fullname_to_label_arg_map`
MadLittleMods Jul 28, 2025
c53d34d
Ignore some process-level metrics
MadLittleMods Jul 28, 2025
07e79fe
Ignore process-level metrics
MadLittleMods Jul 28, 2025
4902046
Fix `JemallocCollector` edge case
MadLittleMods Jul 28, 2025
da38631
Support almost all metric types
MadLittleMods Jul 28, 2025
b46040a
Add some cross-links
MadLittleMods Jul 28, 2025
8d706ac
Lint `Metric` types
MadLittleMods Jul 28, 2025
478edc2
Ignore `Metric` usage where necessary
MadLittleMods Jul 28, 2025
62ce1e0
Add changelog
MadLittleMods Jul 28, 2025
9166d55
Remove irrelevant comment
MadLittleMods Jul 29, 2025
2d947a4
Beef up comment
MadLittleMods Jul 29, 2025
89076a4
Mark off TODO about `Metric`
MadLittleMods Jul 29, 2025
a90e19b
Merge branch 'develop' into madlittlemods/18592-lint-metric
MadLittleMods Jul 29, 2025
44e30cf
Merge branch 'develop' into madlittlemods/18592-lint-metric
MadLittleMods Jul 29, 2025
86a0bcd
Merge branch 'develop' into madlittlemods/18592-lint-metric
MadLittleMods Jul 29, 2025
ab5a756
Uncomment merged lints
MadLittleMods Jul 29, 2025
8c380c0
Merge branch 'develop' into madlittlemods/18592-lint-metric
MadLittleMods Jul 29, 2025
bec7bfb
We can now use the full list
MadLittleMods Jul 29, 2025
21ef4b4
Workaround mypy limitation only calling `get_base_class_hook` for one…
MadLittleMods Jul 31, 2025
61a6f39
More sound logic
MadLittleMods Jul 31, 2025
4965340
Use lint instead of assert
MadLittleMods Jul 31, 2025
a7573a7
Make sure pydantic mypy hooks are still being called
MadLittleMods Aug 1, 2025
d2efe1d
Add more explicit instructions
MadLittleMods Aug 1, 2025
f2f1b9e
Merge branch 'develop' into madlittlemods/18592-lint-metric
MadLittleMods Aug 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 85 additions & 6 deletions scripts-dev/mypy_synapse_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@

from typing import Callable, Optional, Tuple, Type, Union

import attr
import mypy.types
from mypy.erasetype import remove_instance_last_known_values
from mypy.errorcodes import ErrorCode
from mypy.nodes import ARG_NAMED_OPT, ListExpr, NameExpr, TempNode, Var
from mypy.nodes import ARG_NAMED_OPT, ListExpr, NameExpr, TempNode, TupleExpr, Var
from mypy.plugin import (
FunctionLike,
FunctionSigContext,
Expand All @@ -55,12 +56,68 @@
)


@attr.s(auto_attribs=True)
class ArgLocation:
keyword_name: str
"""
The keyword argument name for this argument
"""
position: int
"""
The positional index of this argument
"""


# FIXME: Ideally, we'd be able to cross-check this list to make sure it includes
# everything as part of the lints by checking for anything that inherits from
# `prometheus_client.metrics.MetricWrapperBase` or
# `prometheus_client.metrics_core.Metric` but I don't know of a good mypy hook to
# inspect this.
Comment thread
MadLittleMods marked this conversation as resolved.
Outdated
prometheus_metric_fullname_to_label_arg_map = {
"prometheus_client.metrics.MetricWrapperBase": ArgLocation("labelnames", 2),
"prometheus_client.metrics.Counter": ArgLocation("labelnames", 2),
"prometheus_client.metrics.Histogram": ArgLocation("labelnames", 2),
"prometheus_client.metrics.Gauge": ArgLocation("labelnames", 2),
"prometheus_client.metrics.Summary": ArgLocation("labelnames", 2),
"prometheus_client.metrics.Info": ArgLocation("labelnames", 2),
"prometheus_client.metrics.Enum": ArgLocation("labelnames", 2),
# We don't include `prometheus_client.metrics_core.Metric` here because it "is
# intended only for internal use by the [Prometheus] instrumentation client" and
# custom collectors should use the metric families listed below instead.
# Additionally, it doesn't have a `labelnames` argument.
# "prometheus_client.metrics_core.Metric"
"prometheus_client.metrics_core.UnknownMetricFamily": ArgLocation("labels", 3),
"prometheus_client.metrics_core.CounterMetricFamily": ArgLocation("labels", 3),
"prometheus_client.metrics_core.GaugeMetricFamily": ArgLocation("labels", 3),
"prometheus_client.metrics_core.SummaryMetricFamily": ArgLocation("labels", 3),
"prometheus_client.metrics_core.InfoMetricFamily": ArgLocation("labels", 3),
"prometheus_client.metrics_core.HistogramMetricFamily": ArgLocation("labels", 3),
"prometheus_client.metrics_core.GaugeHistogramMetricFamily": ArgLocation(
"labels", 3
),
"prometheus_client.metrics_core.StateSetMetricFamily": ArgLocation("labels", 3),
# Our custom metrics:
# TODO: "synapse.metrics.GaugeHistogramMetricFamilyWithLabels": ArgLocation("labelnames", 4),
}
"""
Map from the fullname of the Prometheus `Metric`/`Collector` classes to the keyword
argument name and positional index of the label names.

This is useful because different metrics have different signatures for passing in label
names and we just need to know where to look.
"""


class SynapsePlugin(Plugin):
def get_function_signature_hook(
self, fullname: str
) -> Optional[Callable[[FunctionSigContext], FunctionLike]]:
# TODO: Use `prometheus_metric_fullname_to_label_arg_map.keys()` once we've
# updated all of the metrics, see
# https://github.com/element-hq/synapse/issues/18592
if fullname in (
"prometheus_client.metrics.Counter",
"prometheus_client.metrics_core.GaugeMetricFamily",
# TODO: Add other prometheus_client metrics that need checking as we
# refactor, see https://github.com/element-hq/synapse/issues/18592
):
Expand Down Expand Up @@ -103,16 +160,34 @@ def check_prometheus_metric_instantiation(ctx: FunctionSigContext) -> CallableTy
check, e.g. `# type: ignore[missing-server-name-label]`.
"""
# The true signature, this isn't being modified so this is what will be returned.
signature: CallableType = ctx.default_signature
signature = ctx.default_signature
definition = signature.definition
assert definition is not None, (
f"Expected the signature definition to be set for anything passed to "
f"`check_prometheus_metric_instantiation`, but it was None. Context: {ctx.context}"
)

# For whatever reason, the `fullname` here is different from the `fullname` we see
# in `get_function_signature_hook(...)` so let's get our familiar version.
fullname = definition.fullname.removesuffix(".__init__")
arg_location = prometheus_metric_fullname_to_label_arg_map.get(fullname)
assert arg_location is not None, (
f"Expected to find {fullname} in `prometheus_metric_fullname_to_label_arg_map`, "
f"but it was not found. This is a problem with our custom mypy plugin. "
f"Please add it to the map. Context: {ctx.context}"
)

# Sanity check the arguments are still as expected in this version of
# `prometheus_client`. ex. `Counter(name, documentation, labelnames, ...)`
#
# `signature.arg_names` should be: ["name", "documentation", "labelnames", ...]
if len(signature.arg_names) < 3 or signature.arg_names[2] != "labelnames":
if (
len(signature.arg_names) < (arg_location.position + 1)
or signature.arg_names[arg_location.position] != arg_location.keyword_name
):
ctx.api.fail(
f"Expected the 3rd argument of {signature.name} to be 'labelnames', but got "
f"{signature.arg_names[2]}",
f"{signature.arg_names[arg_location.position]}",
ctx.context,
)
return signature
Expand All @@ -135,8 +210,12 @@ def check_prometheus_metric_instantiation(ctx: FunctionSigContext) -> CallableTy
# ...
# ]
# ```
labelnames_arg_expression = ctx.args[2][0] if len(ctx.args[2]) > 0 else None
if isinstance(labelnames_arg_expression, ListExpr):
labelnames_arg_expression = (
ctx.args[arg_location.position][0]
if len(ctx.args[arg_location.position]) > 0
else None
)
if isinstance(labelnames_arg_expression, (ListExpr, TupleExpr)):
# Check if the `labelnames` argument includes the `server_name` label (`SERVER_NAME_LABEL`).
for labelname_expression in labelnames_arg_expression.items:
if (
Expand Down
Loading