Skip to content

Commit e80070b

Browse files
ottermataJenkins
authored andcommitted
graphing: leave a combined graph's axis to auto-scale over several objects
A range bound that is a quantity names one object's thresholds, so over several matched ones it resolved against whichever came first. Drop it and keep only the numeric ends, as the legacy combined path does. CMK-37904 Change-Id: Ib2ab00b0a953a2a2e4f3c1486b5a2b497c61fbc5 JIRA-Ref: CMK-37904
1 parent 96c4091 commit e80070b

3 files changed

Lines changed: 155 additions & 10 deletions

File tree

packages/cmk-graphing-engine/cmk/graphing_engine/_api_plugins.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def is_scalar(quantity: ApiQuantity) -> bool:
6868
assert_never(quantity)
6969

7070

71-
def _metric_names_in_quantity(quantity: ApiQuantity) -> Iterable[MetricName]:
71+
def metric_names_in_quantity(quantity: ApiQuantity) -> Iterable[MetricName]:
7272
match quantity:
7373
case str():
7474
yield MetricName(quantity)
@@ -90,7 +90,7 @@ def _metric_names_in_quantity(quantity: ApiQuantity) -> Iterable[MetricName]:
9090
| metrics_v1.Fraction()
9191
):
9292
for operand in operands_of(quantity):
93-
yield from _metric_names_in_quantity(operand)
93+
yield from metric_names_in_quantity(operand)
9494
case _:
9595
assert_never(quantity)
9696

@@ -103,6 +103,6 @@ def drawn_metric_names_of_graph(
103103
name
104104
for quantity in (*graph.compound_lines, *graph.simple_lines)
105105
if not is_scalar(quantity)
106-
for name in _metric_names_in_quantity(quantity)
106+
for name in metric_names_in_quantity(quantity)
107107
}
108108
)

packages/cmk-graphing-engine/cmk/graphing_engine/_from_api.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from cmk.graphing.v2_unstable import graphs as graphs_v2_unstable
1313
from cmk.graphing.v2_unstable import metrics as metrics_v2_unstable
1414

