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> 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> 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");