Skip to content

Commit

Permalink
Use a mocked OciExtension using Proxy that will inject a mocked Monit…
Browse files Browse the repository at this point in the history
…oring interface.

Additional Note:
This fix also includes a resolution to helidon-io#7739 that is caused by the Monitoring.postMetricData() to be called before the test metrics are registered.
  • Loading branch information
klustria committed Mar 14, 2024
1 parent abc804d commit 7a2ff81
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 43 deletions.
5 changes: 0 additions & 5 deletions integrations/oci/metrics/cdi/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,6 @@
<artifactId>hamcrest-all</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
*/
package io.helidon.integrations.oci.metrics.cdi;

import java.lang.annotation.Annotation;
import java.lang.reflect.Proxy;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import io.helidon.config.Config;
import io.helidon.integrations.oci.metrics.OciMetricsSupport;
import io.helidon.integrations.oci.sdk.cdi.OciExtension;
import io.helidon.metrics.api.Counter;
import io.helidon.metrics.api.Meter;
import io.helidon.metrics.api.MeterRegistry;
Expand All @@ -40,43 +42,45 @@
import com.oracle.bmc.monitoring.requests.PostMetricDataRequest;
import com.oracle.bmc.monitoring.responses.PostMetricDataResponse;

import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Initialized;
import jakarta.enterprise.event.Observes;

