From a3de35c91089216357b559f4b45045726c852474 Mon Sep 17 00:00:00 2001 From: Marek Skacelik Date: Wed, 22 Nov 2023 11:42:54 +0100 Subject: [PATCH] added tests for scheduler/quartz OTel integration. --- .../opentelemetry-quartz/pom.xml | 144 ++++++++++++++++++ .../opentelemetry/quartz/CountResource.java | 40 +++++ .../it/opentelemetry/quartz/Counter.java | 30 ++++ .../quartz/ExporterResource.java | 38 +++++ .../quartz/FailedBasicScheduler.java | 17 +++ .../quartz/FailedJobDefinitionScheduler.java | 29 ++++ .../quartz/FailedManualScheduler.java | 52 +++++++ .../quartz/JobDefinitionCounter.java | 37 +++++ .../quartz/ManualScheduledCounter.java | 59 +++++++ .../src/main/resources/application.properties | 11 ++ .../quartz/OpenTelemetryQuartzIT.java | 8 + .../quartz/OpenTelemetryQuartzTest.java | 106 +++++++++++++ integration-tests/pom.xml | 1 + 13 files changed, 572 insertions(+) create mode 100644 integration-tests/opentelemetry-quartz/pom.xml create mode 100644 integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/CountResource.java create mode 100644 integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/Counter.java create mode 100644 integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/ExporterResource.java create mode 100644 integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/FailedBasicScheduler.java create mode 100644 integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/FailedJobDefinitionScheduler.java create mode 100644 integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/FailedManualScheduler.java create mode 100644 integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/JobDefinitionCounter.java create mode 100644 integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/ManualScheduledCounter.java create mode 100644 integration-tests/opentelemetry-quartz/src/main/resources/application.properties create mode 100644 integration-tests/opentelemetry-quartz/src/test/java/io/quarkus/it/opentelemetry/quartz/OpenTelemetryQuartzIT.java create mode 100644 integration-tests/opentelemetry-quartz/src/test/java/io/quarkus/it/opentelemetry/quartz/OpenTelemetryQuartzTest.java diff --git a/integration-tests/opentelemetry-quartz/pom.xml b/integration-tests/opentelemetry-quartz/pom.xml new file mode 100644 index 00000000000000..bcf6afe916dbf1 --- /dev/null +++ b/integration-tests/opentelemetry-quartz/pom.xml @@ -0,0 +1,144 @@ + + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-integration-test-opentelemetry-quartz + Quarkus - Integration Tests - OpenTelemetry Quartz + + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-resteasy + + + io.quarkus + quarkus-quartz + + + io.quarkus + quarkus-opentelemetry + + + io.quarkus + quarkus-rest-client-jackson + + + + + io.opentelemetry + opentelemetry-sdk-testing + + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.awaitility + awaitility + test + + + + io.quarkus + quarkus-arc-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-quartz-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-resteasy-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-opentelemetry-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-rest-client-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + diff --git a/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/CountResource.java b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/CountResource.java new file mode 100644 index 00000000000000..dd6f36e8606555 --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/CountResource.java @@ -0,0 +1,40 @@ +package io.quarkus.it.opentelemetry.quartz; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/scheduler/count") +public class CountResource { + + @Inject + Counter counter; + + @Inject + ManualScheduledCounter manualScheduledCounter; + + @Inject + JobDefinitionCounter jobDefinitionCounter; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public Integer getCount() { + return counter.get(); + } + + @GET + @Path("manual") + @Produces(MediaType.TEXT_PLAIN) + public Integer getManualCount() { + return manualScheduledCounter.get(); + } + + @GET + @Path("job-definition") + @Produces(MediaType.TEXT_PLAIN) + public Integer getJobDefinitionCount() { + return jobDefinitionCounter.get(); + } +} diff --git a/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/Counter.java b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/Counter.java new file mode 100644 index 00000000000000..0199a8fb362a55 --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/Counter.java @@ -0,0 +1,30 @@ +package io.quarkus.it.opentelemetry.quartz; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.scheduler.Scheduled; + +@ApplicationScoped +public class Counter { + + AtomicInteger counter; + + @PostConstruct + void init() { + counter = new AtomicInteger(); + } + + public int get() { + return counter.get(); + } + + @Scheduled(cron = "*/1 * * * * ?", identity = "myCounter") + void increment() throws InterruptedException { + Thread.sleep(100l); + counter.incrementAndGet(); + } + +} diff --git a/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/ExporterResource.java b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/ExporterResource.java new file mode 100644 index 00000000000000..5f6407cc16167b --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/ExporterResource.java @@ -0,0 +1,38 @@ +package io.quarkus.it.opentelemetry.quartz; + +import java.util.List; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.data.SpanData; + +@Path("") +public class ExporterResource { + @Inject + InMemorySpanExporter inMemorySpanExporter; + + @GET + @Path("/export") + public List export() { // only export scheduled spans + return inMemorySpanExporter.getFinishedSpanItems() + .stream() + .filter(sd -> !sd.getName().contains("export") && !sd.getName().contains("GET")) + .collect(Collectors.toList()); + } + + @ApplicationScoped + static class InMemorySpanExporterProducer { + @Produces + @Singleton + InMemorySpanExporter inMemorySpanExporter() { + return InMemorySpanExporter.create(); + } + } +} diff --git a/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/FailedBasicScheduler.java b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/FailedBasicScheduler.java new file mode 100644 index 00000000000000..9450827e7e328a --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/FailedBasicScheduler.java @@ -0,0 +1,17 @@ +package io.quarkus.it.opentelemetry.quartz; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.scheduler.Scheduled; + +@ApplicationScoped +public class FailedBasicScheduler { + + @Scheduled(cron = "*/1 * * * * ?", identity = "myFailedBasicScheduler") + void init() throws InterruptedException { + Thread.sleep(100l); + throw new RuntimeException("error occurred in myFailedBasicScheduler."); + + } + +} diff --git a/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/FailedJobDefinitionScheduler.java b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/FailedJobDefinitionScheduler.java new file mode 100644 index 00000000000000..705b5c357c9aa5 --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/FailedJobDefinitionScheduler.java @@ -0,0 +1,29 @@ +package io.quarkus.it.opentelemetry.quartz; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import io.quarkus.quartz.QuartzScheduler; +import io.quarkus.runtime.Startup; + +@ApplicationScoped +@Startup +public class FailedJobDefinitionScheduler { + + @Inject + QuartzScheduler scheduler; + + @PostConstruct + void init() { + scheduler.newJob("myFailedJobDefinition").setCron("*/1 * * * * ?").setTask(ex -> { + try { + Thread.sleep(100l); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + throw new RuntimeException("error occurred in myFailedJobDefinition."); + }).schedule(); + } + +} diff --git a/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/FailedManualScheduler.java b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/FailedManualScheduler.java new file mode 100644 index 00000000000000..21dc72c5f5adf1 --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/FailedManualScheduler.java @@ -0,0 +1,52 @@ +package io.quarkus.it.opentelemetry.quartz; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.SchedulerException; +import org.quartz.SimpleScheduleBuilder; +import org.quartz.Trigger; +import org.quartz.TriggerBuilder; + +import io.quarkus.runtime.Startup; +import io.quarkus.runtime.annotations.RegisterForReflection; + +@Startup +@ApplicationScoped +public class FailedManualScheduler { + @Inject + org.quartz.Scheduler quartz; + + @PostConstruct + void init() throws SchedulerException { + JobDetail job = JobBuilder.newJob(CountingJob.class).withIdentity("myFailedManualJob", "myFailedGroup").build(); + Trigger trigger = TriggerBuilder + .newTrigger() + .withIdentity("myFailedTrigger", "myFailedGroup") + .startNow() + .withSchedule(SimpleScheduleBuilder + .simpleSchedule() + .repeatForever() + .withIntervalInSeconds(1)) + .build(); + quartz.scheduleJob(job, trigger); + } + + @RegisterForReflection + public static class CountingJob implements Job { + @Override + public void execute(JobExecutionContext jobExecutionContext) { + try { + Thread.sleep(100l); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + throw new RuntimeException("error occurred in myFailedManualJob."); + } + } +} diff --git a/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/JobDefinitionCounter.java b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/JobDefinitionCounter.java new file mode 100644 index 00000000000000..70d1038080df30 --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/JobDefinitionCounter.java @@ -0,0 +1,37 @@ +package io.quarkus.it.opentelemetry.quartz; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import io.quarkus.quartz.QuartzScheduler; +import io.quarkus.runtime.Startup; + +@ApplicationScoped +@Startup +public class JobDefinitionCounter { + + @Inject + QuartzScheduler scheduler; + + AtomicInteger counter; + + @PostConstruct + void init() { + counter = new AtomicInteger(); + scheduler.newJob("myJobDefinition").setCron("*/1 * * * * ?").setTask(ex -> { + try { + Thread.sleep(100l); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + counter.incrementAndGet(); + }).schedule(); + } + + public int get() { + return counter.get(); + } +} diff --git a/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/ManualScheduledCounter.java b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/ManualScheduledCounter.java new file mode 100644 index 00000000000000..00aca09f07c110 --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/ManualScheduledCounter.java @@ -0,0 +1,59 @@ +package io.quarkus.it.opentelemetry.quartz; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.SchedulerException; +import org.quartz.SimpleScheduleBuilder; +import org.quartz.Trigger; +import org.quartz.TriggerBuilder; + +import io.quarkus.runtime.Startup; +import io.quarkus.runtime.annotations.RegisterForReflection; + +@Startup +@ApplicationScoped +public class ManualScheduledCounter { + @Inject + org.quartz.Scheduler quartz; + private static AtomicInteger counter = new AtomicInteger(); + + public int get() { + return counter.get(); + } + + @PostConstruct + void init() throws SchedulerException { + JobDetail job = JobBuilder.newJob(CountingJob.class).withIdentity("myManualJob", "myGroup").build(); + Trigger trigger = TriggerBuilder + .newTrigger() + .withIdentity("myTrigger", "myGroup") + .startNow() + .withSchedule(SimpleScheduleBuilder + .simpleSchedule() + .repeatForever() + .withIntervalInSeconds(1)) + .build(); + quartz.scheduleJob(job, trigger); + } + + @RegisterForReflection + public static class CountingJob implements Job { + @Override + public void execute(JobExecutionContext jobExecutionContext) { + try { + Thread.sleep(100l); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + counter.incrementAndGet(); + } + } +} diff --git a/integration-tests/opentelemetry-quartz/src/main/resources/application.properties b/integration-tests/opentelemetry-quartz/src/main/resources/application.properties new file mode 100644 index 00000000000000..2961ca2930e64c --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/main/resources/application.properties @@ -0,0 +1,11 @@ +quarkus.application.name=opentelemetry-quartz-instrumentation-it +quarkus.application.version=999-SNAPSHOT + +disabled=disabled +off=off + +# speed up build +quarkus.otel.bsp.schedule.delay=100 +quarkus.otel.bsp.export.timeout=5s + +quarkus.scheduler.tracing.enabled=true \ No newline at end of file diff --git a/integration-tests/opentelemetry-quartz/src/test/java/io/quarkus/it/opentelemetry/quartz/OpenTelemetryQuartzIT.java b/integration-tests/opentelemetry-quartz/src/test/java/io/quarkus/it/opentelemetry/quartz/OpenTelemetryQuartzIT.java new file mode 100644 index 00000000000000..c23bdf3b456a84 --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/test/java/io/quarkus/it/opentelemetry/quartz/OpenTelemetryQuartzIT.java @@ -0,0 +1,8 @@ +package io.quarkus.it.opentelemetry.quartz; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class OpenTelemetryQuartzIT extends OpenTelemetryQuartzTest { + +} diff --git a/integration-tests/opentelemetry-quartz/src/test/java/io/quarkus/it/opentelemetry/quartz/OpenTelemetryQuartzTest.java b/integration-tests/opentelemetry-quartz/src/test/java/io/quarkus/it/opentelemetry/quartz/OpenTelemetryQuartzTest.java new file mode 100644 index 00000000000000..f40e7953040c59 --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/test/java/io/quarkus/it/opentelemetry/quartz/OpenTelemetryQuartzTest.java @@ -0,0 +1,106 @@ +package io.quarkus.it.opentelemetry.quartz; + +import static io.restassured.RestAssured.get; +import static io.restassured.RestAssured.given; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.common.mapper.TypeRef; +import io.restassured.response.Response; + +@QuarkusTest +public class OpenTelemetryQuartzTest { + + private static long DURATION_IN_NANOSECONDS = 100_000_000; // Thread.sleep(100l) for each job + + @Test + public void quartzSpanTest() { + // ensure that scheduled job is called + assertCounter("/scheduler/count", 1, Duration.ofSeconds(1)); + // assert programmatically scheduled job is called + assertCounter("/scheduler/count/manual", 1, Duration.ofSeconds(1)); + // assert JobDefinition type scheduler + assertCounter("/scheduler/count/job-definition", 1, Duration.ofSeconds(1)); + + // ------- SPAN ASSERTS ------- + List> spans = getSpans(); + + assertJobSpan(spans, "myCounter", DURATION_IN_NANOSECONDS); // identity + assertJobSpan(spans, "myGroup.myManualJob", DURATION_IN_NANOSECONDS); // group + identity + assertJobSpan(spans, "myJobDefinition", DURATION_IN_NANOSECONDS); // identity + + // errors + assertErrorJobSpan(spans, "myFailedBasicScheduler", DURATION_IN_NANOSECONDS, + "error occurred in myFailedBasicScheduler."); + assertErrorJobSpan(spans, "myFailedGroup.myFailedManualJob", DURATION_IN_NANOSECONDS, + "error occurred in myFailedManualJob."); + assertErrorJobSpan(spans, "myFailedJobDefinition", DURATION_IN_NANOSECONDS, + "error occurred in myFailedJobDefinition."); + + } + + private void assertCounter(String counterPath, int expectedCount, Duration timeout) { + await().atMost(timeout) + .until(() -> { + Response response = given().when().get(counterPath); + int code = response.statusCode(); + if (code != 200) { + return false; + } + String body = response.asString(); + int count = Integer.valueOf(body); + return count >= expectedCount; + }); + + } + + private List> getSpans() { + return get("/export").body().as(new TypeRef<>() { + }); + } + + private void assertJobSpan(List> spans, String expectedName, long expectedDuration) { + assertNotNull(spans); + assertFalse(spans.isEmpty()); + Map span = spans.stream().filter(map -> map.get("name").equals(expectedName)).findFirst().orElse(null); + assertNotNull(span, "Span with name '" + expectedName + "' not found."); + assertEquals(SpanKind.INTERNAL.toString(), span.get("kind"), "Span with name '" + expectedName + "' is not internal."); + + long start = (long) span.get("startEpochNanos"); + long end = (long) span.get("endEpochNanos"); + long delta = (end - start); + assertTrue(delta >= expectedDuration, + "Duration of span with name '" + expectedName + + "' is not longer than 100ms, actual duration: " + delta + " (ns)"); + } + + private void assertErrorJobSpan(List> spans, String expectedName, long expectedDuration, + String expectedErrorMessage) { + assertJobSpan(spans, expectedName, expectedDuration); + Map span = spans.stream().filter(map -> map.get("name").equals(expectedName)).findFirst() + .orElseThrow(AssertionError::new); // this assert should never be thrown, since we already checked it in `assertJobSpan` + + Map statusAttributes = (Map) span.get("status"); + assertNotNull(statusAttributes, "Span with name '" + expectedName + "' is not an ERROR"); + assertEquals(StatusCode.ERROR.toString(), statusAttributes.get("statusCode"), + "Span with name '" + expectedName + "' is not an ERROR"); + + Map exception = (Map) ((List>) span.get("events")).stream() + .map(map -> map.get("exception")).findFirst().orElseThrow(AssertionError::new); + // the reason for using contains instead of `equals` or `assertEqual` is that for non-manual schedulers the error message contains exception class + assertTrue(((String) exception.get("message")).contains(expectedErrorMessage), + "Span with name '" + expectedName + "' has wrong error message"); + } +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 1b406e057b3d8a..1195446f39c63a 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -356,6 +356,7 @@ opentelemetry opentelemetry-spi opentelemetry-jdbc-instrumentation + opentelemetry-quartz opentelemetry-vertx opentelemetry-reactive opentelemetry-grpc