2828import  mypy .types 
2929from  mypy .erasetype  import  remove_instance_last_known_values 
3030from  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+ )
3338from  mypy .typeops  import  bind_self 
3439from  mypy .types  import  (
3540    AnyType ,
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
4758class  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+ 
68171def  _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