15-
from ._api_plugins import ApiQuantity, is_scalar, operands_of
15+
from ._api_plugins import ApiQuantity, is_scalar, metric_names_in_quantity, operands_of
1616
from ._display import (
1717
FALLBACK_ATTRIBUTES,
1818
metric_display_attributes,
@@ -171,9 +171,13 @@ def _parse_quantity(quantity: ApiQuantity, context: _ParseContext) -> QuantityPr
171171
assert_never(quantity)
172172

173173

174-
def _parse_bound(bound: int | float | ApiQuantity, context: _ParseContext) -> Bound:
174+
def _parse_bound(bound: int | float | ApiQuantity, context: _ParseContext) -> Bound | None:
175175
if isinstance(bound, int | float):
176176
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
177181
return _parse_quantity(bound, context)
178182

179183

@@ -183,10 +187,11 @@ def _parse_range(
183187
) -> MinimalRange | None:
184188
if graph.minimal_range is None:
185189
return None
186-
return MinimalRange(
187-
lower=_parse_bound(graph.minimal_range.lower, context),
188-
upper=_parse_bound(graph.minimal_range.upper, context),
189-
)
190+
lower = _parse_bound(graph.minimal_range.lower, context)
191+
upper = _parse_bound(graph.minimal_range.upper, context)
192+
if lower is None and upper is None:
193+
return None
194+
return MinimalRange(lower=lower, upper=upper)
190195

191196

192197
def _widest_bound(
@@ -196,7 +201,8 @@ def _widest_bound(
196201
) -> Bound | None:
197202
if isinstance(of_upper, int | float) and isinstance(of_lower, int | float):
198203
return pick(of_upper, of_lower)
199-
# Two quantity bounds cannot be compared before they are evaluated, so the upper half's wins.
204+
# Two quantity bounds cannot be compared before they are evaluated, so the upper half's wins; a
205+
# half whose end was dropped has none, and then the other half's end is the range.
200206
return of_lower if of_upper is None else of_upper
201207

202208

packages/cmk-graphing-engine/tests/test_matching.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
build_curve,
1515
build_matched_graphs,
1616
ConsolidationFunction,
17+
Constant,
1718
evaluate_graphs,
1819
EvaluatedGraph,
1920
FetchedData,
@@ -22,6 +23,7 @@
2223
Line,
2324
MetricName,
2425
MetricProtocol,
26+
MinimalRange,
2527
parse_graph_from_api,
2628
PerformanceData,
2729
QuantityProtocol,
@@ -761,3 +763,140 @@ def test_build_matched_graphs_matches_via_a_service_without_the_conflicting_metr
761763

762764
# h2 carries the conflicting metric, but h1 does not, so the plugin still matches via h1.
763765
assert "cpu" in {d.name for d in discovered}
766+
767+
768+
def _graph_with_a_quantity_bound() -> graphs_v1.Graph:
769+
return graphs_v1.Graph(
770+
name="cpu",
771+
title=Title("CPU"),
772+
minimal_range=graphs_v1.MinimalRange(
773+
0, metrics_v1.MaximumOf("cpu_user", metrics_v1.Color.GREEN)
774+
),
775+
simple_lines=["cpu_user"],
776+
)
777+
778+
779+
def test_build_matched_graphs_keeps_a_quantity_range_bound_for_one_service() -> None:
780+
service = _service()
781+
cpu_user = MetricName("cpu_user")
782+
fetch_data = _FakeRRDFetchData(
783+
performance_response={service: _perf_data(_perf(cpu_user, maximum=100.0))}
784+
)
785+
786+
[discovered] = _discover([_graph_with_a_quantity_bound()], fetch_data=fetch_data)
787+
788+
assert discovered.vertical_range is not None
789+
assert discovered.vertical_range.lower == 0
790+
upper = discovered.vertical_range.upper
791+
assert isinstance(upper, ScalarOf)
792+
assert upper.metric == _rrd(cpu_user)
793+
assert upper.scalar_kind is ScalarKind.MAXIMUM
794+
795+
796+
def test_build_matched_graphs_drops_a_quantity_range_bound_for_multiple_services() -> None:
797+
h1, h2 = _services()
798+
cpu_user = MetricName("cpu_user")
799+
fetch_data = _FakeRRDFetchData(
800+
performance_response={
801+
h1: _perf_data(_perf(cpu_user, maximum=100.0)),
802+
h2: _perf_data(_perf(cpu_user, maximum=200.0)),
803+
}
804+
)
805+
806+
[discovered] = _discover_combined([_graph_with_a_quantity_bound()], fetch_data=fetch_data)
807+
808+
# The numeric end is kept; the threshold end belongs to no one matched object, so it is dropped
809+
# rather than resolved against an arbitrary one.
810+
assert discovered.vertical_range == MinimalRange(lower=0, upper=None)
811+
812+
813+
def test_build_matched_graphs_drops_a_wholly_quantity_range_for_multiple_services() -> None:
814+
h1, h2 = _services()
815+
cpu_user = MetricName("cpu_user")
816+
plugin = graphs_v1.Graph(
817+
name="cpu",
818+
title=Title("CPU"),
819+
minimal_range=graphs_v1.MinimalRange(
820+
metrics_v1.MinimumOf("cpu_user", metrics_v1.Color.GREEN),
821+
metrics_v1.MaximumOf("cpu_user", metrics_v1.Color.GREEN),
822+
),
823+
simple_lines=["cpu_user"],
824+
)
825+
fetch_data = _FakeRRDFetchData(
826+
performance_response={
827+
h1: _perf_data(_perf(cpu_user, minimum=0.0, maximum=100.0)),
828+
h2: _perf_data(_perf(cpu_user, minimum=0.0, maximum=200.0)),
829+
}
830+
)
831+
832+
[discovered] = _discover_combined([plugin], fetch_data=fetch_data)
833+
834+
# Neither end survives, so there is no range rather than one holding nothing.
835+
assert discovered.vertical_range is None
836+
837+
838+
def test_build_matched_graphs_keeps_a_constant_range_bound_for_multiple_services() -> None:
839+
h1, h2 = _services()
840+
cpu_user = MetricName("cpu_user")
841+
plugin = graphs_v1.Graph(
842+
name="cpu",
843+
title=Title("CPU"),
844+
minimal_range=graphs_v1.MinimalRange(
845+
0,
846+
metrics_v1.Constant(
847+
Title("Ceiling"),
848+
metrics_v1.Unit(metrics_v1.DecimalNotation("")),
849+
metrics_v1.Color.GRAY,
850+
100.0,
851+
),
852+
),
853+
simple_lines=["cpu_user"],
854+
)
855+
fetch_data = _FakeRRDFetchData(
856+
performance_response={h1: _perf_data(_perf(cpu_user)), h2: _perf_data(_perf(cpu_user))}
857+
)
858+
859+
[discovered] = _discover_combined([plugin], fetch_data=fetch_data)
860+
861+
# A bound of constants alone names no metric, so it needs no object and survives.
862+
assert discovered.vertical_range is not None
863+
assert discovered.vertical_range.lower == 0
864+
upper = discovered.vertical_range.upper
865+
assert isinstance(upper, Constant)
866+
assert upper.value == 100.0
867+
868+
869+
def test_build_matched_graphs_lets_a_kept_end_win_over_a_dropped_one_when_bidirectional() -> None:
870+
h1, h2 = _services()
871+
cpu_user = MetricName("cpu_user")
872+
cpu_system = MetricName("cpu_system")
873+
plugin = graphs_v1.Bidirectional(
874+
name="cpu",
875+
title=Title("CPU"),
876+
upper=graphs_v1.Graph(
877+
name="upper",
878+
title=Title("Upper"),
879+
minimal_range=graphs_v1.MinimalRange(
880+
0, metrics_v1.MaximumOf("cpu_user", metrics_v1.Color.GRAY)
881+
),
882+
simple_lines=["cpu_user"],
883+
),
884+
lower=graphs_v1.Graph(
885+
name="lower",
886+
title=Title("Lower"),
887+
minimal_range=graphs_v1.MinimalRange(0, 50),
888+
simple_lines=["cpu_system"],
889+
),
890+
)
891+
fetch_data = _FakeRRDFetchData(
892+
performance_response={
893+
h1: _perf_data(_perf(cpu_user, maximum=100.0), _perf(cpu_system)),
894+
h2: _perf_data(_perf(cpu_user, maximum=200.0), _perf(cpu_system)),
895+
}
896+
)
897+
898+
[discovered] = _discover_combined([plugin], fetch_data=fetch_data)
899+
900+
# The upper half's threshold end is dropped, so the lower half's numeric end is the range - where
901+
# a threshold resolved against one arbitrary object would have masked it.
902+
assert discovered.vertical_range == MinimalRange(lower=0, upper=50)

0 commit comments

Comments
 (0)