diff --git a/.github/scripts/update_sdk_version.sh b/.github/scripts/update_sdk_version.sh index 0e5726ecca..b2e1033be9 100755 --- a/.github/scripts/update_sdk_version.sh +++ b/.github/scripts/update_sdk_version.sh @@ -12,6 +12,11 @@ mvn versions:set -DnewVersion=$DAPR_JAVA_SDK_VERSION -DprocessDependencies=true mvn versions:set-property -Dproperty=dapr.sdk.alpha.version -DnewVersion=$DAPR_JAVA_SDK_ALPHA_VERSION mvn versions:set-property -Dproperty=dapr.sdk.version -DnewVersion=$DAPR_JAVA_SDK_VERSION mvn versions:set-property -Dproperty=dapr.sdk.version -DnewVersion=$DAPR_JAVA_SDK_VERSION -f sdk-tests/pom.xml +# BOMs are standalone (no parent), so versions:set skips them — update explicitly. +mvn versions:set -DnewVersion=$DAPR_JAVA_SDK_VERSION -f sdk-bom/pom.xml +mvn versions:set-property -Dproperty=dapr.sdk.version -DnewVersion=$DAPR_JAVA_SDK_VERSION -f sdk-bom/pom.xml +mvn versions:set -DnewVersion=$DAPR_JAVA_SDK_VERSION -f dapr-spring/dapr-spring-bom/pom.xml +mvn versions:set-property -Dproperty=dapr.sdk.version -DnewVersion=$DAPR_JAVA_SDK_VERSION -f dapr-spring/dapr-spring-bom/pom.xml mvn versions:set-property -Dproperty=dapr.sdk.alpha.version -DnewVersion=$DAPR_JAVA_SDK_ALPHA_VERSION -f sdk-tests/pom.xml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 966d461be2..49b3c77cad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ jobs: - name: Run tests run: ./mvnw clean install -B -q -DskipITs=true - name: Codecov - uses: codecov/codecov-action@v5.5.2 + uses: codecov/codecov-action@v6.0.0 with: token: ${{ secrets.CODECOV_TOKEN }} - name: Upload test report for sdk diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 397aea3528..af55da253b 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -53,4 +53,18 @@ jobs: git remote set-url origin https://x-access-token:${{ secrets.DAPR_BOT_TOKEN }}@github.com/${GITHUB_REPOSITORY}.git # Copy first to allow automation to use the latest version and not the release branch's version. cp -R ./.github/scripts ${RUNNER_TEMP}/ - ${RUNNER_TEMP}/scripts/create-release.sh ${{ inputs.rel_version }} \ No newline at end of file + ${RUNNER_TEMP}/scripts/create-release.sh ${{ inputs.rel_version }} + - name: Create GitHub Release with auto-generated notes + if: ${{ !endsWith(inputs.rel_version, '-SNAPSHOT') && !contains(inputs.rel_version, '-rc-') }} + env: + GITHUB_TOKEN: ${{ secrets.DAPR_BOT_TOKEN }} + REL_VERSION: ${{ inputs.rel_version }} + run: | + # Normalize release version by stripping an optional leading 'v' + REL_VERSION="${REL_VERSION#v}" + TAG="v${REL_VERSION}" + if gh release view "${TAG}" >/dev/null 2>&1; then + echo "Release ${TAG} already exists, skipping creation." + else + gh release create "${TAG}" --generate-notes + fi \ No newline at end of file diff --git a/.github/workflows/dapr_bot.yml b/.github/workflows/dapr_bot.yml index 9b101690ba..9823fbb0e5 100644 --- a/.github/workflows/dapr_bot.yml +++ b/.github/workflows/dapr_bot.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Comment analyzer - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: github-token: ${{secrets.DAPR_BOT_TOKEN}} script: | diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml index f6e6efb393..08414ff75f 100644 --- a/.github/workflows/fossa.yml +++ b/.github/workflows/fossa.yml @@ -35,12 +35,12 @@ jobs: uses: actions/checkout@v5 - name: "Run FOSSA Scan" - uses: fossas/fossa-action@v1.8.0 # Use a specific version if locking is preferred + uses: fossas/fossa-action@v1.9.0 # Use a specific version if locking is preferred with: api-key: ${{ env.FOSSA_API_KEY }} - name: "Run FOSSA Test" - uses: fossas/fossa-action@v1.8.0 # Use a specific version if locking is preferred + uses: fossas/fossa-action@v1.9.0 # Use a specific version if locking is preferred with: api-key: ${{ env.FOSSA_API_KEY }} run-tests: true diff --git a/.github/workflows/validate-docs.yml b/.github/workflows/validate-docs.yml index 20c9f6c382..6e07dd0ed5 100644 --- a/.github/workflows/validate-docs.yml +++ b/.github/workflows/validate-docs.yml @@ -17,8 +17,8 @@ on: jobs: build: name: "Validate Javadocs generation" - runs-on: linux-arm64-latest-4-cores - timeout-minutes: 30 + runs-on: oracle-vm-4cpu-16gb-arm64 + timeout-minutes: 60 env: JDK_VER: 17 steps: diff --git a/.gitignore b/.gitignore index 76cb9c94c1..b4490ec90f 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,7 @@ hs_err_pid* # macOS .DS_Store + +# Claude Code / OMC +.omc/ +.omx/ diff --git a/README.md b/README.md index 6dcd63497f..6ab95b01b7 100644 --- a/README.md +++ b/README.md @@ -59,47 +59,143 @@ For the full list of available APIs, see the [Dapr API reference](https://docs.d If using [SDKMAN!](https://sdkman.io), execute `sdk env install` to install the required JDK. ### Importing Dapr's Java SDK -For a Maven project, add the following to your `pom.xml` file: + +#### Using a BOM (recommended) + +Two BOMs are published: + +- **`io.dapr:dapr-sdk-bom`** — core SDK modules (`dapr-sdk`, `dapr-sdk-actors`, `dapr-sdk-workflows`, `dapr-sdk-autogen`, `durabletask-client`, `testcontainers-dapr`) plus security-patched transitive dependencies (Netty, Jackson, commons-compress, commons-codec). +- **`io.dapr.spring:dapr-spring-bom`** — Spring-specific modules (`dapr-sdk-springboot`, `dapr-spring-*`). Imports `dapr-sdk-bom` transitively, so Spring users only need this single BOM. + +Pick the one that matches your project. Importing a BOM ensures you inherit security fixes for transitive dependencies like the Netty CVEs. + +##### Core (non-Spring) projects + +For Maven: ```xml ... + + + + io.dapr + dapr-sdk-bom + 1.18.0 + pom + import + + + + - ... - + io.dapr dapr-sdk - 1.17.0 io.dapr dapr-sdk-actors - 1.17.0 - + + ... + +``` + +For Gradle: +```groovy +dependencies { + implementation platform('io.dapr:dapr-sdk-bom:1.18.0') + + // Dapr's core SDK with all features, except Actors. + implementation 'io.dapr:dapr-sdk' + // Dapr's SDK for Actors (optional). + implementation 'io.dapr:dapr-sdk-actors' +} +``` + +##### Spring Boot projects + +For Maven: +```xml + + ... + + + + io.dapr.spring + dapr-spring-bom + 1.18.0 + pom + import + + + + + + io.dapr dapr-sdk-springboot - 1.17.0 - ... + + + io.dapr.spring + dapr-spring-boot-starter + ... ``` -For a Gradle project, add the following to your `build.gradle` file: +For Gradle: +```groovy +dependencies { + implementation platform('io.dapr.spring:dapr-spring-bom:1.18.0') + + // Dapr's SDK integration with Spring Boot. + implementation 'io.dapr:dapr-sdk-springboot' + // Optional Spring Boot starter. + implementation 'io.dapr.spring:dapr-spring-boot-starter' +} +``` +#### Without the BOM + +If you prefer to manage versions manually, specify the version on each dependency: + +For Maven: +```xml + + ... + + + io.dapr + dapr-sdk + 1.17.2 + + + io.dapr + dapr-sdk-actors + 1.17.2 + + + io.dapr + dapr-sdk-springboot + 1.17.2 + + + ... + ``` + +For Gradle: +```groovy dependencies { -... - // Dapr's core SDK with all features, except Actors. - compile('io.dapr:dapr-sdk:1.17.0') - // Dapr's SDK for Actors (optional). - compile('io.dapr:dapr-sdk-actors:1.17.0') - // Dapr's SDK integration with SpringBoot (optional). - compile('io.dapr:dapr-sdk-springboot:1.17.0') + implementation 'io.dapr:dapr-sdk:1.17.2' + implementation 'io.dapr:dapr-sdk-actors:1.17.2' + implementation 'io.dapr:dapr-sdk-springboot:1.17.2' } ``` diff --git a/dapr-spring/dapr-spring-6-data/pom.xml b/dapr-spring/dapr-spring-6-data/pom.xml index 5142cfdac1..46411f07c3 100644 --- a/dapr-spring/dapr-spring-6-data/pom.xml +++ b/dapr-spring/dapr-spring-6-data/pom.xml @@ -17,8 +17,8 @@ jar - 4.0.2 - + 4.0.5 + 6.0.2 diff --git a/dapr-spring/dapr-spring-bom/pom.xml b/dapr-spring/dapr-spring-bom/pom.xml new file mode 100644 index 0000000000..d15820a210 --- /dev/null +++ b/dapr-spring/dapr-spring-bom/pom.xml @@ -0,0 +1,185 @@ + + 4.0.0 + + io.dapr.spring + dapr-spring-bom + 1.18.0-SNAPSHOT + pom + dapr-spring-bom + Dapr Spring Bill of Materials (BOM). Import this POM to manage versions + of dapr-sdk-springboot and all dapr-spring-* modules. Imports dapr-sdk-bom + transitively, so Spring users only need this single BOM. + https://dapr.io + + + + Apache License Version 2.0 + https://opensource.org/licenses/Apache-2.0 + + + + + + Dapr + daprweb@microsoft.com + Dapr + https://dapr.io + + + + + https://github.com/dapr/java-sdk + scm:git:https://github.com/dapr/java-sdk.git + HEAD + + + + + ossrh + https://central.sonatype.com/repository/maven-snapshots/ + + + + + true + 1.18.0-SNAPSHOT + + + + + + org.apache.maven.plugins + maven-site-plugin + 3.12.1 + + true + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.7.0 + true + + ossrh + https://ossrh-staging-api.central.sonatype.com + true + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.1.0 + + + sign-artifacts + verify + + sign + + + + --batch + --pinentry-mode + loopback + + + + + + + + + + + + + + + + io.dapr + dapr-sdk-bom + ${dapr.sdk.version} + pom + import + + + + + + + io.dapr + dapr-sdk-springboot + ${dapr.sdk.version} + + + + + + + io.dapr.spring + dapr-spring-data + ${dapr.sdk.version} + + + io.dapr.spring + dapr-spring-6-data + ${dapr.sdk.version} + + + io.dapr.spring + dapr-spring-messaging + ${dapr.sdk.version} + + + io.dapr.spring + dapr-spring-workflows + ${dapr.sdk.version} + + + io.dapr.spring + dapr-spring-boot-properties + ${dapr.sdk.version} + + + io.dapr.spring + dapr-spring-boot-autoconfigure + ${dapr.sdk.version} + + + io.dapr.spring + dapr-spring-boot-4-autoconfigure + ${dapr.sdk.version} + + + io.dapr.spring + dapr-spring-boot-tests + ${dapr.sdk.version} + + + io.dapr.spring + dapr-spring-boot-starter + ${dapr.sdk.version} + + + io.dapr.spring + dapr-spring-boot-4-starter + ${dapr.sdk.version} + + + io.dapr.spring + dapr-spring-boot-starter-test + ${dapr.sdk.version} + + + io.dapr.spring + dapr-spring-boot-4-starter-test + ${dapr.sdk.version} + + + + + diff --git a/dapr-spring/dapr-spring-boot-4-autoconfigure/pom.xml b/dapr-spring/dapr-spring-boot-4-autoconfigure/pom.xml index 17dabbcb78..5ed01dc328 100644 --- a/dapr-spring/dapr-spring-boot-4-autoconfigure/pom.xml +++ b/dapr-spring/dapr-spring-boot-4-autoconfigure/pom.xml @@ -16,13 +16,21 @@ jar - 4.0.2 - + 4.0.5 + 6.0.2 + + + tools.jackson + jackson-bom + 3.1.1 + pom + import + org.springframework.boot spring-boot-dependencies @@ -76,6 +84,20 @@ spring-data-keyvalue true + + io.micrometer + micrometer-observation + true + + + io.dapr.spring + dapr-spring-boot-observation + + + io.micrometer + micrometer-observation-test + test + org.springframework.boot spring-boot-starter-web @@ -88,7 +110,7 @@ org.testcontainers - junit-jupiter + testcontainers-junit-jupiter test diff --git a/dapr-spring/dapr-spring-boot-4-autoconfigure/src/main/java/io/dapr/spring/boot4/autoconfigure/client/DaprClientSB4AutoConfiguration.java b/dapr-spring/dapr-spring-boot-4-autoconfigure/src/main/java/io/dapr/spring/boot4/autoconfigure/client/DaprClientSB4AutoConfiguration.java index cbe107ebb4..e36b0affea 100644 --- a/dapr-spring/dapr-spring-boot-4-autoconfigure/src/main/java/io/dapr/spring/boot4/autoconfigure/client/DaprClientSB4AutoConfiguration.java +++ b/dapr-spring/dapr-spring-boot-4-autoconfigure/src/main/java/io/dapr/spring/boot4/autoconfigure/client/DaprClientSB4AutoConfiguration.java @@ -22,8 +22,11 @@ import io.dapr.spring.boot.properties.client.ClientPropertiesDaprConnectionDetails; import io.dapr.spring.boot.properties.client.DaprClientProperties; import io.dapr.spring.boot.properties.client.DaprConnectionDetails; +import io.dapr.spring.observation.client.ObservationDaprClient; +import io.dapr.spring.observation.client.ObservationDaprWorkflowClient; import io.dapr.workflows.client.DaprWorkflowClient; import io.dapr.workflows.runtime.WorkflowRuntimeBuilder; +import io.micrometer.observation.ObservationRegistry; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -91,14 +94,25 @@ DaprClientBuilder daprClientBuilder(DaprConnectionDetails daprConnectionDetails, @Bean @ConditionalOnMissingBean - DaprClient daprClient(DaprClientBuilder daprClientBuilder) { - return daprClientBuilder.build(); + DaprClient daprClient(DaprClientBuilder daprClientBuilder, + ObjectProvider observationRegistryProvider) { + DaprClient client = daprClientBuilder.build(); + ObservationRegistry registry = observationRegistryProvider.getIfAvailable(); + if (registry != null && !registry.isNoop()) { + return new ObservationDaprClient(client, registry); + } + return client; } @Bean @ConditionalOnMissingBean - DaprWorkflowClient daprWorkflowClient(DaprConnectionDetails daprConnectionDetails) { + DaprWorkflowClient daprWorkflowClient(DaprConnectionDetails daprConnectionDetails, + ObjectProvider observationRegistryProvider) { Properties properties = createPropertiesFromConnectionDetails(daprConnectionDetails); + ObservationRegistry registry = observationRegistryProvider.getIfAvailable(); + if (registry != null && !registry.isNoop()) { + return new ObservationDaprWorkflowClient(properties, registry); + } return new DaprWorkflowClient(properties); } diff --git a/dapr-spring/dapr-spring-boot-autoconfigure/pom.xml b/dapr-spring/dapr-spring-boot-autoconfigure/pom.xml index 59ad076197..5d13434f14 100644 --- a/dapr-spring/dapr-spring-boot-autoconfigure/pom.xml +++ b/dapr-spring/dapr-spring-boot-autoconfigure/pom.xml @@ -53,6 +53,20 @@ org.springframework spring-context + + io.micrometer + micrometer-observation + true + + + io.dapr.spring + dapr-spring-boot-observation + + + io.micrometer + micrometer-observation-test + test + org.springframework.boot spring-boot-starter-web diff --git a/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfiguration.java b/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfiguration.java index 041e300d68..9a763ba7c7 100644 --- a/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfiguration.java +++ b/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfiguration.java @@ -21,8 +21,12 @@ import io.dapr.spring.boot.properties.client.ClientPropertiesDaprConnectionDetails; import io.dapr.spring.boot.properties.client.DaprClientProperties; import io.dapr.spring.boot.properties.client.DaprConnectionDetails; +import io.dapr.spring.observation.client.ObservationDaprClient; +import io.dapr.spring.observation.client.ObservationDaprWorkflowClient; import io.dapr.workflows.client.DaprWorkflowClient; import io.dapr.workflows.runtime.WorkflowRuntimeBuilder; +import io.micrometer.observation.ObservationRegistry; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -81,14 +85,25 @@ DaprClientBuilder daprClientBuilder(DaprConnectionDetails daprConnectionDetails) @Bean @ConditionalOnMissingBean - DaprClient daprClient(DaprClientBuilder daprClientBuilder) { - return daprClientBuilder.build(); + DaprClient daprClient(DaprClientBuilder daprClientBuilder, + ObjectProvider observationRegistryProvider) { + DaprClient client = daprClientBuilder.build(); + ObservationRegistry registry = observationRegistryProvider.getIfAvailable(); + if (registry != null && !registry.isNoop()) { + return new ObservationDaprClient(client, registry); + } + return client; } @Bean @ConditionalOnMissingBean - DaprWorkflowClient daprWorkflowClient(DaprConnectionDetails daprConnectionDetails) { + DaprWorkflowClient daprWorkflowClient(DaprConnectionDetails daprConnectionDetails, + ObjectProvider observationRegistryProvider) { Properties properties = createPropertiesFromConnectionDetails(daprConnectionDetails); + ObservationRegistry registry = observationRegistryProvider.getIfAvailable(); + if (registry != null && !registry.isNoop()) { + return new ObservationDaprWorkflowClient(properties, registry); + } return new DaprWorkflowClient(properties); } diff --git a/dapr-spring/dapr-spring-boot-autoconfigure/src/test/java/io/dapr/spring/boot/autoconfigure/client/DaprClientObservationAutoConfigurationTest.java b/dapr-spring/dapr-spring-boot-autoconfigure/src/test/java/io/dapr/spring/boot/autoconfigure/client/DaprClientObservationAutoConfigurationTest.java new file mode 100644 index 0000000000..5141af8021 --- /dev/null +++ b/dapr-spring/dapr-spring-boot-autoconfigure/src/test/java/io/dapr/spring/boot/autoconfigure/client/DaprClientObservationAutoConfigurationTest.java @@ -0,0 +1,161 @@ +/* + * Copyright 2025 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.spring.boot.autoconfigure.client; + +import io.dapr.client.DaprClient; +import io.dapr.client.DaprClientBuilder; +import io.dapr.spring.observation.client.ObservationDaprClient; +import io.dapr.spring.observation.client.ObservationDaprWorkflowClient; +import io.dapr.workflows.client.DaprWorkflowClient; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistry; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Integration tests for the observation wiring in {@link DaprClientAutoConfiguration}. + * + *

