diff --git a/integration-tests/opentelemetry-quartz/pom.xml b/integration-tests/opentelemetry-quartz/pom.xml new file mode 100644 index 00000000000000..0637aaee833c2a --- /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-reactive + + + io.quarkus + quarkus-quartz + + + io.quarkus + quarkus-opentelemetry + + + io.quarkus + quarkus-resteasy-reactive-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-reactive-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-opentelemetry-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-resteasy-reactive-jackson + ${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..7f98b9ae528a53 --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/main/resources/application.properties @@ -0,0 +1,8 @@ +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..eb67fe58a86cb2 --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/test/java/io/quarkus/it/opentelemetry/quartz/OpenTelemetryQuartzIT.java @@ -0,0 +1,10 @@ +package io.quarkus.it.opentelemetry.quartz; + +import io.quarkus.test.junit.DisabledOnIntegrationTest; +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +@DisabledOnIntegrationTest("native mode testing span does not have a field 'exception' (only in integration-test, not in quarkus app)") +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..7f3897f6c36712 --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/test/java/io/quarkus/it/opentelemetry/quartz/OpenTelemetryQuartzTest.java @@ -0,0 +1,104 @@ +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); + assertTrue(((String) exception.get("message")).contains(expectedErrorMessage), + "Span with name '" + expectedName + "' has wrong error message"); + } +} diff --git a/integration-tests/opentelemetry-scheduler/pom.xml b/integration-tests/opentelemetry-scheduler/pom.xml new file mode 100644 index 00000000000000..f0fc716016933e --- /dev/null +++ b/integration-tests/opentelemetry-scheduler/pom.xml @@ -0,0 +1,144 @@ + + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-integration-test-opentelemetry-scheduler + Quarkus - Integration Tests - OpenTelemetry Scheduler + + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-resteasy-reactive + + + io.quarkus + quarkus-scheduler + + + io.quarkus + quarkus-opentelemetry + + + io.quarkus + quarkus-resteasy-reactive-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-scheduler-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-resteasy-reactive-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-opentelemetry-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-resteasy-reactive-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + diff --git a/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/CountResource.java b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/CountResource.java new file mode 100644 index 00000000000000..90906d9bc92889 --- /dev/null +++ b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/CountResource.java @@ -0,0 +1,30 @@ +package io.quarkus.it.opentelemetry.scheduler; + +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 + JobDefinitionCounter jobDefinitionCounter; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public Integer getCount() { + return counter.get(); + } + + @GET + @Path("job-definition") + @Produces(MediaType.TEXT_PLAIN) + public Integer getJobDefinitionCount() { + return jobDefinitionCounter.get(); + } +} diff --git a/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/Counter.java b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/Counter.java new file mode 100644 index 00000000000000..c175e7716fe9f5 --- /dev/null +++ b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/Counter.java @@ -0,0 +1,30 @@ +package io.quarkus.it.opentelemetry.scheduler; + +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-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/ExporterResource.java b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/ExporterResource.java new file mode 100644 index 00000000000000..b7d246eb466738 --- /dev/null +++ b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/ExporterResource.java @@ -0,0 +1,38 @@ +package io.quarkus.it.opentelemetry.scheduler; + +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-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/FailedBasicScheduler.java b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/FailedBasicScheduler.java new file mode 100644 index 00000000000000..f03822402cd377 --- /dev/null +++ b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/FailedBasicScheduler.java @@ -0,0 +1,17 @@ +package io.quarkus.it.opentelemetry.scheduler; + +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-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/FailedJobDefinitionScheduler.java b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/FailedJobDefinitionScheduler.java new file mode 100644 index 00000000000000..41d45b6ceff4d1 --- /dev/null +++ b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/FailedJobDefinitionScheduler.java @@ -0,0 +1,29 @@ +package io.quarkus.it.opentelemetry.scheduler; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import io.quarkus.runtime.Startup; +import io.quarkus.scheduler.Scheduler; + +@ApplicationScoped +@Startup +public class FailedJobDefinitionScheduler { + + @Inject + Scheduler 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-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/JobDefinitionCounter.java b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/JobDefinitionCounter.java new file mode 100644 index 00000000000000..16ee5db3e1273e --- /dev/null +++ b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/JobDefinitionCounter.java @@ -0,0 +1,37 @@ +package io.quarkus.it.opentelemetry.scheduler; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import io.quarkus.runtime.Startup; +import io.quarkus.scheduler.Scheduler; + +@ApplicationScoped +@Startup +public class JobDefinitionCounter { + + @Inject + Scheduler 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-scheduler/src/main/resources/application.properties b/integration-tests/opentelemetry-scheduler/src/main/resources/application.properties new file mode 100644 index 00000000000000..7f98b9ae528a53 --- /dev/null +++ b/integration-tests/opentelemetry-scheduler/src/main/resources/application.properties @@ -0,0 +1,8 @@ +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-scheduler/src/test/java/io/quarkus/it/opentelemetry/scheduler/OpenTelemetrySchedulerIT.java b/integration-tests/opentelemetry-scheduler/src/test/java/io/quarkus/it/opentelemetry/scheduler/OpenTelemetrySchedulerIT.java new file mode 100644 index 00000000000000..b17f67a2ff30d9 --- /dev/null +++ b/integration-tests/opentelemetry-scheduler/src/test/java/io/quarkus/it/opentelemetry/scheduler/OpenTelemetrySchedulerIT.java @@ -0,0 +1,10 @@ +package io.quarkus.it.opentelemetry.scheduler; + +import io.quarkus.test.junit.DisabledOnIntegrationTest; +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +@DisabledOnIntegrationTest("native mode testing span does not have a field 'exception' (only in integration-test, not in quarkus app)") +public class OpenTelemetrySchedulerIT extends OpenTelemetrySchedulerTest { + +} diff --git a/integration-tests/opentelemetry-scheduler/src/test/java/io/quarkus/it/opentelemetry/scheduler/OpenTelemetrySchedulerTest.java b/integration-tests/opentelemetry-scheduler/src/test/java/io/quarkus/it/opentelemetry/scheduler/OpenTelemetrySchedulerTest.java new file mode 100644 index 00000000000000..c33b61ccff3868 --- /dev/null +++ b/integration-tests/opentelemetry-scheduler/src/test/java/io/quarkus/it/opentelemetry/scheduler/OpenTelemetrySchedulerTest.java @@ -0,0 +1,99 @@ +package io.quarkus.it.opentelemetry.scheduler; + +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 OpenTelemetrySchedulerTest { + + private static long DURATION_IN_NANOSECONDS = 100_000_000; // Thread.sleep(100l) for each job + + @Test + public void schedulerSpanTest() { + // ensure that scheduled job is called + assertCounter("/scheduler/count", 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, "myJobDefinition", DURATION_IN_NANOSECONDS); // identity + + // errors + assertErrorJobSpan(spans, "myFailedBasicScheduler", DURATION_IN_NANOSECONDS, + "error occurred in myFailedBasicScheduler."); + 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); + 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..357260c45418cf 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -356,6 +356,8 @@ opentelemetry opentelemetry-spi opentelemetry-jdbc-instrumentation + opentelemetry-quartz + opentelemetry-scheduler opentelemetry-vertx opentelemetry-reactive opentelemetry-grpc