import jakarta.enterprise.inject.Default;
import jakarta.enterprise.inject.spi.AfterBeanDiscovery;
import jakarta.enterprise.inject.spi.Extension;
import jakarta.enterprise.inject.spi.InjectionPoint;
import jakarta.enterprise.inject.spi.ProcessInjectionPoint;
import jakarta.enterprise.inject.spi.configurator.BeanConfigurator;
import org.glassfish.jersey.ext.cdi1x.internal.CdiComponentProvider;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import static jakarta.interceptor.Interceptor.Priority.LIBRARY_BEFORE;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@HelidonTest(resetPerTest = true)
@DisableDiscovery
// Add bean that will simulate oci metrics posting
@AddBean(OciMetricsCdiExtensionTest.MockOciMetricsBean.class)
// Helidon MP Extensions
@AddExtension(ServerCdiExtension.class)
@AddExtension(JaxRsCdiExtension.class)
@AddExtension(ConfigCdiExtension.class)
@AddExtension(OciExtension.class)
@AddExtension(CdiComponentProvider.class)
// Add an extension that will simulate a mocked OciExtension that will inject a mocked Monitoring object
@AddExtension(OciMetricsCdiExtensionTest.MockOciMonitoringExtension.class)
// ConfigSources
@AddConfig(key = "ocimetrics.compartmentId",
value = OciMetricsCdiExtensionTest.MetricDataDetailsOCIParams.compartmentId)
@AddConfig(key = "ocimetrics.namespace",
value = OciMetricsCdiExtensionTest.MetricDataDetailsOCIParams.namespace)
@AddConfig(key = "ocimetrics.resourceGroup",
value = OciMetricsCdiExtensionTest.MetricDataDetailsOCIParams.resourceGroup)
@AddConfig(key = "ocimetrics.initialDelay", value = "1")
@AddConfig(key = "ocimetrics.delay", value = "2")
@AddConfig(key = "ocimetrics.initialDelay", value = "0")
@AddConfig(key = "ocimetrics.delay", value = "1")
class OciMetricsCdiExtensionTest {
private static String METRIC_NAME_SUFFIX = "DummyCounter";
private static volatile int testMetricCount = 0;
private static CountDownLatch countDownLatch = new CountDownLatch(1);
private static PostMetricDataDetails postMetricDataDetails;
Expand Down Expand Up @@ -108,13 +112,13 @@ void testDisableOciMetrics() throws InterruptedException {
}

private void validateOciMetricsSupport(boolean enabled) throws InterruptedException {
Counter c1 = registry.getOrCreate(Counter.builder("baseDummyCounter")
Counter c1 = registry.getOrCreate(Counter.builder(Meter.Scope.BASE + METRIC_NAME_SUFFIX)
.scope(Meter.Scope.BASE));
c1.increment();
Counter c2 = registry.getOrCreate(Counter.builder("vendorDummyCounter")
Counter c2 = registry.getOrCreate(Counter.builder(Meter.Scope.VENDOR + METRIC_NAME_SUFFIX)
.scope(Meter.Scope.VENDOR));
c2.increment();
Counter c3 = registry.getOrCreate(Counter.builder("appDummyCounter")
Counter c3 = registry.getOrCreate(Counter.builder(Meter.Scope.APPLICATION + METRIC_NAME_SUFFIX)
.scope(Meter.Scope.APPLICATION));
c3.increment();

Expand All @@ -130,12 +134,9 @@ private void validateOciMetricsSupport(boolean enabled) throws InterruptedExcept
assertThat(activateOciMetricsSupportIsInvoked, is(true));
// System meters in the registry might vary over time. Instead of looking for a specific number of meters,
// make sure the three we added are in the OCI metric data.
long dummyCounterCount = postMetricDataDetails.getMetricData().stream()
.filter(details -> details.getName().contains("DummyCounter"))
.count();
assertThat(dummyCounterCount, is(3L));
assertThat(testMetricCount, is(3));

MetricDataDetails metricDataDetails = postMetricDataDetails.getMetricData().get(0);
MetricDataDetails metricDataDetails = postMetricDataDetails.getMetricData().getFirst();
assertThat(metricDataDetails.getCompartmentId(),
is(MetricDataDetailsOCIParams.compartmentId));
assertThat(metricDataDetails.getNamespace(), is(MetricDataDetailsOCIParams.namespace));
Expand All @@ -157,25 +158,75 @@ interface MetricDataDetailsOCIParams {
String resourceGroup = "dummy_resourceGroup";
}

static class MockOciMetricsBean extends OciMetricsBean {
@Override
void registerOciMetrics(@Observes @Priority(LIBRARY_BEFORE + 20) @Initialized(ApplicationScoped.class) Object ignore,
Config rootConfig, Monitoring monitoringClient) {
Monitoring mockedMonitoringClient = mock(Monitoring.class);
when(mockedMonitoringClient.getEndpoint()).thenReturn("http://www.DummyEndpoint.com");
doAnswer(invocationOnMock -> {
PostMetricDataRequest postMetricDataRequest = invocationOnMock.getArgument(0);
postMetricDataDetails = postMetricDataRequest.getPostMetricDataDetails();
testMetricCount = postMetricDataDetails.getMetricData().size();
// Give signal that metrics has been posted
countDownLatch.countDown();
return PostMetricDataResponse.builder()
.__httpStatusCode__(200)
.build();
}).when(mockedMonitoringClient).postMetricData(any());
super.registerOciMetrics(ignore, rootConfig, mockedMonitoringClient);
// Use this to replace OciExtension, but will only process OCI Monitoring annotation. If the Monitoring
// annotation is found, a Mocked Monitoring object will be injected as a bean
static public class MockOciMonitoringExtension implements Extension {
boolean monitoringFound;
Set<Annotation> monitoringQualifiers;

void processInjectionPoint(@Observes final ProcessInjectionPoint<?, ?> event) {
if (event != null) {
InjectionPoint ip = event.getInjectionPoint();
Class<?> c = (Class<?>) ip.getAnnotated().getBaseType();
final Set<Annotation> existingQualifiers = ip.getQualifiers();
if (c == Monitoring.class && existingQualifiers != null && !existingQualifiers.isEmpty()) {
monitoringFound = true;
monitoringQualifiers = existingQualifiers;
}
}
}

Monitoring getMockedMonitoring() {
// Use Proxy to mock only getEndPoint() and postMetricDataDetails() methods of the Monitoring interface,
// as those are the only ones needed by the test
return
(Monitoring) Proxy.newProxyInstance(
Monitoring.class.getClassLoader(),
new Class[] {Monitoring.class},
(proxy, method, args) -> {
if (method.getName().equals("getEndpoint")) {
return "http://www.DummyEndpoint.com";
} else if (method.getName().equals("postMetricData")) {
// startupMetricLatch.await(5, TimeUnit.SECONDS);
PostMetricDataRequest postMetricDataRequest = (PostMetricDataRequest) args[0];
postMetricDataDetails = postMetricDataRequest.getPostMetricDataDetails();
testMetricCount = (int) postMetricDataDetails.getMetricData().stream()
.filter(details -> details.getName().contains(METRIC_NAME_SUFFIX))
.count();
PostMetricDataResponse response = PostMetricDataResponse.builder()
.__httpStatusCode__(200)
.build();

// Give signal that metrics will be posted if the right no. of metrics has been retrieved.
// If not, that means that the metrics have not been registered yet, so try again on the
// next invocation.
if (testMetricCount > 0) {
countDownLatch.countDown();
}
return response;
}
return null;
});
}

void afterBeanDiscovery(@Observes AfterBeanDiscovery event) {
if (monitoringFound) {
BeanConfigurator<Object> beanConfigurator = event.addBean()
.types(Monitoring.class)
.scope(ApplicationScoped.class)
.addQualifiers(monitoringQualifiers);
beanConfigurator = monitoringQualifiers != null ? beanConfigurator.addQualifiers(monitoringQualifiers) :
beanConfigurator.addQualifier(Default.Literal.INSTANCE);
// Add the mocked Monitoring as a bean
beanConfigurator.produceWith(obj -> getMockedMonitoring());
} else {
throw new IllegalStateException("Monitoring was never injected. Check if OciMetricsBean.registerOciMetrics() "
+ "has changed and does not inject Monitoring anymore.");
}
}
}

static class MockOciMetricsBean extends OciMetricsBean {
// Override so we can test if this is invoked when enabled or skipped when disabled
@Override
protected void activateOciMetricsSupport(Config rootConfig, Config ociMetricsConfig, OciMetricsSupport.Builder builder) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@
import java.lang.reflect.Field;

import io.helidon.integrations.oci.metrics.OciMetricsSupport;
import io.helidon.microprofile.config.ConfigCdiExtension;
import io.helidon.microprofile.server.JaxRsCdiExtension;
import io.helidon.microprofile.server.ServerCdiExtension;
import io.helidon.microprofile.testing.junit5.AddBean;
import io.helidon.microprofile.testing.junit5.AddConfig;
import io.helidon.microprofile.testing.junit5.AddExtension;
import io.helidon.microprofile.testing.junit5.DisableDiscovery;
import io.helidon.microprofile.testing.junit5.HelidonTest;

import jakarta.inject.Inject;
Expand All @@ -31,7 +36,15 @@
import static org.hamcrest.Matchers.is;

@HelidonTest
@DisableDiscovery
@AddBean(OverridingOciMetricsBean.class)
// Use OciMetricsCdiExtensionTest.MockOciMonitoringExtension to avoid Monitoring client from being instantiated
// with OCI authentication
@AddExtension(OciMetricsCdiExtensionTest.MockOciMonitoringExtension.class)
// Supporting Extensions for CDI
@AddExtension(ServerCdiExtension.class)
@AddExtension(JaxRsCdiExtension.class)
@AddExtension(ConfigCdiExtension.class)
@AddConfig(key = "oci.metrics.product", value = TestOverridingBean.PRODUCT)
@AddConfig(key = "oci.metrics.fleet", value = TestOverridingBean.FLEET)
class TestOverridingBean {
Expand All @@ -53,7 +66,7 @@ void checkOverriding() throws NoSuchFieldException, IllegalAccessException {
String resourceGroup = getStringField("resourceGroup", ociMetricsSupport);

assertThat("Effective namespace", namespace, is(equalTo(PRODUCT)));
assertThat("Effective namespace", resourceGroup, is(equalTo(FLEET)));
assertThat("Effective resourceGroup", resourceGroup, is(equalTo(FLEET)));
}

private String getStringField(String fieldName, OciMetricsSupport ociMetricsSupport)
Expand Down

0 comments on commit 7a2ff81

Please sign in to comment.