Verifies two key requirements: + *

    + *
  1. When a non-noop {@link ObservationRegistry} is present, both {@code DaprClient} and + * {@code DaprWorkflowClient} beans are wrapped with observation decorators.
  2. + *
  3. Consumers inject the beans by their base types ({@code DaprClient}, + * {@code DaprWorkflowClient}) — no code changes needed.
  4. + *
+ */ +class DaprClientObservationAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DaprClientAutoConfiguration.class)); + + // ------------------------------------------------------------------------- + // Without ObservationRegistry — plain beans + // ------------------------------------------------------------------------- + + @Test + @DisplayName("DaprClient is a plain client when no ObservationRegistry is present") + void daprClientIsPlainWhenNoRegistry() { + contextRunner + .withBean(DaprClientBuilder.class, () -> mockBuilderReturningMockClient()) + .run(context -> { + assertThat(context).hasSingleBean(DaprClient.class); + assertThat(context.getBean(DaprClient.class)) + .isNotInstanceOf(ObservationDaprClient.class); + }); + } + + // ------------------------------------------------------------------------- + // With ObservationRegistry — wrapped beans + // ------------------------------------------------------------------------- + + @Test + @DisplayName("DaprClient is ObservationDaprClient when a non-noop ObservationRegistry is present") + void daprClientIsObservationWrappedWhenRegistryPresent() { + contextRunner + .withBean(DaprClientBuilder.class, () -> mockBuilderReturningMockClient()) + .withBean(ObservationRegistry.class, TestObservationRegistry::create) + .run(context -> { + assertThat(context).hasSingleBean(DaprClient.class); + assertThat(context.getBean(DaprClient.class)) + .isInstanceOf(ObservationDaprClient.class); + }); + } + + @Test + @DisplayName("DaprWorkflowClient is ObservationDaprWorkflowClient when a non-noop registry is present") + void daprWorkflowClientIsObservationWrappedWhenRegistryPresent() { + contextRunner + .withBean(DaprClientBuilder.class, () -> mockBuilderReturningMockClient()) + .withBean(ObservationRegistry.class, TestObservationRegistry::create) + .run(context -> { + assertThat(context).hasSingleBean(DaprWorkflowClient.class); + assertThat(context.getBean(DaprWorkflowClient.class)) + .isInstanceOf(ObservationDaprWorkflowClient.class); + }); + } + + // ------------------------------------------------------------------------- + // Transparency — beans remain injectable by base type + // ------------------------------------------------------------------------- + + @Test + @DisplayName("Consumers can inject DaprClient by its base type regardless of wrapping") + void daprClientInjectableByBaseType() { + contextRunner + .withBean(DaprClientBuilder.class, () -> mockBuilderReturningMockClient()) + .withBean(ObservationRegistry.class, TestObservationRegistry::create) + .run(context -> { + // Injecting as DaprClient works — no ClassCastException, no code changes needed + DaprClient client = context.getBean(DaprClient.class); + assertThat(client).isNotNull(); + assertThat(client).isInstanceOf(DaprClient.class); + }); + } + + @Test + @DisplayName("Consumers can inject DaprWorkflowClient by its base type regardless of wrapping") + void daprWorkflowClientInjectableByBaseType() { + contextRunner + .withBean(DaprClientBuilder.class, () -> mockBuilderReturningMockClient()) + .withBean(ObservationRegistry.class, TestObservationRegistry::create) + .run(context -> { + // Injecting as DaprWorkflowClient works — ObservationDaprWorkflowClient IS-A DaprWorkflowClient + DaprWorkflowClient workflowClient = context.getBean(DaprWorkflowClient.class); + assertThat(workflowClient).isNotNull(); + assertThat(workflowClient).isInstanceOf(DaprWorkflowClient.class); + }); + } + + @Test + @DisplayName("Noop ObservationRegistry results in plain (unwrapped) DaprClient") + void noopRegistryResultsInPlainClient() { + contextRunner + .withBean(DaprClientBuilder.class, () -> mockBuilderReturningMockClient()) + .withBean(ObservationRegistry.class, ObservationRegistry::create) // NOOP registry + .run(context -> { + assertThat(context).hasSingleBean(DaprClient.class); + assertThat(context.getBean(DaprClient.class)) + .isNotInstanceOf(ObservationDaprClient.class); + }); + } + + @Test + @DisplayName("User-provided DaprClient bean is not replaced by autoconfiguration") + void userProvidedDaprClientIsNotReplaced() { + DaprClient userClient = mock(DaprClient.class); + contextRunner + .withBean(ObservationRegistry.class, TestObservationRegistry::create) + .withBean(DaprClient.class, () -> userClient) + .run(context -> { + assertThat(context).hasSingleBean(DaprClient.class); + assertThat(context.getBean(DaprClient.class)).isSameAs(userClient); + }); + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private static DaprClientBuilder mockBuilderReturningMockClient() { + DaprClientBuilder builder = mock(DaprClientBuilder.class); + when(builder.build()).thenReturn(mock(DaprClient.class)); + return builder; + } +} diff --git a/dapr-spring/dapr-spring-boot-observation/pom.xml b/dapr-spring/dapr-spring-boot-observation/pom.xml new file mode 100644 index 0000000000..c78b98cc94 --- /dev/null +++ b/dapr-spring/dapr-spring-boot-observation/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + + io.dapr.spring + dapr-spring-parent + 1.18.0-SNAPSHOT + ../pom.xml + + + dapr-spring-boot-observation + dapr-spring-boot-observation + Spring-agnostic Micrometer Observation decorators for DaprClient and DaprWorkflowClient, with OpenTelemetry trace propagation to the Dapr sidecar over gRPC. + jar + + + + io.dapr + dapr-sdk + + + io.dapr + dapr-sdk-workflows + + + io.micrometer + micrometer-observation + + + io.opentelemetry + opentelemetry-api + + + + + io.micrometer + micrometer-observation-test + test + + + org.junit.jupiter + junit-jupiter + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + + + org.assertj + assertj-core + test + + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + + + + diff --git a/dapr-spring/dapr-spring-boot-observation/src/main/java/io/dapr/spring/observation/client/ObservationDaprClient.java b/dapr-spring/dapr-spring-boot-observation/src/main/java/io/dapr/spring/observation/client/ObservationDaprClient.java new file mode 100644 index 0000000000..575a1887fc --- /dev/null +++ b/dapr-spring/dapr-spring-boot-observation/src/main/java/io/dapr/spring/observation/client/ObservationDaprClient.java @@ -0,0 +1,799 @@ +/* + * Copyright 2025 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.spring.observation.client; + +import io.dapr.client.DaprClient; +import io.dapr.client.domain.BulkPublishRequest; +import io.dapr.client.domain.BulkPublishResponse; +import io.dapr.client.domain.ConfigurationItem; +import io.dapr.client.domain.DaprMetadata; +import io.dapr.client.domain.DeleteJobRequest; +import io.dapr.client.domain.DeleteStateRequest; +import io.dapr.client.domain.ExecuteStateTransactionRequest; +import io.dapr.client.domain.GetBulkSecretRequest; +import io.dapr.client.domain.GetBulkStateRequest; +import io.dapr.client.domain.GetConfigurationRequest; +import io.dapr.client.domain.GetJobRequest; +import io.dapr.client.domain.GetJobResponse; +import io.dapr.client.domain.GetSecretRequest; +import io.dapr.client.domain.GetStateRequest; +import io.dapr.client.domain.HttpExtension; +import io.dapr.client.domain.InvokeBindingRequest; +import io.dapr.client.domain.InvokeMethodRequest; +import io.dapr.client.domain.PublishEventRequest; +import io.dapr.client.domain.SaveStateRequest; +import io.dapr.client.domain.ScheduleJobRequest; +import io.dapr.client.domain.State; +import io.dapr.client.domain.StateOptions; +import io.dapr.client.domain.SubscribeConfigurationRequest; +import io.dapr.client.domain.SubscribeConfigurationResponse; +import io.dapr.client.domain.TransactionalStateOperation; +import io.dapr.client.domain.UnsubscribeConfigurationRequest; +import io.dapr.client.domain.UnsubscribeConfigurationResponse; +import io.dapr.utils.TypeRef; +import io.grpc.Channel; +import io.grpc.stub.AbstractStub; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.context.Context; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * A {@link DaprClient} decorator that creates Micrometer Observation spans (bridged to OpenTelemetry) + * for each non-deprecated method call. Consumers continue to use {@link DaprClient} as-is; no code + * changes are required on their side. + * + *

