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..44831b3 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; @@ -32,6 +33,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 +75,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 +134,7 @@ private GenericContainer buildTargetContainer(String agentPath, String extens @AfterEach void reset() throws IOException { - tracingBackendMock.reset(); + backendMock.reset(); configurationServerMock.reset(); } @@ -141,7 +144,7 @@ protected void stopTarget() { @AfterAll static void cleanup() { - tracingBackendMock.stop(); + backendMock.stop(); configurationServerMock.stop(); } @@ -182,9 +185,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( @@ -200,20 +206,44 @@ protected Collection waitForTraces() .collect(Collectors.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(); + }) + .collect(Collectors.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 +262,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 +278,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 99% 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..68c9606 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,7 +15,7 @@ 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";