Skip to content

Commit ac8e46e

Browse files
committed
feature: add AggregatedMetrics to support multiple Metrics implementations
Signed-off-by: David Sondermann <[email protected]>
1 parent 9a19dcb commit ac8e46e

File tree

2 files changed

+285
-0
lines changed

2 files changed

+285
-0
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package io.javaoperatorsdk.operator.api.monitoring;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
import java.util.Objects;
6+
7+
import io.fabric8.kubernetes.api.model.HasMetadata;
8+
import io.javaoperatorsdk.operator.api.reconciler.RetryInfo;
9+
import io.javaoperatorsdk.operator.processing.Controller;
10+
import io.javaoperatorsdk.operator.processing.event.Event;
11+
import io.javaoperatorsdk.operator.processing.event.ResourceID;
12+
13+
/**
14+
* An aggregated implementation of the {@link Metrics} interface that delegates method calls to a
15+
* collection of {@link Metrics} instances using the composite pattern.
16+
*
17+
* <p>This class allows multiple metrics providers to be combined into a single metrics instance,
18+
* enabling simultaneous collection of metrics data by different monitoring systems or providers.
19+
* All method calls are delegated to each metrics instance in the list in the order they were
20+
* provided to the constructor.
21+
*
22+
* <p><strong>Important:</strong> The {@link #timeControllerExecution(ControllerExecution)} method
23+
* is handled specially - it is only invoked on the first metrics instance in the list, since it's
24+
* not an idempotent operation and can only be executed once. The controller execution cannot be
25+
* repeated multiple times as it would produce side effects and potentially inconsistent results.
26+
*
27+
* <p>All other methods are called on every metrics instance in the list, preserving the order of
28+
* execution as specified in the constructor.
29+
*
30+
* @see Metrics
31+
*/
32+
public final class AggregatedMetrics implements Metrics {
33+
34+
private final List<Metrics> metricsList;
35+
36+
/**
37+
* Creates a new AggregatedMetrics instance that will delegate method calls to the provided list
38+
* of metrics instances.
39+
*
40+
* @param metricsList the list of metrics instances to delegate to; must not be null and must
41+
* contain at least one metrics instance
42+
* @throws NullPointerException if metricsList is null
43+
* @throws IllegalArgumentException if metricsList is empty
44+
*/
45+
public AggregatedMetrics(List<Metrics> metricsList) {
46+
Objects.requireNonNull(metricsList, "metricsList must not be null");
47+
if (metricsList.isEmpty()) {
48+
throw new IllegalArgumentException("metricsList must contain at least one Metrics instance");
49+
}
50+
this.metricsList = List.copyOf(metricsList);
51+
}
52+
53+
@Override
54+
public void controllerRegistered(Controller<? extends HasMetadata> controller) {
55+
metricsList.forEach(metrics -> metrics.controllerRegistered(controller));
56+
}
57+
58+
@Override
59+
public void receivedEvent(Event event, Map<String, Object> metadata) {
60+
metricsList.forEach(metrics -> metrics.receivedEvent(event, metadata));
61+
}
62+
63+
@Override
64+
public void reconcileCustomResource(
65+
HasMetadata resource, RetryInfo retryInfo, Map<String, Object> metadata) {
66+
metricsList.forEach(metrics -> metrics.reconcileCustomResource(resource, retryInfo, metadata));
67+
}
68+
69+
@Override
70+
public void failedReconciliation(
71+
HasMetadata resource, Exception exception, Map<String, Object> metadata) {
72+
metricsList.forEach(metrics -> metrics.failedReconciliation(resource, exception, metadata));
73+
}
74+
75+
@Override
76+
public void reconciliationExecutionStarted(HasMetadata resource, Map<String, Object> metadata) {
77+
metricsList.forEach(metrics -> metrics.reconciliationExecutionStarted(resource, metadata));
78+
}
79+
80+
@Override
81+
public void reconciliationExecutionFinished(HasMetadata resource, Map<String, Object> metadata) {
82+
metricsList.forEach(metrics -> metrics.reconciliationExecutionFinished(resource, metadata));
83+
}
84+
85+
@Override
86+
public void cleanupDoneFor(ResourceID resourceID, Map<String, Object> metadata) {
87+
metricsList.forEach(metrics -> metrics.cleanupDoneFor(resourceID, metadata));
88+
}
89+
90+
@Override
91+
public void finishedReconciliation(HasMetadata resource, Map<String, Object> metadata) {
92+
metricsList.forEach(metrics -> metrics.finishedReconciliation(resource, metadata));
93+
}
94+
95+
@Override
96+
public <T> T timeControllerExecution(ControllerExecution<T> execution) throws Exception {
97+
return metricsList.get(0).timeControllerExecution(execution);
98+
}
99+
100+
@Override
101+
public <T extends Map<?, ?>> T monitorSizeOf(T map, String name) {
102+
metricsList.forEach(metrics -> metrics.monitorSizeOf(map, name));
103+
return map;
104+
}
105+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package io.javaoperatorsdk.operator.api.monitoring;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
6+
import org.junit.jupiter.api.Test;
7+
8+
import io.fabric8.kubernetes.api.model.HasMetadata;
9+
import io.javaoperatorsdk.operator.api.reconciler.RetryInfo;
10+
import io.javaoperatorsdk.operator.processing.Controller;
11+
import io.javaoperatorsdk.operator.processing.event.Event;
12+
import io.javaoperatorsdk.operator.processing.event.ResourceID;
13+
14+
import static org.assertj.core.api.Assertions.assertThat;
15+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
16+
import static org.mockito.ArgumentMatchers.any;
17+
import static org.mockito.Mockito.*;
18+
19+
class AggregatedMetricsTest {
20+
21+
private final Metrics metrics1 = mock();
22+
private final Metrics metrics2 = mock();
23+
private final Metrics metrics3 = mock();
24+
private final Controller<HasMetadata> controller = mock();
25+
private final Event event = mock();
26+
private final HasMetadata resource = mock();
27+
private final RetryInfo retryInfo = mock();
28+
private final ResourceID resourceID = mock();
29+
private final Metrics.ControllerExecution<String> controllerExecution = mock();
30+
31+
private final Map<String, Object> metadata = Map.of("kind", "TestResource");
32+
private final AggregatedMetrics aggregatedMetrics =
33+
new AggregatedMetrics(List.of(metrics1, metrics2, metrics3));
34+
35+
@Test
36+
void constructor_shouldThrowNullPointerExceptionWhenMetricsListIsNull() {
37+
assertThatThrownBy(() -> new AggregatedMetrics(null))
38+
.isInstanceOf(NullPointerException.class)
39+
.hasMessage("metricsList must not be null");
40+
}
41+
42+
@Test
43+
void constructor_shouldThrowIllegalArgumentExceptionWhenMetricsListIsEmpty() {
44+
assertThatThrownBy(() -> new AggregatedMetrics(List.of()))
45+
.isInstanceOf(IllegalArgumentException.class)
46+
.hasMessage("metricsList must contain at least one Metrics instance");
47+
}
48+
49+
@Test
50+
void controllerRegistered_shouldDelegateToAllMetricsInOrder() {
51+
aggregatedMetrics.controllerRegistered(controller);
52+
53+
final var inOrder = inOrder(metrics1, metrics2, metrics3);
54+
inOrder.verify(metrics1).controllerRegistered(controller);
55+
inOrder.verify(metrics2).controllerRegistered(controller);
56+
inOrder.verify(metrics3).controllerRegistered(controller);
57+
verifyNoMoreInteractions(metrics1, metrics2, metrics3);
58+
}
59+
60+
@Test
61+
void receivedEvent_shouldDelegateToAllMetricsInOrder() {
62+
aggregatedMetrics.receivedEvent(event, metadata);
63+
64+
final var inOrder = inOrder(metrics1, metrics2, metrics3);
65+
inOrder.verify(metrics1).receivedEvent(event, metadata);
66+
inOrder.verify(metrics2).receivedEvent(event, metadata);
67+
inOrder.verify(metrics3).receivedEvent(event, metadata);
68+
verifyNoMoreInteractions(metrics1, metrics2, metrics3);
69+
}
70+
71+
@Test
72+
void reconcileCustomResource_shouldDelegateToAllMetricsInOrder() {
73+
aggregatedMetrics.reconcileCustomResource(resource, retryInfo, metadata);
74+
75+
final var inOrder = inOrder(metrics1, metrics2, metrics3);
76+
inOrder.verify(metrics1).reconcileCustomResource(resource, retryInfo, metadata);
77+
inOrder.verify(metrics2).reconcileCustomResource(resource, retryInfo, metadata);
78+
inOrder.verify(metrics3).reconcileCustomResource(resource, retryInfo, metadata);
79+
verifyNoMoreInteractions(metrics1, metrics2, metrics3);
80+
}
81+
82+
@Test
83+
void failedReconciliation_shouldDelegateToAllMetricsInOrder() {
84+
final var exception = new RuntimeException("Test exception");
85+
86+
aggregatedMetrics.failedReconciliation(resource, exception, metadata);
87+
88+
final var inOrder = inOrder(metrics1, metrics2, metrics3);
89+
inOrder.verify(metrics1).failedReconciliation(resource, exception, metadata);
90+
inOrder.verify(metrics2).failedReconciliation(resource, exception, metadata);
91+
inOrder.verify(metrics3).failedReconciliation(resource, exception, metadata);
92+
verifyNoMoreInteractions(metrics1, metrics2, metrics3);
93+
}
94+
95+
@Test
96+
void reconciliationExecutionStarted_shouldDelegateToAllMetricsInOrder() {
97+
aggregatedMetrics.reconciliationExecutionStarted(resource, metadata);
98+
99+
final var inOrder = inOrder(metrics1, metrics2, metrics3);
100+
inOrder.verify(metrics1).reconciliationExecutionStarted(resource, metadata);
101+
inOrder.verify(metrics2).reconciliationExecutionStarted(resource, metadata);
102+
inOrder.verify(metrics3).reconciliationExecutionStarted(resource, metadata);
103+
verifyNoMoreInteractions(metrics1, metrics2, metrics3);
104+
}
105+
106+
@Test
107+
void reconciliationExecutionFinished_shouldDelegateToAllMetricsInOrder() {
108+
aggregatedMetrics.reconciliationExecutionFinished(resource, metadata);
109+
110+
final var inOrder = inOrder(metrics1, metrics2, metrics3);
111+
inOrder.verify(metrics1).reconciliationExecutionFinished(resource, metadata);
112+
inOrder.verify(metrics2).reconciliationExecutionFinished(resource, metadata);
113+
inOrder.verify(metrics3).reconciliationExecutionFinished(resource, metadata);
114+
verifyNoMoreInteractions(metrics1, metrics2, metrics3);
115+
}
116+
117+
@Test
118+
void cleanupDoneFor_shouldDelegateToAllMetricsInOrder() {
119+
aggregatedMetrics.cleanupDoneFor(resourceID, metadata);
120+
121+
final var inOrder = inOrder(metrics1, metrics2, metrics3);
122+
inOrder.verify(metrics1).cleanupDoneFor(resourceID, metadata);
123+
inOrder.verify(metrics2).cleanupDoneFor(resourceID, metadata);
124+
inOrder.verify(metrics3).cleanupDoneFor(resourceID, metadata);
125+
verifyNoMoreInteractions(metrics1, metrics2, metrics3);
126+
}
127+
128+
@Test
129+
void finishedReconciliation_shouldDelegateToAllMetricsInOrder() {
130+
aggregatedMetrics.finishedReconciliation(resource, metadata);
131+
132+
final var inOrder = inOrder(metrics1, metrics2, metrics3);
133+
inOrder.verify(metrics1).finishedReconciliation(resource, metadata);
134+
inOrder.verify(metrics2).finishedReconciliation(resource, metadata);
135+
inOrder.verify(metrics3).finishedReconciliation(resource, metadata);
136+
verifyNoMoreInteractions(metrics1, metrics2, metrics3);
137+
}
138+
139+
@Test
140+
void timeControllerExecution_shouldOnlyDelegateToFirstMetrics() throws Exception {
141+
final var expectedResult = "execution result";
142+
when(metrics1.timeControllerExecution(controllerExecution)).thenReturn(expectedResult);
143+
144+
final var result = aggregatedMetrics.timeControllerExecution(controllerExecution);
145+
146+
assertThat(result).isEqualTo(expectedResult);
147+
verify(metrics1).timeControllerExecution(controllerExecution);
148+
verify(metrics2, never()).timeControllerExecution(any());
149+
verify(metrics3, never()).timeControllerExecution(any());
150+
verifyNoMoreInteractions(metrics1, metrics2, metrics3);
151+
}
152+
153+
@Test
154+
void timeControllerExecution_shouldPropagateException() throws Exception {
155+
final var expectedException = new RuntimeException("Controller execution failed");
156+
when(metrics1.timeControllerExecution(controllerExecution)).thenThrow(expectedException);
157+
158+
assertThatThrownBy(() -> aggregatedMetrics.timeControllerExecution(controllerExecution))
159+
.isSameAs(expectedException);
160+
161+
verify(metrics1).timeControllerExecution(controllerExecution);
162+
verify(metrics2, never()).timeControllerExecution(any());
163+
verify(metrics3, never()).timeControllerExecution(any());
164+
verifyNoMoreInteractions(metrics1, metrics2, metrics3);
165+
}
166+
167+
@Test
168+
void monitorSizeOf_shouldDelegateToAllMetricsInOrderAndReturnOriginalMap() {
169+
final var testMap = Map.of("key1", "value1");
170+
final var mapName = "testMap";
171+
172+
final var result = aggregatedMetrics.monitorSizeOf(testMap, mapName);
173+
174+
assertThat(result).isSameAs(testMap);
175+
verify(metrics1).monitorSizeOf(testMap, mapName);
176+
verify(metrics2).monitorSizeOf(testMap, mapName);
177+
verify(metrics3).monitorSizeOf(testMap, mapName);
178+
verifyNoMoreInteractions(metrics1, metrics2, metrics3);
179+
}
180+
}

0 commit comments

Comments
 (0)