diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..a0d1178 --- /dev/null +++ b/docker/README.md @@ -0,0 +1 @@ +`docker-compose up -d` to start a local opentelemetry collector + zipkin for development purposes \ No newline at end of file diff --git a/docker/collector-config.yaml b/docker/collector-config.yaml new file mode 100644 index 0000000..61025e4 --- /dev/null +++ b/docker/collector-config.yaml @@ -0,0 +1,32 @@ +receivers: + otlp: + protocols: + grpc: + http: + cors: + allowed_origins: + - http://* + - https://* + +exporters: + zipkin: + endpoint: "http://zipkin-all-in-one:9411/api/v2/spans" + prometheus: + endpoint: "0.0.0.0:9464" + +processors: + batch: + +service: + telemetry: + logs: + level: "debug" + pipelines: + traces: + receivers: [otlp] + exporters: [zipkin] + processors: [batch] + metrics: + receivers: [otlp] + exporters: [prometheus] + processors: [batch] diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml new file mode 100644 index 0000000..065c2fb --- /dev/null +++ b/docker/docker-compose.yaml @@ -0,0 +1,18 @@ +version: "3" +services: + collector: + image: otel/opentelemetry-collector-contrib:0.53.0 + command: ["--config=/conf/collector-config.yaml"] + volumes: + - ./collector-config.yaml:/conf/collector-config.yaml + ports: + - "9464:9464" + - "4317:4317" + - "4318:4318" + depends_on: + - zipkin-all-in-one + + zipkin-all-in-one: + image: openzipkin/zipkin:latest + ports: + - "9411:9411" diff --git a/pom.xml b/pom.xml index 37f06b5..99825e9 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ 17 2022.0.4 - 0.19.0 + 0.20.0 @@ -87,6 +87,12 @@ spring-rabbit-test test + + io.micrometer + micrometer-tracing-test + test + + diff --git a/src/main/java/com/knecon/fforesight/tracing/DefaultTracingAutoConfiguration.java b/src/main/java/com/knecon/fforesight/tracing/DefaultTracingAutoConfiguration.java index 35608b7..8ae3faf 100644 --- a/src/main/java/com/knecon/fforesight/tracing/DefaultTracingAutoConfiguration.java +++ b/src/main/java/com/knecon/fforesight/tracing/DefaultTracingAutoConfiguration.java @@ -1,29 +1,45 @@ package com.knecon.fforesight.tracing; -import org.springframework.boot.actuate.autoconfigure.tracing.ConditionalOnEnabledTracing; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.http.server.observation.ServerRequestObservationContext; - +import io.micrometer.context.ContextRegistry; import io.micrometer.observation.ObservationPredicate; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.contextpropagation.ObservationAwareSpanThreadLocalAccessor; import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.tracing.ConditionalOnEnabledTracing; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.PropertySource; +import org.springframework.http.server.observation.ServerRequestObservationContext; -@Slf4j -@ComponentScan @AutoConfiguration +@ConditionalOnBean({Tracer.class}) +@ConditionalOnEnabledTracing +@Import({ + TaskExecutionConfiguration.class, + TracingRabbitConfiguration.class, + WebMvcConfiguration.class +}) +@PropertySource("classpath:tracing-task.properties") +@Slf4j public class DefaultTracingAutoConfiguration { + @Autowired + Tracer tracer; + @PostConstruct public void postConstruct() { + ContextRegistry.getInstance().registerThreadLocalAccessor(new ObservationAwareSpanThreadLocalAccessor(tracer)); + log.info("Tracing AutoConfiguration Loaded!"); } @Bean - @ConditionalOnEnabledTracing ObservationPredicate actuatorServerContextPredicate() { // There might be a better solution soon: https://github.com/spring-projects/spring-boot/issues/34801 diff --git a/src/main/java/com/knecon/fforesight/tracing/TaskExecutionConfiguration.java b/src/main/java/com/knecon/fforesight/tracing/TaskExecutionConfiguration.java new file mode 100644 index 0000000..7c75a8c --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tracing/TaskExecutionConfiguration.java @@ -0,0 +1,44 @@ +package com.knecon.fforesight.tracing; + +import com.knecon.fforesight.tenantcommons.task.KneconTaskDecorator; +import io.micrometer.context.ContextSnapshot; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.task.TaskExecutorCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; + +import java.util.function.Supplier; + +@Configuration(proxyBeanMethods = false) +@EnableAsync +@Slf4j +public class TaskExecutionConfiguration { + + private final static Supplier IDENTITY_KNECON_TASK_DECORATOR_SUPPLIER = () -> runna -> runna; + + @Value("${spring.task.execution.pool.warnPercentageThreshold:80}") + private int executorWarnPercentageThreshold; + + @Bean + public TaskExecutorCustomizer tracingTaskExecutorCustomizer(ObjectProvider taskDecorator) { + return taskExecutor -> { + taskExecutor.setTaskDecorator((KneconTaskDecorator) runnable -> { + val taskDecoratorUsed = taskDecorator.getIfUnique(IDENTITY_KNECON_TASK_DECORATOR_SUPPLIER); + val decoratedRunnable = taskDecoratorUsed.decorate(runnable); + val actualQueueSize = taskExecutor.getQueueSize() + 1; + val queueOccupancyPercentage = actualQueueSize * 100.f / taskExecutor.getMaxPoolSize(); + + if (queueOccupancyPercentage >= executorWarnPercentageThreshold) { + log.warn("Executor pool [ " + taskExecutor + " ] queue size reached " + actualQueueSize + "/" + taskExecutor.getMaxPoolSize() + + " entries awaiting execution triggering the warn level set for " + executorWarnPercentageThreshold +"% occupancy."); + } + + return ContextSnapshot.captureAll(new Object[0]).wrap(decoratedRunnable); + }); + }; + } +} diff --git a/src/main/java/com/knecon/fforesight/tracing/TracingRabbitConfiguration.java b/src/main/java/com/knecon/fforesight/tracing/TracingRabbitConfiguration.java index 1410981..f026bcf 100644 --- a/src/main/java/com/knecon/fforesight/tracing/TracingRabbitConfiguration.java +++ b/src/main/java/com/knecon/fforesight/tracing/TracingRabbitConfiguration.java @@ -1,22 +1,15 @@ package com.knecon.fforesight.tracing; -import org.springframework.amqp.rabbit.config.ContainerCustomizer; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; -import org.springframework.boot.actuate.autoconfigure.tracing.ConditionalOnEnabledTracing; +import com.knecon.fforesight.tenantcommons.RabbitTemplateMultiCustomizer; +import com.knecon.fforesight.tenantcommons.SimpleMessageListenerContainerCustomizer; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import com.knecon.fforesight.tenantcommons.RabbitTemplateMultiCustomizer; -import com.knecon.fforesight.tenantcommons.SimpleMessageListenerContainerCustomizer; - -import lombok.extern.slf4j.Slf4j; - @Slf4j -@Configuration -@ConditionalOnEnabledTracing -@ConditionalOnClass({RabbitTemplate.class, SimpleMessageListenerContainer.class, ContainerCustomizer.class}) +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ SimpleMessageListenerContainerCustomizer.class, RabbitTemplateMultiCustomizer.class }) public class TracingRabbitConfiguration { @Bean diff --git a/src/main/java/com/knecon/fforesight/tracing/WebMvcConfiguration.java b/src/main/java/com/knecon/fforesight/tracing/WebMvcConfiguration.java new file mode 100644 index 0000000..0a70780 --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tracing/WebMvcConfiguration.java @@ -0,0 +1,17 @@ +package com.knecon.fforesight.tracing; + +import io.micrometer.context.ContextSnapshot; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration(proxyBeanMethods = false) +public class WebMvcConfiguration implements WebMvcConfigurer { + + @Override + public void configureAsyncSupport(AsyncSupportConfigurer configurer) { + + configurer.setTaskExecutor(new SimpleAsyncTaskExecutor(r -> new Thread(ContextSnapshot.captureAll().wrap(r)))); + } +} diff --git a/src/main/resources/tracing-task.properties b/src/main/resources/tracing-task.properties new file mode 100644 index 0000000..9dbf5fd --- /dev/null +++ b/src/main/resources/tracing-task.properties @@ -0,0 +1,10 @@ +# +# Configure task executor specs +# +# For more available props, +# see: https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java +# +spring.task.execution.pool.coreSize=4 +spring.task.execution.pool.maxSize=4 +spring.task.execution.pool.queueCapacity=10000 +spring.task.execution.threadNamePrefix=TracingAwareTaskExecutor- diff --git a/src/test/java/com/knecon/fforesight/tracing/ObservationEnabledTest.java b/src/test/java/com/knecon/fforesight/tracing/ObservationEnabledTest.java new file mode 100644 index 0000000..758147f --- /dev/null +++ b/src/test/java/com/knecon/fforesight/tracing/ObservationEnabledTest.java @@ -0,0 +1,59 @@ +package com.knecon.fforesight.tracing; + +import com.knecon.fforesight.tenantcommons.RabbitTemplateMultiCustomizer; +import com.knecon.fforesight.tenantcommons.SimpleMessageListenerContainerCustomizer; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +@ExtendWith(OutputCaptureExtension.class) +@Slf4j +public class ObservationEnabledTest { + + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("management.tracing.enabled=true") + .withUserConfiguration(SharedTestConfiguration.class, DefaultTracingAutoConfiguration.class); + + @Test + public void testTracingAutoConfigurationLoaded(CapturedOutput output) { + this.contextRunner.run(context -> { + assertThat(output.getOut()).contains("Tracing AutoConfiguration Loaded!"); + }); + } + + @Test + public void testRabbitTracingEnabled() { + + this.contextRunner.run(context -> { + var rabbitTemplateCustomizer = context.getBean(RabbitTemplateMultiCustomizer.class); + var rabbitTemplate = mock(RabbitTemplate.class); + + rabbitTemplateCustomizer.customize(rabbitTemplate); + verify(rabbitTemplate).setObservationEnabled(true); + }); + } + + + @Test + public void testMessageListenerContainerTracingEnabled() { + + this.contextRunner.run(context -> { + var containerConfigurer = context.getBean(SimpleMessageListenerContainerCustomizer.class); + var container = mock(SimpleMessageListenerContainer.class); + + containerConfigurer.customize(container); + verify(container).setObservationEnabled(true); + }); + } + +} diff --git a/src/test/java/com/knecon/fforesight/tracing/SharedTestConfiguration.java b/src/test/java/com/knecon/fforesight/tracing/SharedTestConfiguration.java new file mode 100644 index 0000000..cbf61c3 --- /dev/null +++ b/src/test/java/com/knecon/fforesight/tracing/SharedTestConfiguration.java @@ -0,0 +1,16 @@ +package com.knecon.fforesight.tracing; + +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.test.simple.SimpleTracer; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +@AutoConfiguration +public class SharedTestConfiguration { + + @Bean + Tracer simpleTracer() { + return new SimpleTracer(); + } + +} diff --git a/src/test/java/com/knecon/fforesight/tracing/TaskExecutionTracingTest.java b/src/test/java/com/knecon/fforesight/tracing/TaskExecutionTracingTest.java new file mode 100644 index 0000000..6876be4 --- /dev/null +++ b/src/test/java/com/knecon/fforesight/tracing/TaskExecutionTracingTest.java @@ -0,0 +1,89 @@ +package com.knecon.fforesight.tracing; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.Span; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.test.simple.SimpleTracer; +import lombok.extern.slf4j.Slf4j; +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@ExtendWith(OutputCaptureExtension.class) +@Slf4j +public class TaskExecutionTracingTest { + + private static final String ASYNC_SPAN_NAME = "Async Span"; + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("management.tracing.enabled=true") + .withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class)) + .withUserConfiguration(SharedTestConfiguration.class, AsyncLogic.class, DefaultTracingAutoConfiguration.class); + private final ObservationRegistry observationRegistry = ObservationRegistry.create(); + + + @Test + public void asyncMethodTraceTest(CapturedOutput output) { + this.contextRunner.run(context -> { + SimpleTracer tracer = (SimpleTracer)context.getBean(Tracer.class); + var asyncLogic = context.getBean(AsyncLogic.class); + + Observation firstSpan = Observation.createNotStarted("First span", observationRegistry).highCardinalityKeyValue("test", "test 1"); + try (Observation.Scope scope = firstSpan.start().openScope()) { + log.info("Async in test with observation - before call"); + + Span secondSpan = tracer.nextSpan().name("Second span").tag("test", "test 2"); + try (Tracer.SpanInScope scope2 = tracer.withSpan(secondSpan.start())) { + log.info("Async in test with span - before call"); + Future future = asyncLogic.asyncCall(); + String spanIdFromFuture = future.get(1, TimeUnit.SECONDS); + log.info("Async in test with span - after call"); + BDDAssertions.then(spanIdFromFuture).isEqualTo(secondSpan.context().spanId()); + } + finally { + secondSpan.end(); + } + + log.info("Async in test with observation - after call"); + } + finally { + firstSpan.stop(); + } + + var tracerSpans = tracer.getSpans().stream().toList(); + + assertThat(tracerSpans.size()).isEqualTo(2); + assertThat(tracerSpans.get(1).getName()).isEqualTo(ASYNC_SPAN_NAME); + assertThat(tracerSpans.get(1).getParentId()).isEqualTo(tracerSpans.get(0).getSpanId()); + }); + } + + @Component + static class AsyncLogic { + @Autowired + Tracer tracer; + + @Async("applicationTaskExecutor") + public Future asyncCall() { + log.info("TASK EXECUTOR"); + tracer.nextSpan().name(ASYNC_SPAN_NAME).tag("test", "test 3").start().end(); + String spanId = tracer.currentSpan().context().spanId(); + return CompletableFuture.supplyAsync(() -> spanId); + } + } +} diff --git a/src/test/java/com/knecon/fforesight/tracing/TracingTests.java b/src/test/java/com/knecon/fforesight/tracing/TracingTests.java deleted file mode 100644 index a120b0a..0000000 --- a/src/test/java/com/knecon/fforesight/tracing/TracingTests.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.knecon.fforesight.tracing; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import org.junit.jupiter.api.Test; -import org.springframework.amqp.rabbit.config.ContainerCustomizer; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.amqp.RabbitTemplateCustomizer; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import com.knecon.fforesight.tenantcommons.MultiTenancyAutoConfiguration; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class TracingTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(TracingRabbitConfiguration.class, - MultiTenancyAutoConfiguration.class)); - - - @Test - public void testTracingEnabled() { - - this.contextRunner.withPropertyValues("management.tracing.enabled=true").run(context -> { - var rabbitTemplateCustomizer = context.getBean(RabbitTemplateCustomizer.class); - var rabbitTemplate = mock(RabbitTemplate.class); - - rabbitTemplateCustomizer.customize(rabbitTemplate); - verify(rabbitTemplate).setObservationEnabled(true); - }).run(context -> { - var containerConfigurer = context.getBean(ContainerCustomizer.class); - var container = mock(SimpleMessageListenerContainer.class); - - containerConfigurer.configure(container); - verify(container).setObservationEnabled(true); - }); - } - -}