3737
3838
3939class QuantityBuilderProtocol (Protocol ):
40- def __call__ ( self , metrics : Sequence [ RRDMetric ]) -> QuantityProtocol : ...
40+ """Combines what a graph draws for each matched object into the one quantity drawing them."""
4141
42+ def __call__ (self , per_object : Sequence [QuantityProtocol ]) -> QuantityProtocol : ...
4243
43- def build_single_quantity (metrics : Sequence [RRDMetric ]) -> QuantityProtocol :
44- (metric ,) = metrics
45- return metric
44+
45+ def build_single_quantity (per_object : Sequence [QuantityProtocol ]) -> QuantityProtocol :
46+ (quantity ,) = per_object
47+ return quantity
4648
4749
4850def drawn_quantity (
@@ -53,21 +55,49 @@ def drawn_quantity(
5355 return quantity_builder ([rrd_metric_of (service , metric_name ) for service in services ])
5456
5557
58+ @dataclass (frozen = True )
59+ class _ObjectContext :
60+ """One of the objects a graph was matched on, to build what it draws for that object."""
61+
62+ service : Service
63+ localizer : Callable [[str ], str ]
64+ registered_metrics : Mapping [str , metrics_v1 .Metric ]
65+
66+ def metric (self , metric_name : str ) -> RRDMetric :
67+ return rrd_metric_of (self .service , metric_name )
68+
69+
5670@dataclass (frozen = True )
5771class _ParseContext :
5872 services : Sequence [Service ]
5973 quantity_builder : QuantityBuilderProtocol
6074 localizer : Callable [[str ], str ]
6175 registered_metrics : Mapping [str , metrics_v1 .Metric ]
6276
63- def drawn (self , metric_name : str ) -> QuantityProtocol :
64- return drawn_quantity (metric_name , self .services , self .quantity_builder )
65-
66- def scalar (self , metric_name : str ) -> RRDMetric :
67- return rrd_metric_of (self .services [0 ], metric_name )
77+ def _of (self , service : Service ) -> _ObjectContext :
78+ return _ObjectContext (service , self .localizer , self .registered_metrics )
79+
80+ def single_object (self ) -> _ObjectContext | None :
81+ # A rule names one object's thresholds; over several matched ones there is none to name them
82+ # against, so no rule is built.
83+ return self ._of (self .services [0 ]) if len (self .services ) == 1 else None
84+
85+ def object_for (self , bound : ApiQuantity ) -> _ObjectContext | None :
86+ # A bound naming a metric is read from one object; over several matched ones there is none, so
87+ # that end is left to auto-scale. A bound of constants alone is read without one.
88+ if any (metric_names_in_quantity (bound )):
89+ return self .single_object ()
90+ return self ._of (self .services [0 ])
91+
92+ def drawn (self , quantity : ApiQuantity ) -> QuantityProtocol :
93+ # Built once per matched object and combined, so an operation only ever takes that object's
94+ # operands - never a quantity spanning the objects, which cannot be an operand.
95+ return self .quantity_builder (
96+ [_parse_quantity (quantity , self ._of (service )) for service in self .services ]
97+ )
6898
6999
70- def _curve_display (quantity : ApiQuantity , context : _ParseContext ) -> CurveAttributes :
100+ def _curve_display (quantity : ApiQuantity , context : _ObjectContext ) -> CurveAttributes :
71101 match quantity :
72102 case str ():
73103 return metric_display_attributes (
@@ -109,39 +139,39 @@ def _curve_display(quantity: ApiQuantity, context: _ParseContext) -> CurveAttrib
109139 assert_never (quantity )
110140
111141
112- def _parse_quantity (quantity : ApiQuantity , context : _ParseContext ) -> QuantityProtocol :
142+ def _parse_quantity (quantity : ApiQuantity , context : _ObjectContext ) -> QuantityProtocol :
113143 match quantity :
114144 case str ():
115- return context .drawn (quantity )
145+ return context .metric (quantity )
116146 case metrics_v1 .Constant ():
117147 return Constant (quantity .value , display = _curve_display (quantity , context ))
118148 case metrics_v2_unstable .LowerWarningOf ():
119149 return ScalarOf (
120- metric = context .scalar (quantity .metric_name ),
150+ metric = context .metric (quantity .metric_name ),
121151 scalar_kind = ScalarKind .LOWER_WARNING ,
122152 )
123153 case metrics_v2_unstable .LowerCriticalOf ():
124154 return ScalarOf (
125- metric = context .scalar (quantity .metric_name ),
155+ metric = context .metric (quantity .metric_name ),
126156 scalar_kind = ScalarKind .LOWER_CRITICAL ,
127157 )
128158 case metrics_v1 .WarningOf ():
129159 return ScalarOf (
130- metric = context .scalar (quantity .metric_name ), scalar_kind = ScalarKind .WARNING
160+ metric = context .metric (quantity .metric_name ), scalar_kind = ScalarKind .WARNING
131161 )
132162 case metrics_v1 .CriticalOf ():
133163 return ScalarOf (
134- metric = context .scalar (quantity .metric_name ), scalar_kind = ScalarKind .CRITICAL
164+ metric = context .metric (quantity .metric_name ), scalar_kind = ScalarKind .CRITICAL
135165 )
136166 case metrics_v1 .MinimumOf ():
137167 return ScalarOf (
138- metric = context .scalar (quantity .metric_name ),
168+ metric = context .metric (quantity .metric_name ),
139169 scalar_kind = ScalarKind .MINIMUM ,
140170 color = parse_color (quantity .color ),
141171 )
142172 case metrics_v1 .MaximumOf ():
143173 return ScalarOf (
144- metric = context .scalar (quantity .metric_name ),
174+ metric = context .metric (quantity .metric_name ),
145175 scalar_kind = ScalarKind .MAXIMUM ,
146176 color = parse_color (quantity .color ),
147177 )
@@ -174,11 +204,8 @@ def _parse_quantity(quantity: ApiQuantity, context: _ParseContext) -> QuantityPr
174204def _parse_bound (bound : int | float | ApiQuantity , context : _ParseContext ) -> Bound | None :
175205 if isinstance (bound , int | float ):
176206 return bound
177- # A bound naming a metric is read from one object; over several matched ones there is none, so
178- # that end is left to auto-scale. A bound of constants alone is read without one.
179- if len (context .services ) > 1 and any (metric_names_in_quantity (bound )):
180- return None
181- return _parse_quantity (bound , context )
207+ object_context = context .object_for (bound )
208+ return None if object_context is None else _parse_quantity (bound , object_context )
182209
183210
184211def _parse_range (
@@ -242,19 +269,26 @@ def _parse_lines(
242269 * ,
243270 inverse : bool ,
244271) -> tuple [Sequence [Stack ], Sequence [Line ], Sequence [Rule ]]:
245- def _curve (q : ApiQuantity ) -> Curve :
246- return build_curve (
247- _parse_quantity (q , context ), context .localizer , context .registered_metrics
248- )
272+ def _curve (quantity : QuantityProtocol ) -> Curve :
273+ return build_curve (quantity , context .localizer , context .registered_metrics )
249274
250- stack_members = [_curve (q ) for q in graph .compound_lines if not is_scalar (q )]
275+ stack_members = [_curve (context . drawn ( q ) ) for q in graph .compound_lines if not is_scalar (q )]
251276 stacks = [Stack (members = stack_members , inverse = inverse )] if stack_members else []
252- lines = [Line (curve = _curve (q ), inverse = inverse ) for q in graph .simple_lines if not is_scalar (q )]
253- rules = [
254- Rule (curve = _curve (q ), inverse = inverse )
255- for q in (* graph .compound_lines , * graph .simple_lines )
256- if is_scalar (q )
277+ lines = [
278+ Line (curve = _curve (context .drawn (q )), inverse = inverse )
279+ for q in graph .simple_lines
280+ if not is_scalar (q )
257281 ]
282+ single_object = context .single_object ()
283+ rules : Sequence [Rule ] = (
284+ ()
285+ if single_object is None
286+ else [
287+ Rule (curve = _curve (_parse_quantity (q , single_object )), inverse = inverse )
288+ for q in (* graph .compound_lines , * graph .simple_lines )
289+ if is_scalar (q )
290+ ]
291+ )
258292 return stacks , lines , rules
259293
260294
0 commit comments