This is an automated email from the ASF dual-hosted git repository.
fjtiradosarti pushed a commit to branch main
in repository
https://gitbox.apache.org/repos/asf/incubator-kie-kogito-examples.git
The following commit(s) were added to refs/heads/main by this push:
new b79fdfdaa [SRVLOGIC-777] OpenTelemetry E2E automated IT in examples
(#2164)
b79fdfdaa is described below
commit b79fdfdaa123951634184c82fabb460b0ed258bd
Author: Gonzalo Muñoz <[email protected]>
AuthorDate: Fri Feb 6 11:55:33 2026 +0100
[SRVLOGIC-777] OpenTelemetry E2E automated IT in examples (#2164)
---
serverless-workflow-examples/pom.xml | 3 +-
.../README.md | 177 ++++++++
.../pom.xml | 264 +++++++++++
.../sw/opentelemetry/jaeger/ErrorFunctions.java | 30 ++
.../sw/opentelemetry/jaeger/RegisterResource.java | 41 ++
.../src/main/resources/application.properties | 76 ++++
.../src/main/resources/error.sw.json | 34 ++
.../src/main/resources/greet.sw.json | 71 +++
.../src/main/resources/greetSubflow.sw.json | 42 ++
.../src/main/resources/persist-wait.sw.json | 75 ++++
.../src/main/resources/specs/persist-callback.yaml | 21 +
.../sw/opentelemetry/jaeger/JaegerQueryClient.java | 98 +++++
.../opentelemetry/jaeger/JaegerTestResource.java | 125 ++++++
.../jaeger/OpenTelemetryJaegerIT.java | 30 ++
.../jaeger/OpenTelemetryJaegerTest.java | 149 +++++++
.../opentelemetry/jaeger/helper/JaegerHelper.java | 485 +++++++++++++++++++++
.../opentelemetry/jaeger/helper/JaegerPoller.java | 79 ++++
.../jaeger/helper/JaegerQueryClient.java | 177 ++++++++
.../jaeger/persistence/CloudEvents.java | 45 ++
.../jaeger/persistence/PersistWait01StartIT.java | 70 +++
.../jaeger/persistence/PersistWait02ResumeIT.java | 131 ++++++
.../jaeger/persistence/PersistWaitPidStore.java | 53 +++
.../jaeger/persistence/PostgresTestResource.java | 54 +++
23 files changed, 2329 insertions(+), 1 deletion(-)
diff --git a/serverless-workflow-examples/pom.xml
b/serverless-workflow-examples/pom.xml
index a83c8bf9d..4e3ce6776 100644
--- a/serverless-workflow-examples/pom.xml
+++ b/serverless-workflow-examples/pom.xml
@@ -81,7 +81,8 @@
<module>serverless-workflow-timeouts-showcase-operator-devprofile</module>
<module>serverless-workflow-python-quarkus</module>
<module>sonataflow-fluent</module>
- <module>serverless-workflow-fault-tolerance</module>
+ <module>serverless-workflow-fault-tolerance</module>
+ <module>serverless-workflow-opentelemetry-jaeger-quarkus</module>
</modules>
</profile>
diff --git
a/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/README.md
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/README.md
new file mode 100644
index 000000000..5ffb62d45
--- /dev/null
+++
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/README.md
@@ -0,0 +1,177 @@
+# Serverless Workflow – OpenTelemetry (Quarkus) + Jaeger
+This module demonstrates **end-to-end OpenTelemetry tracing** for
+**SonataFlow** running on **Quarkus**, with
+traces exported to **Jaeger** and validated via **integration tests**.
+
+It covers:
+- HTTP entrypoints
+- Workflow execution spans
+- Subflows
+- Error paths
+- Persistence + resume via events
+- Native image execution (GraalVM)
+
+All traces are asserted by querying Jaeger directly.
+
+---
+
+## What is being tested
+
+The module contains **6 integration tests**, grouped by concern.
+
+### 1. `OpenTelemetryJaegerIT`
+**Baseline tracing validation**
+
+Verifies that:
+- HTTP requests generate server spans
+- Workflow execution produces `sonataflow.process.*` spans
+- Mandatory workflow tags are present:
+ - `sonataflow.process.id`
+ - `sonataflow.process.instance.id`
+ - `sonataflow.workflow.state`
+- Traces are correctly exported to Jaeger via OTLP
+
+This is the “smoke test” for OpenTelemetry wiring.
+
+---
+
+### 2. `OpenTelemetryJaegerIT` (transaction fallback)
+**Transaction propagation logic**
+
+Validates that:
+- When an explicit transaction header is present, it is propagated
+- When the transaction header is missing, the workflow **falls back to the
process instance id**
+- The resolved transaction id is visible in workflow spans
+
+This ensures correlation is always available in traces.
+
+---
+
+### 3. `OpenTelemetryJaegerIT` (subflow)
+**Subflow tracing and correlation**
+
+Validates that:
+- A parent workflow calling a subflow generates **multiple workflow spans**
+- Parent and subflow spans share the same:
+ - `sonataflow.transaction.id`
+ - `sonataflow.process.instance.id`
+- Subflow execution is visible in Jaeger as part of the same trace
+
+This confirms **subflow-workflow trace continuity**.
+
+---
+
+### 4. `PersistWait01StartIT`
+**Persistent workflow start**
+
+Validates that:
+- A workflow configured with persistence starts correctly
+- The process instance id is generated and stored
+- A trace exists for the “start + persist” phase
+- Workflow state before suspension is visible in tracing data
+
+This test proves persistence does not break tracing.
+
+---
+
+### 5. `PersistWait02ResumeIT`
+**Resume after persistence via event**
+
+Validates that:
+- A persisted workflow is resumed via a CloudEvent (`/resume`)
+- The workflow continues execution from the suspended state
+- Context (transaction id, instance id, workflow state) is preserved
+- Traces after resume belong to the same logical workflow execution
+
+This is the key test proving **persistence + events + tracing** work together.
+
+---
+
+### 6. `OpenTelemetryJaegerIT` (error workflow)
+**Error propagation in traces**
+
+Validates that:
+- A failing workflow produces error spans
+- Error conditions are visible in Jaeger
+- Workflow metadata is still attached to errored spans
+
+This ensures observability even when workflows fail.
+
+All tests are executed **both in JVM mode and native mode**.
+
+---
+
+## Debugging Jaeger traces
+
+You can enable verbose trace logging during test execution by passing:
+
+```sh
+-Dtest.jaeger.debug=true
+```
+
+When this flag is enabled, tests will print:
+
+- All span operation names found in the trace
+
+- Workflow-related tags (sonataflow.*, transaction, tracker)
+
+- Workflow state transitions observed in the trace
+
+The flag is disabled by default to keep CI logs concise.
+
+---
+
+## Native image testing
+
+Note that this requires GRAALVM_HOME to point to a valid GraalVM installation
+
+```sh
+mvn clean install -Pnative
+```
+
+Run the full integration test suite against the native binary
+
+- GraalVM builds a native image and wire extensions at build time
+- Persistence, Flyway, OpenTelemetry, and messaging must all work correctly
+- Tracing must survive native compilation
+
+### How native tests are executed
+- The application is built as a native binary
+- The binary is launched by Quarkus test infrastructure
+- Testcontainers provide:
+ - Jaeger (OTLP + Query)
+ - PostgreSQL (workflow persistence)
+- The same tests assert behavior against the native binary
+
+
+---
+
+## Supporting infrastructure
+
+During tests, the following containers are started automatically:
+
+- **Jaeger all-in-one**
+ - OTLP gRPC
+ - Query API used for assertions
+- **PostgreSQL**
+ - Used by SonataFlow persistence
+ - Schema initialized via Flyway
+
+No external services are required.
+
+---
+
+## Summary
+
+This module provides a **reference example** for assurance of:
+
+- OpenTelemetry tracing for SonataFlow workflows
+- Jaeger-based trace validation
+- Workflow correlation and context propagation
+- Persistent workflows with event-based resume
+- JVM and native execution parity
+
+It is intended as both:
+- A regression quality test
+- A blueprint for production-ready observability setups
+
diff --git
a/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/pom.xml
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/pom.xml
new file mode 100644
index 000000000..41e3a3ddd
--- /dev/null
+++
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/pom.xml
@@ -0,0 +1,264 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you 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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.kie.kogito.examples</groupId>
+ <artifactId>serverless-workflow-examples-parent</artifactId>
+ <version>999-SNAPSHOT</version>
+ <relativePath>../serverless-workflow-examples-parent/pom.xml</relativePath>
+ </parent>
+
+ <groupId>org.kie.kogito.examples</groupId>
+ <artifactId>serverless-workflow-opentelemetry-jaeger-quarkus</artifactId>
+ <version>1.0-SNAPSHOT</version>
+
+ <packaging>jar</packaging>
+ <name>Kogito Example :: Serverless Workflow :: OpenTelemetry</name>
+ <description>Kogito Serverless Workflow OpenTelemetry - Quarkus</description>
+
+ <properties>
+ <quarkus-plugin.version>3.20.3</quarkus-plugin.version>
+ <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
+ <quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
+ <quarkus.platform.version>3.20.3</quarkus.platform.version>
+ <kogito.bom.group-id>org.kie.kogito</kogito.bom.group-id>
+ <kogito.bom.artifact-id>kogito-bom</kogito.bom.artifact-id>
+ <kogito.bom.version>999-SNAPSHOT</kogito.bom.version>
+ <version.org.kie.kogito>999-SNAPSHOT</version.org.kie.kogito>
+ <maven.compiler.release>17</maven.compiler.release>
+ <version.compiler.plugin>3.13.0</version.compiler.plugin>
+ <version.surefire.plugin>3.3.1</version.surefire.plugin>
+
<version.failsafe.plugin>${version.surefire.plugin}</version.failsafe.plugin>
+ <testcontainers.version>1.19.7</testcontainers.version>
+ <okhttp.version>4.12.0</okhttp.version>
+ </properties>
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>${quarkus.platform.group-id}</groupId>
+ <artifactId>${quarkus.platform.artifact-id}</artifactId>
+ <version>${quarkus.platform.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>${kogito.bom.group-id}</groupId>
+ <artifactId>${kogito.bom.artifact-id}</artifactId>
+ <version>${kogito.bom.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.testcontainers</groupId>
+ <artifactId>testcontainers-bom</artifactId>
+ <version>${testcontainers.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.kie.sonataflow</groupId>
+ <artifactId>sonataflow-quarkus</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>io.quarkus</groupId>
+ <artifactId>quarkus-resteasy</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.quarkus</groupId>
+ <artifactId>quarkus-resteasy-jackson</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>io.quarkus</groupId>
+ <artifactId>quarkus-smallrye-health</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>io.quarkus</groupId>
+ <artifactId>quarkus-opentelemetry</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.kie.sonataflow</groupId>
+ <artifactId>sonataflow-addons-quarkus-opentelemetry</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.kie</groupId>
+ <artifactId>kie-addons-quarkus-persistence-jdbc</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.quarkus</groupId>
+ <artifactId>quarkus-jdbc-postgresql</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>okhttp</artifactId>
+ <version>${okhttp.version}</version>
+ </dependency>
+
+ <!-- Tests -->
+ <dependency>
+ <groupId>io.quarkus</groupId>
+ <artifactId>quarkus-junit5</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>io.rest-assured</groupId>
+ <artifactId>rest-assured</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.awaitility</groupId>
+ <artifactId>awaitility</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.testcontainers</groupId>
+ <artifactId>testcontainers</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.testcontainers</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.testcontainers</groupId>
+ <artifactId>postgresql</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <finalName>${project.artifactId}</finalName>
+
+ <plugins>
+ <plugin>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>${version.compiler.plugin}</version>
+ <configuration>
+ <release>${maven.compiler.release}</release>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>${quarkus.platform.group-id}</groupId>
+ <artifactId>quarkus-maven-plugin</artifactId>
+ <version>${quarkus-plugin.version}</version>
+ <extensions>true</extensions>
+ <executions>
+ <execution>
+ <goals>
+ <goal>build</goal>
+ <goal>generate-code</goal>
+ <goal>generate-code-tests</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+
+ <plugin>
+ <artifactId>maven-failsafe-plugin</artifactId>
+ <version>${version.failsafe.plugin}</version>
+ <configuration>
+ <systemPropertyVariables>
+
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
+ <maven.home>${maven.home}</maven.home>
+ </systemPropertyVariables>
+ <parallel>none</parallel>
+ </configuration>
+ <executions>
+ <execution>
+ <goals>
+ <goal>integration-test</goal>
+ <goal>verify</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+
+ <plugin>
+ <groupId>com.github.ekryd.sortpom</groupId>
+ <artifactId>sortpom-maven-plugin</artifactId>
+ <version>4.0.0</version>
+ <configuration>
+ <lineSeparator>\n</lineSeparator>
+ </configuration>
+ <executions>
+ <execution>
+ <goals>
+ <goal>sort</goal>
+ </goals>
+ <phase>validate</phase>
+ </execution>
+ </executions>
+ </plugin>
+
+ </plugins>
+ </build>
+
+ <profiles>
+ <!-- Common pattern in examples to build container images -->
+ <profile>
+ <id>container</id>
+ <activation>
+ <property>
+ <name>container</name>
+ </property>
+ </activation>
+ <properties>
+ <quarkus.profile>container</quarkus.profile>
+ </properties>
+ <dependencies>
+ <dependency>
+ <groupId>io.quarkus</groupId>
+ <artifactId>quarkus-container-image-jib</artifactId>
+ </dependency>
+ </dependencies>
+ </profile>
+ <profile>
+ <id>native</id>
+ <activation>
+ <property>
+ <name>native</name>
+ </property>
+ </activation>
+ <properties>
+ <quarkus.native.enabled>true</quarkus.native.enabled>
+ </properties>
+ </profile>
+ </profiles>
+
+</project>
diff --git
a/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/ErrorFunctions.java
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/ErrorFunctions.java
new file mode 100644
index 000000000..cd984061b
--- /dev/null
+++
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/ErrorFunctions.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.kie.kogito.examples.sw.opentelemetry.jaeger;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+@ApplicationScoped
+public class ErrorFunctions {
+
+ public void fail(String message) {
+ throw new IllegalStateException(message);
+ }
+}
diff --git
a/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/RegisterResource.java
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/RegisterResource.java
new file mode 100644
index 000000000..c18d2ef6c
--- /dev/null
+++
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/RegisterResource.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.kie.kogito.examples.sw.opentelemetry.jaeger;
+
+import java.util.Map;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+@Path("/register")
+public class RegisterResource {
+
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ public Map<String, Object> register(JsonNode payload) {
+ return Map.of("registered", true);
+ }
+}
diff --git
a/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/resources/application.properties
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/resources/application.properties
new file mode 100644
index 000000000..a7bc4a412
--- /dev/null
+++
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/resources/application.properties
@@ -0,0 +1,76 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+#
+
+
+quarkus.devservices.enabled=false
+quarkus.keycloak.devservices.enabled=false
+
+# Run KIE persistence migrations via Quarkus Flyway (works in native)
+quarkus.flyway.enabled=true
+quarkus.flyway.migrate-at-start=true
+quarkus.flyway.clean-at-start=false
+#quarkus.flyway.locations=classpath:kie-flyway/db/persistence-jdbc/postgresql
+
+# Prevent double-migration
+kie.flyway.enabled=false
+
+# Ensure native image contains the migration SQLs
+quarkus.native.resources.includes=kie-flyway/db/persistence-jdbc/.*
+
+quarkus.native.native-image-xmx=8g
+
+quarkus.application.name=sw-opentelemetry-jaeger-example
+
+quarkus.otel.enabled=true
+
+# Give Jaeger a stable service name to query
+quarkus.otel.service.name=sw-opentelemetry-jaeger-example
+
+# profile to pack this example into a container, to use it execute activate
the maven container profile, -Dcontainer
+%container.quarkus.container-image.build=true
+%container.quarkus.container-image.push=false
+%container.quarkus.container-image.group=${USER}
+%container.quarkus.container-image.registry=dev.local
+%container.quarkus.container-image.tag=1.0-SNAPSHOT
+
+# Point Flyway at the KIE migrations
+%prod.quarkus.flyway.locations=classpath:kie-flyway/db/persistence-jdbc/postgresql
+%prod.quarkus.flyway.enabled=true
+%prod.quarkus.flyway.migrate-at-start=true
+%prod.quarkus.flyway.clean-at-start=false
+%prod.quarkus.flyway.baseline-on-migrate=false
+
+%prod.kie.flyway.enabled=false
+
+%integration-test.quarkus.datasource.db-kind=postgresql
+%integration-test.quarkus.datasource.jdbc.url=${quarkus.datasource.jdbc.url}
+%integration-test.quarkus.datasource.username=${quarkus.datasource.username}
+%integration-test.quarkus.datasource.password=${quarkus.datasource.password}
+
+%integration-test.callbackBaseUrl=${test.url}
+
+quarkus.log.level=INFO
+quarkus.log.category."io.quarkus".level=INFO
+quarkus.log.category."org.testcontainers".level=INFO
+
+quarkus.rest-client."persist_callback_yaml".url=${test.url}
+
+mp.messaging.incoming.resume.connector=quarkus-http
+mp.messaging.incoming.resume.path=/resume
+mp.messaging.incoming.resume.method=POST
diff --git
a/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/resources/error.sw.json
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/resources/error.sw.json
new file mode 100644
index 000000000..38c606fc6
--- /dev/null
+++
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/resources/error.sw.json
@@ -0,0 +1,34 @@
+{
+ "id": "error",
+ "version": "1.0",
+ "specVersion": "0.8",
+ "name": "Error workflow",
+ "description": "Deterministic failure to validate error traces in Jaeger.",
+ "start": "FailNow",
+ "states": [
+ {
+ "name": "FailNow",
+ "type": "operation",
+ "actions": [
+ {
+ "name": "fail",
+ "functionRef": {
+ "refName": "failFn",
+ "arguments": {
+ "message": "Intentional failure for opentelemetry tests"
+ }
+ }
+ }
+ ],
+ "end": true
+ }
+ ],
+ "functions": [
+ {
+ "name": "failFn",
+ "type": "custom",
+ "operation":
"service:org.kie.kogito.examples.sw.opentelemetry.jaeger.ErrorFunctions::fail"
+ }
+ ]
+}
+
diff --git
a/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/resources/greet.sw.json
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/resources/greet.sw.json
new file mode 100644
index 000000000..0c79708ad
--- /dev/null
+++
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/resources/greet.sw.json
@@ -0,0 +1,71 @@
+{
+ "id": "greet",
+ "version": "1.0",
+ "specVersion": "0.8",
+ "name": "Greet parent workflow",
+ "description": "Parent workflow that logs, calls a subflow, then returns a
static response.",
+ "start": "Init",
+ "states": [
+ {
+ "name": "Init",
+ "type": "operation",
+ "actions": [
+ {
+ "name": "log-start",
+ "functionRef": {
+ "refName": "logInfo",
+ "arguments": {
+ "message": "Parent workflow started"
+ }
+ }
+ }
+ ],
+ "transition": "CallSubflow"
+ },
+ {
+ "name": "CallSubflow",
+ "type": "operation",
+ "actions": [
+ {
+ "name": "invoke-subflow",
+ "subFlowRef": {
+ "workflowId": "greetSubflow"
+ }
+ }
+ ],
+ "transition": "Finish"
+ },
+ {
+ "name": "Finish",
+ "type": "operation",
+ "actions": [
+ {
+ "name": "log-end",
+ "functionRef": {
+ "refName": "logInfo",
+ "arguments": {
+ "message": "Parent workflow completed"
+ }
+ }
+ }
+ ],
+ "transition": "Done"
+ },
+ {
+ "name": "Done",
+ "type": "inject",
+ "data": {
+ "message": "Hello from parent workflow"
+ },
+ "end": true
+ }
+ ],
+ "functions": [
+ {
+ "name": "logInfo",
+ "type": "custom",
+ "operation": "sysout:INFO"
+ }
+ ]
+}
+
diff --git
a/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/resources/greetSubflow.sw.json
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/resources/greetSubflow.sw.json
new file mode 100644
index 000000000..7e5840f46
--- /dev/null
+++
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/resources/greetSubflow.sw.json
@@ -0,0 +1,42 @@
+{
+ "id": "greetSubflow",
+ "version": "1.0",
+ "specVersion": "0.8",
+ "name": "Greet subflow",
+ "description": "Subflow that logs and returns a static response.",
+ "start": "SubflowLog",
+ "states": [
+ {
+ "name": "SubflowLog",
+ "type": "operation",
+ "actions": [
+ {
+ "name": "log-subflow",
+ "functionRef": {
+ "refName": "logInfo",
+ "arguments": {
+ "message": "Subflow executed"
+ }
+ }
+ }
+ ],
+ "transition": "SubflowDone"
+ },
+ {
+ "name": "SubflowDone",
+ "type": "inject",
+ "data": {
+ "message": "Hello from subflow"
+ },
+ "end": true
+ }
+ ],
+ "functions": [
+ {
+ "name": "logInfo",
+ "type": "custom",
+ "operation": "sysout:INFO"
+ }
+ ]
+}
+
diff --git
a/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/resources/persist-wait.sw.json
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/resources/persist-wait.sw.json
new file mode 100644
index 000000000..8daa2db76
--- /dev/null
+++
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/resources/persist-wait.sw.json
@@ -0,0 +1,75 @@
+{
+ "id": "persistWait",
+ "version": "1.0",
+ "name": "Persistence + Callback resume example",
+ "description": "Waits for an HTTP callback event and resumes after restart",
+ "start": "printWaitMessage",
+ "events": [
+ {
+ "name": "resumeEvent",
+ "source": "",
+ "type": "resume"
+ }
+ ],
+ "functions": [
+ {
+ "name": "callBack",
+ "type": "rest",
+ "operation": "specs/persist-callback.yaml#callBack"
+ },
+ {
+ "name": "printMessage",
+ "type": "custom",
+ "operation": "sysout:INFO"
+ }
+ ],
+ "states": [
+ {
+ "name": "printWaitMessage",
+ "type": "operation",
+ "actions": [
+ {
+ "name": "printBeforeEvent",
+ "functionRef": {
+ "refName": "printMessage",
+ "arguments": {
+ "message": "Init - waiting for resume event"
+ }
+ }
+ }
+ ],
+ "transition": "waitForEvent"
+ },
+ {
+ "name": "waitForEvent",
+ "type": "callback",
+ "action": {
+ "functionRef": {
+ "refName": "callBack",
+ "arguments": {
+ "processInstanceId": "$WORKFLOW.instanceId"
+ }
+ }
+ },
+ "eventRef": "resumeEvent",
+ "transition": "finish"
+ },
+ {
+ "name": "finish",
+ "type": "operation",
+ "actions": [
+ {
+ "name": "printAfterEvent",
+ "functionRef": {
+ "refName": "printMessage",
+ "arguments": {
+ "message": "After resume - context should be restored"
+ }
+ }
+ }
+ ],
+ "end": true
+ }
+ ]
+}
+
diff --git
a/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/resources/specs/persist-callback.yaml
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/resources/specs/persist-callback.yaml
new file mode 100644
index 000000000..1d3469486
--- /dev/null
+++
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/main/resources/specs/persist-callback.yaml
@@ -0,0 +1,21 @@
+openapi: 3.0.3
+info:
+ title: Persist Callback
+ version: "1.0"
+paths:
+ /register:
+ post:
+ operationId: callBack
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ processInstanceId:
+ type: string
+ responses:
+ "202":
+ description: Accepted
+
diff --git
a/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/JaegerQueryClient.java
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/JaegerQueryClient.java
new file mode 100644
index 000000000..c67a99e61
--- /dev/null
+++
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/JaegerQueryClient.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.kie.kogito.examples.sw.opentelemetry.jaeger;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Minimal Jaeger Query API client for tests. Uses endpoints: - GET
/api/services - GET
+ * /api/traces?service=...&limit=... - GET /api/traces/{traceId}
+ */
+public class JaegerQueryClient {
+
+ private final HttpClient http;
+ private final ObjectMapper mapper;
+ private final String baseUrl;
+
+ public JaegerQueryClient(String baseUrl) {
+ this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0,
baseUrl.length() - 1) : baseUrl;
+ this.http =
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
+ this.mapper = new ObjectMapper();
+ }
+
+ public List<String> listServices() {
+ JsonNode root = getJson("/api/services");
+ List<String> services = new ArrayList<>();
+ JsonNode data = root.get("data");
+ if (data != null && data.isArray()) {
+ data.forEach(n -> services.add(n.asText()));
+ }
+ return services;
+ }
+
+ public Optional<String> findLatestTraceIdForService(String serviceName,
int limit) {
+ String qs = "?service=" + urlEncode(serviceName) + "&limit=" + limit;
+ JsonNode root = getJson("/api/traces" + qs);
+ JsonNode data = root.get("data");
+ if (data == null || !data.isArray() || data.isEmpty()) {
+ return Optional.empty();
+ }
+ // The API generally returns recent traces first, but we keep it
simple:
+ JsonNode first = data.get(0);
+ JsonNode traceId = first.get("traceID");
+ return traceId == null ? Optional.empty() :
Optional.of(traceId.asText());
+ }
+
+ public JsonNode getTrace(String traceId) {
+ return getJson("/api/traces/" + urlEncode(traceId));
+ }
+
+ private JsonNode getJson(String path) {
+ try {
+ HttpRequest req = HttpRequest.newBuilder().uri(URI.create(baseUrl
+ path)).timeout(Duration.ofSeconds(10))
+ .GET().build();
+ HttpResponse<String> resp = http.send(req,
HttpResponse.BodyHandlers.ofString());
+ if (resp.statusCode() != 200) {
+ throw new IllegalStateException(
+ "Jaeger API call failed: " + path + " status=" +
resp.statusCode() + " body=" + resp.body());
+ }
+ return mapper.readTree(resp.body());
+ } catch (IOException | InterruptedException e) {
+ throw new RuntimeException("Jaeger API call failed: " + path, e);
+ }
+ }
+
+ private static String urlEncode(String s) {
+ return URLEncoder.encode(s, StandardCharsets.UTF_8);
+ }
+}
diff --git
a/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/JaegerTestResource.java
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/JaegerTestResource.java
new file mode 100644
index 000000000..a283bd71a
--- /dev/null
+++
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/JaegerTestResource.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.kie.kogito.examples.sw.opentelemetry.jaeger;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.jboss.logging.Logger;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+
+import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
+
+/**
+ * Starts a Jaeger all-in-one container (with OTLP ingestion enabled) for
tests. Exposes: - Jaeger Query/UI: 16686 -
+ * OTLP gRPC: 4317 - OTLP HTTP: 4318 Provides Quarkus config so the app
exports traces to Jaeger over OTLP.
+ */
+public class JaegerTestResource implements QuarkusTestResourceLifecycleManager
{
+
+ private static final Logger LOGGER =
Logger.getLogger(JaegerTestResource.class);
+
+ // Pin a specific version for CI stability (avoid "latest")
+ private static final String JAEGER_IMAGE = "jaegertracing/all-in-one:1.54";
+
+ private static final int JAEGER_QUERY_PORT = 16686;
+ private static final int OTLP_GRPC_PORT = 4317;
+ private static final int OTLP_HTTP_PORT = 4318;
+
+ private GenericContainer<?> jaeger;
+
+ @Override
+ public Map<String, String> start() {
+ jaeger = new GenericContainer<>(JAEGER_IMAGE)
+ .withExposedPorts(JAEGER_QUERY_PORT, OTLP_GRPC_PORT,
OTLP_HTTP_PORT)
+ // Enable OTLP ingestion in Jaeger all-in-one
+ .withEnv("COLLECTOR_OTLP_ENABLED", "true")
+ // Wait until Jaeger query API is responsive (better than only
port-open)
+
.waitingFor(Wait.forHttp("/api/services").forPort(JAEGER_QUERY_PORT).forStatusCode(200)
+ .withStartupTimeout(Duration.ofSeconds(60)));
+
+ jaeger.start();
+
+ String host = jaeger.getHost();
+ Integer mappedOtlpGrpc = jaeger.getMappedPort(OTLP_GRPC_PORT);
+ Integer mappedOtlpHttp = jaeger.getMappedPort(OTLP_HTTP_PORT);
+ Integer mappedQuery = jaeger.getMappedPort(JAEGER_QUERY_PORT);
+
+ // OTLP endpoint for Quarkus OTel exporter.
+ // For gRPC, Quarkus typically expects http://host:port and
protocol=grpc.
+ String otlpGrpcEndpoint = "http://" + host + ":" + mappedOtlpGrpc;
+
+ // Jaeger Query URL (handy for tests that call the Jaeger HTTP API)
+ String jaegerQueryBaseUrl = "http://" + host + ":" + mappedQuery;
+
+ LOGGER.infof("Jaeger started: query=%s, otlpGrpc=%s,
otlpHttp=http://%s:%d", jaegerQueryBaseUrl,
+ otlpGrpcEndpoint, host, mappedOtlpHttp);
+
+ Map<String, String> cfg = new HashMap<>();
+
+ // --- OTel export configuration (set both common variants to be safe
across Quarkus lines) ---
+ // cfg.put("quarkus.otel.traces.exporter", "otlp");
+
+ // Preferred OTLP exporter config (Quarkus 3.x commonly uses these)
+ cfg.put("quarkus.otel.exporter.otlp.endpoint", otlpGrpcEndpoint);
+ cfg.put("quarkus.otel.exporter.otlp.protocol", "grpc");
+
+ // Some setups split traces endpoint (harmless if ignored)
+ cfg.put("quarkus.otel.exporter.otlp.traces.endpoint",
otlpGrpcEndpoint);
+ cfg.put("quarkus.otel.exporter.otlp.traces.protocol", "grpc");
+
+ // Optional: make Jaeger query base URL available to the tests
+ cfg.put("test.jaeger.query.base-url", jaegerQueryBaseUrl);
+
+ return cfg;
+ }
+
+ @Override
+ public void stop() {
+ if (jaeger != null) {
+ try {
+ jaeger.stop();
+ } catch (Exception e) {
+ LOGGER.warn("Failed to stop Jaeger container cleanly", e);
+ } finally {
+ jaeger = null;
+ }
+ }
+ }
+
+ /**
+ * Convenience accessors for tests (optional). Note: These require the
resource instance to be started in the same
+ * JVM.
+ */
+ public String getJaegerQueryBaseUrl() {
+ if (jaeger == null) {
+ throw new IllegalStateException("Jaeger container is not started");
+ }
+ return "http://" + jaeger.getHost() + ":" +
jaeger.getMappedPort(JAEGER_QUERY_PORT);
+ }
+
+ public String getOtlpGrpcEndpoint() {
+ if (jaeger == null) {
+ throw new IllegalStateException("Jaeger container is not started");
+ }
+ return "http://" + jaeger.getHost() + ":" +
jaeger.getMappedPort(OTLP_GRPC_PORT);
+ }
+}
diff --git
a/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/OpenTelemetryJaegerIT.java
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/OpenTelemetryJaegerIT.java
new file mode 100644
index 000000000..c551b3e2b
--- /dev/null
+++
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/OpenTelemetryJaegerIT.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.kie.kogito.examples.sw.opentelemetry.jaeger;
+
+import io.quarkus.test.junit.QuarkusIntegrationTest;
+
+/**
+ * Runs OpenTelemetryJaegerTest as a Quarkus integration test (packaged
artifact). This is the class that Failsafe will
+ * pick up.
+ */
+@QuarkusIntegrationTest
+public class OpenTelemetryJaegerIT extends OpenTelemetryJaegerTest {
+}
diff --git
a/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/OpenTelemetryJaegerTest.java
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/OpenTelemetryJaegerTest.java
new file mode 100644
index 000000000..b9684d91a
--- /dev/null
+++
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/OpenTelemetryJaegerTest.java
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.kie.kogito.examples.sw.opentelemetry.jaeger;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.is;
+
+import java.time.Duration;
+import java.util.Optional;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import org.awaitility.Awaitility;
+
+import io.quarkus.test.common.QuarkusTestResource;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.kie.kogito.examples.sw.opentelemetry.jaeger.helper.JaegerPoller;
+import
org.kie.kogito.examples.sw.opentelemetry.jaeger.helper.JaegerQueryClient;
+import static
org.kie.kogito.examples.sw.opentelemetry.jaeger.helper.JaegerHelper.*;
+
+@QuarkusTestResource(JaegerTestResource.class)
+public abstract class OpenTelemetryJaegerTest {
+
+ private static final String GREET_ENDPOINT = "/greet";
+ private static final String ERROR_ENDPOINT = "/error";
+
+ /**
+ * Test 1: traces exist and contain SonataFlow tags + transaction/tracker
tags
+ */
+ @Test
+ void test1_tracesExistAndContainSonataflowTags() {
+ triggerWorkflow(GREET_ENDPOINT, "{\"name\":\"Ada\"}", "txn-001",
"corr-123");
+
+ JsonNode trace =
waitForTraceWithTracker("sonataflow.tracker.correlation_id", "corr-123");
+
+ assertHasAnySpans(trace);
+
+ String processInstanceId = assertAnyWorkflowSpanHasTag(trace,
"sonataflow.process.instance.id");
+ assertFalse(processInstanceId.isBlank());
+ assertAnyWorkflowSpanHasTag(trace, "sonataflow.process.id", "greet");
+ assertAnyWorkflowSpanHasTag(trace,
"sonataflow.tracker.correlation_id", "corr-123");
+ assertAnyWorkflowSpanHasTag(trace, "sonataflow.transaction.id",
"txn-001");
+
+ assertHasAtLeastOneHttpServerSpan(trace);
+ assertWorkflowStatesContainAtLeast(trace, "Init", "CallSubflow",
"Finish", "Done");
+ }
+
+ /**
+ * Test 2: fallback when no X-TRANSACTION-ID header: transaction tag sets
to process.instance.id
+ */
+ @Test
+ void test2_transactionFallbackWhenHeaderMissing() {
+ // No transaction header
+ given().header("X-TRACKER-correlation_id", "corr-fallback-1")
+ .contentType("application/json")
+ .body("{\"name\":\"Edu\"}")
+ .when()
+ .post(GREET_ENDPOINT)
+ .then()
+ .statusCode(anyOf(is(200), is(201)));
+
+ JsonNode trace =
waitForTraceWithTracker("sonataflow.tracker.correlation_id", "corr-fallback-1");
+
+ String processInstanceId = assertAnyWorkflowSpanHasTag(trace,
"sonataflow.process.instance.id");
+ assertFalse(processInstanceId.isBlank());
+
+ // transactiond.id is sent with processInstanceId
+ assertAnyWorkflowSpanHasTag(trace, "sonataflow.transaction.id",
processInstanceId);
+ }
+
+ /**
+ * Test 3: subflow correlation Parent workflow triggers subflow.
+ * Validate tracker + transaction are present
+ * (correlation across parent/subflow)
+ */
+ @Test
+ void test3_subflowPlaceholderStateIsTracedAndCorrelated() {
+ given().header("X-TRANSACTION-ID", "txn-subflow-001")
+ .header("X-TRACKER-testcase", "t3-subflow")
+ .contentType("application/json")
+ .body("{\"name\":\"Ada\"}")
+ .when()
+ .post(GREET_ENDPOINT).then()
+ .statusCode(anyOf(is(200), is(201)));
+
+ JsonNode trace =
waitForTraceWithTracker("sonataflow.tracker.testcase", "t3-subflow");
+
+ assertWorkflowStatesContainAtLeast(trace, "Init", "CallSubflow",
"Done");
+
+ assertAnyWorkflowSpanHasTag(trace, "sonataflow.transaction.id",
"txn-subflow-001");
+
+ String pid = assertAnyWorkflowSpanHasTag(trace,
"sonataflow.process.instance.id");
+ assertFalse(pid.isBlank());
+
+ JsonNode callSubflowSpan = assertWorkflowHasAnyState(trace,
"CallSubflow");
+ assertSpanHasTag(callSubflowSpan, "sonataflow.transaction.id");
+
+ JsonNode parentSpan = assertAnySpanOperationNameEquals(trace,
"sonataflow.process.greet.execute");
+ JsonNode subflowSpan = assertAnySpanOperationNameEquals(trace,
"sonataflow.process.greetSubflow.execute");
+
+ String parentTx = requireTagValue(parentSpan,
"sonataflow.transaction.id");
+ String subflowTx = requireTagValue(subflowSpan,
"sonataflow.transaction.id");
+
+ assertEquals(parentTx, subflowTx, "Transaction id must match between
parent and subflow spans");
+ }
+
+ /**
+ * Test 4: error workflow shows error
+ */
+ @Test
+ void test4_errorWorkflowShowsError() {
+ given().header("X-TRANSACTION-ID", "txn-error-001")
+ .header("X-TRACKER-testcase", "t4-error")
+ .contentType("application/json")
+ .body("{\"name\":\"Soul\"}")
+ .when()
+ .post(ERROR_ENDPOINT).then()
+ .statusCode(anyOf(is(400), is(500)));
+
+ JsonNode trace =
waitForTraceWithTracker("sonataflow.tracker.testcase", "t4-error");
+
+ assertAnySpanLooksErrored(trace);
+ assertAnyWorkflowSpanHasTag(trace, "sonataflow.transaction.id",
"txn-error-001");
+ }
+
+}
diff --git
a/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/helper/JaegerHelper.java
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/helper/JaegerHelper.java
new file mode 100644
index 000000000..54d83a522
--- /dev/null
+++
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/helper/JaegerHelper.java
@@ -0,0 +1,485 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.kie.kogito.examples.sw.opentelemetry.jaeger.helper;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Optional;
+import java.util.Set;
+import com.fasterxml.jackson.databind.JsonNode;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.is;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.Optional;
+
+import org.jboss.logging.Logger;
+
+import org.awaitility.Awaitility;
+
+/**
+ * Helper methods for Jaeger trace JSON.
+ * Jaeger trace format: GET /api/traces/{traceId}
+ * Response: { "data": [{"traceID": "...", "spans": [...] } ] }
+ */
+public final class JaegerHelper {
+
+ private static final Logger LOGGER = Logger.getLogger(JaegerHelper.class);
+ private static final String DEBUG_JAEGER_PROP = "test.jaeger.debug";
+
+ // Must match application.properties: quarkus.otel.service.name
+ public static final String SERVICE_NAME =
"sw-opentelemetry-jaeger-example";
+ public static final String JAEGER_QUERY_BASE_URL_KEY =
"test.jaeger.query.base-url";
+
+ public static final Duration TIMEOUT = Duration.ofSeconds(30);
+ public static final Duration POLL = Duration.ofMillis(500);
+
+ private JaegerHelper() {
+ }
+
+ public static JsonNode extractFirstTrace(JsonNode traceResponse) {
+ JsonNode data = traceResponse.get("data");
+ if (data == null || !data.isArray() || data.isEmpty()) {
+ throw new AssertionError("Expected Jaeger response to contain
non-empty 'data' array");
+ }
+ return data.get(0);
+ }
+
+ public static void assertHasAnySpans(JsonNode jaegerTrace) {
+ JsonNode spans = jaegerTrace.get("spans");
+ if (spans == null || !spans.isArray() || spans.isEmpty()) {
+ throw new AssertionError("Expected trace to contain non-empty
'spans' array");
+ }
+ }
+
+ public static String assertAnyWorkflowSpanHasTag(JsonNode trace, String
key) {
+ return assertAnyWorkflowSpanHasTag(trace, key, null, false);
+ }
+
+ public static String assertAnyWorkflowSpanHasTag(JsonNode trace, String
key, String expectedValue) {
+ return assertAnyWorkflowSpanHasTag(trace, key, expectedValue, true);
+ }
+
+ public static void assertHasAtLeastOneHttpServerSpan(JsonNode trace) {
+ JsonNode spans = trace.get("spans");
+ if (spans == null || !spans.isArray() || spans.isEmpty()) {
+ throw new AssertionError("No spans found in trace");
+ }
+
+ for (JsonNode span : spans) {
+ String op = span.hasNonNull("operationName") ?
span.get("operationName").asText() : "";
+ if (op.startsWith("POST ") || op.startsWith("GET ") ||
op.startsWith("PUT ") || op.startsWith("DELETE ")) {
+ return;
+ }
+ // Alternative: check for span.kind=server tag
+ if (spanHasTag(span, "span.kind", "server")) {
+ return;
+ }
+ }
+
+ throw new AssertionError(
+ "Did not find any HTTP server span (operationName starting
with HTTP verb or span.kind=server)");
+ }
+
+ public static void assertWorkflowStatesContainAtLeast(JsonNode trace,
String... expectedStates) {
+ Set<String> states = collectWorkflowStates(trace);
+ for (String s : expectedStates) {
+ if (!states.contains(s)) {
+ throw new AssertionError("Expected workflow states to contain
'" + s + "', but states were: " + states);
+ }
+ }
+ }
+
+ public static JsonNode assertWorkflowHasAnyState(JsonNode trace, String
state) {
+ return findWorkflowSpanByState(trace, state)
+ .orElseThrow(() -> new AssertionError("Did not find workflow
span with state '" + state + "'"));
+ }
+
+ public static String assertSpanHasTag(JsonNode span, String tagKey) {
+ return findTagValue(span, tagKey)
+ .orElseThrow(() -> new AssertionError("Expected span to
contain tag '" + tagKey + "'"));
+ }
+
+ public static Optional<String> findTagValue(JsonNode span, String tagKey) {
+ JsonNode tags = span.get("tags");
+ if (tags == null || !tags.isArray()) {
+ return Optional.empty();
+ }
+
+ for (JsonNode tag : tags) {
+ if (tagKey.equals(tag.path("key").asText())) {
+ return Optional.ofNullable(tag.path("value").asText(null));
+ }
+ }
+ return Optional.empty();
+ }
+
+ public static Set<String> collectWorkflowStates(JsonNode trace) {
+ Set<String> states = new HashSet<>();
+ JsonNode spans = trace.get("spans");
+ if (spans == null || !spans.isArray()) {
+ return states;
+ }
+
+ for (JsonNode span : spans) {
+ String op = span.hasNonNull("operationName") ?
span.get("operationName").asText() : "";
+ if (!op.startsWith("sonataflow.process.")) {
+ continue;
+ }
+
+ JsonNode tags = span.get("tags");
+ if (tags == null || !tags.isArray()) {
+ continue;
+ }
+
+ for (JsonNode tag : tags) {
+ String key = tag.hasNonNull("key") ? tag.get("key").asText() :
"";
+ if ("sonataflow.workflow.state".equals(key)) {
+ String val = tag.hasNonNull("value") ?
tag.get("value").asText() : null;
+ if (val != null && !val.isBlank()) {
+ states.add(val);
+ }
+ }
+ }
+ }
+ return states;
+ }
+
+ public static Optional<JsonNode> findWorkflowSpanByState(JsonNode trace,
String expectedState) {
+ JsonNode spans = trace.get("spans");
+ if (spans == null || !spans.isArray()) {
+ return Optional.empty();
+ }
+
+ for (JsonNode span : spans) {
+ if (!isWorkflowSpan(span)) {
+ continue;
+ }
+ Optional<String> state = findTagValue(span,
"sonataflow.workflow.state");
+ if (state.isPresent() && expectedState.equals(state.get())) {
+ return Optional.of(span);
+ }
+ }
+ return Optional.empty();
+ }
+
+ public static boolean isWorkflowSpan(JsonNode span) {
+ if (span == null) {
+ return false;
+ }
+
+ JsonNode op = span.get("operationName");
+ if (op != null && op.isTextual() &&
op.asText().startsWith("sonataflow.process.")) {
+ return true;
+ }
+
+ JsonNode tags = span.get("tags");
+ if (tags == null || !tags.isArray()) {
+ return false;
+ }
+
+ for (JsonNode tag : tags) {
+ JsonNode keyNode = tag.get("key");
+ if (keyNode != null && keyNode.isTextual()) {
+ String key = keyNode.asText();
+ if (key.startsWith("sonataflow.")) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public static JsonNode assertAnySpanOperationNameEquals(JsonNode trace,
String expectedOperationName) {
+ JsonNode spans = trace.get("spans");
+ if (spans == null || !spans.isArray()) {
+ throw new AssertionError("Trace JSON missing spans array");
+ }
+
+ for (JsonNode span : spans) {
+ String op = span.path("operationName").asText("");
+ if (expectedOperationName.equals(op)) {
+ return span;
+ }
+ }
+
+ throw new AssertionError("Did not find any span with operationName='"
+ expectedOperationName + "'");
+ }
+
+ public static String requireTagValue(JsonNode span, String key) {
+ JsonNode tags = span.get("tags");
+ if (tags == null || !tags.isArray()) {
+ throw new AssertionError("Span JSON missing tags array");
+ }
+
+ for (JsonNode tag : tags) {
+ if (key.equals(tag.path("key").asText())) {
+ String value = tag.path("value").asText(null);
+ if (value == null || value.isBlank()) {
+ throw new AssertionError("Tag '" + key + "' value is
empty");
+ }
+ return value;
+ }
+ }
+ throw new AssertionError("Did not find tag '" + key + "' on span");
+ }
+
+ public static void assertAnySpanLooksErrored(JsonNode jaegerTrace) {
+ JsonNode spans = jaegerTrace.get("spans");
+ if (spans == null || !spans.isArray()) {
+ throw new AssertionError("Trace JSON missing spans array");
+ }
+ for (JsonNode span : spans) {
+ JsonNode tags = span.get("tags");
+ if (tags == null || !tags.isArray()) {
+ continue;
+ }
+ for (JsonNode tag : tags) {
+ String key = tag.hasNonNull("key") ? tag.get("key").asText() :
"";
+ if ("error".equalsIgnoreCase(key)) {
+ JsonNode v = tag.get("value");
+ if (v != null && (v.asBoolean(false) ||
"true".equalsIgnoreCase(v.asText()))) {
+ return;
+ }
+ }
+ if (key.toLowerCase().contains("exception") ||
key.toLowerCase().contains("status")) {
+ return;
+ }
+ }
+ }
+ throw new AssertionError("Did not find any span with an error
indicator tag (e.g., error=true)");
+ }
+
+ public static void triggerWorkflow(String endpoint, String body, String
transactionId, String correlationId) {
+ given().header("X-TRANSACTION-ID", transactionId)
+ .header("X-TRACKER-correlation_id", correlationId)
+ .contentType("application/json")
+ .body(body)
+ .when()
+ .post(endpoint).then()
+ .statusCode(anyOf(is(200), is(201)));
+ }
+
+ public static JsonNode waitForTraceWithTracker(String trackerKey, String
trackerValue) {
+ return waitForTraceWithTracker(trackerKey, trackerValue, null, new
String[0]);
+ }
+
+ public static JsonNode waitForTraceWithTracker(String trackerKey, String
trackerValue,
+ String expectedProcessInstanceId, String... requiredStates) {
+ JaegerQueryClient jaeger = new JaegerQueryClient(getJaegerBaseUrl());
+
+
Awaitility.await().atMost(Duration.ofSeconds(30)).pollInterval(Duration.ofMillis(500))
+ .until(() -> jaeger.listServices().contains(SERVICE_NAME));
+
+ return
Awaitility.await().atMost(Duration.ofSeconds(30)).pollInterval(Duration.ofMillis(500)).until(()
-> {
+ // Look at multiple recent traces, not only "latest"
+ List<String> traceIds =
jaeger.findTraceIdsForService(SERVICE_NAME, 30);
+ if (traceIds.isEmpty()) {
+ return null;
+ }
+
+ for (String traceId : traceIds) {
+ JsonNode resp = jaeger.getTrace(traceId); // {data:[{...}]}
+ JsonNode data = resp.get("data");
+ if (data == null || !data.isArray() || data.isEmpty()) {
+ continue;
+ }
+ JsonNode trace = data.get(0);
+
+ // Must have tracker on a workflow span
+ if (!containsWorkflowSpanTag(trace, trackerKey, trackerValue))
{
+ continue;
+ }
+
+ // If PID is provided, require it too
+ if (expectedProcessInstanceId != null &&
!expectedProcessInstanceId.isBlank()
+ && !containsWorkflowSpanTag(trace,
"sonataflow.process.instance.id",
+ expectedProcessInstanceId)) {
+ continue;
+ }
+
+ // If states are required, check that the trace contains them
+ if (requiredStates != null && requiredStates.length > 0) {
+ Set<String> states = collectWorkflowStates(trace);
+ boolean allPresent = true;
+ for (String s : requiredStates) {
+ if (!states.contains(s)) {
+ allPresent = false;
+ break;
+ }
+ }
+ if (!allPresent) {
+ continue;
+ }
+ }
+
+ debugPrintTrace(trace);
+ return trace;
+ }
+ return null;
+ }, t -> t != null);
+ }
+
+ private static boolean containsWorkflowSpanTag(JsonNode trace, String key,
String expectedValue) {
+ JsonNode spans = trace.get("spans");
+ if (spans == null || !spans.isArray()) {
+ return false;
+ }
+
+ for (JsonNode span : spans) {
+ String op = span.hasNonNull("operationName") ?
span.get("operationName").asText() : "";
+ if (!op.startsWith("sonataflow.process.")) {
+ continue;
+ }
+ JsonNode tags = span.get("tags");
+ if (tags == null || !tags.isArray()) {
+ continue;
+ }
+ for (JsonNode tag : tags) {
+ String k = tag.hasNonNull("key") ? tag.get("key").asText() :
"";
+ if (!key.equals(k))
+ continue;
+ String v = tag.hasNonNull("value") ? tag.get("value").asText()
: "";
+ if (expectedValue.equals(v))
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static String getJaegerBaseUrl() {
+ String url = System.getProperty(JAEGER_QUERY_BASE_URL_KEY);
+ if (url == null || url.isBlank()) {
+ url = System.getenv("TEST_JAEGER_QUERY_BASE_URL");
+ }
+ if (url == null || url.isBlank()) {
+ throw new IllegalStateException(
+ "Jaeger base URL not configured. Expected system property:
" + JAEGER_QUERY_BASE_URL_KEY);
+ }
+ return url;
+ }
+
+ private static String assertAnyWorkflowSpanHasTag(JsonNode trace, String
key, String expectedValue,
+ boolean checkValue) {
+ JsonNode spans = trace.get("spans");
+ if (spans == null || !spans.isArray()) {
+ throw new AssertionError("Trace JSON missing spans array");
+ }
+
+ for (JsonNode span : spans) {
+ String op = span.hasNonNull("operationName") ?
span.get("operationName").asText() : "";
+
+ if (!op.startsWith("sonataflow.process.")) {
+ continue;
+ }
+
+ JsonNode tags = span.get("tags");
+ if (tags == null || !tags.isArray()) {
+ continue;
+ }
+
+ for (JsonNode tag : tags) {
+ String k = tag.hasNonNull("key") ? tag.get("key").asText() :
"";
+ if (!key.equals(k)) {
+ continue;
+ }
+
+ String actualValue = tag.hasNonNull("value") ?
tag.get("value").asText() : null;
+
+ if (!checkValue) {
+ if (actualValue == null || actualValue.isBlank()) {
+ throw new AssertionError("Tag '" + key + "' found but
value is empty");
+ }
+ return actualValue;
+ }
+
+ if (expectedValue.equals(actualValue)) {
+ return actualValue;
+ }
+ }
+ }
+
+ if (checkValue) {
+ throw new AssertionError("Did not find workflow span tag '" + key
+ "' with value '" + expectedValue + "'");
+ }
+
+ throw new AssertionError("Did not find workflow span tag '" + key + "'
on any workflow span");
+ }
+
+ private static boolean spanHasTag(JsonNode span, String expectedKey,
String expectedValue) {
+ JsonNode tags = span.get("tags");
+ if (tags == null || !tags.isArray()) {
+ return false;
+ }
+ for (JsonNode tag : tags) {
+ String key = tag.hasNonNull("key") ? tag.get("key").asText() : "";
+ if (!expectedKey.equals(key)) {
+ continue;
+ }
+ String val = tag.hasNonNull("value") ? tag.get("value").asText() :
"";
+ if (expectedValue == null || expectedValue.equals(val)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isJaegerDebugEnabled() {
+ // -Dtest.jaeger.debug=true
+ String v = System.getProperty(DEBUG_JAEGER_PROP);
+ if (v == null || v.isBlank()) {
+ v = System.getenv("TEST_JAEGER_DEBUG");
+ }
+ return "true".equalsIgnoreCase(v) || "1".equals(v) ||
"yes".equalsIgnoreCase(v);
+ }
+
+ public static void debugPrintTrace(JsonNode trace) {
+ if (!isJaegerDebugEnabled()) {
+ return;
+ }
+
+ LOGGER.info("=== Span names ===");
+ for (JsonNode span : trace.get("spans")) {
+ LOGGER.info("span.operationName=" +
span.get("operationName").asText());
+ }
+
+ LOGGER.info("=== Tags containing
process/sonataflow/transaction/tracker ===");
+ for (JsonNode span : trace.get("spans")) {
+ JsonNode tags = span.get("tags");
+ if (tags == null || !tags.isArray()) {
+ continue;
+ }
+
+ String op = span.hasNonNull("operationName") ?
span.get("operationName").asText() : "<unknown>";
+ for (JsonNode tag : tags) {
+ String key = tag.hasNonNull("key") ? tag.get("key").asText() :
"";
+ if (key.contains("process") || key.contains("sonataflow") ||
key.contains("transaction")
+ || key.contains("tracker")) {
+ LOGGER.info(op + " :: " + tag.toPrettyString());
+ }
+ }
+ }
+ }
+
+}
diff --git
a/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/helper/JaegerPoller.java
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/helper/JaegerPoller.java
new file mode 100644
index 000000000..968632fbf
--- /dev/null
+++
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/helper/JaegerPoller.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.kie.kogito.examples.sw.opentelemetry.jaeger.helper;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Simple polling utilities to avoid sleeps in tests and to handle eventual
+ * consistency in Jaeger.
+ */
+public final class JaegerPoller {
+
+ private JaegerPoller() {
+ }
+
+ public static void waitForService(JaegerQueryClient jaeger, String
serviceName, Duration timeout,
+ Duration pollInterval) {
+ Instant deadline = Instant.now().plus(timeout);
+
+ while (Instant.now().isBefore(deadline)) {
+ List<String> services = jaeger.listServices();
+ if (services.contains(serviceName)) {
+ return;
+ }
+ sleep(pollInterval);
+ }
+ throw new AssertionError(
+ "Timed out waiting for Jaeger service '" + serviceName + "' to
appear in /api/services");
+ }
+
+ /**
+ * Waits until Jaeger returns a trace ID for the given service.
+ *
+ * @return the traceId
+ */
+ public static String waitForAnyTrace(JaegerQueryClient jaeger, String
serviceName, int limit, Duration timeout,
+ Duration pollInterval) {
+
+ Instant deadline = Instant.now().plus(timeout);
+
+ while (Instant.now().isBefore(deadline)) {
+ Optional<String> traceId =
jaeger.findLatestTraceIdForService(serviceName, limit);
+ if (traceId.isPresent()) {
+ return traceId.get();
+ }
+ sleep(pollInterval);
+ }
+ throw new AssertionError("Timed out waiting for any trace for service
'" + serviceName + "' in /api/traces");
+ }
+
+ private static void sleep(Duration d) {
+ try {
+ Thread.sleep(Math.max(1, d.toMillis()));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException("Polling interrupted", e);
+ }
+ }
+}
diff --git
a/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/helper/JaegerQueryClient.java
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/helper/JaegerQueryClient.java
new file mode 100644
index 000000000..febf8daa4
--- /dev/null
+++
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/helper/JaegerQueryClient.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.kie.kogito.examples.sw.opentelemetry.jaeger.helper;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Minimal Jaeger Query HTTP API client for tests.
+ * Endpoints used:
+ * - GET /api/services
+ * - GET /api/traces?service=<service>&limit=<n>
+ * - GET /api/traces/<traceId>
+ */
+public class JaegerQueryClient {
+
+ private final String baseUrl;
+ private final HttpClient httpClient;
+ private final ObjectMapper mapper;
+
+ public JaegerQueryClient(String baseUrl) {
+ this.baseUrl = normalizeBaseUrl(baseUrl);
+ this.httpClient =
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
+ this.mapper = new ObjectMapper();
+ }
+
+ public List<String> listServices() {
+ JsonNode root = getJson("/api/services");
+ JsonNode data = root.get("data");
+ List<String> services = new ArrayList<>();
+ if (data != null && data.isArray()) {
+ data.forEach(n -> services.add(n.asText()));
+ }
+ return services;
+ }
+
+ /**
+ * Find traces for a service using Jaeger's HTTP query API.
+ */
+ public JsonNode findTracesByService(String serviceName, int limit) {
+ return findTracesByService(serviceName, limit, "1h");
+ }
+
+ /**
+ * Same as above but configurable lookback
+ */
+ public JsonNode findTracesByService(String serviceName, int limit, String
lookback) {
+ String serviceEnc = URLEncoder.encode(serviceName,
StandardCharsets.UTF_8);
+ String lookbackEnc = URLEncoder.encode(lookback,
StandardCharsets.UTF_8);
+
+ // Jaeger supports: service, limit, lookback.
+ // You can also add start/end, tags, operation, minDuration,
maxDuration if needed.
+ String url = baseUrl + "/api/traces?service=" + serviceEnc + "&limit="
+ limit + "&lookback=" + lookbackEnc;
+
+ HttpRequest req =
HttpRequest.newBuilder().uri(URI.create(url)).GET().header("Accept",
"application/json")
+ .build();
+
+ HttpResponse<String> resp = send(req);
+
+ if (resp.statusCode() < 200 || resp.statusCode() >= 300) {
+ throw new IllegalStateException("Jaeger findTracesByService
failed: HTTP " + resp.statusCode() + " url="
+ + url + " body=" + safeBody(resp.body()));
+ }
+
+ try {
+ return mapper.readTree(resp.body());
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to parse Jaeger response
JSON from " + url + ": " + e.getMessage(),
+ e);
+ }
+ }
+
+ public List<String> findTraceIdsForService(String serviceName, int limit) {
+ JsonNode resp = findTracesByService(serviceName, limit); // implement
using your existing HTTP call
+ JsonNode data = resp.get("data");
+ if (data == null || !data.isArray() || data.isEmpty()) {
+ return List.of();
+ }
+
+ List<String> ids = new ArrayList<>();
+ for (JsonNode item : data) {
+ JsonNode traceID = item.get("traceID");
+ if (traceID != null && !traceID.asText().isBlank()) {
+ ids.add(traceID.asText());
+ }
+ }
+ return ids;
+ }
+
+ public Optional<String> findLatestTraceIdForService(String serviceName,
int limit) {
+ List<String> ids = findTraceIdsForService(serviceName, limit);
+ return ids.isEmpty() ? Optional.empty() : Optional.of(ids.get(0));
+ }
+
+ public JsonNode getTrace(String traceId) {
+ return getJson("/api/traces/" + urlEncode(traceId));
+ }
+
+ private JsonNode getJson(String path) {
+ try {
+ HttpRequest req = HttpRequest.newBuilder().uri(URI.create(baseUrl
+ path)).timeout(Duration.ofSeconds(10))
+ .GET().build();
+ HttpResponse<String> resp = httpClient.send(req,
HttpResponse.BodyHandlers.ofString());
+
+ if (resp.statusCode() != 200) {
+ throw new IllegalStateException("Jaeger API call failed: " +
path + " status=" + resp.statusCode()
+ + " body=" + truncate(resp.body(), 400));
+ }
+ return mapper.readTree(resp.body());
+ } catch (IOException | InterruptedException e) {
+ throw new RuntimeException("Jaeger API call failed: " + path, e);
+ }
+ }
+
+ private static String normalizeBaseUrl(String baseUrl) {
+ if (baseUrl == null || baseUrl.isBlank()) {
+ throw new IllegalArgumentException("baseUrl must not be
null/blank");
+ }
+ return baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() -
1) : baseUrl;
+ }
+
+ private static String urlEncode(String s) {
+ return URLEncoder.encode(s, StandardCharsets.UTF_8);
+ }
+
+ private static String truncate(String s, int max) {
+ if (s == null) {
+ return "";
+ }
+ return s.length() <= max ? s : s.substring(0, max) + "...";
+ }
+
+ private HttpResponse<String> send(HttpRequest req) {
+ try {
+ return httpClient.send(req, HttpResponse.BodyHandlers.ofString());
+ } catch (IOException | InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Failed calling Jaeger: " +
req.uri() + " -> " + e.getMessage(), e);
+ }
+ }
+
+ private static String safeBody(String body) {
+ if (body == null) {
+ return "";
+ }
+ return body.length() > 500 ? body.substring(0, 500) + "..." : body;
+ }
+}
diff --git
a/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/persistence/CloudEvents.java
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/persistence/CloudEvents.java
new file mode 100644
index 000000000..b6cff6c0f
--- /dev/null
+++
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/persistence/CloudEvents.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.kie.kogito.examples.sw.opentelemetry.jaeger.persistence;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+public final class CloudEvents {
+
+ private CloudEvents() {
+ }
+
+ public static Map<String, Object> resumeEventWithProcessInstanceId(String
pid) {
+ Map<String, Object> evt = new HashMap<>();
+ evt.put("specversion", "1.0");
+ evt.put("id", "evt-" + UUID.randomUUID());
+ evt.put("source", "tests");
+ evt.put("type", "resume");
+ evt.put("datacontenttype", "application/json");
+
+ // CloudEvent extension used by SonataFlow callback correlation
+ evt.put("kogitoprocrefid", pid);
+
+ evt.put("data", Map.of());
+ return evt;
+ }
+}
diff --git
a/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/persistence/PersistWait01StartIT.java
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/persistence/PersistWait01StartIT.java
new file mode 100644
index 000000000..7cc5a75db
--- /dev/null
+++
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/persistence/PersistWait01StartIT.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.kie.kogito.examples.sw.opentelemetry.jaeger.persistence;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.is;
+import static
org.kie.kogito.examples.sw.opentelemetry.jaeger.helper.JaegerHelper.*;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.junit.QuarkusIntegrationTest;
+import org.kie.kogito.examples.sw.opentelemetry.jaeger.JaegerTestResource;
+
+@QuarkusIntegrationTest
+@QuarkusTestResource(PostgresTestResource.class)
+@QuarkusTestResource(JaegerTestResource.class)
+public class PersistWait01StartIT {
+
+ private static final String START_ENDPOINT = "/persistWait";
+
+ @Test
+ void start_and_persist_instanceId() {
+ // Start workflow so it reaches the waiting state
+ JsonNode started = given().header("X-TRANSACTION-ID",
"txn-persist-001")
+ .header("X-TRACKER-correlation_id",
"corr-persist-001")
+ .contentType("application/json")
+ .body("{}")
+ .when()
+ .post(START_ENDPOINT)
+ .then()
+ .statusCode(anyOf(is(200),
is(201))).extract().as(JsonNode.class);
+
+ String pid = started.hasNonNull("id") ? started.get("id").asText() :
null;
+ if (pid == null || pid.isBlank()) {
+ throw new AssertionError("Start response did not contain 'id'.
Response: " + started);
+ }
+
+ JsonNode trace =
waitForTraceWithTracker("sonataflow.tracker.correlation_id",
"corr-persist-001");
+ assertAnyWorkflowSpanHasTag(trace, "sonataflow.process.id",
"persistWait");
+ assertAnyWorkflowSpanHasTag(trace, "sonataflow.process.instance.id",
pid);
+
+ assertWorkflowHasAnyState(trace, "printWaitMessage");
+
+ // Verify it exists immediately (before restart in the other test)
+ given().when().get(START_ENDPOINT + "/" + pid).then().statusCode(200);
+
+ PersistWaitPidStore.write(pid);
+ }
+}
diff --git
a/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/persistence/PersistWait02ResumeIT.java
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/persistence/PersistWait02ResumeIT.java
new file mode 100644
index 000000000..4f7c3bbd8
--- /dev/null
+++
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/persistence/PersistWait02ResumeIT.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.kie.kogito.examples.sw.opentelemetry.jaeger.persistence;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.is;
+
+import java.time.Duration;
+import java.util.Optional;
+
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.junit.QuarkusIntegrationTest;
+import org.kie.kogito.examples.sw.opentelemetry.jaeger.JaegerTestResource;
+import org.kie.kogito.examples.sw.opentelemetry.jaeger.helper.JaegerPoller;
+import
org.kie.kogito.examples.sw.opentelemetry.jaeger.helper.JaegerQueryClient;
+import static
org.kie.kogito.examples.sw.opentelemetry.jaeger.helper.JaegerHelper.*;
+
+@QuarkusIntegrationTest
+@QuarkusTestResource(PostgresTestResource.class)
+@QuarkusTestResource(JaegerTestResource.class)
+public class PersistWait02ResumeIT {
+
+ private static final String SERVICE_NAME =
"sw-opentelemetry-jaeger-example";
+ private static final String PROCESS_ID = "persistWait";
+ private static final String BASE = "/persistWait";
+
+ private static final Duration TIMEOUT = Duration.ofSeconds(60);
+ private static final Duration POLL = Duration.ofMillis(500);
+
+ @Test
+ void resume_after_restart_should_continue_and_trace_has_context() {
+ String pid = PersistWaitPidStore.read();
+
+ //instance must still exist
+ given().when()
+ .get(BASE + "/" + pid)
+ .then()
+ .statusCode(200);
+
+ // Send resume CloudEvent
+ given().contentType("application/cloudevents+json")
+ .body(CloudEvents.resumeEventWithProcessInstanceId(pid))
+ .when()
+ .post("/resume")
+ .then()
+ .statusCode(is(202));
+
+ // Verify via Jaeger that we got spans for this process instance
(after resuming)
+ JaegerQueryClient jaeger = new JaegerQueryClient(getJaegerBaseUrl());
+
+ JaegerPoller.waitForService(jaeger, SERVICE_NAME, TIMEOUT, POLL);
+
+ // Poll until a trace appears that contains our processInstanceId tag
+ String traceId = Awaitility.await().atMost(TIMEOUT).pollInterval(POLL)
+ .until(() -> findTraceIdByProcessInstanceId(jaeger,
SERVICE_NAME, pid), Optional::isPresent).get();
+
+ JsonNode traceResponse = jaeger.getTrace(traceId);
+
+ JsonNode trace =
waitForTraceWithTracker("sonataflow.tracker.correlation_id",
"corr-persist-001", pid,
+ "waitForEvent", "finish");
+
+ debugPrintTrace(trace);
+
+ assertAnyWorkflowSpanHasTag(trace, "sonataflow.process.id",
PROCESS_ID);
+ assertAnyWorkflowSpanHasTag(trace, "sonataflow.process.instance.id",
pid);
+
+ assertWorkflowStatesContainAtLeast(trace, "waitForEvent", "finish");
+ }
+
+ private static Optional<String>
findTraceIdByProcessInstanceId(JaegerQueryClient jaeger, String serviceName,
+ String pid) {
+ // Read latest traces and scan them for a span tag
sonataflow.process.instance.id == pid
+ JsonNode traces = jaeger.findTracesByService(serviceName, 20);
+
+ JsonNode data = traces.get("data");
+ if (data == null || !data.isArray() || data.isEmpty()) {
+ return Optional.empty();
+ }
+
+ for (JsonNode t : data) {
+ String traceId = t.hasNonNull("traceID") ?
t.get("traceID").asText() : null;
+ if (traceId == null || traceId.isBlank()) {
+ continue;
+ }
+
+ JsonNode full = jaeger.getTrace(traceId);
+ JsonNode trace = extractFirstTrace(full);
+ try {
+ assertAnyWorkflowSpanHasTag(trace,
"sonataflow.process.instance.id", pid);
+ return Optional.of(traceId);
+ } catch (AssertionError e) {
+ }
+
+ }
+ return Optional.empty();
+ }
+
+ private static String getJaegerBaseUrl() {
+ String key = "test.jaeger.query.base-url";
+ String url = System.getProperty(key);
+ if (url == null || url.isBlank()) {
+ url = System.getenv("TEST_JAEGER_QUERY_BASE_URL");
+ }
+ if (url == null || url.isBlank()) {
+ throw new IllegalStateException("Jaeger base URL not configured.
Expected system property: " + key);
+ }
+ return url;
+ }
+}
diff --git
a/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/persistence/PersistWaitPidStore.java
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/persistence/PersistWaitPidStore.java
new file mode 100644
index 000000000..829cf1e20
--- /dev/null
+++
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/persistence/PersistWaitPidStore.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.kie.kogito.examples.sw.opentelemetry.jaeger.persistence;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public final class PersistWaitPidStore {
+
+ private static final Path FILE = Path.of("target", "persistWait.pid");
+
+ private PersistWaitPidStore() {
+ }
+
+ public static void write(String pid) {
+ try {
+ Files.createDirectories(FILE.getParent());
+ Files.writeString(FILE, pid, StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to write PID file: " + FILE, e);
+ }
+ }
+
+ public static String read() {
+ try {
+ if (!Files.exists(FILE)) {
+ throw new IllegalStateException("PID file not found: " + FILE
+ ". Did PersistWait01StartIT run?");
+ }
+ return Files.readString(FILE, StandardCharsets.UTF_8).trim();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to read PID file: " + FILE, e);
+ }
+ }
+}
diff --git
a/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/persistence/PostgresTestResource.java
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/persistence/PostgresTestResource.java
new file mode 100644
index 000000000..073a592aa
--- /dev/null
+++
b/serverless-workflow-examples/serverless-workflow-opentelemetry-jaeger-quarkus/src/test/java/org/kie/kogito/examples/sw/opentelemetry/jaeger/persistence/PostgresTestResource.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.kie.kogito.examples.sw.opentelemetry.jaeger.persistence;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
+import org.testcontainers.containers.PostgreSQLContainer;
+
+public class PostgresTestResource implements
QuarkusTestResourceLifecycleManager {
+
+ private static final String IMAGE = "postgres:16-alpine";
+ private PostgreSQLContainer<?> postgres;
+
+ @Override
+ public Map<String, String> start() {
+ postgres = new
PostgreSQLContainer<>(IMAGE).withDatabaseName("kogito").withUsername("kogito")
+ .withPassword("kogito");
+ postgres.start();
+
+ Map<String, String> props = new HashMap<>();
+ props.put("quarkus.datasource.db-kind", "postgresql");
+ props.put("quarkus.datasource.jdbc.url", postgres.getJdbcUrl());
+ props.put("quarkus.datasource.username", postgres.getUsername());
+ props.put("quarkus.datasource.password", postgres.getPassword());
+
+ return props;
+ }
+
+ @Override
+ public void stop() {
+ if (postgres != null) {
+ postgres.stop();
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]