Skip to content

Commit e80267d

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

File tree

3 files changed

+292
-0
lines changed

3 files changed

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

0 commit comments

Comments
 (0)