Deprecated methods are delegated directly without any observation. + */ +public class ObservationDaprClient implements DaprClient { + + private final DaprClient delegate; + private final ObservationRegistry observationRegistry; + + /** + * Creates a new {@code ObservationDaprClient}. + * + * @param delegate the underlying {@link DaprClient} to delegate calls to + * @param observationRegistry the Micrometer {@link ObservationRegistry} used to create spans + */ + public ObservationDaprClient(DaprClient delegate, ObservationRegistry observationRegistry) { + this.delegate = Objects.requireNonNull(delegate, "delegate must not be null"); + this.observationRegistry = Objects.requireNonNull(observationRegistry, + "observationRegistry must not be null"); + } + + // ------------------------------------------------------------------------- + // Internal helpers + // ------------------------------------------------------------------------- + + private Mono observe(Observation obs, Supplier> monoSupplier) { + return Mono.defer(() -> { + obs.start(); + // Open a scope so the Micrometer-OTel bridge makes the new span current in the + // OTel thread-local; capture its W3C traceparent immediately, then close the scope. + // The captured context is written into the Reactor context so the downstream gRPC + // call (inside DaprClientImpl.deferContextual) uses this span as its parent. + SpanContext spanCtx; + try (Observation.Scope ignored = obs.openScope()) { + spanCtx = Span.current().getSpanContext(); + } + return monoSupplier.get() + .doOnError(obs::error) + .doFinally(signal -> obs.stop()) + .contextWrite(ctx -> enrichWithSpanContext(ctx, spanCtx)); + }); + } + + private Flux observeFlux(Observation obs, Supplier> fluxSupplier) { + return Flux.defer(() -> { + obs.start(); + SpanContext spanCtx; + try (Observation.Scope ignored = obs.openScope()) { + spanCtx = Span.current().getSpanContext(); + } + return fluxSupplier.get() + .doOnError(obs::error) + .doFinally(signal -> obs.stop()) + .contextWrite(ctx -> enrichWithSpanContext(ctx, spanCtx)); + }); + } + + /** + * Enriches the Reactor {@link Context} with W3C {@code traceparent} (and optionally + * {@code tracestate}) extracted from the given OTel {@link SpanContext}. + * This bridges the Micrometer Observation span into the string-keyed Reactor context that + * {@link io.dapr.client.DaprClientImpl} reads to populate gRPC metadata headers. + */ + private static Context enrichWithSpanContext(Context ctx, SpanContext spanCtx) { + if (spanCtx == null || !spanCtx.isValid()) { + return ctx; + } + ctx = ctx.put("traceparent", TraceContextFormat.formatW3cTraceparent(spanCtx)); + String traceState = TraceContextFormat.formatTraceState(spanCtx); + if (!traceState.isEmpty()) { + ctx = ctx.put("tracestate", traceState); + } + return ctx; + } + + private static String safe(String value) { + return value != null ? value : ""; + } + + private Observation observation(String name) { + return Observation.createNotStarted(name, observationRegistry); + } + + // ------------------------------------------------------------------------- + // Lifecycle + // ------------------------------------------------------------------------- + + @Override + public Mono waitForSidecar(int timeoutInMilliseconds) { + return observe(observation("dapr.client.wait_for_sidecar"), + () -> delegate.waitForSidecar(timeoutInMilliseconds)); + } + + @Override + public Mono shutdown() { + return observe(observation("dapr.client.shutdown"), + () -> delegate.shutdown()); + } + + @Override + public void close() throws Exception { + delegate.close(); + } + + // ------------------------------------------------------------------------- + // Pub/Sub + // ------------------------------------------------------------------------- + + @Override + public Mono publishEvent(String pubsubName, String topicName, Object data) { + return observe( + observation("dapr.client.publish_event") + .highCardinalityKeyValue("dapr.pubsub.name", safe(pubsubName)) + .highCardinalityKeyValue("dapr.topic.name", safe(topicName)), + () -> delegate.publishEvent(pubsubName, topicName, data)); + } + + @Override + public Mono publishEvent(String pubsubName, String topicName, Object data, + Map metadata) { + return observe( + observation("dapr.client.publish_event") + .highCardinalityKeyValue("dapr.pubsub.name", safe(pubsubName)) + .highCardinalityKeyValue("dapr.topic.name", safe(topicName)), + () -> delegate.publishEvent(pubsubName, topicName, data, metadata)); + } + + @Override + public Mono publishEvent(PublishEventRequest request) { + return observe( + observation("dapr.client.publish_event") + .highCardinalityKeyValue("dapr.pubsub.name", safe(request.getPubsubName())) + .highCardinalityKeyValue("dapr.topic.name", safe(request.getTopic())), + () -> delegate.publishEvent(request)); + } + + @Override + public Mono> publishEvents(BulkPublishRequest request) { + return observe( + observation("dapr.client.publish_events") + .highCardinalityKeyValue("dapr.pubsub.name", safe(request.getPubsubName())) + .highCardinalityKeyValue("dapr.topic.name", safe(request.getTopic())), + () -> delegate.publishEvents(request)); + } + + @Override + public Mono> publishEvents(String pubsubName, String topicName, + String contentType, List events) { + return observe( + observation("dapr.client.publish_events") + .highCardinalityKeyValue("dapr.pubsub.name", safe(pubsubName)) + .highCardinalityKeyValue("dapr.topic.name", safe(topicName)), + () -> delegate.publishEvents(pubsubName, topicName, contentType, events)); + } + + @Override + @SuppressWarnings("unchecked") + public Mono> publishEvents(String pubsubName, String topicName, + String contentType, T... events) { + return observe( + observation("dapr.client.publish_events") + .highCardinalityKeyValue("dapr.pubsub.name", safe(pubsubName)) + .highCardinalityKeyValue("dapr.topic.name", safe(topicName)), + () -> delegate.publishEvents(pubsubName, topicName, contentType, events)); + } + + @Override + public Mono> publishEvents(String pubsubName, String topicName, + String contentType, + Map requestMetadata, + List events) { + return observe( + observation("dapr.client.publish_events") + .highCardinalityKeyValue("dapr.pubsub.name", safe(pubsubName)) + .highCardinalityKeyValue("dapr.topic.name", safe(topicName)), + () -> delegate.publishEvents(pubsubName, topicName, contentType, requestMetadata, events)); + } + + @Override + @SuppressWarnings("unchecked") + public Mono> publishEvents(String pubsubName, String topicName, + String contentType, + Map requestMetadata, + T... events) { + return observe( + observation("dapr.client.publish_events") + .highCardinalityKeyValue("dapr.pubsub.name", safe(pubsubName)) + .highCardinalityKeyValue("dapr.topic.name", safe(topicName)), + () -> delegate.publishEvents(pubsubName, topicName, contentType, requestMetadata, events)); + } + + // ------------------------------------------------------------------------- + // Service Invocation — all deprecated, delegate directly + // ------------------------------------------------------------------------- + + @Override + @Deprecated + public Mono invokeMethod(String appId, String methodName, Object data, + HttpExtension httpExtension, Map metadata, + TypeRef type) { + return delegate.invokeMethod(appId, methodName, data, httpExtension, metadata, type); + } + + @Override + @Deprecated + public Mono invokeMethod(String appId, String methodName, Object request, + HttpExtension httpExtension, Map metadata, + Class clazz) { + return delegate.invokeMethod(appId, methodName, request, httpExtension, metadata, clazz); + } + + @Override + @Deprecated + public Mono invokeMethod(String appId, String methodName, Object request, + HttpExtension httpExtension, TypeRef type) { + return delegate.invokeMethod(appId, methodName, request, httpExtension, type); + } + + @Override + @Deprecated + public Mono invokeMethod(String appId, String methodName, Object request, + HttpExtension httpExtension, Class clazz) { + return delegate.invokeMethod(appId, methodName, request, httpExtension, clazz); + } + + @Override + @Deprecated + public Mono invokeMethod(String appId, String methodName, HttpExtension httpExtension, + Map metadata, TypeRef type) { + return delegate.invokeMethod(appId, methodName, httpExtension, metadata, type); + } + + @Override + @Deprecated + public Mono invokeMethod(String appId, String methodName, HttpExtension httpExtension, + Map metadata, Class clazz) { + return delegate.invokeMethod(appId, methodName, httpExtension, metadata, clazz); + } + + @Override + @Deprecated + public Mono invokeMethod(String appId, String methodName, Object request, + HttpExtension httpExtension, Map metadata) { + return delegate.invokeMethod(appId, methodName, request, httpExtension, metadata); + } + + @Override + @Deprecated + public Mono invokeMethod(String appId, String methodName, Object request, + HttpExtension httpExtension) { + return delegate.invokeMethod(appId, methodName, request, httpExtension); + } + + @Override + @Deprecated + public Mono invokeMethod(String appId, String methodName, HttpExtension httpExtension, + Map metadata) { + return delegate.invokeMethod(appId, methodName, httpExtension, metadata); + } + + @Override + @Deprecated + public Mono invokeMethod(String appId, String methodName, byte[] request, + HttpExtension httpExtension, Map metadata) { + return delegate.invokeMethod(appId, methodName, request, httpExtension, metadata); + } + + @Override + @Deprecated + public Mono invokeMethod(InvokeMethodRequest invokeMethodRequest, TypeRef type) { + return delegate.invokeMethod(invokeMethodRequest, type); + } + + // ------------------------------------------------------------------------- + // Bindings + // ------------------------------------------------------------------------- + + @Override + public Mono invokeBinding(String bindingName, String operation, Object data) { + return observe( + observation("dapr.client.invoke_binding") + .highCardinalityKeyValue("dapr.binding.name", safe(bindingName)) + .highCardinalityKeyValue("dapr.binding.operation", safe(operation)), + () -> delegate.invokeBinding(bindingName, operation, data)); + } + + @Override + public Mono invokeBinding(String bindingName, String operation, byte[] data, + Map metadata) { + return observe( + observation("dapr.client.invoke_binding") + .highCardinalityKeyValue("dapr.binding.name", safe(bindingName)) + .highCardinalityKeyValue("dapr.binding.operation", safe(operation)), + () -> delegate.invokeBinding(bindingName, operation, data, metadata)); + } + + @Override + public Mono invokeBinding(String bindingName, String operation, Object data, + TypeRef type) { + return observe( + observation("dapr.client.invoke_binding") + .highCardinalityKeyValue("dapr.binding.name", safe(bindingName)) + .highCardinalityKeyValue("dapr.binding.operation", safe(operation)), + () -> delegate.invokeBinding(bindingName, operation, data, type)); + } + + @Override + public Mono invokeBinding(String bindingName, String operation, Object data, + Class clazz) { + return observe( + observation("dapr.client.invoke_binding") + .highCardinalityKeyValue("dapr.binding.name", safe(bindingName)) + .highCardinalityKeyValue("dapr.binding.operation", safe(operation)), + () -> delegate.invokeBinding(bindingName, operation, data, clazz)); + } + + @Override + public Mono invokeBinding(String bindingName, String operation, Object data, + Map metadata, TypeRef type) { + return observe( + observation("dapr.client.invoke_binding") + .highCardinalityKeyValue("dapr.binding.name", safe(bindingName)) + .highCardinalityKeyValue("dapr.binding.operation", safe(operation)), + () -> delegate.invokeBinding(bindingName, operation, data, metadata, type)); + } + + @Override + public Mono invokeBinding(String bindingName, String operation, Object data, + Map metadata, Class clazz) { + return observe( + observation("dapr.client.invoke_binding") + .highCardinalityKeyValue("dapr.binding.name", safe(bindingName)) + .highCardinalityKeyValue("dapr.binding.operation", safe(operation)), + () -> delegate.invokeBinding(bindingName, operation, data, metadata, clazz)); + } + + @Override + public Mono invokeBinding(InvokeBindingRequest request) { + return observe( + observation("dapr.client.invoke_binding") + .highCardinalityKeyValue("dapr.binding.name", safe(request.getName())) + .highCardinalityKeyValue("dapr.binding.operation", safe(request.getOperation())), + () -> delegate.invokeBinding(request)); + } + + @Override + public Mono invokeBinding(InvokeBindingRequest request, TypeRef type) { + return observe( + observation("dapr.client.invoke_binding") + .highCardinalityKeyValue("dapr.binding.name", safe(request.getName())) + .highCardinalityKeyValue("dapr.binding.operation", safe(request.getOperation())), + () -> delegate.invokeBinding(request, type)); + } + + // ------------------------------------------------------------------------- + // State Management + // ------------------------------------------------------------------------- + + @Override + public Mono> getState(String storeName, State state, TypeRef type) { + return observe( + observation("dapr.client.get_state") + .highCardinalityKeyValue("dapr.store.name", safe(storeName)) + .highCardinalityKeyValue("dapr.state.key", safe(state.getKey())), + () -> delegate.getState(storeName, state, type)); + } + + @Override + public Mono> getState(String storeName, State state, Class clazz) { + return observe( + observation("dapr.client.get_state") + .highCardinalityKeyValue("dapr.store.name", safe(storeName)) + .highCardinalityKeyValue("dapr.state.key", safe(state.getKey())), + () -> delegate.getState(storeName, state, clazz)); + } + + @Override + public Mono> getState(String storeName, String key, TypeRef type) { + return observe( + observation("dapr.client.get_state") + .highCardinalityKeyValue("dapr.store.name", safe(storeName)) + .highCardinalityKeyValue("dapr.state.key", safe(key)), + () -> delegate.getState(storeName, key, type)); + } + + @Override + public Mono> getState(String storeName, String key, Class clazz) { + return observe( + observation("dapr.client.get_state") + .highCardinalityKeyValue("dapr.store.name", safe(storeName)) + .highCardinalityKeyValue("dapr.state.key", safe(key)), + () -> delegate.getState(storeName, key, clazz)); + } + + @Override + public Mono> getState(String storeName, String key, StateOptions options, + TypeRef type) { + return observe( + observation("dapr.client.get_state") + .highCardinalityKeyValue("dapr.store.name", safe(storeName)) + .highCardinalityKeyValue("dapr.state.key", safe(key)), + () -> delegate.getState(storeName, key, options, type)); + } + + @Override + public Mono> getState(String storeName, String key, StateOptions options, + Class clazz) { + return observe( + observation("dapr.client.get_state") + .highCardinalityKeyValue("dapr.store.name", safe(storeName)) + .highCardinalityKeyValue("dapr.state.key", safe(key)), + () -> delegate.getState(storeName, key, options, clazz)); + } + + @Override + public Mono> getState(GetStateRequest request, TypeRef type) { + return observe( + observation("dapr.client.get_state") + .highCardinalityKeyValue("dapr.store.name", safe(request.getStoreName())) + .highCardinalityKeyValue("dapr.state.key", safe(request.getKey())), + () -> delegate.getState(request, type)); + } + + @Override + public Mono>> getBulkState(String storeName, List keys, + TypeRef type) { + return observe( + observation("dapr.client.get_bulk_state") + .highCardinalityKeyValue("dapr.store.name", safe(storeName)), + () -> delegate.getBulkState(storeName, keys, type)); + } + + @Override + public Mono>> getBulkState(String storeName, List keys, + Class clazz) { + return observe( + observation("dapr.client.get_bulk_state") + .highCardinalityKeyValue("dapr.store.name", safe(storeName)), + () -> delegate.getBulkState(storeName, keys, clazz)); + } + + @Override + public Mono>> getBulkState(GetBulkStateRequest request, TypeRef type) { + return observe( + observation("dapr.client.get_bulk_state") + .highCardinalityKeyValue("dapr.store.name", safe(request.getStoreName())), + () -> delegate.getBulkState(request, type)); + } + + @Override + public Mono executeStateTransaction(String storeName, + List> operations) { + return observe( + observation("dapr.client.execute_state_transaction") + .highCardinalityKeyValue("dapr.store.name", safe(storeName)), + () -> delegate.executeStateTransaction(storeName, operations)); + } + + @Override + public Mono executeStateTransaction(ExecuteStateTransactionRequest request) { + return observe( + observation("dapr.client.execute_state_transaction") + .highCardinalityKeyValue("dapr.store.name", safe(request.getStateStoreName())), + () -> delegate.executeStateTransaction(request)); + } + + @Override + public Mono saveBulkState(String storeName, List> states) { + return observe( + observation("dapr.client.save_bulk_state") + .highCardinalityKeyValue("dapr.store.name", safe(storeName)), + () -> delegate.saveBulkState(storeName, states)); + } + + @Override + public Mono saveBulkState(SaveStateRequest request) { + return observe( + observation("dapr.client.save_bulk_state") + .highCardinalityKeyValue("dapr.store.name", safe(request.getStoreName())), + () -> delegate.saveBulkState(request)); + } + + @Override + public Mono saveState(String storeName, String key, Object value) { + return observe( + observation("dapr.client.save_state") + .highCardinalityKeyValue("dapr.store.name", safe(storeName)) + .highCardinalityKeyValue("dapr.state.key", safe(key)), + () -> delegate.saveState(storeName, key, value)); + } + + @Override + public Mono saveState(String storeName, String key, String etag, Object value, + StateOptions options) { + return observe( + observation("dapr.client.save_state") + .highCardinalityKeyValue("dapr.store.name", safe(storeName)) + .highCardinalityKeyValue("dapr.state.key", safe(key)), + () -> delegate.saveState(storeName, key, etag, value, options)); + } + + @Override + public Mono saveState(String storeName, String key, String etag, Object value, + Map meta, StateOptions options) { + return observe( + observation("dapr.client.save_state") + .highCardinalityKeyValue("dapr.store.name", safe(storeName)) + .highCardinalityKeyValue("dapr.state.key", safe(key)), + () -> delegate.saveState(storeName, key, etag, value, meta, options)); + } + + @Override + public Mono deleteState(String storeName, String key) { + return observe( + observation("dapr.client.delete_state") + .highCardinalityKeyValue("dapr.store.name", safe(storeName)) + .highCardinalityKeyValue("dapr.state.key", safe(key)), + () -> delegate.deleteState(storeName, key)); + } + + @Override + public Mono deleteState(String storeName, String key, String etag, + StateOptions options) { + return observe( + observation("dapr.client.delete_state") + .highCardinalityKeyValue("dapr.store.name", safe(storeName)) + .highCardinalityKeyValue("dapr.state.key", safe(key)), + () -> delegate.deleteState(storeName, key, etag, options)); + } + + @Override + public Mono deleteState(DeleteStateRequest request) { + return observe( + observation("dapr.client.delete_state") + .highCardinalityKeyValue("dapr.store.name", safe(request.getStateStoreName())) + .highCardinalityKeyValue("dapr.state.key", safe(request.getKey())), + () -> delegate.deleteState(request)); + } + + // ------------------------------------------------------------------------- + // Secrets + // ------------------------------------------------------------------------- + + @Override + public Mono> getSecret(String storeName, String secretName, + Map metadata) { + return observe( + observation("dapr.client.get_secret") + .highCardinalityKeyValue("dapr.secret.store", safe(storeName)) + .highCardinalityKeyValue("dapr.secret.name", safe(secretName)), + () -> delegate.getSecret(storeName, secretName, metadata)); + } + + @Override + public Mono> getSecret(String storeName, String secretName) { + return observe( + observation("dapr.client.get_secret") + .highCardinalityKeyValue("dapr.secret.store", safe(storeName)) + .highCardinalityKeyValue("dapr.secret.name", safe(secretName)), + () -> delegate.getSecret(storeName, secretName)); + } + + @Override + public Mono> getSecret(GetSecretRequest request) { + return observe( + observation("dapr.client.get_secret") + .highCardinalityKeyValue("dapr.secret.store", safe(request.getStoreName())) + .highCardinalityKeyValue("dapr.secret.name", safe(request.getKey())), + () -> delegate.getSecret(request)); + } + + @Override + public Mono>> getBulkSecret(String storeName) { + return observe( + observation("dapr.client.get_bulk_secret") + .highCardinalityKeyValue("dapr.secret.store", safe(storeName)), + () -> delegate.getBulkSecret(storeName)); + } + + @Override + public Mono>> getBulkSecret(String storeName, + Map metadata) { + return observe( + observation("dapr.client.get_bulk_secret") + .highCardinalityKeyValue("dapr.secret.store", safe(storeName)), + () -> delegate.getBulkSecret(storeName, metadata)); + } + + @Override + public Mono>> getBulkSecret(GetBulkSecretRequest request) { + return observe( + observation("dapr.client.get_bulk_secret") + .highCardinalityKeyValue("dapr.secret.store", safe(request.getStoreName())), + () -> delegate.getBulkSecret(request)); + } + + // ------------------------------------------------------------------------- + // Configuration + // ------------------------------------------------------------------------- + + @Override + public Mono getConfiguration(String storeName, String key) { + return observe( + observation("dapr.client.get_configuration") + .highCardinalityKeyValue("dapr.configuration.store", safe(storeName)) + .highCardinalityKeyValue("dapr.configuration.key", safe(key)), + () -> delegate.getConfiguration(storeName, key)); + } + + @Override + public Mono getConfiguration(String storeName, String key, + Map metadata) { + return observe( + observation("dapr.client.get_configuration") + .highCardinalityKeyValue("dapr.configuration.store", safe(storeName)) + .highCardinalityKeyValue("dapr.configuration.key", safe(key)), + () -> delegate.getConfiguration(storeName, key, metadata)); + } + + @Override + public Mono> getConfiguration(String storeName, String... keys) { + return observe( + observation("dapr.client.get_configuration") + .highCardinalityKeyValue("dapr.configuration.store", safe(storeName)), + () -> delegate.getConfiguration(storeName, keys)); + } + + @Override + public Mono> getConfiguration(String storeName, List keys, + Map metadata) { + return observe( + observation("dapr.client.get_configuration") + .highCardinalityKeyValue("dapr.configuration.store", safe(storeName)), + () -> delegate.getConfiguration(storeName, keys, metadata)); + } + + @Override + public Mono> getConfiguration(GetConfigurationRequest request) { + return observe( + observation("dapr.client.get_configuration") + .highCardinalityKeyValue("dapr.configuration.store", safe(request.getStoreName())), + () -> delegate.getConfiguration(request)); + } + + @Override + public Flux subscribeConfiguration(String storeName, + String... keys) { + return observeFlux( + observation("dapr.client.subscribe_configuration") + .highCardinalityKeyValue("dapr.configuration.store", safe(storeName)), + () -> delegate.subscribeConfiguration(storeName, keys)); + } + + @Override + public Flux subscribeConfiguration(String storeName, + List keys, + Map metadata) { + return observeFlux( + observation("dapr.client.subscribe_configuration") + .highCardinalityKeyValue("dapr.configuration.store", safe(storeName)), + () -> delegate.subscribeConfiguration(storeName, keys, metadata)); + } + + @Override + public Flux subscribeConfiguration( + SubscribeConfigurationRequest request) { + return observeFlux( + observation("dapr.client.subscribe_configuration") + .highCardinalityKeyValue("dapr.configuration.store", safe(request.getStoreName())), + () -> delegate.subscribeConfiguration(request)); + } + + @Override + public Mono unsubscribeConfiguration(String id, + String storeName) { + return observe( + observation("dapr.client.unsubscribe_configuration") + .highCardinalityKeyValue("dapr.configuration.store", safe(storeName)), + () -> delegate.unsubscribeConfiguration(id, storeName)); + } + + @Override + public Mono unsubscribeConfiguration( + UnsubscribeConfigurationRequest request) { + return observe( + observation("dapr.client.unsubscribe_configuration") + .highCardinalityKeyValue("dapr.configuration.store", safe(request.getStoreName())), + () -> delegate.unsubscribeConfiguration(request)); + } + + // ------------------------------------------------------------------------- + // gRPC Stub — no remote call at creation time, no observation needed + // ------------------------------------------------------------------------- + + @Override + public > T newGrpcStub(String appId, Function stubBuilder) { + return delegate.newGrpcStub(appId, stubBuilder); + } + + // ------------------------------------------------------------------------- + // Metadata + // ------------------------------------------------------------------------- + + @Override + public Mono getMetadata() { + return observe(observation("dapr.client.get_metadata"), () -> delegate.getMetadata()); + } + + // ------------------------------------------------------------------------- + // Jobs + // ------------------------------------------------------------------------- + + @Override + public Mono scheduleJob(ScheduleJobRequest scheduleJobRequest) { + return observe( + observation("dapr.client.schedule_job") + .highCardinalityKeyValue("dapr.job.name", safe(scheduleJobRequest.getName())), + () -> delegate.scheduleJob(scheduleJobRequest)); + } + + @Override + public Mono getJob(GetJobRequest getJobRequest) { + return observe( + observation("dapr.client.get_job") + .highCardinalityKeyValue("dapr.job.name", safe(getJobRequest.getName())), + () -> delegate.getJob(getJobRequest)); + } + + @Override + public Mono deleteJob(DeleteJobRequest deleteJobRequest) { + return observe( + observation("dapr.client.delete_job") + .highCardinalityKeyValue("dapr.job.name", safe(deleteJobRequest.getName())), + () -> delegate.deleteJob(deleteJobRequest)); + } +} diff --git a/dapr-spring/dapr-spring-boot-observation/src/main/java/io/dapr/spring/observation/client/ObservationDaprWorkflowClient.java b/dapr-spring/dapr-spring-boot-observation/src/main/java/io/dapr/spring/observation/client/ObservationDaprWorkflowClient.java new file mode 100644 index 0000000000..4366857e75 --- /dev/null +++ b/dapr-spring/dapr-spring-boot-observation/src/main/java/io/dapr/spring/observation/client/ObservationDaprWorkflowClient.java @@ -0,0 +1,379 @@ +/* + * Copyright 2025 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.spring.observation.client; + +import io.dapr.config.Properties; +import io.dapr.internal.opencensus.GrpcHelper; +import io.dapr.workflows.Workflow; +import io.dapr.workflows.client.DaprWorkflowClient; +import io.dapr.workflows.client.NewWorkflowOptions; +import io.dapr.workflows.client.WorkflowState; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import reactor.util.context.Context; + +import javax.annotation.Nullable; +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.TimeoutException; + +/** + * A {@link DaprWorkflowClient} subclass that creates Micrometer Observation spans (bridged to + * OpenTelemetry) for each non-deprecated method call. + * + *

Because this class extends {@link DaprWorkflowClient}, consumers can keep injecting + * {@code DaprWorkflowClient} without any code changes. Deprecated methods fall through to the + * parent implementation without any observation. + * + *

Trace propagation: an {@link OtelTracingClientInterceptor} is registered on the gRPC + * channel. For each synchronous workflow RPC, the observation opens an OTel scope (via + * {@link Observation#openScope()}) before calling {@code super.*}, making the observation span the + * current OTel span in thread-local. The interceptor then reads {@link Span#current()} and injects + * its W3C {@code traceparent} (and {@code grpc-trace-bin}) into the gRPC request headers so the + * Dapr sidecar receives the full trace context. + * + *

Constructor note: calling {@code super(properties, interceptor)} eagerly creates a gRPC + * {@code ManagedChannel}, but the actual TCP connection is established lazily on the first RPC call, + * so construction succeeds even when the Dapr sidecar is not yet available. + */ +public class ObservationDaprWorkflowClient extends DaprWorkflowClient { + + private static final OtelTracingClientInterceptor TRACING_INTERCEPTOR = + new OtelTracingClientInterceptor(); + + private final ObservationRegistry observationRegistry; + + /** + * Creates a new {@code ObservationDaprWorkflowClient}. + * + * @param properties connection properties for the underlying gRPC channel + * @param observationRegistry the Micrometer {@link ObservationRegistry} used to create spans + */ + public ObservationDaprWorkflowClient(Properties properties, + ObservationRegistry observationRegistry) { + super(properties, TRACING_INTERCEPTOR); + this.observationRegistry = Objects.requireNonNull(observationRegistry, + "observationRegistry must not be null"); + } + + // ------------------------------------------------------------------------- + // Internal helpers + // ------------------------------------------------------------------------- + + private Observation observation(String name) { + return Observation.createNotStarted(name, observationRegistry); + } + + // ------------------------------------------------------------------------- + // scheduleNewWorkflow — only String-based "leaf" overloads are overridden. + // Class-based overloads in the parent delegate to this.scheduleNewWorkflow(String, ...) + // via dynamic dispatch, so they naturally pick up these observations. + // ------------------------------------------------------------------------- + + @Override + public String scheduleNewWorkflow(String name) { + Observation obs = observation("dapr.workflow.schedule") + .highCardinalityKeyValue("dapr.workflow.name", name) + .start(); + try { + try (Observation.Scope ignored = obs.openScope()) { + return super.scheduleNewWorkflow(name); + } + } catch (RuntimeException e) { + obs.error(e); + throw e; + } finally { + obs.stop(); + } + } + + @Override + public String scheduleNewWorkflow(String name, Object input) { + Observation obs = observation("dapr.workflow.schedule") + .highCardinalityKeyValue("dapr.workflow.name", name) + .start(); + try { + try (Observation.Scope ignored = obs.openScope()) { + return super.scheduleNewWorkflow(name, input); + } + } catch (RuntimeException e) { + obs.error(e); + throw e; + } finally { + obs.stop(); + } + } + + @Override + public String scheduleNewWorkflow(String name, Object input, + String instanceId) { + Observation obs = observation("dapr.workflow.schedule") + .highCardinalityKeyValue("dapr.workflow.name", name) + .highCardinalityKeyValue("dapr.workflow.instance_id", + instanceId != null ? instanceId : "") + .start(); + try { + try (Observation.Scope ignored = obs.openScope()) { + return super.scheduleNewWorkflow(name, input, instanceId); + } + } catch (RuntimeException e) { + obs.error(e); + throw e; + } finally { + obs.stop(); + } + } + + @Override + public String scheduleNewWorkflow(String name, + NewWorkflowOptions options) { + String instanceId = options != null && options.getInstanceId() != null + ? options.getInstanceId() : ""; + Observation obs = observation("dapr.workflow.schedule") + .highCardinalityKeyValue("dapr.workflow.name", name) + .highCardinalityKeyValue("dapr.workflow.instance_id", instanceId) + .start(); + try { + try (Observation.Scope ignored = obs.openScope()) { + return super.scheduleNewWorkflow(name, options); + } + } catch (RuntimeException e) { + obs.error(e); + throw e; + } finally { + obs.stop(); + } + } + + // ------------------------------------------------------------------------- + // Lifecycle operations + // ------------------------------------------------------------------------- + + @Override + public void suspendWorkflow(String workflowInstanceId, @Nullable String reason) { + Observation obs = observation("dapr.workflow.suspend") + .highCardinalityKeyValue("dapr.workflow.instance_id", workflowInstanceId) + .start(); + try { + try (Observation.Scope ignored = obs.openScope()) { + super.suspendWorkflow(workflowInstanceId, reason); + } + } catch (RuntimeException e) { + obs.error(e); + throw e; + } finally { + obs.stop(); + } + } + + @Override + public void resumeWorkflow(String workflowInstanceId, @Nullable String reason) { + Observation obs = observation("dapr.workflow.resume") + .highCardinalityKeyValue("dapr.workflow.instance_id", workflowInstanceId) + .start(); + try { + try (Observation.Scope ignored = obs.openScope()) { + super.resumeWorkflow(workflowInstanceId, reason); + } + } catch (RuntimeException e) { + obs.error(e); + throw e; + } finally { + obs.stop(); + } + } + + @Override + public void terminateWorkflow(String workflowInstanceId, @Nullable Object output) { + Observation obs = observation("dapr.workflow.terminate") + .highCardinalityKeyValue("dapr.workflow.instance_id", workflowInstanceId) + .start(); + try { + try (Observation.Scope ignored = obs.openScope()) { + super.terminateWorkflow(workflowInstanceId, output); + } + } catch (RuntimeException e) { + obs.error(e); + throw e; + } finally { + obs.stop(); + } + } + + // ------------------------------------------------------------------------- + // State queries + // ------------------------------------------------------------------------- + + @Override + @Nullable + public WorkflowState getWorkflowState(String instanceId, boolean getInputsAndOutputs) { + Observation obs = observation("dapr.workflow.get_state") + .highCardinalityKeyValue("dapr.workflow.instance_id", instanceId) + .start(); + try { + try (Observation.Scope ignored = obs.openScope()) { + return super.getWorkflowState(instanceId, getInputsAndOutputs); + } + } catch (RuntimeException e) { + obs.error(e); + throw e; + } finally { + obs.stop(); + } + } + + // ------------------------------------------------------------------------- + // Waiting + // ------------------------------------------------------------------------- + + @Override + @Nullable + public WorkflowState waitForWorkflowStart(String instanceId, Duration timeout, + boolean getInputsAndOutputs) throws TimeoutException { + Observation obs = observation("dapr.workflow.wait_start") + .highCardinalityKeyValue("dapr.workflow.instance_id", instanceId) + .start(); + try { + try (Observation.Scope ignored = obs.openScope()) { + return super.waitForWorkflowStart(instanceId, timeout, getInputsAndOutputs); + } + } catch (TimeoutException e) { + obs.error(e); + throw e; + } catch (RuntimeException e) { + obs.error(e); + throw e; + } finally { + obs.stop(); + } + } + + @Override + @Nullable + public WorkflowState waitForWorkflowCompletion(String instanceId, Duration timeout, + boolean getInputsAndOutputs) + throws TimeoutException { + Observation obs = observation("dapr.workflow.wait_completion") + .highCardinalityKeyValue("dapr.workflow.instance_id", instanceId) + .start(); + try { + try (Observation.Scope ignored = obs.openScope()) { + return super.waitForWorkflowCompletion(instanceId, timeout, getInputsAndOutputs); + } + } catch (TimeoutException e) { + obs.error(e); + throw e; + } catch (RuntimeException e) { + obs.error(e); + throw e; + } finally { + obs.stop(); + } + } + + // ------------------------------------------------------------------------- + // Events + // ------------------------------------------------------------------------- + + @Override + public void raiseEvent(String workflowInstanceId, String eventName, Object eventPayload) { + Observation obs = observation("dapr.workflow.raise_event") + .highCardinalityKeyValue("dapr.workflow.instance_id", workflowInstanceId) + .highCardinalityKeyValue("dapr.workflow.event_name", eventName) + .start(); + try { + try (Observation.Scope ignored = obs.openScope()) { + super.raiseEvent(workflowInstanceId, eventName, eventPayload); + } + } catch (RuntimeException e) { + obs.error(e); + throw e; + } finally { + obs.stop(); + } + } + + // ------------------------------------------------------------------------- + // Cleanup + // ------------------------------------------------------------------------- + + @Override + public boolean purgeWorkflow(String workflowInstanceId) { + Observation obs = observation("dapr.workflow.purge") + .highCardinalityKeyValue("dapr.workflow.instance_id", workflowInstanceId) + .start(); + try { + try (Observation.Scope ignored = obs.openScope()) { + return super.purgeWorkflow(workflowInstanceId); + } + } catch (RuntimeException e) { + obs.error(e); + throw e; + } finally { + obs.stop(); + } + } + + // Deprecated methods (getInstanceState, waitForInstanceStart, waitForInstanceCompletion, + // purgeInstance) are intentionally not overridden — they fall through to the parent + // implementation without any observation. + + // ------------------------------------------------------------------------- + // gRPC interceptor: injects the current OTel span's traceparent into headers + // ------------------------------------------------------------------------- + + /** + * A gRPC {@link ClientInterceptor} that reads the current OTel span from the thread-local + * context (set by {@link Observation#openScope()}) and injects its W3C {@code traceparent}, + * {@code tracestate}, and {@code grpc-trace-bin} headers into every outbound RPC call. + * + *

The interceptor is stateless: it reads {@link Span#current()} lazily at call time, so the + * same instance can be shared across all calls on the channel. + */ + private static final class OtelTracingClientInterceptor implements ClientInterceptor { + + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions options, Channel channel) { + return new ForwardingClientCall.SimpleForwardingClientCall<>(channel.newCall(method, options)) { + @Override + public void start(Listener responseListener, Metadata headers) { + SpanContext spanCtx = Span.current().getSpanContext(); + if (spanCtx.isValid()) { + // Build a Reactor Context with the OTel span's values and delegate to GrpcHelper, + // which writes traceparent, tracestate AND grpc-trace-bin (the binary format that + // older Dapr sidecar versions require for gRPC trace propagation). + Context reactorCtx = Context.of("traceparent", + TraceContextFormat.formatW3cTraceparent(spanCtx)); + String traceState = TraceContextFormat.formatTraceState(spanCtx); + if (!traceState.isEmpty()) { + reactorCtx = reactorCtx.put("tracestate", traceState); + } + GrpcHelper.populateMetadata(reactorCtx, headers); + } + super.start(responseListener, headers); + } + }; + } + } +} diff --git a/dapr-spring/dapr-spring-boot-observation/src/main/java/io/dapr/spring/observation/client/TraceContextFormat.java b/dapr-spring/dapr-spring-boot-observation/src/main/java/io/dapr/spring/observation/client/TraceContextFormat.java new file mode 100644 index 0000000000..d5e3470907 --- /dev/null +++ b/dapr-spring/dapr-spring-boot-observation/src/main/java/io/dapr/spring/observation/client/TraceContextFormat.java @@ -0,0 +1,57 @@ +/* + * Copyright 2025 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.spring.observation.client; + +import io.opentelemetry.api.trace.SpanContext; + +/** + * Formats an OpenTelemetry {@link SpanContext} as the W3C Trace Context header values + * ({@code traceparent} and {@code tracestate}). + * + *

Package-private: shared between {@link ObservationDaprClient} (Reactor context for async + * gRPC calls on {@code DaprClient}) and {@link ObservationDaprWorkflowClient} (gRPC + * {@code ClientInterceptor} for synchronous workflow calls). This keeps a single source of truth + * for the on-wire trace format. + */ +final class TraceContextFormat { + + private TraceContextFormat() { + } + + /** + * Format a {@link SpanContext} as the value of the W3C {@code traceparent} header (version 00). + */ + static String formatW3cTraceparent(SpanContext spanCtx) { + return "00-" + spanCtx.getTraceId() + "-" + spanCtx.getSpanId() + + "-" + spanCtx.getTraceFlags().asHex(); + } + + /** + * Format a {@link SpanContext}'s trace state as the value of the W3C {@code tracestate} header. + * Returns an empty string if the trace state is empty. + */ + static String formatTraceState(SpanContext spanCtx) { + if (spanCtx.getTraceState().isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + spanCtx.getTraceState().forEach((k, v) -> { + if (sb.length() > 0) { + sb.append(','); + } + sb.append(k).append('=').append(v); + }); + return sb.toString(); + } +} diff --git a/dapr-spring/dapr-spring-boot-observation/src/test/java/io/dapr/spring/observation/client/ObservationDaprClientTest.java b/dapr-spring/dapr-spring-boot-observation/src/test/java/io/dapr/spring/observation/client/ObservationDaprClientTest.java new file mode 100644 index 0000000000..e37a332df2 --- /dev/null +++ b/dapr-spring/dapr-spring-boot-observation/src/test/java/io/dapr/spring/observation/client/ObservationDaprClientTest.java @@ -0,0 +1,381 @@ +/* + * Copyright 2025 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.spring.observation.client; + +import io.dapr.client.DaprClient; +import io.dapr.client.domain.DeleteStateRequest; +import io.dapr.client.domain.GetSecretRequest; +import io.dapr.client.domain.GetStateRequest; +import io.dapr.client.domain.InvokeBindingRequest; +import io.dapr.client.domain.PublishEventRequest; +import io.dapr.client.domain.ScheduleJobRequest; +import io.dapr.client.domain.State; +import io.micrometer.observation.tck.TestObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistryAssert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link ObservationDaprClient}. + * + *

Verifies two key requirements: + *

    + *
  1. Each non-deprecated method creates a correctly named Micrometer Observation span.
  2. + *
  3. The wrapper is fully transparent: it implements {@link DaprClient}, so consumers + * keep injecting {@code DaprClient} without any code changes.
  4. + *
+ */ +@ExtendWith(MockitoExtension.class) +class ObservationDaprClientTest { + + @Mock + private DaprClient delegate; + + private TestObservationRegistry registry; + private ObservationDaprClient client; + + @BeforeEach + void setUp() { + registry = TestObservationRegistry.create(); + client = new ObservationDaprClient(delegate, registry); + } + + // ------------------------------------------------------------------------- + // Transparency — the wrapper IS-A DaprClient + // ------------------------------------------------------------------------- + + @Test + @DisplayName("ObservationDaprClient is assignable to DaprClient (transparent to consumers)") + void isAssignableToDaprClient() { + assertThat(client).isInstanceOf(DaprClient.class); + } + + // ------------------------------------------------------------------------- + // Pub/Sub + // ------------------------------------------------------------------------- + + @Test + @DisplayName("publishEvent(pubsubName, topicName, data) creates span dapr.client.publish_event") + void publishEventCreatesSpan() { + when(delegate.publishEvent("my-pubsub", "my-topic", "payload")).thenReturn(Mono.empty()); + + client.publishEvent("my-pubsub", "my-topic", "payload").block(); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.client.publish_event") + .that() + .hasHighCardinalityKeyValue("dapr.pubsub.name", "my-pubsub") + .hasHighCardinalityKeyValue("dapr.topic.name", "my-topic"); + } + + @Test + @DisplayName("publishEvent(PublishEventRequest) creates span dapr.client.publish_event") + void publishEventRequestCreatesSpan() { + PublishEventRequest request = new PublishEventRequest("my-pubsub", "my-topic", "payload"); + when(delegate.publishEvent(request)).thenReturn(Mono.empty()); + + client.publishEvent(request).block(); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.client.publish_event"); + } + + @Test + @DisplayName("publishEvent span records error when delegate throws") + void publishEventRecordsError() { + RuntimeException boom = new RuntimeException("publish failed"); + when(delegate.publishEvent("pubsub", "topic", "data")).thenReturn(Mono.error(boom)); + + assertThatThrownBy(() -> client.publishEvent("pubsub", "topic", "data").block()) + .isInstanceOf(RuntimeException.class); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.client.publish_event") + .that() + .hasError(); + } + + // ------------------------------------------------------------------------- + // Bindings + // ------------------------------------------------------------------------- + + @Test + @DisplayName("invokeBinding(name, operation, data) creates span dapr.client.invoke_binding") + void invokeBindingCreatesSpan() { + when(delegate.invokeBinding("my-binding", "create", "data")).thenReturn(Mono.empty()); + + client.invokeBinding("my-binding", "create", "data").block(); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.client.invoke_binding") + .that() + .hasHighCardinalityKeyValue("dapr.binding.name", "my-binding") + .hasHighCardinalityKeyValue("dapr.binding.operation", "create"); + } + + @Test + @DisplayName("invokeBinding(InvokeBindingRequest) creates span dapr.client.invoke_binding") + void invokeBindingRequestCreatesSpan() { + InvokeBindingRequest request = new InvokeBindingRequest("my-binding", "create"); + when(delegate.invokeBinding(request)).thenReturn(Mono.empty()); + + client.invokeBinding(request).block(); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.client.invoke_binding"); + } + + // ------------------------------------------------------------------------- + // State + // ------------------------------------------------------------------------- + + @Test + @DisplayName("getState(storeName, key, Class) creates span dapr.client.get_state") + void getStateCreatesSpan() { + when(delegate.getState("my-store", "my-key", String.class)).thenReturn(Mono.empty()); + + client.getState("my-store", "my-key", String.class).block(); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.client.get_state") + .that() + .hasHighCardinalityKeyValue("dapr.store.name", "my-store") + .hasHighCardinalityKeyValue("dapr.state.key", "my-key"); + } + + @Test + @DisplayName("saveState creates span dapr.client.save_state") + void saveStateCreatesSpan() { + when(delegate.saveState("my-store", "my-key", "value")).thenReturn(Mono.empty()); + + client.saveState("my-store", "my-key", "value").block(); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.client.save_state") + .that() + .hasHighCardinalityKeyValue("dapr.store.name", "my-store") + .hasHighCardinalityKeyValue("dapr.state.key", "my-key"); + } + + @Test + @DisplayName("deleteState creates span dapr.client.delete_state") + void deleteStateCreatesSpan() { + when(delegate.deleteState("my-store", "my-key")).thenReturn(Mono.empty()); + + client.deleteState("my-store", "my-key").block(); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.client.delete_state") + .that() + .hasHighCardinalityKeyValue("dapr.store.name", "my-store") + .hasHighCardinalityKeyValue("dapr.state.key", "my-key"); + } + + @Test + @DisplayName("getBulkState creates span dapr.client.get_bulk_state") + void getBulkStateCreatesSpan() { + when(delegate.getBulkState("my-store", List.of("k1", "k2"), String.class)) + .thenReturn(Mono.empty()); + + client.getBulkState("my-store", List.of("k1", "k2"), String.class).block(); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.client.get_bulk_state"); + } + + @Test + @DisplayName("executeStateTransaction creates span dapr.client.execute_state_transaction") + void executeStateTransactionCreatesSpan() { + when(delegate.executeStateTransaction("my-store", List.of())).thenReturn(Mono.empty()); + + client.executeStateTransaction("my-store", List.of()).block(); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.client.execute_state_transaction"); + } + + // ------------------------------------------------------------------------- + // Secrets + // ------------------------------------------------------------------------- + + @Test + @DisplayName("getSecret(storeName, secretName) creates span dapr.client.get_secret") + void getSecretCreatesSpan() { + when(delegate.getSecret("my-vault", "db-password")).thenReturn(Mono.empty()); + + client.getSecret("my-vault", "db-password").block(); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.client.get_secret") + .that() + .hasHighCardinalityKeyValue("dapr.secret.store", "my-vault") + .hasHighCardinalityKeyValue("dapr.secret.name", "db-password"); + } + + @Test + @DisplayName("getBulkSecret creates span dapr.client.get_bulk_secret") + void getBulkSecretCreatesSpan() { + when(delegate.getBulkSecret("my-vault")).thenReturn(Mono.empty()); + + client.getBulkSecret("my-vault").block(); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.client.get_bulk_secret"); + } + + // ------------------------------------------------------------------------- + // Configuration + // ------------------------------------------------------------------------- + + @Test + @DisplayName("getConfiguration creates span dapr.client.get_configuration") + void getConfigurationCreatesSpan() { + when(delegate.getConfiguration("my-config-store", "feature-flag")).thenReturn(Mono.empty()); + + client.getConfiguration("my-config-store", "feature-flag").block(); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.client.get_configuration") + .that() + .hasHighCardinalityKeyValue("dapr.configuration.store", "my-config-store"); + } + + @Test + @DisplayName("subscribeConfiguration creates span dapr.client.subscribe_configuration") + void subscribeConfigurationCreatesSpan() { + when(delegate.subscribeConfiguration("my-store", "k1")).thenReturn(Flux.empty()); + + client.subscribeConfiguration("my-store", "k1").blockLast(); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.client.subscribe_configuration"); + } + + @Test + @DisplayName("unsubscribeConfiguration creates span dapr.client.unsubscribe_configuration") + void unsubscribeConfigurationCreatesSpan() { + when(delegate.unsubscribeConfiguration("sub-id", "my-store")).thenReturn(Mono.empty()); + + client.unsubscribeConfiguration("sub-id", "my-store").block(); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.client.unsubscribe_configuration"); + } + + // ------------------------------------------------------------------------- + // Metadata & Lifecycle + // ------------------------------------------------------------------------- + + @Test + @DisplayName("getMetadata creates span dapr.client.get_metadata") + void getMetadataCreatesSpan() { + when(delegate.getMetadata()).thenReturn(Mono.empty()); + + client.getMetadata().block(); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.client.get_metadata"); + } + + @Test + @DisplayName("waitForSidecar creates span dapr.client.wait_for_sidecar") + void waitForSidecarCreatesSpan() { + when(delegate.waitForSidecar(5000)).thenReturn(Mono.empty()); + + client.waitForSidecar(5000).block(); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.client.wait_for_sidecar"); + } + + @Test + @DisplayName("shutdown creates span dapr.client.shutdown") + void shutdownCreatesSpan() { + when(delegate.shutdown()).thenReturn(Mono.empty()); + + client.shutdown().block(); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.client.shutdown"); + } + + // ------------------------------------------------------------------------- + // Jobs + // ------------------------------------------------------------------------- + + @Test + @DisplayName("scheduleJob creates span dapr.client.schedule_job") + void scheduleJobCreatesSpan() { + ScheduleJobRequest request = new ScheduleJobRequest("nightly-cleanup", + io.dapr.client.domain.JobSchedule.daily()); + when(delegate.scheduleJob(request)).thenReturn(Mono.empty()); + + client.scheduleJob(request).block(); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.client.schedule_job") + .that() + .hasHighCardinalityKeyValue("dapr.job.name", "nightly-cleanup"); + } + + // ------------------------------------------------------------------------- + // Deferred start — observation must not leak if Mono is never subscribed + // ------------------------------------------------------------------------- + + @Test + @DisplayName("Observation does not start if the returned Mono is never subscribed") + void observationDoesNotStartWithoutSubscription() { + // Call the method but do NOT subscribe (no .block()) + client.publishEvent("pubsub", "topic", "data"); + + // Registry must be empty — no observation was started + TestObservationRegistryAssert.assertThat(registry).doesNotHaveAnyObservation(); + } + + // ------------------------------------------------------------------------- + // Deprecated methods — must NOT create spans + // ------------------------------------------------------------------------- + + @Test + @DisplayName("Deprecated invokeMethod delegates without creating a span") + @SuppressWarnings("deprecation") + void deprecatedInvokeMethodDoesNotCreateSpan() { + when(delegate.invokeMethod(anyString(), anyString(), nullable(Object.class), + any(io.dapr.client.domain.HttpExtension.class), any(Class.class))) + .thenReturn(Mono.empty()); + + client.invokeMethod("app", "method", (Object) null, + io.dapr.client.domain.HttpExtension.NONE, String.class).block(); + + // Registry must be empty — no spans for deprecated methods + TestObservationRegistryAssert.assertThat(registry).doesNotHaveAnyObservation(); + } +} diff --git a/dapr-spring/dapr-spring-boot-observation/src/test/java/io/dapr/spring/observation/client/ObservationDaprWorkflowClientTest.java b/dapr-spring/dapr-spring-boot-observation/src/test/java/io/dapr/spring/observation/client/ObservationDaprWorkflowClientTest.java new file mode 100644 index 0000000000..ede8f49ce8 --- /dev/null +++ b/dapr-spring/dapr-spring-boot-observation/src/test/java/io/dapr/spring/observation/client/ObservationDaprWorkflowClientTest.java @@ -0,0 +1,232 @@ +/* + * Copyright 2025 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.spring.observation.client; + +import io.dapr.config.Properties; +import io.dapr.workflows.client.DaprWorkflowClient; +import io.micrometer.observation.tck.TestObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistryAssert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Unit tests for {@link ObservationDaprWorkflowClient}. + * + *

The gRPC {@code ManagedChannel} created in the parent constructor connects lazily, so + * instantiation succeeds without a running Dapr sidecar. Actual RPC calls will fail, but the + * observation lifecycle (start → error → stop) is still validated. + * + *

Verifies two key requirements: + *

    + *
  1. Each non-deprecated method creates a correctly named Micrometer Observation span + * (even when the underlying call fails due to no sidecar being available).
  2. + *
  3. The wrapper extends {@link DaprWorkflowClient}, so consumers keep injecting + * {@code DaprWorkflowClient} without any code changes.
  4. + *
+ */ +class ObservationDaprWorkflowClientTest { + + private TestObservationRegistry registry; + private ObservationDaprWorkflowClient client; + + @BeforeEach + void setUp() { + registry = TestObservationRegistry.create(); + // Properties with no gRPC endpoint — channel created lazily, no sidecar needed + client = new ObservationDaprWorkflowClient(new Properties(), registry); + } + + // ------------------------------------------------------------------------- + // Transparency — the wrapper IS-A DaprWorkflowClient + // ------------------------------------------------------------------------- + + @Test + @DisplayName("ObservationDaprWorkflowClient is assignable to DaprWorkflowClient (transparent)") + void isAssignableToDaprWorkflowClient() { + assertThat(client).isInstanceOf(DaprWorkflowClient.class); + } + + // ------------------------------------------------------------------------- + // scheduleNewWorkflow — span is created even when the call fails (no sidecar) + // ------------------------------------------------------------------------- + + @Test + @DisplayName("scheduleNewWorkflow(String) creates span dapr.workflow.schedule") + void scheduleNewWorkflowByNameCreatesSpan() { + // The gRPC call will fail — but the span must be recorded with an error + assertThatThrownBy(() -> client.scheduleNewWorkflow("MyWorkflow")) + .isInstanceOf(RuntimeException.class); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.workflow.schedule") + .that() + .hasHighCardinalityKeyValue("dapr.workflow.name", "MyWorkflow") + .hasError(); + } + + @Test + @DisplayName("scheduleNewWorkflow(Class) delegates to String overload — span is still created") + void scheduleNewWorkflowByClassDelegatesToStringOverload() { + // The Class-based overload in the parent calls this.scheduleNewWorkflow(canonicalName), + // which resolves to our overridden String-based method — the span is created naturally. + assertThatThrownBy(() -> client.scheduleNewWorkflow(DummyWorkflow.class)) + .isInstanceOf(RuntimeException.class); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.workflow.schedule") + .that() + .hasHighCardinalityKeyValue("dapr.workflow.name", DummyWorkflow.class.getCanonicalName()) + .hasError(); + } + + @Test + @DisplayName("scheduleNewWorkflow(String, Object, String) includes instance_id in span") + void scheduleNewWorkflowWithInstanceIdCreatesSpan() { + assertThatThrownBy(() -> client.scheduleNewWorkflow("MyWorkflow", null, "my-instance-123")) + .isInstanceOf(RuntimeException.class); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.workflow.schedule") + .that() + .hasHighCardinalityKeyValue("dapr.workflow.instance_id", "my-instance-123") + .hasError(); + } + + // ------------------------------------------------------------------------- + // Lifecycle operations + // ------------------------------------------------------------------------- + + @Test + @DisplayName("suspendWorkflow creates span dapr.workflow.suspend") + void suspendWorkflowCreatesSpan() { + assertThatThrownBy(() -> client.suspendWorkflow("instance-1", "pausing")) + .isInstanceOf(RuntimeException.class); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.workflow.suspend") + .that() + .hasHighCardinalityKeyValue("dapr.workflow.instance_id", "instance-1") + .hasError(); + } + + @Test + @DisplayName("resumeWorkflow creates span dapr.workflow.resume") + void resumeWorkflowCreatesSpan() { + assertThatThrownBy(() -> client.resumeWorkflow("instance-1", "resuming")) + .isInstanceOf(RuntimeException.class); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.workflow.resume") + .that() + .hasHighCardinalityKeyValue("dapr.workflow.instance_id", "instance-1") + .hasError(); + } + + @Test + @DisplayName("terminateWorkflow creates span dapr.workflow.terminate") + void terminateWorkflowCreatesSpan() { + assertThatThrownBy(() -> client.terminateWorkflow("instance-1", null)) + .isInstanceOf(RuntimeException.class); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.workflow.terminate") + .that() + .hasHighCardinalityKeyValue("dapr.workflow.instance_id", "instance-1") + .hasError(); + } + + // ------------------------------------------------------------------------- + // State queries + // ------------------------------------------------------------------------- + + @Test + @DisplayName("getWorkflowState creates span dapr.workflow.get_state") + void getWorkflowStateCreatesSpan() { + assertThatThrownBy(() -> client.getWorkflowState("instance-1", false)) + .isInstanceOf(RuntimeException.class); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.workflow.get_state") + .that() + .hasHighCardinalityKeyValue("dapr.workflow.instance_id", "instance-1") + .hasError(); + } + + // ------------------------------------------------------------------------- + // Events + // ------------------------------------------------------------------------- + + @Test + @DisplayName("raiseEvent creates span dapr.workflow.raise_event") + void raiseEventCreatesSpan() { + assertThatThrownBy(() -> client.raiseEvent("instance-1", "OrderPlaced", "payload")) + .isInstanceOf(RuntimeException.class); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.workflow.raise_event") + .that() + .hasHighCardinalityKeyValue("dapr.workflow.instance_id", "instance-1") + .hasHighCardinalityKeyValue("dapr.workflow.event_name", "OrderPlaced") + .hasError(); + } + + // ------------------------------------------------------------------------- + // Cleanup + // ------------------------------------------------------------------------- + + @Test + @DisplayName("purgeWorkflow creates span dapr.workflow.purge") + void purgeWorkflowCreatesSpan() { + assertThatThrownBy(() -> client.purgeWorkflow("instance-1")) + .isInstanceOf(RuntimeException.class); + + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("dapr.workflow.purge") + .that() + .hasHighCardinalityKeyValue("dapr.workflow.instance_id", "instance-1") + .hasError(); + } + + // ------------------------------------------------------------------------- + // Deprecated methods — must NOT create spans + // ------------------------------------------------------------------------- + + @Test + @DisplayName("Deprecated getInstanceState falls through to parent without creating a span") + @SuppressWarnings("deprecation") + void deprecatedGetInstanceStateDoesNotCreateSpan() { + // This will fail (no sidecar) but must not leave any observations in the registry + assertThatThrownBy(() -> client.getInstanceState("instance-1", false)) + .isInstanceOf(RuntimeException.class); + + // No spans should have been created for deprecated methods + TestObservationRegistryAssert.assertThat(registry).doesNotHaveAnyObservation(); + } + + // ------------------------------------------------------------------------- + // Dummy workflow implementation for type-based tests + // ------------------------------------------------------------------------- + + static class DummyWorkflow implements io.dapr.workflows.Workflow { + @Override + public io.dapr.workflows.WorkflowStub create() { + return ctx -> { + }; + } + } +} diff --git a/dapr-spring/dapr-spring-boot-properties/pom.xml b/dapr-spring/dapr-spring-boot-properties/pom.xml index 8b92081e8e..548c277621 100644 --- a/dapr-spring/dapr-spring-boot-properties/pom.xml +++ b/dapr-spring/dapr-spring-boot-properties/pom.xml @@ -47,7 +47,7 @@
org.testcontainers - junit-jupiter + testcontainers-junit-jupiter test diff --git a/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-4-starter-test/pom.xml b/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-4-starter-test/pom.xml index 52cc83fec3..47be20f17d 100644 --- a/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-4-starter-test/pom.xml +++ b/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-4-starter-test/pom.xml @@ -16,8 +16,8 @@ jar - 4.0.2 - + 4.0.5 + 6.0.2 @@ -50,7 +50,7 @@ org.testcontainers - junit-jupiter + testcontainers-junit-jupiter true
diff --git a/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-4-starter/pom.xml b/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-4-starter/pom.xml index 96c3aed531..4744517a8d 100644 --- a/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-4-starter/pom.xml +++ b/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-4-starter/pom.xml @@ -16,8 +16,8 @@ jar - 4.0.2 - + 4.0.5 + 6.0.2 diff --git a/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter-test/pom.xml b/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter-test/pom.xml index 5270ee58fa..93ef0935fc 100644 --- a/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter-test/pom.xml +++ b/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter-test/pom.xml @@ -34,7 +34,7 @@ org.testcontainers - junit-jupiter + testcontainers-junit-jupiter true diff --git a/dapr-spring/dapr-spring-boot-tests/pom.xml b/dapr-spring/dapr-spring-boot-tests/pom.xml index 1290e1ec87..c08d82ac84 100644 --- a/dapr-spring/dapr-spring-boot-tests/pom.xml +++ b/dapr-spring/dapr-spring-boot-tests/pom.xml @@ -26,7 +26,7 @@ org.testcontainers - junit-jupiter + testcontainers-junit-jupiter test diff --git a/dapr-spring/pom.xml b/dapr-spring/pom.xml index 638cee1092..d2f35baaab 100644 --- a/dapr-spring/pom.xml +++ b/dapr-spring/pom.xml @@ -18,11 +18,13 @@ SDK extension for Spring and Spring Boot + dapr-spring-bom dapr-spring-data dapr-spring-6-data dapr-spring-messaging dapr-spring-workflows dapr-spring-boot-properties + dapr-spring-boot-observation dapr-spring-boot-autoconfigure dapr-spring-boot-4-autoconfigure dapr-spring-boot-tests @@ -33,9 +35,9 @@ - 11 - 11 - 11 + 17 + 17 + 17 @@ -75,6 +77,11 @@ dapr-spring-boot-properties ${project.version} + + io.dapr.spring + dapr-spring-boot-observation + ${project.version} + io.dapr.spring dapr-spring-boot-tests diff --git a/docs/allclasses-index.html b/docs/allclasses-index.html index c8c92b7c87..960d1823b6 100644 --- a/docs/allclasses-index.html +++ b/docs/allclasses-index.html @@ -2,7 +2,7 @@ -All Classes and Interfaces (dapr-sdk-parent 1.17.0 API) +All Classes and Interfaces (dapr-sdk-parent 1.17.2 API) diff --git a/docs/allpackages-index.html b/docs/allpackages-index.html index d0d04475b7..132fb24dc0 100644 --- a/docs/allpackages-index.html +++ b/docs/allpackages-index.html @@ -2,7 +2,7 @@ -All Packages (dapr-sdk-parent 1.17.0 API) +All Packages (dapr-sdk-parent 1.17.2 API) diff --git a/docs/constant-values.html b/docs/constant-values.html index 58b72a7c1b..616397dc20 100644 --- a/docs/constant-values.html +++ b/docs/constant-values.html @@ -2,7 +2,7 @@ -Constant Field Values (dapr-sdk-parent 1.17.0 API) +Constant Field Values (dapr-sdk-parent 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/allclasses-index.html b/docs/dapr-sdk-workflows/allclasses-index.html index e3bc60a33b..68baeb716b 100644 --- a/docs/dapr-sdk-workflows/allclasses-index.html +++ b/docs/dapr-sdk-workflows/allclasses-index.html @@ -2,7 +2,7 @@ -All Classes and Interfaces (dapr-sdk-workflows 1.17.0 API) +All Classes and Interfaces (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/allpackages-index.html b/docs/dapr-sdk-workflows/allpackages-index.html index 20fa2c5ebf..61ef888ecb 100644 --- a/docs/dapr-sdk-workflows/allpackages-index.html +++ b/docs/dapr-sdk-workflows/allpackages-index.html @@ -2,7 +2,7 @@ -All Packages (dapr-sdk-workflows 1.17.0 API) +All Packages (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/help-doc.html b/docs/dapr-sdk-workflows/help-doc.html index 6bebff778b..93dc4897a5 100644 --- a/docs/dapr-sdk-workflows/help-doc.html +++ b/docs/dapr-sdk-workflows/help-doc.html @@ -2,7 +2,7 @@ -API Help (dapr-sdk-workflows 1.17.0 API) +API Help (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/index-all.html b/docs/dapr-sdk-workflows/index-all.html index 720b89b4c6..57791379d2 100644 --- a/docs/dapr-sdk-workflows/index-all.html +++ b/docs/dapr-sdk-workflows/index-all.html @@ -2,7 +2,7 @@ -Index (dapr-sdk-workflows 1.17.0 API) +Index (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/index.html b/docs/dapr-sdk-workflows/index.html index e4489fb042..12c817780b 100644 --- a/docs/dapr-sdk-workflows/index.html +++ b/docs/dapr-sdk-workflows/index.html @@ -2,7 +2,7 @@ -Overview (dapr-sdk-workflows 1.17.0 API) +Overview (dapr-sdk-workflows 1.17.2 API) @@ -49,7 +49,7 @@
-

dapr-sdk-workflows 1.17.0 API

+

dapr-sdk-workflows 1.17.2 API

Packages
diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/Workflow.html b/docs/dapr-sdk-workflows/io/dapr/workflows/Workflow.html index b6056d1ca9..df82d32b82 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/Workflow.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/Workflow.html @@ -2,7 +2,7 @@ -Workflow (dapr-sdk-workflows 1.17.0 API) +Workflow (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/WorkflowContext.html b/docs/dapr-sdk-workflows/io/dapr/workflows/WorkflowContext.html index 07c6bee533..9aecdea305 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/WorkflowContext.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/WorkflowContext.html @@ -2,7 +2,7 @@ -WorkflowContext (dapr-sdk-workflows 1.17.0 API) +WorkflowContext (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/WorkflowStub.html b/docs/dapr-sdk-workflows/io/dapr/workflows/WorkflowStub.html index e561214333..7939d66a70 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/WorkflowStub.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/WorkflowStub.html @@ -2,7 +2,7 @@ -WorkflowStub (dapr-sdk-workflows 1.17.0 API) +WorkflowStub (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/class-use/Workflow.html b/docs/dapr-sdk-workflows/io/dapr/workflows/class-use/Workflow.html index 051a813928..ed8e3d30df 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/class-use/Workflow.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/class-use/Workflow.html @@ -2,7 +2,7 @@ -Uses of Interface io.dapr.workflows.Workflow (dapr-sdk-workflows 1.17.0 API) +Uses of Interface io.dapr.workflows.Workflow (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/class-use/WorkflowContext.html b/docs/dapr-sdk-workflows/io/dapr/workflows/class-use/WorkflowContext.html index 9a3c40679c..0d6230f83d 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/class-use/WorkflowContext.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/class-use/WorkflowContext.html @@ -2,7 +2,7 @@ -Uses of Interface io.dapr.workflows.WorkflowContext (dapr-sdk-workflows 1.17.0 API) +Uses of Interface io.dapr.workflows.WorkflowContext (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/class-use/WorkflowStub.html b/docs/dapr-sdk-workflows/io/dapr/workflows/class-use/WorkflowStub.html index abb6a4b8e8..7a511042e8 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/class-use/WorkflowStub.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/class-use/WorkflowStub.html @@ -2,7 +2,7 @@ -Uses of Interface io.dapr.workflows.WorkflowStub (dapr-sdk-workflows 1.17.0 API) +Uses of Interface io.dapr.workflows.WorkflowStub (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/client/DaprWorkflowClient.html b/docs/dapr-sdk-workflows/io/dapr/workflows/client/DaprWorkflowClient.html index 16778612c4..d967a45ebb 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/client/DaprWorkflowClient.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/client/DaprWorkflowClient.html @@ -2,7 +2,7 @@ -DaprWorkflowClient (dapr-sdk-workflows 1.17.0 API) +DaprWorkflowClient (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/client/WorkflowFailureDetails.html b/docs/dapr-sdk-workflows/io/dapr/workflows/client/WorkflowFailureDetails.html index c08488f82b..54e41903a9 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/client/WorkflowFailureDetails.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/client/WorkflowFailureDetails.html @@ -2,7 +2,7 @@ -WorkflowFailureDetails (dapr-sdk-workflows 1.17.0 API) +WorkflowFailureDetails (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/client/WorkflowInstanceStatus.html b/docs/dapr-sdk-workflows/io/dapr/workflows/client/WorkflowInstanceStatus.html index 5d659602ea..5dc823c487 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/client/WorkflowInstanceStatus.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/client/WorkflowInstanceStatus.html @@ -2,7 +2,7 @@ -WorkflowInstanceStatus (dapr-sdk-workflows 1.17.0 API) +WorkflowInstanceStatus (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/client/class-use/DaprWorkflowClient.html b/docs/dapr-sdk-workflows/io/dapr/workflows/client/class-use/DaprWorkflowClient.html index 869f2acdcb..67121e09c7 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/client/class-use/DaprWorkflowClient.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/client/class-use/DaprWorkflowClient.html @@ -2,7 +2,7 @@ -Uses of Class io.dapr.workflows.client.DaprWorkflowClient (dapr-sdk-workflows 1.17.0 API) +Uses of Class io.dapr.workflows.client.DaprWorkflowClient (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/client/class-use/WorkflowFailureDetails.html b/docs/dapr-sdk-workflows/io/dapr/workflows/client/class-use/WorkflowFailureDetails.html index 155fe88c87..78cf2b207f 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/client/class-use/WorkflowFailureDetails.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/client/class-use/WorkflowFailureDetails.html @@ -2,7 +2,7 @@ -Uses of Interface io.dapr.workflows.client.WorkflowFailureDetails (dapr-sdk-workflows 1.17.0 API) +Uses of Interface io.dapr.workflows.client.WorkflowFailureDetails (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/client/class-use/WorkflowInstanceStatus.html b/docs/dapr-sdk-workflows/io/dapr/workflows/client/class-use/WorkflowInstanceStatus.html index 442c41b8fa..f7f25a78c6 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/client/class-use/WorkflowInstanceStatus.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/client/class-use/WorkflowInstanceStatus.html @@ -2,7 +2,7 @@ -Uses of Interface io.dapr.workflows.client.WorkflowInstanceStatus (dapr-sdk-workflows 1.17.0 API) +Uses of Interface io.dapr.workflows.client.WorkflowInstanceStatus (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/client/package-summary.html b/docs/dapr-sdk-workflows/io/dapr/workflows/client/package-summary.html index e92cea89b9..cc335c5992 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/client/package-summary.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/client/package-summary.html @@ -2,7 +2,7 @@ -io.dapr.workflows.client (dapr-sdk-workflows 1.17.0 API) +io.dapr.workflows.client (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/client/package-tree.html b/docs/dapr-sdk-workflows/io/dapr/workflows/client/package-tree.html index 44532ecc89..10bf66fc6e 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/client/package-tree.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/client/package-tree.html @@ -2,7 +2,7 @@ -io.dapr.workflows.client Class Hierarchy (dapr-sdk-workflows 1.17.0 API) +io.dapr.workflows.client Class Hierarchy (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/client/package-use.html b/docs/dapr-sdk-workflows/io/dapr/workflows/client/package-use.html index 1e0038e467..ff9258834c 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/client/package-use.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/client/package-use.html @@ -2,7 +2,7 @@ -Uses of Package io.dapr.workflows.client (dapr-sdk-workflows 1.17.0 API) +Uses of Package io.dapr.workflows.client (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/internal/ApiTokenClientInterceptor.html b/docs/dapr-sdk-workflows/io/dapr/workflows/internal/ApiTokenClientInterceptor.html index f0cad66957..acc4fba4f0 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/internal/ApiTokenClientInterceptor.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/internal/ApiTokenClientInterceptor.html @@ -2,7 +2,7 @@ -ApiTokenClientInterceptor (dapr-sdk-workflows 1.17.0 API) +ApiTokenClientInterceptor (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/internal/class-use/ApiTokenClientInterceptor.html b/docs/dapr-sdk-workflows/io/dapr/workflows/internal/class-use/ApiTokenClientInterceptor.html index 4bec6cca56..72acc8622c 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/internal/class-use/ApiTokenClientInterceptor.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/internal/class-use/ApiTokenClientInterceptor.html @@ -2,7 +2,7 @@ -Uses of Class io.dapr.workflows.internal.ApiTokenClientInterceptor (dapr-sdk-workflows 1.17.0 API) +Uses of Class io.dapr.workflows.internal.ApiTokenClientInterceptor (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/internal/package-summary.html b/docs/dapr-sdk-workflows/io/dapr/workflows/internal/package-summary.html index 5e0db98034..ad57a51c29 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/internal/package-summary.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/internal/package-summary.html @@ -2,7 +2,7 @@ -io.dapr.workflows.internal (dapr-sdk-workflows 1.17.0 API) +io.dapr.workflows.internal (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/internal/package-tree.html b/docs/dapr-sdk-workflows/io/dapr/workflows/internal/package-tree.html index 30922f97e0..45b6b6a04c 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/internal/package-tree.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/internal/package-tree.html @@ -2,7 +2,7 @@ -io.dapr.workflows.internal Class Hierarchy (dapr-sdk-workflows 1.17.0 API) +io.dapr.workflows.internal Class Hierarchy (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/internal/package-use.html b/docs/dapr-sdk-workflows/io/dapr/workflows/internal/package-use.html index 7d08f0f2c0..339601594a 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/internal/package-use.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/internal/package-use.html @@ -2,7 +2,7 @@ -Uses of Package io.dapr.workflows.internal (dapr-sdk-workflows 1.17.0 API) +Uses of Package io.dapr.workflows.internal (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/package-summary.html b/docs/dapr-sdk-workflows/io/dapr/workflows/package-summary.html index 34448e038e..6e1b286ba5 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/package-summary.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/package-summary.html @@ -2,7 +2,7 @@ -io.dapr.workflows (dapr-sdk-workflows 1.17.0 API) +io.dapr.workflows (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/package-tree.html b/docs/dapr-sdk-workflows/io/dapr/workflows/package-tree.html index a7a05b2f84..69c258219a 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/package-tree.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/package-tree.html @@ -2,7 +2,7 @@ -io.dapr.workflows Class Hierarchy (dapr-sdk-workflows 1.17.0 API) +io.dapr.workflows Class Hierarchy (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/package-use.html b/docs/dapr-sdk-workflows/io/dapr/workflows/package-use.html index 9c70de6bfd..2d227c52e1 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/package-use.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/package-use.html @@ -2,7 +2,7 @@ -Uses of Package io.dapr.workflows (dapr-sdk-workflows 1.17.0 API) +Uses of Package io.dapr.workflows (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/WorkflowRuntime.html b/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/WorkflowRuntime.html index 207a60b8f4..01b9dac968 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/WorkflowRuntime.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/WorkflowRuntime.html @@ -2,7 +2,7 @@ -WorkflowRuntime (dapr-sdk-workflows 1.17.0 API) +WorkflowRuntime (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/WorkflowRuntimeBuilder.html b/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/WorkflowRuntimeBuilder.html index dac74c5254..52267404b2 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/WorkflowRuntimeBuilder.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/WorkflowRuntimeBuilder.html @@ -2,7 +2,7 @@ -WorkflowRuntimeBuilder (dapr-sdk-workflows 1.17.0 API) +WorkflowRuntimeBuilder (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/class-use/WorkflowRuntime.html b/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/class-use/WorkflowRuntime.html index 5d28871dad..1880ceba55 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/class-use/WorkflowRuntime.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/class-use/WorkflowRuntime.html @@ -2,7 +2,7 @@ -Uses of Class io.dapr.workflows.runtime.WorkflowRuntime (dapr-sdk-workflows 1.17.0 API) +Uses of Class io.dapr.workflows.runtime.WorkflowRuntime (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/class-use/WorkflowRuntimeBuilder.html b/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/class-use/WorkflowRuntimeBuilder.html index 7304161475..8e15c7995a 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/class-use/WorkflowRuntimeBuilder.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/class-use/WorkflowRuntimeBuilder.html @@ -2,7 +2,7 @@ -Uses of Class io.dapr.workflows.runtime.WorkflowRuntimeBuilder (dapr-sdk-workflows 1.17.0 API) +Uses of Class io.dapr.workflows.runtime.WorkflowRuntimeBuilder (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/package-summary.html b/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/package-summary.html index cd5aa01815..87b5ac6818 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/package-summary.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/package-summary.html @@ -2,7 +2,7 @@ -io.dapr.workflows.runtime (dapr-sdk-workflows 1.17.0 API) +io.dapr.workflows.runtime (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/package-tree.html b/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/package-tree.html index 984433d7a3..ed8ba845c7 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/package-tree.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/package-tree.html @@ -2,7 +2,7 @@ -io.dapr.workflows.runtime Class Hierarchy (dapr-sdk-workflows 1.17.0 API) +io.dapr.workflows.runtime Class Hierarchy (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/package-use.html b/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/package-use.html index 9b8ecdfed3..9d7d11f720 100644 --- a/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/package-use.html +++ b/docs/dapr-sdk-workflows/io/dapr/workflows/runtime/package-use.html @@ -2,7 +2,7 @@ -Uses of Package io.dapr.workflows.runtime (dapr-sdk-workflows 1.17.0 API) +Uses of Package io.dapr.workflows.runtime (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/overview-summary.html b/docs/dapr-sdk-workflows/overview-summary.html index 3c5763540c..9b12d7c251 100644 --- a/docs/dapr-sdk-workflows/overview-summary.html +++ b/docs/dapr-sdk-workflows/overview-summary.html @@ -2,7 +2,7 @@ -dapr-sdk-workflows 1.17.0 API +dapr-sdk-workflows 1.17.2 API diff --git a/docs/dapr-sdk-workflows/overview-tree.html b/docs/dapr-sdk-workflows/overview-tree.html index 3b120d41af..44490dd876 100644 --- a/docs/dapr-sdk-workflows/overview-tree.html +++ b/docs/dapr-sdk-workflows/overview-tree.html @@ -2,7 +2,7 @@ -Class Hierarchy (dapr-sdk-workflows 1.17.0 API) +Class Hierarchy (dapr-sdk-workflows 1.17.2 API) diff --git a/docs/dapr-sdk-workflows/project-reports.html b/docs/dapr-sdk-workflows/project-reports.html index d9a845c3c6..18a9f447b2 100644 --- a/docs/dapr-sdk-workflows/project-reports.html +++ b/docs/dapr-sdk-workflows/project-reports.html @@ -1,6 +1,6 @@ @@ -25,8 +25,8 @@