diff --git a/dapr-spring/dapr-spring-boot-autoconfigure/pom.xml b/dapr-spring/dapr-spring-boot-autoconfigure/pom.xml
index 5cbcba6ae8..bab81a4ea6 100644
--- a/dapr-spring/dapr-spring-boot-autoconfigure/pom.xml
+++ b/dapr-spring/dapr-spring-boot-autoconfigure/pom.xml
@@ -29,23 +29,30 @@
org.springframework.boot
- spring-boot-starter
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure
org.springframework.boot
spring-boot-autoconfigure-processor
true
+
+ io.micrometer
+ micrometer-tracing-bridge-otel
+
+
+ io.opentelemetry
+ opentelemetry-exporter-otlp
+
org.springframework.data
spring-data-keyvalue
true
-
- org.springframework.boot
- spring-boot-starter-web
- test
-
org.testcontainers
testcontainers
diff --git a/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/observation/DaprKeyValueContext.java b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/observation/DaprKeyValueContext.java
new file mode 100644
index 0000000000..6c9b694b6b
--- /dev/null
+++ b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/observation/DaprKeyValueContext.java
@@ -0,0 +1,81 @@
+package io.dapr.spring.data.observation;
+
+import io.micrometer.observation.transport.SenderContext;
+
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * {@link SenderContext} for Dapr KeyValue context.
+ *
+ */
+public final class DaprKeyValueContext extends SenderContext {
+
+ private final String beanName;
+
+ private final String keyValueStore;
+
+
+ private DaprKeyValueContext(KeyValueHolder keyValueHolder, String keyValueStore, String beanName) {
+ super((carrier, key, value) -> keyValueHolder.property(key, value));
+ setCarrier(keyValueHolder);
+ this.beanName = beanName;
+ this.keyValueStore = keyValueStore;
+ }
+
+ /**
+ * Create a new context.
+ * @param kvStore KVStore to be used
+ * @param beanName name of the bean used usually (typically a {@code DaprMessagingTemplate})
+ * @return DaprMessageSenderContext
+ */
+ public static DaprKeyValueContext newContext(String kvStore, String beanName) {
+ KeyValueHolder keyValueHolder = new KeyValueHolder();
+ return new DaprKeyValueContext(keyValueHolder, kvStore, beanName);
+ }
+
+ public Map properties() {
+ return getCarrier().properties();
+ }
+
+
+ /**
+ * The name of the bean interacting with the KeyValue Store (typically a {@code DaprKeyValueTemplate}).
+ * @return the name of the bean interacting with the KeyValue store
+ */
+ public String getBeanName() {
+ return this.beanName;
+ }
+
+ /**
+ * The KeyValue store used for storing/retriving data.
+ * @return the key value store used
+ */
+ public String getKeyValueStore() {
+ return this.keyValueStore;
+ }
+
+
+ /**
+ * Acts as a carrier for a Dapr KeyValue and records the propagated properties for
+ * later access by the Dapr.
+ */
+ public static final class KeyValueHolder {
+
+ private final Map properties = new HashMap<>();
+
+ private KeyValueHolder() {
+ }
+
+ public void property(String key, String value) {
+ this.properties.put(key, value);
+ }
+
+ public Map properties() {
+ return this.properties;
+ }
+
+ }
+
+}
diff --git a/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/observation/DaprKeyValueTemplateObservation.java b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/observation/DaprKeyValueTemplateObservation.java
new file mode 100644
index 0000000000..a98431d1bf
--- /dev/null
+++ b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/observation/DaprKeyValueTemplateObservation.java
@@ -0,0 +1,56 @@
+package io.dapr.spring.data.observation;
+
+import io.micrometer.common.docs.KeyName;
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.Observation.Context;
+import io.micrometer.observation.ObservationConvention;
+import io.micrometer.observation.docs.ObservationDocumentation;
+
+/**
+ * An {@link Observation} for {@link io.dapr.spring.data.DaprKeyValueTemplate}.
+ *
+ */
+public enum DaprKeyValueTemplateObservation implements ObservationDocumentation {
+
+ /**
+ * Observation created when a Dapr template interacts with a KVStore.
+ */
+ TEMPLATE_OBSERVATION {
+
+ @Override
+ public Class extends ObservationConvention extends Context>> getDefaultConvention() {
+ return DefaultDaprKeyValueTemplateObservationConvention.class;
+ }
+
+ @Override
+ public String getPrefix() {
+ return "spring.dapr.data.template";
+ }
+
+ @Override
+ public KeyName[] getLowCardinalityKeyNames() {
+ return TemplateLowCardinalityTags.values();
+ }
+
+ };
+
+ /**
+ * Low cardinality tags.
+ */
+ public enum TemplateLowCardinalityTags implements KeyName {
+
+ /**
+ * Bean name of the template that interacts with the kv store.
+ */
+ BEAN_NAME {
+
+ @Override
+ public String asString() {
+ return "spring.dapr.data.template.name";
+ }
+
+ }
+
+ }
+
+}
diff --git a/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/observation/DaprKeyValueTemplateObservationConvention.java b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/observation/DaprKeyValueTemplateObservationConvention.java
new file mode 100644
index 0000000000..623899f6b3
--- /dev/null
+++ b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/observation/DaprKeyValueTemplateObservationConvention.java
@@ -0,0 +1,23 @@
+package io.dapr.spring.data.observation;
+
+
+import io.micrometer.observation.Observation.Context;
+import io.micrometer.observation.ObservationConvention;
+
+/**
+ * {@link ObservationConvention} for Dapr KV template .
+ *
+ */
+public interface DaprKeyValueTemplateObservationConvention extends ObservationConvention {
+
+ @Override
+ default boolean supportsContext(Context context) {
+ return context instanceof DaprKeyValueContext;
+ }
+
+ @Override
+ default String getName() {
+ return "spring.dapr.data.template";
+ }
+
+}
diff --git a/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/observation/DefaultDaprKeyValueTemplateObservationConvention.java b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/observation/DefaultDaprKeyValueTemplateObservationConvention.java
new file mode 100644
index 0000000000..acde9dd651
--- /dev/null
+++ b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/observation/DefaultDaprKeyValueTemplateObservationConvention.java
@@ -0,0 +1,35 @@
+package io.dapr.spring.data.observation;
+
+import io.micrometer.common.KeyValues;
+
+/**
+ * Default {@link DefaultDaprKeyValueTemplateObservationConvention} for Dapr template key values.
+ *
+ */
+public class DefaultDaprKeyValueTemplateObservationConvention implements DaprKeyValueTemplateObservationConvention {
+
+ /**
+ * A singleton instance of the convention.
+ */
+ public static final DefaultDaprKeyValueTemplateObservationConvention INSTANCE =
+ new DefaultDaprKeyValueTemplateObservationConvention();
+
+ @Override
+ public KeyValues getLowCardinalityKeyValues(DaprKeyValueContext context) {
+ return KeyValues.of(DaprKeyValueTemplateObservation.TemplateLowCardinalityTags.BEAN_NAME.asString(),
+ context.getBeanName());
+ }
+
+ // Remove once addressed:
+ // https://github.com/micrometer-metrics/micrometer-docs-generator/issues/30
+ @Override
+ public String getName() {
+ return "spring.dapr.data.template";
+ }
+
+ @Override
+ public String getContextualName(DaprKeyValueContext context) {
+ return context.getKeyValueStore() + " store";
+ }
+
+}
diff --git a/dapr-spring/dapr-spring-messaging/pom.xml b/dapr-spring/dapr-spring-messaging/pom.xml
index 135e904db0..e761aebd74 100644
--- a/dapr-spring/dapr-spring-messaging/pom.xml
+++ b/dapr-spring/dapr-spring-messaging/pom.xml
@@ -12,6 +12,16 @@
dapr-spring-messaging
dapr-spring-messaging
Dapr Spring Messaging
+
+
+ io.opentelemetry
+ opentelemetry-api
+
+
+ io.micrometer
+ micrometer-tracing
+
+
jar
diff --git a/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/DaprMessagingOperations.java b/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/DaprMessagingOperations.java
index ac1092c9aa..bfae729d11 100644
--- a/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/DaprMessagingOperations.java
+++ b/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/DaprMessagingOperations.java
@@ -15,6 +15,10 @@
import reactor.core.publisher.Mono;
+/**
+ * Create a new DaprMessagingOperations.
+ * @param payload type
+ */
public interface DaprMessagingOperations {
/**
diff --git a/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/DaprMessagingTemplate.java b/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/DaprMessagingTemplate.java
index 584d91fa53..6d94fdeb48 100644
--- a/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/DaprMessagingTemplate.java
+++ b/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/DaprMessagingTemplate.java
@@ -15,20 +15,122 @@
import io.dapr.client.DaprClient;
import io.dapr.client.domain.Metadata;
+import io.dapr.spring.messaging.observation.DaprMessageSenderContext;
+import io.dapr.spring.messaging.observation.DaprTemplateObservation;
+import io.dapr.spring.messaging.observation.DaprTemplateObservationConvention;
+import io.dapr.spring.messaging.observation.DefaultDaprTemplateObservationConvention;
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationRegistry;
+import io.micrometer.tracing.Tracer;
+import io.opentelemetry.api.OpenTelemetry;
+import org.springframework.beans.factory.BeanNameAware;
+import org.springframework.beans.factory.SmartInitializingSingleton;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.core.log.LogAccessor;
+import org.springframework.lang.Nullable;
+import reactor.core.publisher.Hooks;
import reactor.core.publisher.Mono;
-
+import reactor.util.context.Context;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.Map;
-public class DaprMessagingTemplate implements DaprMessagingOperations {
+/**
+ * Create a new DaprMessagingTemplate.
+ * @param templated message type
+ */
+public class DaprMessagingTemplate implements DaprMessagingOperations,
+ ApplicationContextAware, BeanNameAware, SmartInitializingSingleton {
+
+ private final LogAccessor logger = new LogAccessor(this.getClass());
private static final String MESSAGE_TTL_IN_SECONDS = "10";
private final DaprClient daprClient;
private final String pubsubName;
- public DaprMessagingTemplate(DaprClient daprClient, String pubsubName) {
+ /**
+ * Whether to record observations.
+ */
+ private final boolean observationEnabled;
+
+ /**
+ * Micrometer's Tracer.
+ */
+ @Nullable
+ private Tracer tracer;
+
+ /**
+ * Micrometer's Tracer.
+ */
+ @Nullable
+ private OpenTelemetry openTelemetry;
+
+ /**
+ * The registry to record observations with.
+ */
+ @Nullable
+ private ObservationRegistry observationRegistry;
+
+ /**
+ * The optional custom observation convention to use when recording observations.
+ */
+ @Nullable
+ private DaprTemplateObservationConvention observationConvention;
+
+ @Nullable
+ private ApplicationContext applicationContext;
+
+
+ private String beanName = "";
+
+ /**
+ * New DaprMessagingTemplate.
+ *
+ * @param daprClient the DaprClient that will be used for sending the message
+ * @param pubsubName the configured PubSub Dapr Component
+ * @param observationEnabled if observation is enabled
+ */
+ public DaprMessagingTemplate(DaprClient daprClient, String pubsubName, boolean observationEnabled) {
this.daprClient = daprClient;
this.pubsubName = pubsubName;
+ this.observationEnabled = observationEnabled;
+ Hooks.enableAutomaticContextPropagation();
+ }
+
+ /**
+ * If observations are enabled, attempt to obtain the Observation registry and
+ * convention.
+ */
+ @Override
+ public void afterSingletonsInstantiated() {
+ if (!this.observationEnabled) {
+ this.logger.debug(() -> "Observations are not enabled - not recording");
+ return;
+ }
+ if (this.applicationContext == null) {
+ this.logger.warn(() -> "Observations enabled but application context null - not recording");
+ return;
+ }
+ this.observationRegistry = this.applicationContext.getBeanProvider(ObservationRegistry.class)
+ .getIfUnique(() -> this.observationRegistry);
+ this.tracer = this.applicationContext.getBeanProvider(Tracer.class)
+ .getIfUnique(() -> this.tracer);
+ this.openTelemetry = this.applicationContext.getBeanProvider(OpenTelemetry.class)
+ .getIfUnique(() -> this.openTelemetry);
+ this.observationConvention = this.applicationContext.getBeanProvider(DaprTemplateObservationConvention.class)
+ .getIfUnique(() -> this.observationConvention);
+ }
+
+ @Override
+ public void setBeanName(String beanName) {
+ this.beanName = beanName;
+ }
+
+ @Override
+ public void setApplicationContext(ApplicationContext applicationContext) {
+ this.applicationContext = applicationContext;
}
@Override
@@ -46,10 +148,48 @@ private void doSend(String topic, T message) {
}
private Mono doSendAsync(String topic, T message) {
- return daprClient.publishEvent(pubsubName,
- topic,
- message,
- Map.of(Metadata.TTL_IN_SECONDS, MESSAGE_TTL_IN_SECONDS));
+ this.logger.trace(() -> "Sending msg to '%s' topic".formatted(topic));
+
+ DaprMessageSenderContext senderContext = DaprMessageSenderContext.newContext(topic, this.beanName);
+ Observation observation = newObservation(senderContext);
+
+ return observation.observe(() -> {
+ System.out.printf("**** Sending [%s]%n", message);
+
+ return daprClient.publishEvent(pubsubName,
+ topic,
+ message,
+ Collections.singletonMap(Metadata.TTL_IN_SECONDS, MESSAGE_TTL_IN_SECONDS))
+ .contextWrite(this::addTracingHeaders)
+ .doOnError(
+ (err) -> {
+
+ this.logger.error(err, () -> "Failed to send msg to '%s' topic".formatted(topic));
+ observation.error(err);
+ observation.stop();
+ }
+ ).doOnSuccess((err) -> {
+ this.logger.trace(() -> "Sent msg to '%s' topic".formatted(topic));
+ observation.stop();
+ });
+ });
+ }
+
+ private Context addTracingHeaders(reactor.util.context.Context context) {
+ Map map = new HashMap<>();
+ openTelemetry.getPropagators().getTextMapPropagator()
+ .inject(io.opentelemetry.context.Context.current(), map, (carrier, key, value) -> {
+ map.put(key, value);
+ });
+ return context.putAll(Context.of(map).readOnly());
+ }
+
+ private Observation newObservation(DaprMessageSenderContext senderContext) {
+ if (this.observationRegistry == null) {
+ return Observation.NOOP;
+ }
+ return DaprTemplateObservation.TEMPLATE_OBSERVATION.observation(this.observationConvention,
+ DefaultDaprTemplateObservationConvention.INSTANCE, () -> senderContext, this.observationRegistry);
}
private static class SendMessageBuilderImpl implements SendMessageBuilder {
diff --git a/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/observation/DaprMessageSenderContext.java b/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/observation/DaprMessageSenderContext.java
new file mode 100644
index 0000000000..6e8a86de32
--- /dev/null
+++ b/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/observation/DaprMessageSenderContext.java
@@ -0,0 +1,80 @@
+package io.dapr.spring.messaging.observation;
+
+import io.micrometer.observation.transport.SenderContext;
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * {@link SenderContext} for Dapr Messaging.
+ *
+ */
+public final class DaprMessageSenderContext extends SenderContext {
+
+ private final String beanName;
+
+ private final String destination;
+
+
+ private DaprMessageSenderContext(MessageHolder messageHolder, String topic, String beanName) {
+ super((carrier, key, value) -> messageHolder.property(key, value));
+ setCarrier(messageHolder);
+ this.beanName = beanName;
+ this.destination = topic;
+ }
+
+ /**
+ * Create a new context.
+ * @param topic topic to be used
+ * @param beanName name of the bean used usually (typically a {@code DaprMessagingTemplate})
+ * @return DaprMessageSenderContext
+ */
+ public static DaprMessageSenderContext newContext(String topic, String beanName) {
+ MessageHolder messageHolder = new MessageHolder();
+ return new DaprMessageSenderContext(messageHolder, topic, beanName);
+ }
+
+ public Map properties() {
+ return getCarrier().properties();
+ }
+
+
+ /**
+ * The name of the bean sending the message (typically a {@code DaprMessagingTemplate}).
+ * @return the name of the bean sending the message
+ */
+ public String getBeanName() {
+ return this.beanName;
+ }
+
+ /**
+ * The destination topic for the message.
+ * @return the topic the message is being sent to
+ */
+ public String getDestination() {
+ return this.destination;
+ }
+
+
+ /**
+ * Acts as a carrier for a Dapr message and records the propagated properties for
+ * later access by the Dapr.
+ */
+ public static final class MessageHolder {
+
+ private final Map properties = new HashMap<>();
+
+ private MessageHolder() {
+ }
+
+ public void property(String key, String value) {
+ this.properties.put(key, value);
+ }
+
+ public Map properties() {
+ return this.properties;
+ }
+
+ }
+
+}
diff --git a/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/observation/DaprTemplateObservation.java b/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/observation/DaprTemplateObservation.java
new file mode 100644
index 0000000000..9d77429058
--- /dev/null
+++ b/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/observation/DaprTemplateObservation.java
@@ -0,0 +1,56 @@
+package io.dapr.spring.messaging.observation;
+
+import io.micrometer.common.docs.KeyName;
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.Observation.Context;
+import io.micrometer.observation.ObservationConvention;
+import io.micrometer.observation.docs.ObservationDocumentation;
+
+/**
+ * An {@link Observation} for {@link io.dapr.spring.messaging.DaprMessagingTemplate}.
+ *
+ */
+public enum DaprTemplateObservation implements ObservationDocumentation {
+
+ /**
+ * Observation created when a Dapr template sends a message.
+ */
+ TEMPLATE_OBSERVATION {
+
+ @Override
+ public Class extends ObservationConvention extends Context>> getDefaultConvention() {
+ return DefaultDaprTemplateObservationConvention.class;
+ }
+
+ @Override
+ public String getPrefix() {
+ return "spring.dapr.messaging.template";
+ }
+
+ @Override
+ public KeyName[] getLowCardinalityKeyNames() {
+ return TemplateLowCardinalityTags.values();
+ }
+
+ };
+
+ /**
+ * Low cardinality tags.
+ */
+ public enum TemplateLowCardinalityTags implements KeyName {
+
+ /**
+ * Bean name of the template that sent the message.
+ */
+ BEAN_NAME {
+
+ @Override
+ public String asString() {
+ return "spring.dapr.messaging.template.name";
+ }
+
+ }
+
+ }
+
+}
diff --git a/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/observation/DaprTemplateObservationConvention.java b/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/observation/DaprTemplateObservationConvention.java
new file mode 100644
index 0000000000..9fdcac854e
--- /dev/null
+++ b/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/observation/DaprTemplateObservationConvention.java
@@ -0,0 +1,23 @@
+package io.dapr.spring.messaging.observation;
+
+
+import io.micrometer.observation.Observation.Context;
+import io.micrometer.observation.ObservationConvention;
+
+/**
+ * {@link ObservationConvention} for Dapr template .
+ *
+ */
+public interface DaprTemplateObservationConvention extends ObservationConvention {
+
+ @Override
+ default boolean supportsContext(Context context) {
+ return context instanceof DaprMessageSenderContext;
+ }
+
+ @Override
+ default String getName() {
+ return "spring.dapr.messaging.template";
+ }
+
+}
diff --git a/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/observation/DefaultDaprTemplateObservationConvention.java b/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/observation/DefaultDaprTemplateObservationConvention.java
new file mode 100644
index 0000000000..8be8522492
--- /dev/null
+++ b/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/observation/DefaultDaprTemplateObservationConvention.java
@@ -0,0 +1,35 @@
+package io.dapr.spring.messaging.observation;
+
+import io.micrometer.common.KeyValues;
+
+/**
+ * Default {@link DefaultDaprTemplateObservationConvention} for Dapr template key values.
+ *
+ */
+public class DefaultDaprTemplateObservationConvention implements DaprTemplateObservationConvention {
+
+ /**
+ * A singleton instance of the convention.
+ */
+ public static final DefaultDaprTemplateObservationConvention INSTANCE =
+ new DefaultDaprTemplateObservationConvention();
+
+ @Override
+ public KeyValues getLowCardinalityKeyValues(DaprMessageSenderContext context) {
+ return KeyValues.of(DaprTemplateObservation.TemplateLowCardinalityTags.BEAN_NAME.asString(),
+ context.getBeanName());
+ }
+
+ // Remove once addressed:
+ // https://github.com/micrometer-metrics/micrometer-docs-generator/issues/30
+ @Override
+ public String getName() {
+ return "spring.dapr.messaging.template";
+ }
+
+ @Override
+ public String getContextualName(DaprMessageSenderContext context) {
+ return context.getDestination() + " send";
+ }
+
+}
diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerIT.java
index d37680a2d5..d565be86f4 100644
--- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerIT.java
+++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerIT.java
@@ -20,7 +20,9 @@
import io.dapr.client.domain.State;
import io.dapr.config.Properties;
+import io.dapr.testcontainers.Configuration;
import io.dapr.testcontainers.DaprContainer;
+import io.dapr.testcontainers.TracingConfigParameters;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
@@ -64,6 +66,9 @@ public class DaprContainerIT {
private static final DaprContainer DAPR_CONTAINER = new DaprContainer("daprio/daprd")
.withAppName("dapr-app")
.withAppPort(8081)
+ .withConfiguration(new Configuration("my-config",
+ new TracingConfigParameters("1", true,
+ "localhost:4317", false, "grpc")))
.withAppChannelAddress("host.testcontainers.internal");
/**
@@ -103,6 +108,18 @@ public void testDaprContainerDefaults() {
DAPR_CONTAINER.getSubscriptions().size(),
"A subscription should be configured by default if none is provided"
);
+
+ assertNotNull(
+ DAPR_CONTAINER.getConfiguration(),
+ "A configuration should be provided"
+ );
+
+ Configuration configuration = DAPR_CONTAINER.getConfiguration();
+ assertEquals("1",configuration.getTracing().getSamplingRate());
+ assertEquals(true,configuration.getTracing().getStdout());
+ assertEquals("localhost:4317",configuration.getTracing().getOtelEndpoint());
+ assertEquals("grpc",configuration.getTracing().getOtelProtocol());
+ assertEquals(false,configuration.getTracing().getOtelIsSecure());
}
@Test
diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml
index f0111ca638..c0fcf5d3fc 100644
--- a/spotbugs-exclude.xml
+++ b/spotbugs-exclude.xml
@@ -22,6 +22,12 @@
+
+
+
+
+
+
@@ -46,9 +52,15 @@
-
-
-
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/Configuration.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/Configuration.java
new file mode 100644
index 0000000000..5bf1ebc4b6
--- /dev/null
+++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/Configuration.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2024 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.testcontainers;
+
+/**
+ * Represents a Dapr component.
+ */
+public class Configuration {
+ private String name;
+ private TracingConfigParameters tracing;
+
+ //@TODO: add httpPipeline
+ //@TODO: add secrets
+ //@TODO: add components
+ //@TODO: add accessControl
+
+ /**
+ * Creates a new configuration.
+ * @param name Configuration name.
+ * @param tracing TracingConfigParameters tracing configuration parameters.
+ */
+ public Configuration(String name, TracingConfigParameters tracing) {
+ this.name = name;
+ this.tracing = tracing;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public TracingConfigParameters getTracing() {
+ return tracing;
+ }
+}
diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java
index 4ddbd7d6bd..55f86a3618 100644
--- a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java
+++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java
@@ -13,6 +13,8 @@
package io.dapr.testcontainers;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.Wait;
@@ -28,6 +30,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -38,6 +41,7 @@
public class DaprContainer extends GenericContainer {
+ private static final Logger log = LoggerFactory.getLogger(DaprContainer.class);
private static final int DAPRD_DEFAULT_HTTP_PORT = 3500;
private static final int DAPRD_DEFAULT_GRPC_PORT = 50001;
private static final WaitStrategy WAIT_STRATEGY = Wait.forHttp("/v1.0/healthz/outbound")
@@ -45,6 +49,7 @@ public class DaprContainer extends GenericContainer {
.forStatusCodeMatching(statusCode -> statusCode >= 200 && statusCode <= 399);
private final Set components = new HashSet<>();
+ private Configuration configuration;
private final Set subscriptions = new HashSet<>();
private DaprProtocol protocol = DaprProtocol.HTTP;
private String appName;
@@ -86,6 +91,10 @@ public Set getSubscriptions() {
return subscriptions;
}
+ public Configuration getConfiguration() {
+ return configuration;
+ }
+
public DaprContainer withAppPort(Integer port) {
this.appPort = port;
return this;
@@ -149,6 +158,11 @@ public DaprContainer withComponent(Path path) {
return this;
}
+ public DaprContainer withConfiguration(Configuration configuration) {
+ this.configuration = configuration;
+ return this;
+ }
+
public int getHttpPort() {
return getMappedPort(DAPRD_DEFAULT_HTTP_PORT);
}
@@ -170,6 +184,41 @@ public DaprContainer withAppChannelAddress(String appChannelAddress) {
return this;
}
+
+ /**
+ * Get a map of Dapr component details.
+ * @param configuration A Dapr Configuration.
+ * @return Map of component details.
+ */
+ public Map configurationToMap(Configuration configuration) {
+ Map configurationProps = new HashMap<>();
+ configurationProps.put("apiVersion", "dapr.io/v1alpha1");
+ configurationProps.put("kind", "Configuration");
+
+ Map configurationMetadata = new LinkedHashMap<>();
+ configurationMetadata.put("name", configuration.getName());
+ configurationProps.put("metadata", configurationMetadata);
+
+ Map configurationSpec = new HashMap<>();
+
+
+ Map configurationTracing = new HashMap<>();
+ Map configurationTracingOtel = new HashMap<>();
+ if (configuration.getTracing() != null) {
+ configurationTracing.put("samplingRate", configuration.getTracing().getSamplingRate());
+ configurationTracing.put("stdout", configuration.getTracing().getStdout());
+ configurationTracingOtel.put("endpointAddress", configuration.getTracing().getOtelEndpoint());
+ configurationTracingOtel.put("isSecure", configuration.getTracing().getOtelIsSecure());
+ configurationTracingOtel.put("protocol", configuration.getTracing().getOtelProtocol());
+ }
+
+ configurationTracing.put("otel", configurationTracingOtel);
+ configurationSpec.put("tracing", configurationTracing);
+
+ configurationProps.put("spec", configurationSpec);
+ return Collections.unmodifiableMap(configurationProps);
+ }
+
/**
* Get a map of Dapr component details.
* @param component A Dapr Component.
@@ -255,11 +304,21 @@ protected void configure() {
cmds.add(Integer.toString(appPort));
}
+ if (configuration != null) {
+ cmds.add("--config");
+ cmds.add("/dapr-resources/" + configuration.getName() + ".yaml");
+ }
+
cmds.add("--log-level");
cmds.add(daprLogLevel.toString());
- cmds.add("-components-path");
+ cmds.add("--resources-path");
cmds.add("/dapr-resources");
- withCommand(cmds.toArray(new String[]{}));
+
+ String[] cmdArray = cmds.toArray(new String[]{});
+ log.info("> `daprd` Command: \n");
+ log.info("\t" + Arrays.toString(cmdArray) + "\n");
+
+ withCommand(cmdArray);
if (components.isEmpty()) {
components.add(new Component("kvstore", "state.in-memory", "v1", Collections.emptyMap()));
@@ -275,6 +334,11 @@ protected void configure() {
withCopyToContainer(Transferable.of(componentYaml), "/dapr-resources/" + component.getName() + ".yaml");
}
+ if (configuration != null) {
+ String configurationYaml = configurationToYaml(configuration);
+ withCopyToContainer(Transferable.of(configurationYaml), "/dapr-resources/" + configuration.getName() + ".yaml");
+ }
+
for (Subscription subscription : subscriptions) {
String subscriptionYaml = subscriptionToYaml(subscription);
withCopyToContainer(Transferable.of(subscriptionYaml), "/dapr-resources/" + subscription.getName() + ".yaml");
@@ -283,14 +347,43 @@ protected void configure() {
dependsOn(placementContainer);
}
+ /**
+ * Get a Yaml representation of a Subscription.
+ * @param subscription A Dapr Subscription.
+ * @return String representing the Subscription in Yaml format
+ */
public String subscriptionToYaml(Subscription subscription) {
Map subscriptionMap = subscriptionToMap(subscription);
- return yaml.dumpAsMap(subscriptionMap);
+ String subscriptionYaml = yaml.dumpAsMap(subscriptionMap);
+ log.info("> Subscription YAML: \n");
+ log.info("\t\n" + subscriptionYaml + "\n");
+ return subscriptionYaml;
}
+ /**
+ * Get a Yaml representation of a Component.
+ * @param component A Dapr Subscription.
+ * @return String representing the Component in Yaml format
+ */
public String componentToYaml(Component component) {
Map componentMap = componentToMap(component);
- return yaml.dumpAsMap(componentMap);
+ String componentYaml = yaml.dumpAsMap(componentMap);
+ log.info("> Component YAML: \n");
+ log.info("\t\n" + componentYaml + "\n");
+ return componentYaml;
+ }
+
+ /**
+ * Get a Yaml representation of a Configuration.
+ * @param configuration A Dapr Subscription.
+ * @return String representing the Configuration in Yaml format
+ */
+ public String configurationToYaml(Configuration configuration) {
+ Map configurationMap = configurationToMap(configuration);
+ String configurationYaml = yaml.dumpAsMap(configurationMap);
+ log.info("> Configuration YAML: \n");
+ log.info("\t\n" + configurationYaml + "\n");
+ return configurationYaml;
}
public String getAppName() {
diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/TracingConfigParameters.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/TracingConfigParameters.java
new file mode 100644
index 0000000000..1983946d52
--- /dev/null
+++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/TracingConfigParameters.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2024 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.testcontainers;
+
+/**
+ * Represents a Dapr tracing configuration parameters .
+ */
+public class TracingConfigParameters {
+
+ private String samplingRate;
+ private Boolean stdout;
+ private String otelEndpoint;
+ private Boolean otelIsSecure;
+ private String otelProtocol;
+
+ //@TODO: add zipkin parameters
+
+
+ /**
+ * Creates a new configuration.
+ */
+ public TracingConfigParameters() {
+ }
+
+ /**
+ * Creates a new configuration.
+ * @param samplingRate tracing sampling rate
+ * @param stdout if it should send traces to the system standard output
+ * @param otelEndpoint if using OpenTelemetry where the collector endpoint is
+ * @param otelIsSecure if using OpenTelemetry if the channel is secure
+ * @param otelProtocol if using OpenTelemetry which protocol is being used http or grpc
+ */
+ public TracingConfigParameters(String samplingRate, Boolean stdout,
+ String otelEndpoint, Boolean otelIsSecure, String otelProtocol) {
+ this.samplingRate = samplingRate;
+ this.stdout = stdout;
+ this.otelEndpoint = otelEndpoint;
+ this.otelIsSecure = otelIsSecure;
+ this.otelProtocol = otelProtocol;
+ }
+
+ public String getSamplingRate() {
+ return this.samplingRate;
+ }
+
+ public Boolean getStdout() {
+ return this.stdout;
+ }
+
+ public String getOtelEndpoint() {
+ return this.otelEndpoint;
+ }
+
+ public Boolean getOtelIsSecure() {
+ return this.otelIsSecure;
+ }
+
+ public String getOtelProtocol() {
+ return this.otelProtocol;
+ }
+}
diff --git a/testcontainers-dapr/src/test/java/io/dapr/testcontainers/DaprComponentTest.java b/testcontainers-dapr/src/test/java/io/dapr/testcontainers/DaprComponentTest.java
index 2a5db5967d..6196f245b8 100644
--- a/testcontainers-dapr/src/test/java/io/dapr/testcontainers/DaprComponentTest.java
+++ b/testcontainers-dapr/src/test/java/io/dapr/testcontainers/DaprComponentTest.java
@@ -21,6 +21,7 @@
import java.util.Collections;
import java.util.Set;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -95,6 +96,37 @@ public void subscriptionSerializationTest() {
assertEquals(expectedSubscriptionYaml, subscriptionYaml);
}
+
+ @Test
+ public void configurationSerializationTest() {
+
+ DaprContainer dapr = new DaprContainer("daprio/daprd")
+ .withAppName("dapr-app")
+ .withAppPort(8081)
+ .withConfiguration(new Configuration("my-config",
+ new TracingConfigParameters("1", true,
+ "localhost:4317", false, "grpc")))
+ .withAppChannelAddress("host.testcontainers.internal");
+
+ Configuration configuration = dapr.getConfiguration();
+ assertNotNull(configuration);
+
+ String configurationYaml = dapr.configurationToYaml(configuration);
+ String expectedConfigurationYaml = "metadata:\n" + " name: my-config\n"
+ + "apiVersion: dapr.io/v1alpha1\n"
+ + "kind: Configuration\n"
+ + "spec:\n"
+ + " tracing:\n"
+ + " stdout: true\n"
+ + " samplingRate: '1'\n"
+ + " otel:\n"
+ + " endpointAddress: localhost:4317\n"
+ + " protocol: grpc\n"
+ + " isSecure: false\n";
+ assertEquals(expectedConfigurationYaml, configurationYaml);
+ }
+
+
@Test
public void withComponentFromPath() {
URL stateStoreYaml = this.getClass().getClassLoader().getResource("dapr-resources/statestore.yaml");