From b367a60f82024f4acaca246b08387581e3d16f92 Mon Sep 17 00:00:00 2001 From: Eduard Schander <66794307+EddeCCC@users.noreply.github.com> Date: Thu, 21 Nov 2024 10:51:52 +0100 Subject: [PATCH] Feature: selfmonitoring instrumented classes (#30) * feature: add MetricFactory * little refactor * test: add MetricFactoryTest * refactor MetricFactoryTest * test: add self-monitoring int-test * fix sonarcloud issues --- inspectit-gepard-agent/build.gradle | 1 + .../state/InstrumentationState.java | 14 ++- .../agent/internal/metrics/MetricFactory.java | 27 ++++ .../internal/otel/OpenTelemetryAccessor.java | 10 +- .../integrationtest/IntegrationTestBase.java | 69 +++++++--- ...ock.java => ObservabilityBackendMock.java} | 10 +- .../spring/metrics/SelfMonitoringIntTest.java | 119 ++++++++++++++++++ .../{ScopeTest.java => ScopeIntTest.java} | 2 +- .../{TracingTest.java => TracingIntTest.java} | 10 +- .../internal/metrics/MetricFactoryTest.java | 63 ++++++++++ 10 files changed, 293 insertions(+), 32 deletions(-) create mode 100644 inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/internal/metrics/MetricFactory.java rename inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/{TracingBackendMock.java => ObservabilityBackendMock.java} (85%) create mode 100644 inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/metrics/SelfMonitoringIntTest.java rename inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/scope/{ScopeTest.java => ScopeIntTest.java} (98%) rename inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/trace/{TracingTest.java => TracingIntTest.java} (94%) create mode 100644 inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/internal/metrics/MetricFactoryTest.java diff --git a/inspectit-gepard-agent/build.gradle b/inspectit-gepard-agent/build.gradle index b0e07d0..3b2bc4b 100644 --- a/inspectit-gepard-agent/build.gradle +++ b/inspectit-gepard-agent/build.gradle @@ -108,6 +108,7 @@ dependencies { testCompileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") testCompileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") testCompileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-bootstrap") + testCompileOnly("io.opentelemetry:opentelemetry-exporter-logging") testImplementation("org.junit.jupiter:junit-jupiter-api:${versions.junit}") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${versions.junit}") diff --git a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/state/InstrumentationState.java b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/state/InstrumentationState.java index 5a71dad..be10828 100644 --- a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/state/InstrumentationState.java +++ b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/state/InstrumentationState.java @@ -3,13 +3,16 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; +import io.opentelemetry.api.metrics.ObservableDoubleMeasurement; import java.util.Objects; +import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import rocks.inspectit.gepard.agent.instrumentation.hook.MethodHookManager; import rocks.inspectit.gepard.agent.instrumentation.state.configuration.resolver.ConfigurationResolver; import rocks.inspectit.gepard.agent.internal.instrumentation.InstrumentedType; import rocks.inspectit.gepard.agent.internal.instrumentation.model.ClassInstrumentationConfiguration; +import rocks.inspectit.gepard.agent.internal.metrics.MetricFactory; /** Stores the instrumentation configuration of all instrumented classes. */ public class InstrumentationState { @@ -36,7 +39,9 @@ private InstrumentationState( */ public static InstrumentationState create( ConfigurationResolver configurationResolver, MethodHookManager methodHookManager) { - return new InstrumentationState(configurationResolver, methodHookManager); + InstrumentationState state = new InstrumentationState(configurationResolver, methodHookManager); + state.setUpSelfMonitoring(); + return state; } /** @@ -124,4 +129,11 @@ private void updateMethodHooks( log.error("There was an error while updating the hooks of class {}", clazz.getName(), e); } } + + /** Sets up self-monitoring to record the amount of instrumented classes. */ + private void setUpSelfMonitoring() { + Consumer callback = + (measurement) -> measurement.record(activeInstrumentations.estimatedSize()); + MetricFactory.createObservableDoubleGauge("inspectit.self.instrumented-classes", callback); + } } diff --git a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/internal/metrics/MetricFactory.java b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/internal/metrics/MetricFactory.java new file mode 100644 index 0000000..a8aabb6 --- /dev/null +++ b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/internal/metrics/MetricFactory.java @@ -0,0 +1,27 @@ +/* (C) 2024 */ +package rocks.inspectit.gepard.agent.internal.metrics; + +import io.opentelemetry.api.metrics.ObservableDoubleGauge; +import io.opentelemetry.api.metrics.ObservableDoubleMeasurement; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; +import java.util.function.Consumer; +import rocks.inspectit.gepard.agent.internal.otel.OpenTelemetryAccessor; + +/** Creates new OpenTelemetry instruments (metrics). */ +public class MetricFactory { + + private MetricFactory() {} + + /** + * Creates an observable gauge metric. This gauge will record a measurement via calling the + * callback function everytime it is observed by the {@link PeriodicMetricReader}. + * + * @param name the name of the gauge + * @param callback the callback function to record a measurement + * @return the created gauge + */ + public static ObservableDoubleGauge createObservableDoubleGauge( + String name, Consumer callback) { + return OpenTelemetryAccessor.getMeter().gaugeBuilder(name).buildWithCallback(callback); + } +} diff --git a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/internal/otel/OpenTelemetryAccessor.java b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/internal/otel/OpenTelemetryAccessor.java index 2da215a..f0d22c6 100644 --- a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/internal/otel/OpenTelemetryAccessor.java +++ b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/internal/otel/OpenTelemetryAccessor.java @@ -3,6 +3,7 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.trace.Tracer; /** @@ -13,7 +14,7 @@ public class OpenTelemetryAccessor { /** The instrumentation scope name we use for our spans and metrics */ - public static final String INSTRUMENTATION_SCOPE_NAME = "inspectit-gepard"; + public static final String INSTRUMENTATION_SCOPE_NAME = "rocks.inspectit.gepard"; /** Our global OpenTelemetry instance */ private static OpenTelemetry openTelemetry; @@ -36,4 +37,11 @@ public static void setOpenTelemetry(OpenTelemetry otel) { public static Tracer getTracer() { return openTelemetry.getTracer(INSTRUMENTATION_SCOPE_NAME); } + + /** + * @return the meter to create metric instruments + */ + public static Meter getMeter() { + return openTelemetry.getMeter(INSTRUMENTATION_SCOPE_NAME); + } } diff --git a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/IntegrationTestBase.java b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/IntegrationTestBase.java index 2cc21bb..7c829e4 100644 --- a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/IntegrationTestBase.java +++ b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/IntegrationTestBase.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.util.JsonFormat; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest; import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; import io.opentelemetry.proto.trace.v1.Span; import java.io.IOException; @@ -14,7 +15,6 @@ import java.util.Collections; import java.util.Map; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; import okhttp3.OkHttpClient; @@ -32,6 +32,8 @@ import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.containers.wait.strategy.WaitStrategy; import org.testcontainers.utility.MountableFile; +import rocks.inspectit.gepard.agent.configuration.http.HttpConfigurationCallback; +import rocks.inspectit.gepard.agent.instrumentation.hook.MethodHookManager; import rocks.inspectit.gepard.agent.integrationtest.utils.OkHttpUtils; /** @@ -72,14 +74,14 @@ protected Map getExtraEnv() { } // The Observability Backend Mock - private static TracingBackendMock tracingBackendMock; + private static ObservabilityBackendMock backendMock; // The configuration Server Mock public static ConfigurationServerMock configurationServerMock; @BeforeAll static void setup() { - tracingBackendMock = TracingBackendMock.create(network); - tracingBackendMock.start(); + backendMock = ObservabilityBackendMock.create(network); + backendMock.start(); configurationServerMock = ConfigurationServerMock.create(network); configurationServerMock.start(); @@ -131,7 +133,7 @@ private GenericContainer buildTargetContainer(String agentPath, String extens @AfterEach void reset() throws IOException { - tracingBackendMock.reset(); + backendMock.reset(); configurationServerMock.reset(); } @@ -141,7 +143,7 @@ protected void stopTarget() { @AfterAll static void cleanup() { - tracingBackendMock.stop(); + backendMock.stop(); configurationServerMock.stop(); } @@ -182,9 +184,12 @@ protected static Stream getSpanStream(Collection it.getSpansList().stream()); } + /** + * @return the traces received by the backend mock + */ protected Collection waitForTraces() throws IOException, InterruptedException { - String content = waitForContent(); + String content = waitForContent("traces"); return StreamSupport.stream(OBJECT_MAPPER.readTree(content).spliterator(), false) .map( @@ -197,23 +202,47 @@ protected Collection waitForTraces() } return builder.build(); }) - .collect(Collectors.toList()); + .toList(); } - private String waitForContent() throws IOException, InterruptedException { + /** + * @return the metrics received by the backend mock + */ + protected Collection waitForMetrics() + throws IOException, InterruptedException { + String content = waitForContent("metrics"); + + return StreamSupport.stream(OBJECT_MAPPER.readTree(content).spliterator(), false) + .map( + it -> { + ExportMetricsServiceRequest.Builder builder = + ExportMetricsServiceRequest.newBuilder(); + try { + JsonFormat.parser().merge(OBJECT_MAPPER.writeValueAsString(it), builder); + } catch (InvalidProtocolBufferException | JsonProcessingException e) { + e.printStackTrace(); + } + return builder.build(); + }) + .toList(); + } + + /** + * Waits for specific content from the backend mock + * + * @param type traces, metrics or logs + */ + private String waitForContent(String type) throws IOException, InterruptedException { long previousSize = 0; long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(30); String content = "[]"; while (System.currentTimeMillis() < deadline) { - Request request = - new Request.Builder() - .url( - String.format( - "http://%s:%d/get-traces", - tracingBackendMock.getServer().getHost(), - tracingBackendMock.getServer().getMappedPort(8080))) - .build(); + String url = + String.format( + "http://%s:%d/get-%s", + backendMock.getServer().getHost(), backendMock.getServer().getMappedPort(8080), type); + Request request = new Request.Builder().url(url).build(); try (ResponseBody body = client.newCall(request).execute().body()) { content = body.string(); @@ -232,7 +261,8 @@ private String waitForContent() throws IOException, InterruptedException { /** * Waits until the instrumentation was applied in the method hooks for the specified amount of - * times. The test should not fail here, if no further update message was found. + * times. The test should not fail here, if no further update message was found.
+ * We use the log message from {@link MethodHookManager}. */ protected void awaitInstrumentationUpdate(int amount) { String updateMessage = @@ -247,7 +277,8 @@ protected void awaitInstrumentationUpdate(int amount) { /** * Waits until the configuration was polled one more time. The test should not fail here, if no - * further update message was found. + * further update message was found.
+ * We use the log message from {@link HttpConfigurationCallback}. */ protected void awaitConfigurationUpdate() { String updateMessage = diff --git a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/TracingBackendMock.java b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/ObservabilityBackendMock.java similarity index 85% rename from inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/TracingBackendMock.java rename to inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/ObservabilityBackendMock.java index 1645b75..83126ce 100644 --- a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/TracingBackendMock.java +++ b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/ObservabilityBackendMock.java @@ -17,9 +17,9 @@ * This class is used to create a mock for a tracing backend. We can write and retrieve traces * there. */ -public class TracingBackendMock { +public class ObservabilityBackendMock { - private static final Logger logger = LoggerFactory.getLogger(TracingBackendMock.class); + private static final Logger logger = LoggerFactory.getLogger(ObservabilityBackendMock.class); private static final DockerImageName MOCK_IMAGE = DockerImageName.parse( "ghcr.io/open-telemetry/opentelemetry-java-instrumentation/smoke-test-fake-backend") @@ -28,7 +28,7 @@ public class TracingBackendMock { private final GenericContainer server; - private TracingBackendMock(Network network) { + private ObservabilityBackendMock(Network network) { server = new GenericContainer<>(MOCK_IMAGE) .withNetwork(network) @@ -38,8 +38,8 @@ private TracingBackendMock(Network network) { .withLogConsumer(new Slf4jLogConsumer(logger)); } - static TracingBackendMock create(Network network) { - return new TracingBackendMock(network); + static ObservabilityBackendMock create(Network network) { + return new ObservabilityBackendMock(network); } void start() { diff --git a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/metrics/SelfMonitoringIntTest.java b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/metrics/SelfMonitoringIntTest.java new file mode 100644 index 0000000..099f972 --- /dev/null +++ b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/metrics/SelfMonitoringIntTest.java @@ -0,0 +1,119 @@ +/* (C) 2024 */ +package rocks.inspectit.gepard.agent.integrationtest.spring.metrics; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static rocks.inspectit.gepard.agent.internal.otel.OpenTelemetryAccessor.INSTRUMENTATION_SCOPE_NAME; + +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest; +import io.opentelemetry.proto.metrics.v1.Metric; +import io.opentelemetry.proto.metrics.v1.NumberDataPoint; +import io.opentelemetry.proto.metrics.v1.ResourceMetrics; +import io.opentelemetry.proto.metrics.v1.ScopeMetrics; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import rocks.inspectit.gepard.agent.integrationtest.spring.SpringTestBase; + +/** + * Note: Currently self-monitoring is always enabled.
+ * Later, this should be configurable. Thus, we will have to provide a proper self-monitoring + * configuration. + */ +class SelfMonitoringIntTest extends SpringTestBase { + + @Override + protected Map getExtraEnv() { + // Set metric export interval to 10s (default 60s) + // Might have to be adjusted + return Map.of("OTEL_METRIC_EXPORT_INTERVAL", "10000"); + } + + @Test + void shouldRecordMetricForInstrumentedClassWhenScopesAreActive() throws Exception { + configurationServerMock.configServerSetup(configDir + "multiple-scopes.json"); + startTarget("/opentelemetry-extensions.jar"); + awaitInstrumentationUpdate(1); + + Collection exportedMetrics = waitForMetrics(); + + assertGaugeMetric(exportedMetrics, "inspectit.self.instrumented-classes", 2.0); + } + + @Test + void shouldNotRecordMetricWhenScopesAreInactive() throws Exception { + configurationServerMock.configServerSetup(configDir + "empty-config.json"); + startTarget("/opentelemetry-extensions.jar"); + awaitInstrumentationUpdate(1); + + Collection exportedMetrics = waitForMetrics(); + + assertGaugeMetric(exportedMetrics, "inspectit.self.instrumented-classes", 0.0); + } + + /** + * This method asserts that a metric with the given name exists and that the last recorded value + * of this metric equals the expected value. The metric has to be a gauge. + * + * @param exportedMetrics the collection of exported metrics + * @param name the metric name + * @param expectedValue the expected value + */ + private void assertGaugeMetric( + Collection exportedMetrics, String name, Double expectedValue) { + List metrics = getMetrics(exportedMetrics); + Optional maybeMetric = findMetric(metrics, name); + + assertTrue(maybeMetric.isPresent()); + + List dataPoints = maybeMetric.get().getGauge().getDataPointsList(); + double lastValue = dataPoints.get(dataPoints.size() - 1).getAsDouble(); + + assertEquals(expectedValue, lastValue); + } + + /** + * Maps the provided {@link ExportMetricsServiceRequest}s to simple {@link Metric} objects. + * + * @param exportedMetrics the exported metrics + * @return the collection of metric objects + */ + private List getMetrics(Collection exportedMetrics) { + return exportedMetrics.stream() + .flatMap( + exportedMetric -> + exportedMetric.getResourceMetricsList().stream() + .flatMap( + resourceMetrics -> + filterAndExtractMetrics(resourceMetrics.getScopeMetricsList()))) + .toList(); + } + + /** + * Filters and extracts all {@link Metric}s from {@link ScopeMetrics}. We filter for metrics + * created by inspectIT. + * + * @param scopeMetricsList the list of {@link ScopeMetrics} from {@link ResourceMetrics} + * @return the collection of filtered metrics as stream + */ + private Stream filterAndExtractMetrics(List scopeMetricsList) { + return scopeMetricsList.stream() + .filter( + scopeMetrics -> scopeMetrics.getScope().getName().equals(INSTRUMENTATION_SCOPE_NAME)) + .flatMap(scopeMetric -> scopeMetric.getMetricsList().stream()); + } + + /** + * Tries to find the provided metric name in list of metrics. + * + * @param metrics the list of metrics + * @param metricName the metric name to look for + * @return the found metric or empty + */ + private Optional findMetric(List metrics, String metricName) { + return metrics.stream().filter(metric -> metric.getName().equals(metricName)).findFirst(); + } +} diff --git a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/scope/ScopeTest.java b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/scope/ScopeIntTest.java similarity index 98% rename from inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/scope/ScopeTest.java rename to inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/scope/ScopeIntTest.java index bc491b5..fbf711e 100644 --- a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/scope/ScopeTest.java +++ b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/scope/ScopeIntTest.java @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test; import rocks.inspectit.gepard.agent.integrationtest.spring.SpringTestBase; -public class ScopeTest extends SpringTestBase { +public class ScopeIntTest extends SpringTestBase { @Test void scopeWithoutMethodInstrumentsAllMethods() throws Exception { diff --git a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/trace/TracingTest.java b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/trace/TracingIntTest.java similarity index 94% rename from inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/trace/TracingTest.java rename to inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/trace/TracingIntTest.java index 8bab78f..ac6cbe3 100644 --- a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/trace/TracingTest.java +++ b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/trace/TracingIntTest.java @@ -15,11 +15,11 @@ import rocks.inspectit.gepard.agent.integrationtest.spring.SpringTestBase; /** Should check, if traces are received in our tracing backend according to our configuration. */ -class TracingTest extends SpringTestBase { +class TracingIntTest extends SpringTestBase { - private static final String parentSpanName = "WebController.greeting"; + private static final String PARENT_SPAN_NAME = "WebController.greeting"; - private static final String childSpanName = "WebController.withSpan"; + private static final String CHILD_SPAN_NAME = "WebController.withSpan"; @Test void shouldSendSpansToBackendWhenScopesAreActive() throws Exception { @@ -30,7 +30,7 @@ void shouldSendSpansToBackendWhenScopesAreActive() throws Exception { sendRequestToTarget("/greeting"); Collection traces = waitForTraces(); - assertSpans(traces, parentSpanName, childSpanName); + assertSpans(traces, PARENT_SPAN_NAME, CHILD_SPAN_NAME); } @Test @@ -42,7 +42,7 @@ void shouldNotSendSpansToBackendWhenScopesAreInactive() throws Exception { sendRequestToTarget("/greeting"); Collection traces = waitForTraces(); - assertNoSpans(traces, parentSpanName, childSpanName); + assertNoSpans(traces, PARENT_SPAN_NAME, CHILD_SPAN_NAME); } /** diff --git a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/internal/metrics/MetricFactoryTest.java b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/internal/metrics/MetricFactoryTest.java new file mode 100644 index 0000000..8b8f044 --- /dev/null +++ b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/internal/metrics/MetricFactoryTest.java @@ -0,0 +1,63 @@ +/* (C) 2024 */ +package rocks.inspectit.gepard.agent.internal.metrics; + +import static org.mockito.Mockito.*; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.metrics.ObservableDoubleGauge; +import io.opentelemetry.api.metrics.ObservableDoubleMeasurement; +import io.opentelemetry.exporter.logging.LoggingMetricExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; +import java.time.Duration; +import java.util.function.Consumer; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import rocks.inspectit.gepard.agent.internal.otel.OpenTelemetryAccessor; + +@ExtendWith(MockitoExtension.class) +class MetricFactoryTest { + + @Mock private Consumer mockCallback; + + // Interval to read and export metrics + private static final Duration INTERVAL = Duration.ofMillis(500); + + @BeforeAll + static void beforeAll() { + GlobalOpenTelemetry.resetForTest(); + + // Build our own OpenTelemetrySdk, so we don't use the NOOP implementations + SdkMeterProvider meterProvider = + SdkMeterProvider.builder() + .registerMetricReader( + PeriodicMetricReader.builder(LoggingMetricExporter.create()) + .setInterval(INTERVAL) + .build()) + .build(); + OpenTelemetrySdk.builder().setMeterProvider(meterProvider).buildAndRegisterGlobal(); + OpenTelemetryAccessor.setOpenTelemetry(GlobalOpenTelemetry.get()); + } + + @Test + void shouldRecordValueWithCallbackWhenCreatingGauge() { + Consumer errorCallback = + (measurement) -> { + throw new RuntimeException("Test exception"); + }; + + ObservableDoubleGauge gauge1 = + MetricFactory.createObservableDoubleGauge("test-gauge", mockCallback); + ObservableDoubleGauge gauge2 = + MetricFactory.createObservableDoubleGauge("fail-gauge", errorCallback); + + verify(mockCallback, timeout(INTERVAL.toMillis() + 100)).accept(any()); + + gauge1.close(); + gauge2.close(); + } +}