Skip to content

Commit f36afa7

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

File tree

3 files changed

+267
-0
lines changed

3 files changed

+267
-0
lines changed

micrometer-support/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@
5050
<artifactId>kubernetes-httpclient-vertx</artifactId>
5151
<scope>test</scope>
5252
</dependency>
53+
<dependency>
54+
<groupId>org.mockito</groupId>
55+
<artifactId>mockito-core</artifactId>
56+
<scope>test</scope>
57+
</dependency>
5358
</dependencies>
5459

5560
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package io.javaoperatorsdk.operator.monitoring.micrometer;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
6+
import io.fabric8.kubernetes.api.model.HasMetadata;
7+
import io.javaoperatorsdk.operator.api.monitoring.Metrics;
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 record AggregatedMetrics(List<Metrics> metricsList) implements Metrics {
33+
34+
/**
35+
* Creates a new AggregatedMetrics instance that will delegate method calls to the provided list
36+
* of metrics instances.
37+
*
38+
* @param metricsList the list of metrics instances to delegate to; must not be null and should
39+
* contain at least one metrics instance for meaningful operation
40+
*/
41+
public AggregatedMetrics {}
42+
43+
@Override
44+
public void controllerRegistered(Controller<? extends HasMetadata> controller) {
45+
metricsList.forEach(metrics -> metrics.controllerRegistered(controller));
46+
}
47+
48+
@Override
49+
public void receivedEvent(Event event, Map<String, Object> metadata) {
50+
metricsList.forEach(metrics -> metrics.receivedEvent(event, metadata));
51+
}
52+
53+
@Override
54+
public void reconcileCustomResource(
55+
HasMetadata resource, RetryInfo retryInfo, Map<String, Object> metadata) {
56+
metricsList.forEach(metrics -> metrics.reconcileCustomResource(resource, retryInfo, metadata));
57+
}
58+
59+
@Override
60+
public void failedReconciliation(
61+
HasMetadata resource, Exception exception, Map<String, Object> metadata) {
62+
metricsList.forEach(metrics -> metrics.failedReconciliation(resource, exception, metadata));
63+
}
64+
65+
@Override
66+
public void reconciliationExecutionStarted(HasMetadata resource, Map<String, Object> metadata) {
67+
metricsList.forEach(metrics -> metrics.reconciliationExecutionStarted(resource, metadata));
68+
}
69+
70+
@Override
71+
public void reconciliationExecutionFinished(HasMetadata resource, Map<String, Object> metadata) {
72+
metricsList.forEach(metrics -> metrics.reconciliationExecutionFinished(resource, metadata));
73+
}
74+
75+
@Override
76+
public void cleanupDoneFor(ResourceID resourceID, Map<String, Object> metadata) {
77+
metricsList.forEach(metrics -> metrics.cleanupDoneFor(resourceID, metadata));
78+
}
79+
80+
@Override
81+
public void finishedReconciliation(HasMetadata resource, Map<String, Object> metadata) {
82+
metricsList.forEach(metrics -> metrics.finishedReconciliation(resource, metadata));
83+
}
84+
85+
@Override
86+
public <T> T timeControllerExecution(ControllerExecution<T> execution) throws Exception {
87+
return metricsList.get(0).timeControllerExecution(execution);
88+
}
89+
90+
@Override
91+
public <T extends Map<?, ?>> T monitorSizeOf(T map, String name) {
92+
metricsList.forEach(metrics -> metrics.monitorSizeOf(map, name));
93+
return map;
94+
}
95+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package io.javaoperatorsdk.operator.monitoring.micrometer;
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.monitoring.Metrics;
10+
import io.javaoperatorsdk.operator.api.reconciler.RetryInfo;
11+
import io.javaoperatorsdk.operator.processing.Controller;
12+
import io.javaoperatorsdk.operator.processing.event.Event;
13+
import io.javaoperatorsdk.operator.processing.event.ResourceID;
14+
15+
import static org.assertj.core.api.Assertions.assertThat;
16+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
17+
import static org.mockito.ArgumentMatchers.any;
18+
import static org.mockito.Mockito.*;
19+
20+
class AggregatedMetricsTest {
21+
22+
private final Metrics metrics1 = mock();
23+
private final Metrics metrics2 = mock();
24+
private final Metrics metrics3 = mock();
25+
private final Controller<HasMetadata> controller = mock();
26+
private final Event event = mock();
27+
private final HasMetadata resource = mock();
28+
private final RetryInfo retryInfo = mock();
29+
private final ResourceID resourceID = mock();
30+
private final Metrics.ControllerExecution<String> controllerExecution = mock();
31+
32+
private final Map<String, Object> metadata = Map.of("kind", "TestResource");
33+
private final AggregatedMetrics aggregatedMetrics =
34+
new AggregatedMetrics(List.of(metrics1, metrics2, metrics3));
35+
36+
@Test
37+
void controllerRegistered_shouldDelegateToAllMetricsInOrder() {
38+
aggregatedMetrics.controllerRegistered(controller);
39+
40+
final var inOrder = inOrder(metrics1, metrics2, metrics3);
41+
inOrder.verify(metrics1).controllerRegistered(controller);
42+
inOrder.verify(metrics2).controllerRegistered(controller);
43+
inOrder.verify(metrics3).controllerRegistered(controller);
44+
verifyNoMoreInteractions(metrics1, metrics2, metrics3);
45+
}
46+
47+
@Test
48+
void receivedEvent_shouldDelegateToAllMetricsInOrder() {
49+
aggregatedMetrics.receivedEvent(event, metadata);
50+
51+
final var inOrder = inOrder(metrics1, metrics2, metrics3);
52+
inOrder.verify(metrics1).receivedEvent(event, metadata);
53+
inOrder.verify(metrics2).receivedEvent(event, metadata);
54+
inOrder.verify(metrics3).receivedEvent(event, metadata);
55+
verifyNoMoreInteractions(metrics1, metrics2, metrics3);
56+
}
57+
58+
@Test
59+
void reconcileCustomResource_shouldDelegateToAllMetricsInOrder() {
60+
aggregatedMetrics.reconcileCustomResource(resource, retryInfo, metadata);
61+
62+
final var inOrder = inOrder(metrics1, metrics2, metrics3);
63+
inOrder.verify(metrics1).reconcileCustomResource(resource, retryInfo, metadata);
64+
inOrder.verify(metrics2).reconcileCustomResource(resource, retryInfo, metadata);
65+
inOrder.verify(metrics3).reconcileCustomResource(resource, retryInfo, metadata);
66+
verifyNoMoreInteractions(metrics1, metrics2, metrics3);
67+
}
68+
69+
@Test
70+
void failedReconciliation_shouldDelegateToAllMetricsInOrder() {
71+
final var exception = new RuntimeException("Test exception");
72+
73+
aggregatedMetrics.failedReconciliation(resource, exception, metadata);
74+
75+
final var inOrder = inOrder(metrics1, metrics2, metrics3);
76+
inOrder.verify(metrics1).failedReconciliation(resource, exception, metadata);
77+
inOrder.verify(metrics2).failedReconciliation(resource, exception, metadata);
78+
inOrder.verify(metrics3).failedReconciliation(resource, exception, metadata);
79+
verifyNoMoreInteractions(metrics1, metrics2, metrics3);
80+
}
81+
82+
@Test
83+
void reconciliationExecutionStarted_shouldDelegateToAllMetricsInOrder() {
84+
aggregatedMetrics.reconciliationExecutionStarted(resource, metadata);
85+
86+
final var inOrder = inOrder(metrics1, metrics2, metrics3);
87+
inOrder.verify(metrics1).reconciliationExecutionStarted(resource, metadata);
88+
inOrder.verify(metrics2).reconciliationExecutionStarted(resource, metadata);
89+
inOrder.verify(metrics3).reconciliationExecutionStarted(resource, metadata);
90+
verifyNoMoreInteractions(metrics1, metrics2, metrics3);
91+
}
92+
93+
@Test
94+
void reconciliationExecutionFinished_shouldDelegateToAllMetricsInOrder() {
95+
aggregatedMetrics.reconciliationExecutionFinished(resource, metadata);
96+
97+
final var inOrder = inOrder(metrics1, metrics2, metrics3);
98+
inOrder.verify(metrics1).reconciliationExecutionFinished(resource, metadata);
99+
inOrder.verify(metrics2).reconciliationExecutionFinished(resource, metadata);
100+
inOrder.verify(metrics3).reconciliationExecutionFinished(resource, metadata);
101+
verifyNoMoreInteractions(metrics1, metrics2, metrics3);
102+
}
103+
104+
@Test
105+
void cleanupDoneFor_shouldDelegateToAllMetricsInOrder() {
106+
aggregatedMetrics.cleanupDoneFor(resourceID, metadata);
107+
108+
final var inOrder = inOrder(metrics1, metrics2, metrics3);
109+
inOrder.verify(metrics1).cleanupDoneFor(resourceID, metadata);
110+
inOrder.verify(metrics2).cleanupDoneFor(resourceID, metadata);
111+
inOrder.verify(metrics3).cleanupDoneFor(resourceID, metadata);
112+
verifyNoMoreInteractions(metrics1, metrics2, metrics3);
113+
}
114+
115+
@Test
116+
void finishedReconciliation_shouldDelegateToAllMetricsInOrder() {
117+
aggregatedMetrics.finishedReconciliation(resource, metadata);
118+
119+
final var inOrder = inOrder(metrics1, metrics2, metrics3);
120+
inOrder.verify(metrics1).finishedReconciliation(resource, metadata);
121+
inOrder.verify(metrics2).finishedReconciliation(resource, metadata);
122+
inOrder.verify(metrics3).finishedReconciliation(resource, metadata);
123+
verifyNoMoreInteractions(metrics1, metrics2, metrics3);
124+
}
125+
126+
@Test
127+
void timeControllerExecution_shouldOnlyDelegateToFirstMetrics() throws Exception {
128+
final var expectedResult = "execution result";
129+
when(metrics1.timeControllerExecution(controllerExecution)).thenReturn(expectedResult);
130+
131+
final var result = aggregatedMetrics.timeControllerExecution(controllerExecution);
132+
133+
assertThat(result).isEqualTo(expectedResult);
134+
verify(metrics1).timeControllerExecution(controllerExecution);
135+
verify(metrics2, never()).timeControllerExecution(any());
136+
verify(metrics3, never()).timeControllerExecution(any());
137+
verifyNoMoreInteractions(metrics1, metrics2, metrics3);
138+
}
139+
140+
@Test
141+
void timeControllerExecution_shouldPropagateException() throws Exception {
142+
final var expectedException = new RuntimeException("Controller execution failed");
143+
when(metrics1.timeControllerExecution(controllerExecution)).thenThrow(expectedException);
144+
145+
assertThatThrownBy(() -> aggregatedMetrics.timeControllerExecution(controllerExecution))
146+
.isSameAs(expectedException);
147+
148+
verify(metrics1).timeControllerExecution(controllerExecution);
149+
verify(metrics2, never()).timeControllerExecution(any());
150+
verify(metrics3, never()).timeControllerExecution(any());
151+
verifyNoMoreInteractions(metrics1, metrics2, metrics3);
152+
}
153+
154+
@Test
155+
void monitorSizeOf_shouldDelegateToAllMetricsInOrderAndReturnOriginalMap() {
156+
final var testMap = Map.of("key1", "value1");
157+
final var mapName = "testMap";
158+
159+
final var result = aggregatedMetrics.monitorSizeOf(testMap, mapName);
160+
161+
assertThat(result).isSameAs(testMap);
162+
verify(metrics1).monitorSizeOf(testMap, mapName);
163+
verify(metrics2).monitorSizeOf(testMap, mapName);
164+
verify(metrics3).monitorSizeOf(testMap, mapName);
165+
verifyNoMoreInteractions(metrics1, metrics2, metrics3);
166+
}
167+
}

0 commit comments

Comments
 (0)