Skip to content

Commit b14eed7

Browse files
committed
Add in base linting for metrics
From #18656
1 parent b7e7f53 commit b14eed7

File tree

1 file changed

+105
-2
lines changed

1 file changed

+105
-2
lines changed

scripts-dev/mypy_synapse_plugin.py

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,13 @@
2828
import mypy.types
2929
from mypy.erasetype import remove_instance_last_known_values
3030
from mypy.errorcodes import ErrorCode
31-
from mypy.nodes import ARG_NAMED_OPT, TempNode, Var
32-
from mypy.plugin import FunctionSigContext, MethodSigContext, Plugin
31+
from mypy.nodes import ARG_NAMED_OPT, ListExpr, NameExpr, TempNode, Var
32+
from mypy.plugin import (
33+
FunctionLike,
34+
FunctionSigContext,
35+
MethodSigContext,
36+
Plugin,
37+
)
3338
from mypy.typeops import bind_self
3439
from mypy.types import (
3540
AnyType,
@@ -43,11 +48,30 @@
4348
UnionType,
4449
)
4550

51+
PROMETHEUS_METRIC_MISSING_SERVER_NAME_LABEL = ErrorCode(
52+
"missing-server-name-label",
53+
"`SERVER_NAME_LABEL` required in metric",
54+
category="per-homeserver-tenant-metrics",
55+
)
56+
4657

4758
class SynapsePlugin(Plugin):
59+
def get_function_signature_hook(
60+
self, fullname: str
61+
) -> Optional[Callable[[FunctionSigContext], FunctionLike]]:
62+
if fullname in (
63+
"prometheus_client.metrics.Gauge",
64+
# TODO: Add other prometheus_client metrics that need checking as we
65+
# refactor, see https://github.com/element-hq/synapse/issues/18592
66+
):
67+
return check_prometheus_metric_instantiation
68+
69+
return None
70+
4871
def get_method_signature_hook(
4972
self, fullname: str
5073
) -> Optional[Callable[[MethodSigContext], CallableType]]:
74+
# print(f"m fullname={fullname}")
5175
if fullname.startswith(
5276
(
5377
"synapse.util.caches.descriptors.CachedFunction.__call__",
@@ -65,6 +89,85 @@ def get_method_signature_hook(
6589
return None
6690

6791

92+
def check_prometheus_metric_instantiation(ctx: FunctionSigContext) -> CallableType:
93+
"""
94+
Ensure that the `prometheus_client` metrics include the `SERVER_NAME_LABEL` label
95+
when instantiated.
96+
97+
This is important because we support multiple Synapse instances running in the same
98+
process, where all metrics share a single global `REGISTRY`. The `server_name` label
99+
ensures metrics are correctly separated by homeserver.
100+
101+
There are also some metrics that apply at the process level, such as CPU usage,
102+
Python garbage collection, Twisted reactor tick time which shouldn't have the
103+
`SERVER_NAME_LABEL`. In those cases, use use a type ignore comment to disable the
104+
check, e.g. `# type: ignore[missing-server-name-label]`.
105+
"""
106+
# The true signature, this isn't being modified so this is what will be returned.
107+
signature: CallableType = ctx.default_signature
108+
109+
# Sanity check the arguments are still as expected in this version of
110+
# `prometheus_client`. ex. `Counter(name, documentation, labelnames, ...)`
111+
#
112+
# `signature.arg_names` should be: ["name", "documentation", "labelnames", ...]
113+
if len(signature.arg_names) < 3 or signature.arg_names[2] != "labelnames":
114+
ctx.api.fail(
115+
f"Expected the 3rd argument of {signature.name} to be 'labelnames', but got "
116+
f"{signature.arg_names[2]}",
117+
ctx.context,
118+
)
119+
return signature
120+
121+
# Ensure mypy is passing the correct number of arguments because we are doing some
122+
# dirty indexing into `ctx.args` later on.
123+
assert len(ctx.args) == len(signature.arg_names), (
124+
f"Expected the list of arguments in the {signature.name} signature ({len(signature.arg_names)})"
125+
f"to match the number of arguments from the function signature context ({len(ctx.args)})"
126+
)
127+
128+
# Check if the `labelnames` argument includes `SERVER_NAME_LABEL`
129+
#
130+
# `ctx.args` should look like this:
131+
# ```
132+
# [
133+
# [StrExpr("name")],
134+
# [StrExpr("documentation")],
135+
# [ListExpr([StrExpr("label1"), StrExpr("label2")])]
136+
# ...
137+
# ]
138+
# ```
139+
labelnames_arg_expression = ctx.args[2][0] if len(ctx.args[2]) > 0 else None
140+
if isinstance(labelnames_arg_expression, ListExpr):
141+
# Check if the `labelnames` argument includes the `server_name` label (`SERVER_NAME_LABEL`).
142+
for labelname_expression in labelnames_arg_expression.items:
143+
if (
144+
isinstance(labelname_expression, NameExpr)
145+
and labelname_expression.fullname == "synapse.metrics.SERVER_NAME_LABEL"
146+
):
147+
# Found the `SERVER_NAME_LABEL`, all good!
148+
break
149+
else:
150+
ctx.api.fail(
151+
f"Expected {signature.name} to include `SERVER_NAME_LABEL` in the list of labels. "
152+
"If this is a process-level metric (vs homeserver-level), use a type ignore comment "
153+
"to disable this check.",
154+
ctx.context,
155+
code=PROMETHEUS_METRIC_MISSING_SERVER_NAME_LABEL,
156+
)
157+
else:
158+
ctx.api.fail(
159+
f"Expected the `labelnames` argument of {signature.name} to be a list of label names "
160+
f"(including `SERVER_NAME_LABEL`), but got {labelnames_arg_expression}. "
161+
"If this is a process-level metric (vs homeserver-level), use a type ignore comment "
162+
"to disable this check.",
163+
ctx.context,
164+
code=PROMETHEUS_METRIC_MISSING_SERVER_NAME_LABEL,
165+
)
166+
return signature
167+
168+
return signature
169+
170+
68171
def _get_true_return_type(signature: CallableType) -> mypy.types.Type:
69172
"""
70173
Get the "final" return type of a callable which might return an Awaitable/Deferred.

0 commit comments

Comments
 (0)