You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cxf.apache.org by re...@apache.org on 2023/08/07 01:28:33 UTC

[cxf] 01/02: CXF-8457 Provide OpenTelemetry support (#1340)

This is an automated email from the ASF dual-hosted git repository.

reta pushed a commit to branch 3.5.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git

commit 6862febbbc582535b948303b1e088f96f2da62bf
Author: Emile de Weerd <em...@deweerd.fr>
AuthorDate: Thu Aug 3 02:16:15 2023 +0200

    CXF-8457 Provide OpenTelemetry support (#1340)
    
    A new module under integration/tracing is introduced to add support for tracing with OpenTelemetry (short OTel).
    
    A few additional changes have been done:
    
    * Add the OTel BOMs in the parent/pom.xml dependency management (too much? should keep only the dependencies that are used?)
    * The new module is strongly inspired by the integration/tracing/tracing-opentracing module
    * The tracing systest was enriched with tests dedicated to OpenTelemetry in the JaxRS and JaxWS contexts
    * The necessary OTel dependencies are added to distribution/javadoc to be able to produce the documentation
    * A new Jax-RS sample showcases span creation on client call and server call
---
 bom/pom.xml                                        |   5 +
 distribution/javadoc/pom.xml                       |   8 +
 .../jax_rs/tracing_opentelemetry/README.txt        |  34 ++
 .../pom.xml                                        |  47 ++-
 .../java/demo/jaxrs/tracing/GoogleBooksApi.java    |  31 ++
 .../demo/jaxrs/tracing/OpenTelemetrySetup.java     |  52 +++
 .../java/demo/jaxrs/tracing/client/Client.java     |  51 +++
 .../java/demo/jaxrs/tracing/server/Catalog.java    | 158 +++++++
 .../jaxrs/tracing/server/CatalogApplication.java   |  45 ++
 .../demo/jaxrs/tracing/server/CatalogStore.java    |  71 ++++
 .../java/demo/jaxrs/tracing/server/Server.java     |  55 +++
 .../samples/jax_rs/tracing_opentracing/pom.xml     |   2 +-
 distribution/src/main/release/samples/pom.xml      |   1 +
 integration/pom.xml                                |   1 +
 integration/tracing/tracing-opentelemetry/pom.xml  | 101 +++++
 .../AbstractOpenTelemetryClientInterceptor.java    |  64 +++
 .../AbstractOpenTelemetryClientProvider.java       | 132 ++++++
 .../AbstractOpenTelemetryInterceptor.java          |  69 +++
 .../AbstractOpenTelemetryProvider.java             | 119 ++++++
 .../opentelemetry/OpenTelemetryClientFeature.java  |  61 +++
 .../OpenTelemetryClientStartInterceptor.java       |  63 +++
 .../OpenTelemetryClientStopInterceptor.java        |  51 +++
 .../opentelemetry/OpenTelemetryContext.java        | 122 ++++++
 .../opentelemetry/OpenTelemetryFeature.java        |  84 ++++
 .../OpenTelemetryStartInterceptor.java             |  51 +++
 .../OpenTelemetryStopInterceptor.java              |  66 +++
 .../cxf/tracing/opentelemetry/ScopedSpan.java      |  42 ++
 .../cxf/tracing/opentelemetry/TraceScope.java      |  47 +++
 .../internal/TextMapInjectAdapter.java             |  54 +++
 .../jaxrs/OpenTelemetryClientProvider.java         |  61 +++
 .../jaxrs/OpenTelemetryContextProvider.java        |  50 +++
 .../opentelemetry/jaxrs/OpenTelemetryFeature.java  |  57 +++
 .../opentelemetry/jaxrs/OpenTelemetryProvider.java |  81 ++++
 .../OpenTelemetryTracerContextClassProvider.java   |  29 ++
 .../org.apache.cxf.jaxrs.ext.ContextClassProvider  |   1 +
 .../opentelemetry/OpenTelemetryContextTest.java    |  55 +++
 parent/pom.xml                                     |  19 +
 systests/tracing/pom.xml                           |  15 +
 .../cxf/systest/jaxrs/tracing/BookStore.java       |   6 +-
 .../jaxrs/tracing/opentelemetry/HasAttribute.java  |  54 +++
 .../jaxrs/tracing/opentelemetry/HasSpan.java       |  68 +++
 .../tracing/opentelemetry/IsLogContaining.java     |  45 ++
 .../opentelemetry/OpenTelemetryTracingTest.java    | 466 +++++++++++++++++++++
 .../jaxws/tracing/opentelemetry/BookStore.java     |  71 ++++
 .../opentelemetry/OpenTelemetryTracingTest.java    | 281 +++++++++++++
 45 files changed, 3026 insertions(+), 20 deletions(-)

diff --git a/bom/pom.xml b/bom/pom.xml
index 3aff92d88a..da1d69df95 100644
--- a/bom/pom.xml
+++ b/bom/pom.xml
@@ -69,6 +69,11 @@
                 <artifactId>cxf-integration-tracing-opentracing</artifactId>
                 <version>${project.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.apache.cxf</groupId>
+                <artifactId>cxf-integration-tracing-opentelemetry</artifactId>
+                <version>${project.version}</version>
+            </dependency>
             <dependency>
                 <groupId>org.apache.cxf</groupId>
                 <artifactId>cxf-rt-bindings-coloc</artifactId>
diff --git a/distribution/javadoc/pom.xml b/distribution/javadoc/pom.xml
index 431b127b16..cf205ed663 100644
--- a/distribution/javadoc/pom.xml
+++ b/distribution/javadoc/pom.xml
@@ -422,6 +422,14 @@
             <groupId>io.dropwizard.metrics</groupId>
             <artifactId>metrics-jmx</artifactId>
         </dependency>
+        <dependency>
+            <groupId>io.opentelemetry</groupId>
+            <artifactId>opentelemetry-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.opentelemetry</groupId>
+            <artifactId>opentelemetry-semconv</artifactId>
+        </dependency>
         <dependency>
             <groupId>io.opentracing</groupId>
             <artifactId>opentracing-util</artifactId>
diff --git a/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/README.txt b/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/README.txt
new file mode 100644
index 0000000000..0de4a4f3a2
--- /dev/null
+++ b/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/README.txt
@@ -0,0 +1,34 @@
+JAX-RS OpenTelemetry Demo
+=================
+
+The demo shows a basic usage of OpenTelemetry API distributed tracer
+with REST based Web Services using JAX-RS 2.0 (JSR-339). The REST server provides the 
+following services at URL http://localhost:9000/catalog: 
+
+ - GET to http://localhost:9000/catalog 
+ - POST to http://localhost:9000/catalog 
+ - GET to http://localhost:9000/catalog/<id> 
+ - DELETE to URL http://localhost:9000/catalog/<id> 
+ - GET to URL http://localhost:9000/catalog/search?q=<query>
+
+The last endpoint calls public Google Books API in order to search the books by 
+query criteria. It demonstrates the integration with native OpenTelemetry instrumentation.
+
+Building and running the demo using Maven
+---------------------------------------
+
+From the base directory of this sample (i.e., where this README file is
+located), the Maven pom.xml file can be used to build and run the demo. 
+
+
+Using either UNIX or Windows:
+
+  mvn install
+  mvn -Pserver  (from one command line window)
+  mvn -Pclient  (from a second command line window)
+    
+
+To remove the target dir, run mvn clean".
+
+
+
diff --git a/distribution/src/main/release/samples/jax_rs/tracing_opentracing/pom.xml b/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/pom.xml
similarity index 80%
copy from distribution/src/main/release/samples/jax_rs/tracing_opentracing/pom.xml
copy to distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/pom.xml
index 7957c188eb..88c38f708d 100644
--- a/distribution/src/main/release/samples/jax_rs/tracing_opentracing/pom.xml
+++ b/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/pom.xml
@@ -19,9 +19,9 @@
 -->
 <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/maven-v4_0_0.xsd">
     <modelVersion>4.0.0</modelVersion>
-    <artifactId>jax_rs_tracing_opentracing</artifactId>
-    <name>JAX-RS Demo Using Distributed Tracing with OpenTracing API and Jaeger</name>
-    <description>JAX-RS Demo Using Distributed Tracing with OpenTracing API and Jaeger</description>
+    <artifactId>jax_rs_tracing_opentelemetry</artifactId>
+    <name>JAX-RS Demo Using Distributed Tracing with OpenTelemetry</name>
+    <description>JAX-RS Demo Using Distributed Tracing with OpenTelemetry</description>
     <parent>
         <groupId>org.apache.cxf.samples</groupId>
         <artifactId>cxf-samples</artifactId>
@@ -83,33 +83,33 @@
         <dependency>
             <groupId>org.apache.cxf</groupId>
             <artifactId>cxf-rt-transports-http</artifactId>
-            <version>3.5.7-SNAPSHOT</version>
+            <version>${cxf.version}</version>
         </dependency>
         <!-- This dependency is needed if you're using the Jetty container -->
         <dependency>
             <groupId>org.apache.cxf</groupId>
             <artifactId>cxf-rt-transports-http-jetty</artifactId>
-            <version>3.5.7-SNAPSHOT</version>
+            <version>${cxf.version}</version>
         </dependency>
         <dependency>
             <groupId>org.apache.cxf</groupId>
             <artifactId>cxf-rt-frontend-jaxrs</artifactId>
-            <version>3.5.7-SNAPSHOT</version>
+            <version>${cxf.version}</version>
         </dependency>
         <dependency>
             <groupId>org.apache.cxf</groupId>
             <artifactId>cxf-rt-rs-client</artifactId>
-            <version>3.5.7-SNAPSHOT</version>
+            <version>${cxf.version}</version>
         </dependency>    
         <dependency>
             <groupId>org.apache.cxf</groupId>
-            <artifactId>cxf-integration-tracing-opentracing</artifactId>
-            <version>3.5.7-SNAPSHOT</version>
+            <artifactId>cxf-integration-tracing-opentelemetry</artifactId>
+            <version>${cxf.version}</version>
         </dependency>
         <dependency>
             <groupId>org.apache.cxf</groupId>
             <artifactId>cxf-rt-rs-extension-providers</artifactId>
-            <version>3.5.7-SNAPSHOT</version>
+            <version>${cxf.version}</version>
         </dependency>
         <dependency>
             <groupId>jakarta.json</groupId>
@@ -118,7 +118,7 @@
         <dependency>
             <groupId>org.glassfish</groupId>
             <artifactId>javax.json</artifactId>
-        </dependency>                    
+        </dependency>
         <dependency>
             <groupId>jakarta.ws.rs</groupId>
             <artifactId>jakarta.ws.rs-api</artifactId>
@@ -127,10 +127,6 @@
             <groupId>org.eclipse.jetty</groupId>
             <artifactId>jetty-webapp</artifactId>
         </dependency>
-        <dependency>
-            <groupId>io.jaegertracing</groupId>
-            <artifactId>jaeger-core</artifactId>
-        </dependency>
         <dependency>
             <groupId>io.github.openfeign</groupId>
             <artifactId>feign-core</artifactId>
@@ -146,5 +142,26 @@
             <artifactId>feign-opentracing</artifactId>
             <version>0.4.1</version>
         </dependency>
+        <dependency>
+            <groupId>io.opentelemetry</groupId>
+            <artifactId>opentelemetry-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.opentelemetry</groupId>
+            <artifactId>opentelemetry-sdk</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.opentelemetry</groupId>
+            <artifactId>opentelemetry-semconv</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.opentelemetry</groupId>
+            <artifactId>opentelemetry-exporter-logging</artifactId>
+        </dependency>
+        <!-- We need the Opentracing SHIM because feign does not have an Opentelemetry client wrapper -->
+        <dependency>
+            <groupId>io.opentelemetry</groupId>
+            <artifactId>opentelemetry-opentracing-shim</artifactId>
+        </dependency>
     </dependencies>
 </project>
diff --git a/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/src/main/java/demo/jaxrs/tracing/GoogleBooksApi.java b/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/src/main/java/demo/jaxrs/tracing/GoogleBooksApi.java
new file mode 100644
index 0000000000..f5e0080cef
--- /dev/null
+++ b/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/src/main/java/demo/jaxrs/tracing/GoogleBooksApi.java
@@ -0,0 +1,31 @@
+/**
+ * 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 demo.jaxrs.tracing;
+
+import feign.Headers;
+import feign.Param;
+import feign.RequestLine;
+import feign.Response;
+
+public interface GoogleBooksApi {
+    @RequestLine("GET /books/v1/volumes?q={q}")
+    @Headers("Accept: application/json")
+    Response search(@Param("q") String query);
+}
diff --git a/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/src/main/java/demo/jaxrs/tracing/OpenTelemetrySetup.java b/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/src/main/java/demo/jaxrs/tracing/OpenTelemetrySetup.java
new file mode 100644
index 0000000000..dd5a04c851
--- /dev/null
+++ b/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/src/main/java/demo/jaxrs/tracing/OpenTelemetrySetup.java
@@ -0,0 +1,52 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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 demo.jaxrs.tracing;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
+import io.opentelemetry.context.propagation.ContextPropagators;
+import io.opentelemetry.exporter.logging.LoggingSpanExporter;
+import io.opentelemetry.opentracingshim.OpenTracingShim;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.resources.Resource;
+import io.opentelemetry.sdk.trace.SdkTracerProvider;
+import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
+import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;
+import io.opentracing.util.GlobalTracer;
+
+public class OpenTelemetrySetup {
+
+    public static OpenTelemetrySdk setup(String serviceName) {
+        Resource resource = Resource.getDefault()
+            .merge(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, serviceName)));
+
+        SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder()
+            .addSpanProcessor(BatchSpanProcessor.builder(LoggingSpanExporter.create()).build())
+            .setResource(resource).build();
+
+        OpenTelemetrySdk openTelemetrySdk = OpenTelemetrySdk.builder().setTracerProvider(sdkTracerProvider)
+            .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
+            .buildAndRegisterGlobal();
+
+        // injecting OTel OpenTracing SHIM for feign opentracing to work
+        GlobalTracer.registerIfAbsent(OpenTracingShim.createTracerShim(openTelemetrySdk));
+
+        return openTelemetrySdk;
+    }
+}
diff --git a/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/src/main/java/demo/jaxrs/tracing/client/Client.java b/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/src/main/java/demo/jaxrs/tracing/client/Client.java
new file mode 100644
index 0000000000..eda7265da2
--- /dev/null
+++ b/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/src/main/java/demo/jaxrs/tracing/client/Client.java
@@ -0,0 +1,51 @@
+/**
+ * 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 demo.jaxrs.tracing.client;
+
+import java.util.Arrays;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import demo.jaxrs.tracing.OpenTelemetrySetup;
+import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.cxf.tracing.opentelemetry.OpenTelemetryFeature;
+import org.apache.cxf.tracing.opentelemetry.jaxrs.OpenTelemetryClientProvider;
+
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+
+public final class Client {
+    private Client() {
+    }
+
+    public static void main(final String[] args) throws Exception {
+        try (OpenTelemetrySdk openTelemetry = OpenTelemetrySetup.setup(Client.class.getName())) {
+            final OpenTelemetryClientProvider provider = new OpenTelemetryClientProvider(openTelemetry,
+                                                                                         OpenTelemetryFeature.DEFAULT_INSTRUMENTATION_NAME);
+
+            final Response response = WebClient
+                .create("http://localhost:9000/catalog", Arrays.asList(provider))
+                .accept(MediaType.APPLICATION_JSON).get();
+
+            System.out.println(response.readEntity(String.class));
+            response.close();
+        }
+    }
+}
diff --git a/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/src/main/java/demo/jaxrs/tracing/server/Catalog.java b/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/src/main/java/demo/jaxrs/tracing/server/Catalog.java
new file mode 100644
index 0000000000..2e1f880c1d
--- /dev/null
+++ b/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/src/main/java/demo/jaxrs/tracing/server/Catalog.java
@@ -0,0 +1,158 @@
+/**
+ * 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 demo.jaxrs.tracing.server;
+
+
+import java.io.IOException;
+import java.io.Reader;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.FormParam;
+import javax.ws.rs.GET;
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.container.AsyncResponse;
+import javax.ws.rs.container.Suspended;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import org.apache.cxf.tracing.Traceable;
+import org.apache.cxf.tracing.TracerContext;
+
+import demo.jaxrs.tracing.GoogleBooksApi;
+import feign.Feign;
+import feign.httpclient.ApacheHttpClient;
+import feign.opentracing.TracingClient;
+import io.opentracing.Tracer;
+
+@Path("/catalog")
+public class Catalog {
+    private final ExecutorService executor = Executors.newFixedThreadPool(2);
+    private final CatalogStore store;
+
+    public Catalog() {
+        store = new CatalogStore();
+    }
+
+    @POST
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response addBook(@Context final UriInfo uriInfo, @Context final TracerContext tracing,
+            @FormParam("title") final String title)  {
+        try {
+            final String id = UUID.randomUUID().toString();
+
+            executor.submit(
+                tracing.wrap("Inserting New Book",
+                    new Traceable<Void>() {
+                        public Void call(final TracerContext context) throws Exception {
+                            store.put(id, title);
+                            return null;
+                        }
+                    }
+                )
+            ).get(10, TimeUnit.SECONDS);
+
+            return Response
+                .created(uriInfo.getRequestUriBuilder().path(id).build())
+                .build();
+        } catch (final Exception ex) {
+            return Response
+                .serverError()
+                .entity(Json
+                     .createObjectBuilder()
+                     .add("error", ex.getMessage())
+                     .build())
+                .build();
+        }
+    }
+
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    public void getBooks(@Suspended final AsyncResponse response,
+            @Context final TracerContext tracing) throws Exception {
+        tracing.continueSpan(new Traceable<Void>() {
+            @Override
+            public Void call(final TracerContext context) throws Exception {
+                executor.submit(tracing.wrap("Looking for books", new Traceable<Void>() {
+                    @Override
+                    public Void call(final TracerContext context) throws Exception {
+                        response.resume(store.scan());
+                        return null;
+                    }
+                }));
+
+                return null;
+            }
+        });
+    }
+
+    @GET
+    @Path("/{id}")
+    @Produces(MediaType.APPLICATION_JSON)
+    public JsonObject getBook(@PathParam("id") final String id) throws IOException {
+        final JsonObject book = store.get(id);
+
+        if (book == null) {
+            throw new NotFoundException("Book with does not exists: " + id);
+        }
+
+        return book;
+    }
+
+    @DELETE
+    @Path("/{id}")
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response delete(@PathParam("id") final String id) throws IOException {
+        if (!store.remove(id)) {
+            throw new NotFoundException("Book with does not exists: " + id);
+        }
+
+        return Response.ok().build();
+    }
+    
+    @GET
+    @Path("/search")
+    @Produces(MediaType.APPLICATION_JSON)
+    public JsonObject search(@QueryParam("q") final String query, @Context final TracerContext tracing) throws Exception {
+        final GoogleBooksApi api = Feign
+            .builder()
+            .client(new TracingClient(new ApacheHttpClient(), tracing.unwrap(Tracer.class)))
+            .target(GoogleBooksApi.class, "https://www.googleapis.com");
+     
+        final feign.Response response = api.search(query);
+        try (final Reader reader = response.body().asReader()) {
+            return Json.createReader(reader).readObject();
+        }
+    }
+}
+
+
diff --git a/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/src/main/java/demo/jaxrs/tracing/server/CatalogApplication.java b/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/src/main/java/demo/jaxrs/tracing/server/CatalogApplication.java
new file mode 100644
index 0000000000..5db4f67d48
--- /dev/null
+++ b/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/src/main/java/demo/jaxrs/tracing/server/CatalogApplication.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 demo.jaxrs.tracing.server;
+
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.ws.rs.ApplicationPath;
+import javax.ws.rs.core.Application;
+
+import org.apache.cxf.jaxrs.provider.jsrjsonp.JsrJsonpProvider;
+import org.apache.cxf.tracing.opentelemetry.jaxrs.OpenTelemetryFeature;
+
+@ApplicationPath("/")
+public class CatalogApplication extends Application {
+    @Override
+    public Set<Object> getSingletons() {
+        return new HashSet<>(
+            Arrays.asList(
+                new Catalog(),
+                new OpenTelemetryFeature(),
+                new JsrJsonpProvider()
+            )
+        );
+    }
+}
diff --git a/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/src/main/java/demo/jaxrs/tracing/server/CatalogStore.java b/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/src/main/java/demo/jaxrs/tracing/server/CatalogStore.java
new file mode 100644
index 0000000000..9d6b8c7045
--- /dev/null
+++ b/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/src/main/java/demo/jaxrs/tracing/server/CatalogStore.java
@@ -0,0 +1,71 @@
+/**
+ * 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 demo.jaxrs.tracing.server;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.json.Json;
+import javax.json.JsonArray;
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonObject;
+
+public class CatalogStore {
+    private final Map<String, String> books = new ConcurrentHashMap<>();
+
+    public CatalogStore() {
+    }
+
+    public boolean remove(final String key) throws IOException {
+        return books.remove(key) != null;
+    }
+
+    public JsonObject get(final String key) throws IOException {
+        final String title = books.get(key);
+
+        if (title != null) {
+            return Json.createObjectBuilder()
+                .add("id", key)
+                .add("title", title)
+                .build();
+        }
+
+        return null;
+    }
+
+    public void put(final String key, final String title) throws IOException {
+        books.put(key, title);
+    }
+
+    public JsonArray scan() throws IOException {
+        final JsonArrayBuilder builder = Json.createArrayBuilder();
+
+        for (Map.Entry<String, String> entry: books.entrySet()) {
+            builder.add(Json.createObjectBuilder()
+                .add("id", entry.getKey())
+                .add("title", entry.getValue())
+            );
+        }
+
+        return builder.build();
+    }
+
+}
diff --git a/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/src/main/java/demo/jaxrs/tracing/server/Server.java b/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/src/main/java/demo/jaxrs/tracing/server/Server.java
new file mode 100644
index 0000000000..975964e7b0
--- /dev/null
+++ b/distribution/src/main/release/samples/jax_rs/tracing_opentelemetry/src/main/java/demo/jaxrs/tracing/server/Server.java
@@ -0,0 +1,55 @@
+/**
+ * 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 demo.jaxrs.tracing.server;
+
+import demo.jaxrs.tracing.OpenTelemetrySetup;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import org.apache.cxf.jaxrs.servlet.CXFNonSpringJaxrsServlet;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+
+public class Server {
+    protected Server() throws Exception {
+        org.eclipse.jetty.server.Server server = new org.eclipse.jetty.server.Server(9000);
+
+        // Register and map the dispatcher servlet
+        final ServletHolder servletHolder = new ServletHolder(new CXFNonSpringJaxrsServlet());
+        final ServletContextHandler context = new ServletContextHandler();
+        context.setContextPath("/");
+        context.addServlet(servletHolder, "/*");
+
+        servletHolder.setInitParameter("javax.ws.rs.Application", CatalogApplication.class.getName());
+
+        try (OpenTelemetrySdk ignored = OpenTelemetrySetup.setup(CatalogApplication.class.getName())) {
+            server.setHandler(context);
+            server.start();
+            server.join();
+        }
+    }
+
+    public static void main(String[] args) throws Exception {
+        new Server();
+        System.out.println("Server ready...");
+
+        Thread.sleep(5 * 6000 * 1000);
+        System.out.println("Server exiting");
+        System.exit(0);
+    }
+}
diff --git a/distribution/src/main/release/samples/jax_rs/tracing_opentracing/pom.xml b/distribution/src/main/release/samples/jax_rs/tracing_opentracing/pom.xml
index 7957c188eb..4f79add941 100644
--- a/distribution/src/main/release/samples/jax_rs/tracing_opentracing/pom.xml
+++ b/distribution/src/main/release/samples/jax_rs/tracing_opentracing/pom.xml
@@ -118,7 +118,7 @@
         <dependency>
             <groupId>org.glassfish</groupId>
             <artifactId>javax.json</artifactId>
-        </dependency>                    
+        </dependency>
         <dependency>
             <groupId>jakarta.ws.rs</groupId>
             <artifactId>jakarta.ws.rs-api</artifactId>
diff --git a/distribution/src/main/release/samples/pom.xml b/distribution/src/main/release/samples/pom.xml
index 4190d5ead7..644dd9144b 100644
--- a/distribution/src/main/release/samples/pom.xml
+++ b/distribution/src/main/release/samples/pom.xml
@@ -103,6 +103,7 @@
         <module>jax_rs/tracing_opentracing</module>
         <module>jax_rs/tracing_opentracing_camel</module>
         <module>jax_rs/tracing_opentracing_osgi</module>
+        <module>jax_rs/tracing_opentelemetry</module>
         <module>jax_rs/websocket</module>
         <module>jax_rs/websocket_osgi</module>
         <module>jax_rs/websocket_web</module>
diff --git a/integration/pom.xml b/integration/pom.xml
index 10986d5ecd..49fc6dc421 100644
--- a/integration/pom.xml
+++ b/integration/pom.xml
@@ -34,6 +34,7 @@
         <module>cdi</module>
         <module>tracing/tracing-brave</module>
         <module>tracing/tracing-opentracing</module>
+        <module>tracing/tracing-opentelemetry</module>
         <module>spring-boot</module>
     </modules>
 </project>
diff --git a/integration/tracing/tracing-opentelemetry/pom.xml b/integration/tracing/tracing-opentelemetry/pom.xml
new file mode 100644
index 0000000000..c21af82979
--- /dev/null
+++ b/integration/tracing/tracing-opentelemetry/pom.xml
@@ -0,0 +1,101 @@
+<?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:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>cxf-integration-tracing-opentelemetry</artifactId>
+    <packaging>bundle</packaging>
+    <name>Apache CXF Distributed Tracing using OpenTelemetry</name>
+    <description>Apache CXF Distributed using OpenTelemetry</description>
+    <url>https://cxf.apache.org</url>
+    <parent>
+        <groupId>org.apache.cxf</groupId>
+        <artifactId>cxf-parent</artifactId>
+        <version>3.5.7-SNAPSHOT</version>
+        <relativePath>../../../parent/pom.xml</relativePath>
+    </parent>
+
+    <properties>
+        <cxf.module.name>org.apache.cxf.tracing.opentelemetry</cxf.module.name>
+        <cxf.osgi.export>
+            org.apache.cxf.tracing,
+            org.apache.cxf.tracing.opentelemetry,
+            org.apache.cxf.tracing.opentelemetry.jaxrs
+        </cxf.osgi.export>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>jakarta.xml.ws</groupId>
+            <artifactId>jakarta.xml.ws-api</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cxf</groupId>
+            <artifactId>cxf-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cxf</groupId>
+            <artifactId>cxf-rt-frontend-jaxrs</artifactId>
+            <version>${project.version}</version>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cxf</groupId>
+            <artifactId>cxf-rt-frontend-jaxws</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cxf</groupId>
+            <artifactId>cxf-rt-management</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.opentelemetry</groupId>
+            <artifactId>opentelemetry-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.opentelemetry</groupId>
+            <artifactId>opentelemetry-semconv</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-jdk14</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cxf</groupId>
+            <artifactId>cxf-rt-transports-http-jetty</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.opentelemetry</groupId>
+            <artifactId>opentelemetry-sdk-testing</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/AbstractOpenTelemetryClientInterceptor.java b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/AbstractOpenTelemetryClientInterceptor.java
new file mode 100644
index 0000000000..004b9e5256
--- /dev/null
+++ b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/AbstractOpenTelemetryClientInterceptor.java
@@ -0,0 +1,64 @@
+/**
+ * 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.apache.cxf.tracing.opentelemetry;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+
+import org.apache.cxf.message.Message;
+import org.apache.cxf.phase.PhaseInterceptor;
+
+import io.opentelemetry.api.OpenTelemetry;
+
+public abstract class AbstractOpenTelemetryClientInterceptor extends AbstractOpenTelemetryClientProvider
+    implements PhaseInterceptor<Message> {
+
+    private String phase;
+
+    protected AbstractOpenTelemetryClientInterceptor(final String phase, final OpenTelemetry openTelemetry,
+                                                     final String instrumentationName) {
+        super(openTelemetry, instrumentationName);
+        this.phase = phase;
+    }
+
+    public Collection<PhaseInterceptor<? extends Message>> getAdditionalInterceptors() {
+        return null;
+    }
+
+    public Set<String> getAfter() {
+        return Collections.emptySet();
+    }
+
+    public Set<String> getBefore() {
+        return Collections.emptySet();
+    }
+
+    public String getId() {
+        return getClass().getName();
+    }
+
+    public String getPhase() {
+        return phase;
+    }
+
+    public void handleFault(Message message) {
+    }
+
+}
diff --git a/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/AbstractOpenTelemetryClientProvider.java b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/AbstractOpenTelemetryClientProvider.java
new file mode 100644
index 0000000000..2287952f9f
--- /dev/null
+++ b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/AbstractOpenTelemetryClientProvider.java
@@ -0,0 +1,132 @@
+/**
+ * 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.apache.cxf.tracing.opentelemetry;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import org.apache.cxf.common.logging.LogUtils;
+import org.apache.cxf.phase.PhaseInterceptorChain;
+import org.apache.cxf.tracing.AbstractTracingProvider;
+import org.apache.cxf.tracing.opentelemetry.internal.TextMapInjectAdapter;
+
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.api.trace.StatusCode;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
+
+public abstract class AbstractOpenTelemetryClientProvider extends AbstractTracingProvider {
+    protected static final Logger LOG = LogUtils.getL7dLogger(AbstractOpenTelemetryClientProvider.class);
+    protected static final String TRACE_SPAN = "org.apache.cxf.tracing.client.opentelemetry.span";
+
+    private final Tracer tracer;
+    private final OpenTelemetry openTelemetry;
+
+    public AbstractOpenTelemetryClientProvider(final OpenTelemetry openTelemetry, final String instrumentationName) {
+        this.openTelemetry = openTelemetry;
+        this.tracer = openTelemetry.getTracer(instrumentationName);
+    }
+
+    protected TraceScopeHolder<TraceScope> startTraceSpan(final Map<String, List<String>> requestHeaders,
+                                                          URI uri, String method) {
+        Context parentContext = Context.current();
+        Span activeSpan = tracer.spanBuilder(buildSpanDescription(uri.toString(), method))
+            .setParent(parentContext).setSpanKind(SpanKind.CLIENT)
+            .setAttribute(SemanticAttributes.HTTP_METHOD, method)
+            .setAttribute(SemanticAttributes.HTTP_URL, uri.toString())
+            // TODO: Enhance with semantics from request
+            .startSpan();
+        Scope scope = activeSpan.makeCurrent();
+
+        openTelemetry.getPropagators().getTextMapPropagator().inject(Context.current(), requestHeaders,
+                                                                           TextMapInjectAdapter.get());
+
+        // In case of asynchronous client invocation, the span should be detached as JAX-RS
+        // client request / response filters are going to be executed in different threads.
+        Span span = null;
+        if (isAsyncInvocation()) {
+            span = activeSpan;
+            scope.close();
+        }
+
+        return new TraceScopeHolder<TraceScope>(new TraceScope(activeSpan, scope, null),
+                                                span != null /* detached */);
+    }
+
+    private boolean isAsyncInvocation() {
+        return !PhaseInterceptorChain.getCurrentMessage().getExchange().isSynchronous();
+    }
+
+    protected void stopTraceSpan(final TraceScopeHolder<TraceScope> holder, final int responseStatus) {
+        if (holder == null) {
+            return;
+        }
+
+        final TraceScope traceScope = holder.getScope();
+        if (traceScope != null) {
+            Span span = traceScope.getSpan();
+            Scope scope = traceScope.getScope();
+
+            // If the client invocation was asynchronous , the trace span has been created
+            // in another thread and should be re-attached to the current one.
+            if (holder.isDetached()) {
+                scope = span.makeCurrent();
+            }
+
+            span.setAttribute(SemanticAttributes.HTTP_STATUS_CODE.getKey(), responseStatus);
+
+            span.end();
+
+            scope.close();
+        }
+    }
+
+    protected void stopTraceSpan(final TraceScopeHolder<TraceScope> holder, final Throwable ex) {
+        if (holder == null) {
+            return;
+        }
+
+        final TraceScope traceScope = holder.getScope();
+        if (traceScope != null) {
+            Span span = traceScope.getSpan();
+            Scope scope = traceScope.getScope();
+
+            // If the client invocation was asynchronous , the trace span has been created
+            // in another thread and should be re-attached to the current one.
+            if (holder.isDetached()) {
+                scope = span.makeCurrent();
+            }
+
+            span.setStatus(StatusCode.ERROR);
+            if (ex != null) {
+                span.recordException(ex, Attributes.of(SemanticAttributes.EXCEPTION_ESCAPED, true));
+            }
+            span.end();
+
+            scope.close();
+        }
+    }
+}
diff --git a/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/AbstractOpenTelemetryInterceptor.java b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/AbstractOpenTelemetryInterceptor.java
new file mode 100644
index 0000000000..7ec051fabd
--- /dev/null
+++ b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/AbstractOpenTelemetryInterceptor.java
@@ -0,0 +1,69 @@
+/**
+ * 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.apache.cxf.tracing.opentelemetry;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+
+import org.apache.cxf.message.Message;
+import org.apache.cxf.phase.PhaseInterceptor;
+
+import io.opentelemetry.api.OpenTelemetry;
+
+public abstract class AbstractOpenTelemetryInterceptor extends AbstractOpenTelemetryProvider
+    implements PhaseInterceptor<Message> {
+
+    private final String phase;
+
+    protected AbstractOpenTelemetryInterceptor(String phase, OpenTelemetry openTelemetry,
+                                               String instrumentationName) {
+        super(openTelemetry, instrumentationName);
+        this.phase = phase;
+    }
+
+    @Override
+    public Set<String> getAfter() {
+        return Collections.emptySet();
+    }
+
+    @Override
+    public Set<String> getBefore() {
+        return Collections.emptySet();
+    }
+
+    @Override
+    public String getId() {
+        return getClass().getName();
+    }
+
+    @Override
+    public String getPhase() {
+        return phase;
+    }
+
+    @Override
+    public Collection<PhaseInterceptor<? extends Message>> getAdditionalInterceptors() {
+        return null;
+    }
+
+    @Override
+    public void handleFault(Message message) {
+    }
+}
diff --git a/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/AbstractOpenTelemetryProvider.java b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/AbstractOpenTelemetryProvider.java
new file mode 100644
index 0000000000..193327fc95
--- /dev/null
+++ b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/AbstractOpenTelemetryProvider.java
@@ -0,0 +1,119 @@
+/**
+ * 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.apache.cxf.tracing.opentelemetry;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import org.apache.cxf.common.logging.LogUtils;
+import org.apache.cxf.phase.PhaseInterceptorChain;
+import org.apache.cxf.tracing.AbstractTracingProvider;
+import org.apache.cxf.tracing.opentelemetry.internal.TextMapInjectAdapter;
+
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanBuilder;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
+
+public abstract class AbstractOpenTelemetryProvider extends AbstractTracingProvider {
+    protected static final Logger LOG = LogUtils.getL7dLogger(AbstractOpenTelemetryProvider.class);
+    protected static final String TRACE_SPAN = "org.apache.cxf.tracing.opentelemetry.span";
+
+    protected final OpenTelemetry openTelemetry;
+
+    protected final Tracer tracer;
+
+    protected AbstractOpenTelemetryProvider(final OpenTelemetry openTelemetry, final String instrumentationName) {
+        this.openTelemetry = openTelemetry;
+        this.tracer = openTelemetry.getTracer(instrumentationName);
+    }
+
+    protected TraceScopeHolder<TraceScope> startTraceSpan(final Map<String, List<String>> requestHeaders,
+                                                          URI uri, String method) {
+
+        Context parent = openTelemetry.getPropagators().getTextMapPropagator()
+            .extract(Context.current(), requestHeaders, TextMapInjectAdapter.get());
+        Scope parentScope = parent.makeCurrent();
+
+        SpanBuilder spanBuilder = tracer.spanBuilder(buildSpanDescription(uri.getPath(), method))
+            .setSpanKind(SpanKind.SERVER)
+            .setAttribute(SemanticAttributes.HTTP_METHOD, method)
+            .setAttribute(SemanticAttributes.HTTP_URL, uri.toString());
+        Span activeSpan = spanBuilder.startSpan();
+        Scope scope = activeSpan.makeCurrent();
+
+        // Set additional tags
+
+        // If the service resource is using asynchronous processing mode, the trace
+        // scope will be closed in another thread and as such should be detached.
+        Span span = null;
+        if (isAsyncResponse()) {
+            // Do not modify the current context span
+            span = activeSpan;
+            propagateContinuationSpan(span);
+            scope.close();
+            parentScope.close();
+        }
+
+        return new TraceScopeHolder<TraceScope>(new TraceScope(activeSpan, scope, parentScope), span != null);
+    }
+
+    protected void stopTraceSpan(final Map<String, List<String>> requestHeaders,
+                                 final Map<String, List<Object>> responseHeaders, final int responseStatus,
+                                 final TraceScopeHolder<TraceScope> holder) {
+
+        if (holder == null) {
+            return;
+        }
+
+        final TraceScope traceScope = holder.getScope();
+        if (traceScope != null) {
+            Span span = traceScope.getSpan();
+            Scope scope = traceScope.getScope();
+
+            // If the service resource is using asynchronous processing mode, the trace
+            // scope has been created in another thread and should be re-attached to the current
+            // one.
+            if (holder.isDetached()) {
+                scope = span.makeCurrent();
+            }
+
+            span.setAttribute(SemanticAttributes.HTTP_STATUS_CODE, responseStatus);
+            span.end();
+
+            scope.close();
+            traceScope.getParentScope().close();
+        }
+    }
+
+    protected boolean isAsyncResponse() {
+        return !PhaseInterceptorChain.getCurrentMessage().getExchange().isSynchronous();
+    }
+
+    private void propagateContinuationSpan(final Span continuation) {
+        PhaseInterceptorChain.getCurrentMessage().put(Span.class, continuation);
+    }
+
+}
diff --git a/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryClientFeature.java b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryClientFeature.java
new file mode 100644
index 0000000000..60f3edc11f
--- /dev/null
+++ b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryClientFeature.java
@@ -0,0 +1,61 @@
+/**
+ * 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.apache.cxf.tracing.opentelemetry;
+
+import org.apache.cxf.Bus;
+import org.apache.cxf.annotations.Provider;
+import org.apache.cxf.annotations.Provider.Scope;
+import org.apache.cxf.annotations.Provider.Type;
+import org.apache.cxf.common.injection.NoJSR250Annotations;
+import org.apache.cxf.feature.AbstractPortableFeature;
+import org.apache.cxf.feature.DelegatingFeature;
+import org.apache.cxf.interceptor.InterceptorProvider;
+
+import io.opentelemetry.api.OpenTelemetry;
+
+@NoJSR250Annotations
+@Provider(value = Type.Feature, scope = Scope.Client)
+public class OpenTelemetryClientFeature extends DelegatingFeature<OpenTelemetryClientFeature.Portable> {
+    public OpenTelemetryClientFeature(OpenTelemetry openTelemetry) {
+        super(new Portable(openTelemetry));
+    }
+
+    public OpenTelemetryClientFeature(OpenTelemetry openTelemetry, String instrumentationName) {
+        super(new Portable(openTelemetry, instrumentationName));
+    }
+
+    public static class Portable implements AbstractPortableFeature {
+        private OpenTelemetryClientStartInterceptor out;
+        private OpenTelemetryClientStopInterceptor in;
+        public Portable(OpenTelemetry openTelemetry) {
+            this(openTelemetry, OpenTelemetryFeature.DEFAULT_INSTRUMENTATION_NAME);
+        }
+
+        public Portable(OpenTelemetry openTelemetry, String instrumentationName) {
+            out = new OpenTelemetryClientStartInterceptor(openTelemetry, instrumentationName);
+            in = new OpenTelemetryClientStopInterceptor(openTelemetry, instrumentationName);
+        }
+
+        @Override
+        public void doInitializeProvider(InterceptorProvider provider, Bus bus) {
+            provider.getInInterceptors().add(in);
+            provider.getOutInterceptors().add(out);
+        }
+    }
+}
diff --git a/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryClientStartInterceptor.java b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryClientStartInterceptor.java
new file mode 100644
index 0000000000..d2396cdf0f
--- /dev/null
+++ b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryClientStartInterceptor.java
@@ -0,0 +1,63 @@
+/**
+ * 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.apache.cxf.tracing.opentelemetry;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.cxf.helpers.CastUtils;
+import org.apache.cxf.interceptor.Fault;
+import org.apache.cxf.message.Message;
+import org.apache.cxf.phase.Phase;
+
+import io.opentelemetry.api.OpenTelemetry;
+
+public class OpenTelemetryClientStartInterceptor extends AbstractOpenTelemetryClientInterceptor {
+    public OpenTelemetryClientStartInterceptor(final OpenTelemetry openTelemetry, final String instrumentationName) {
+        this(Phase.PRE_STREAM, openTelemetry, instrumentationName);
+    }
+
+    public OpenTelemetryClientStartInterceptor(final String phase, final OpenTelemetry openTelemetry, 
+            final String instrumentationName) {
+        super(phase, openTelemetry, instrumentationName);
+    }
+
+    @Override
+    public void handleMessage(Message message) throws Fault {
+        final Map<String, List<String>> headers = CastUtils
+            .cast((Map<?, ?>)message.get(Message.PROTOCOL_HEADERS));
+        final TraceScopeHolder<TraceScope> holder = super.startTraceSpan(headers, getUri(message),
+                                                                         (String)message
+                                                                             .get(Message.HTTP_REQUEST_METHOD));
+
+        if (holder != null) {
+            message.getExchange().put(TRACE_SPAN, holder);
+        }
+    }
+
+    @Override
+    public void handleFault(Message message) {
+        @SuppressWarnings("unchecked")
+        final TraceScopeHolder<TraceScope> holder = (TraceScopeHolder<TraceScope>)message.getExchange()
+            .get(TRACE_SPAN);
+
+        final Exception ex = message.getContent(Exception.class);
+        super.stopTraceSpan(holder, ex);
+    }
+}
diff --git a/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryClientStopInterceptor.java b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryClientStopInterceptor.java
new file mode 100644
index 0000000000..ce23c8944b
--- /dev/null
+++ b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryClientStopInterceptor.java
@@ -0,0 +1,51 @@
+/**
+ * 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.apache.cxf.tracing.opentelemetry;
+
+import org.apache.cxf.interceptor.Fault;
+import org.apache.cxf.message.Message;
+import org.apache.cxf.phase.Phase;
+
+import io.opentelemetry.api.OpenTelemetry;
+
+public class OpenTelemetryClientStopInterceptor extends AbstractOpenTelemetryClientInterceptor {
+    public OpenTelemetryClientStopInterceptor(final OpenTelemetry openTelemetry,
+                                              final String instrumentationName) {
+        this(Phase.RECEIVE, openTelemetry, instrumentationName);
+    }
+
+    public OpenTelemetryClientStopInterceptor(final String phase, final OpenTelemetry openTelemetry,
+                                              final String instrumentationName) {
+        super(phase, openTelemetry, instrumentationName);
+    }
+
+    @Override
+    public void handleMessage(Message message) throws Fault {
+        @SuppressWarnings("unchecked")
+        final TraceScopeHolder<TraceScope> holder = (TraceScopeHolder<TraceScope>)message.getExchange()
+            .get(TRACE_SPAN);
+
+        Integer responseCode = (Integer)message.get(Message.RESPONSE_CODE);
+        if (responseCode == null) {
+            responseCode = 200;
+        }
+
+        super.stopTraceSpan(holder, responseCode);
+    }
+}
diff --git a/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryContext.java b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryContext.java
new file mode 100644
index 0000000000..5acd465592
--- /dev/null
+++ b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryContext.java
@@ -0,0 +1,122 @@
+/**
+ * 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.apache.cxf.tracing.opentelemetry;
+
+import java.util.concurrent.Callable;
+
+import org.apache.cxf.tracing.Traceable;
+import org.apache.cxf.tracing.TracerContext;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanBuilder;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+
+public class OpenTelemetryContext implements TracerContext {
+    private final Tracer tracer;
+    private final Span continuation;
+
+    public OpenTelemetryContext(final Tracer tracer) {
+        this(tracer, null);
+    }
+
+    public OpenTelemetryContext(final Tracer tracer, final Span continuation) {
+        this.tracer = tracer;
+        this.continuation = continuation;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public Scope startSpan(final String description) {
+        return newOrChildSpan(description, null);
+    }
+
+    @Override
+    public <T> T continueSpan(final Traceable<T> traceable) throws Exception {
+        Scope scope = null;
+
+        if (Span.current() != null && continuation != null && !Span.current().getSpanContext().getSpanId()
+            .equals(continuation.getSpanContext().getSpanId())) {
+            scope = continuation.makeCurrent();
+        }
+
+        try { // NOPMD
+            return traceable.call(new OpenTelemetryContext(tracer));
+        } finally {
+            if (continuation != null && scope != null) {
+                scope.close();
+            }
+        }
+    }
+
+    @Override
+    public <T> Callable<T> wrap(final String description, final Traceable<T> traceable) {
+        final Callable<T> callable = new Callable<T>() {
+            @Override
+            public T call() throws Exception {
+                return traceable.call(new OpenTelemetryContext(tracer));
+            }
+        };
+
+        // Carry over parent from the current thread
+        final Context parent = Context.current();
+        return () -> {
+            try (Scope scope = newOrChildSpan(description, parent)) {
+                return callable.call();
+            }
+        };
+    }
+
+    @Override
+    public void annotate(String key, String value) {
+        final Span current = Span.current();
+        if (current != null) {
+            current.setAttribute(key, value);
+        }
+    }
+
+    @Override
+    public void timeline(String message) {
+        final Span current = Span.current();
+        if (current != null) {
+            current.addEvent(message);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T> T unwrap(final Class<T> clazz) {
+        if (Tracer.class.equals(clazz)) {
+            return (T)tracer;
+        } else {
+            throw new IllegalArgumentException("The class is '" + clazz
+                                               + "'not supported and cannot be unwrapped");
+        }
+    }
+
+    private Scope newOrChildSpan(final String description, Context parent) {
+        SpanBuilder spanBuilder = tracer.spanBuilder(description);
+        if (parent != null) {
+            spanBuilder.setParent(parent);
+        }
+        Span span = spanBuilder.startSpan();
+        return new ScopedSpan(span, span.makeCurrent());
+    }
+}
diff --git a/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryFeature.java b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryFeature.java
new file mode 100644
index 0000000000..6b682a5cc2
--- /dev/null
+++ b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryFeature.java
@@ -0,0 +1,84 @@
+/**
+ * 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.apache.cxf.tracing.opentelemetry;
+
+import org.apache.cxf.Bus;
+import org.apache.cxf.annotations.Provider;
+import org.apache.cxf.annotations.Provider.Scope;
+import org.apache.cxf.annotations.Provider.Type;
+import org.apache.cxf.common.injection.NoJSR250Annotations;
+import org.apache.cxf.feature.AbstractPortableFeature;
+import org.apache.cxf.feature.DelegatingFeature;
+import org.apache.cxf.interceptor.InterceptorProvider;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.OpenTelemetry;
+
+@NoJSR250Annotations
+@Provider(value = Type.Feature, scope = Scope.Server)
+public class OpenTelemetryFeature extends DelegatingFeature<OpenTelemetryFeature.Portable> {
+    public static final String DEFAULT_INSTRUMENTATION_NAME = "org.apache.cxf";
+
+    public OpenTelemetryFeature() {
+        super(new Portable());
+    }
+
+    public OpenTelemetryFeature(final String instrumentationName) {
+        super(new Portable(instrumentationName)); 
+    }
+
+    public OpenTelemetryFeature(final OpenTelemetry openTelemetry) {
+        super(new Portable(openTelemetry));
+    }
+
+    public OpenTelemetryFeature(final OpenTelemetry openTelemetry, final String instrumentationName) {
+        super(new Portable(openTelemetry, instrumentationName));
+    }
+
+    public static class Portable implements AbstractPortableFeature {
+        private OpenTelemetryStartInterceptor in;
+        private OpenTelemetryStopInterceptor out;
+
+        public Portable() {
+            this(GlobalOpenTelemetry.get());
+        }
+
+        public Portable(final String instrumentationName) {
+            this(GlobalOpenTelemetry.get(), instrumentationName);
+        }
+
+        public Portable(final OpenTelemetry openTelemetry) {
+            this(openTelemetry, DEFAULT_INSTRUMENTATION_NAME);
+        }
+
+        public Portable(final OpenTelemetry openTelemetry, final String instrumentationName) {
+            in = new OpenTelemetryStartInterceptor(openTelemetry, instrumentationName);
+            out = new OpenTelemetryStopInterceptor(openTelemetry, instrumentationName);
+        }
+
+        @Override
+        public void doInitializeProvider(InterceptorProvider provider, Bus bus) {
+            provider.getInInterceptors().add(in);
+            provider.getInFaultInterceptors().add(in);
+
+            provider.getOutInterceptors().add(out);
+            provider.getOutFaultInterceptors().add(out);
+        }
+    }
+}
diff --git a/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryStartInterceptor.java b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryStartInterceptor.java
new file mode 100644
index 0000000000..23b6309afb
--- /dev/null
+++ b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryStartInterceptor.java
@@ -0,0 +1,51 @@
+/**
+ * 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.apache.cxf.tracing.opentelemetry;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.cxf.common.injection.NoJSR250Annotations;
+import org.apache.cxf.helpers.CastUtils;
+import org.apache.cxf.interceptor.Fault;
+import org.apache.cxf.message.Message;
+import org.apache.cxf.phase.Phase;
+
+import io.opentelemetry.api.OpenTelemetry;
+
+@NoJSR250Annotations
+public class OpenTelemetryStartInterceptor extends AbstractOpenTelemetryInterceptor {
+    public OpenTelemetryStartInterceptor(OpenTelemetry openTelemetry, String instrumentationName) {
+        super(Phase.PRE_INVOKE, openTelemetry, instrumentationName);
+    }
+
+    @Override
+    public void handleMessage(Message message) throws Fault {
+        final Map<String, List<String>> headers = CastUtils
+            .cast((Map<?, ?>)message.get(Message.PROTOCOL_HEADERS));
+
+        final TraceScopeHolder<TraceScope> holder = super.startTraceSpan(headers, getUri(message),
+                                                                         (String)message
+                                                                             .get(Message.HTTP_REQUEST_METHOD));
+
+        if (holder != null) {
+            message.getExchange().put(TRACE_SPAN, holder);
+        }
+    }
+}
diff --git a/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryStopInterceptor.java b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryStopInterceptor.java
new file mode 100644
index 0000000000..cd129ae519
--- /dev/null
+++ b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryStopInterceptor.java
@@ -0,0 +1,66 @@
+/**
+ * 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.apache.cxf.tracing.opentelemetry;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.cxf.helpers.CastUtils;
+import org.apache.cxf.interceptor.Fault;
+import org.apache.cxf.message.Message;
+import org.apache.cxf.message.MessageUtils;
+import org.apache.cxf.phase.Phase;
+
+import io.opentelemetry.api.OpenTelemetry;
+
+public class OpenTelemetryStopInterceptor extends AbstractOpenTelemetryInterceptor {
+    public OpenTelemetryStopInterceptor(final OpenTelemetry openTelemetry, final String instrumentationName) {
+        super(Phase.POST_MARSHAL, openTelemetry, instrumentationName);
+    }
+
+    @Override
+    public void handleMessage(Message message) throws Fault {
+        Map<String, List<Object>> responseHeaders = CastUtils
+            .cast((Map<?, ?>)message.get(Message.PROTOCOL_HEADERS));
+
+        if (responseHeaders == null) {
+            responseHeaders = new HashMap<>();
+            message.put(Message.PROTOCOL_HEADERS, responseHeaders);
+        }
+
+        boolean isRequestor = MessageUtils.isRequestor(message);
+        Message requestMessage = isRequestor
+            ? message.getExchange().getOutMessage() : message.getExchange().getInMessage();
+        Map<String, List<String>> requestHeaders = CastUtils
+            .cast((Map<?, ?>)requestMessage.get(Message.PROTOCOL_HEADERS));
+
+        @SuppressWarnings("unchecked")
+        final TraceScopeHolder<TraceScope> holder = (TraceScopeHolder<TraceScope>)message.getExchange()
+            .get(TRACE_SPAN);
+
+        Integer responseCode = (Integer)message.get(Message.RESPONSE_CODE);
+        if (responseCode == null) {
+            responseCode = 200;
+        }
+
+        super.stopTraceSpan(requestHeaders, responseHeaders, responseCode, holder);
+    }
+
+}
diff --git a/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/ScopedSpan.java b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/ScopedSpan.java
new file mode 100644
index 0000000000..d4fda948a2
--- /dev/null
+++ b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/ScopedSpan.java
@@ -0,0 +1,42 @@
+/**
+ * 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.apache.cxf.tracing.opentelemetry;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.context.Scope;
+
+/**
+ * The instance of the Scope which is finishing the Span upon close() call.
+ */
+class ScopedSpan implements Scope {
+    private final Span span;
+    private final Scope scope;
+
+    ScopedSpan(final Span span, final Scope scope) {
+        this.span = span;
+        this.scope = scope;
+    }
+
+    @Override
+    public void close() {
+        span.end();
+        scope.close();
+    }
+}
diff --git a/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/TraceScope.java b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/TraceScope.java
new file mode 100644
index 0000000000..1a641b4ae6
--- /dev/null
+++ b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/TraceScope.java
@@ -0,0 +1,47 @@
+/**
+ * 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.apache.cxf.tracing.opentelemetry;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.context.Scope;
+
+public class TraceScope {
+    private final Span span;
+    private final Scope scope;
+
+    private final Scope parentScope;
+
+    TraceScope(final Span span, final Scope scope, final Scope parentScope) {
+        this.span = span;
+        this.scope = scope;
+        this.parentScope = parentScope;
+    }
+
+    public Span getSpan() {
+        return span;
+    }
+
+    public Scope getScope() {
+        return scope;
+    }
+
+    public Scope getParentScope() {
+        return parentScope;
+    }
+}
diff --git a/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/internal/TextMapInjectAdapter.java b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/internal/TextMapInjectAdapter.java
new file mode 100644
index 0000000000..0cd5f5b44a
--- /dev/null
+++ b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/internal/TextMapInjectAdapter.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.apache.cxf.tracing.opentelemetry.internal;
+
+import java.util.List;
+import java.util.Map;
+
+import io.opentelemetry.context.propagation.TextMapGetter;
+import io.opentelemetry.context.propagation.TextMapSetter;
+
+public final class TextMapInjectAdapter
+    implements TextMapGetter<Map<String, List<String>>>, TextMapSetter<Map<String, List<String>>> {
+    private static final TextMapInjectAdapter INSTANCE = new TextMapInjectAdapter();
+
+    private TextMapInjectAdapter() {
+
+    }
+
+    public static TextMapInjectAdapter get() {
+        return INSTANCE;
+    }
+
+    @Override
+    public Iterable<String> keys(Map<String, List<String>> carrier) {
+        return carrier.keySet();
+    }
+
+    @Override
+    public String get(Map<String, List<String>> carrier, String key) {
+        List<String> values = carrier.get(key);
+        return values == null || values.isEmpty() ? null : values.get(values.size() - 1);
+    }
+
+    @Override
+    public void set(Map<String, List<String>> carrier, String key, String value) {
+        carrier.put(key, List.of(value));
+    }
+}
diff --git a/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/jaxrs/OpenTelemetryClientProvider.java b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/jaxrs/OpenTelemetryClientProvider.java
new file mode 100644
index 0000000000..bf69979be0
--- /dev/null
+++ b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/jaxrs/OpenTelemetryClientProvider.java
@@ -0,0 +1,61 @@
+/**
+ * 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.apache.cxf.tracing.opentelemetry.jaxrs;
+
+import java.io.IOException;
+
+import javax.ws.rs.client.ClientRequestContext;
+import javax.ws.rs.client.ClientRequestFilter;
+import javax.ws.rs.client.ClientResponseContext;
+import javax.ws.rs.client.ClientResponseFilter;
+import javax.ws.rs.ext.Provider;
+
+import org.apache.cxf.tracing.opentelemetry.AbstractOpenTelemetryClientProvider;
+import org.apache.cxf.tracing.opentelemetry.TraceScope;
+
+import io.opentelemetry.api.OpenTelemetry;
+
+@Provider
+public class OpenTelemetryClientProvider extends AbstractOpenTelemetryClientProvider
+    implements ClientRequestFilter, ClientResponseFilter {
+
+    public OpenTelemetryClientProvider(final OpenTelemetry openTelemetry, final String instrumentationName) {
+        super(openTelemetry, instrumentationName);
+    }
+
+    @Override
+    public void filter(final ClientRequestContext requestContext) throws IOException {
+        final TraceScopeHolder<TraceScope> holder = super.startTraceSpan(requestContext.getStringHeaders(),
+                                                                         requestContext.getUri(),
+                                                                         requestContext.getMethod());
+
+        if (holder != null) {
+            requestContext.setProperty(TRACE_SPAN, holder);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public void filter(final ClientRequestContext requestContext, final ClientResponseContext responseContext)
+        throws IOException {
+        final TraceScopeHolder<TraceScope> holder = (TraceScopeHolder<TraceScope>)requestContext
+            .getProperty(TRACE_SPAN);
+        super.stopTraceSpan(holder, responseContext.getStatus());
+    }
+}
diff --git a/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/jaxrs/OpenTelemetryContextProvider.java b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/jaxrs/OpenTelemetryContextProvider.java
new file mode 100644
index 0000000000..ba8e424543
--- /dev/null
+++ b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/jaxrs/OpenTelemetryContextProvider.java
@@ -0,0 +1,50 @@
+/**
+ * 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.apache.cxf.tracing.opentelemetry.jaxrs;
+
+import org.apache.cxf.jaxrs.ext.ContextProvider;
+import org.apache.cxf.message.Message;
+import org.apache.cxf.tracing.TracerContext;
+import org.apache.cxf.tracing.opentelemetry.OpenTelemetryContext;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.Tracer;
+
+public class OpenTelemetryContextProvider implements ContextProvider<TracerContext> {
+    private final Tracer tracer;
+
+    public OpenTelemetryContextProvider(final Tracer tracer) {
+        this.tracer = tracer;
+    }
+
+    @Override
+    public TracerContext createContext(final Message message) {
+        // Check if there is a server span passed along with the message
+        final Span continuation = message.get(Span.class);
+
+        // If server span is already present, let us check if it is detached
+        // (asynchronous invocation)
+        if (continuation != null) {
+            return new OpenTelemetryContext(tracer, continuation);
+        }
+
+        return new OpenTelemetryContext(tracer);
+    }
+
+}
diff --git a/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/jaxrs/OpenTelemetryFeature.java b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/jaxrs/OpenTelemetryFeature.java
new file mode 100644
index 0000000000..3e404d6e5a
--- /dev/null
+++ b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/jaxrs/OpenTelemetryFeature.java
@@ -0,0 +1,57 @@
+/**
+ * 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.apache.cxf.tracing.opentelemetry.jaxrs;
+
+import javax.ws.rs.core.Feature;
+import javax.ws.rs.core.FeatureContext;
+import javax.ws.rs.ext.Provider;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.OpenTelemetry;
+
+@Provider
+public class OpenTelemetryFeature implements Feature {
+    private final OpenTelemetry openTelemetry;
+    private final String instrumentationName;
+
+    public OpenTelemetryFeature() {
+        this(GlobalOpenTelemetry.get());
+    }
+
+    public OpenTelemetryFeature(String instrumentationName) {
+        this(GlobalOpenTelemetry.get(), instrumentationName);
+    }
+
+    public OpenTelemetryFeature(final OpenTelemetry openTelemetry) {
+        this(openTelemetry,
+             org.apache.cxf.tracing.opentelemetry.OpenTelemetryFeature.DEFAULT_INSTRUMENTATION_NAME);
+    }
+
+    public OpenTelemetryFeature(final OpenTelemetry openTelemetry, final String instrumentationName) {
+        this.openTelemetry = openTelemetry;
+        this.instrumentationName = instrumentationName;
+    }
+
+    @Override
+    public boolean configure(FeatureContext context) {
+        context.register(new OpenTelemetryProvider(openTelemetry, instrumentationName));
+        context.register(new OpenTelemetryContextProvider(openTelemetry.getTracer(instrumentationName)));
+        return true;
+    }
+}
diff --git a/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/jaxrs/OpenTelemetryProvider.java b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/jaxrs/OpenTelemetryProvider.java
new file mode 100644
index 0000000000..d68132881d
--- /dev/null
+++ b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/jaxrs/OpenTelemetryProvider.java
@@ -0,0 +1,81 @@
+/**
+ * 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.apache.cxf.tracing.opentelemetry.jaxrs;
+
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.container.ContainerRequestFilter;
+import javax.ws.rs.container.ContainerResponseContext;
+import javax.ws.rs.container.ContainerResponseFilter;
+import javax.ws.rs.container.ResourceInfo;
+import javax.ws.rs.container.Suspended;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.ext.Provider;
+
+import org.apache.cxf.tracing.opentelemetry.AbstractOpenTelemetryProvider;
+import org.apache.cxf.tracing.opentelemetry.TraceScope;
+
+import io.opentelemetry.api.OpenTelemetry;
+
+@Provider
+public class OpenTelemetryProvider extends AbstractOpenTelemetryProvider
+    implements ContainerRequestFilter, ContainerResponseFilter {
+    @Context
+    private ResourceInfo resourceInfo;
+
+    public OpenTelemetryProvider(final OpenTelemetry openTelemetry, final String instrumentationName) {
+        super(openTelemetry, instrumentationName);
+    }
+
+    @Override
+    public void filter(final ContainerRequestContext requestContext) throws IOException {
+        final TraceScopeHolder<TraceScope> holder = super.startTraceSpan(requestContext.getHeaders(),
+                                                                         requestContext.getUriInfo()
+                                                                             .getRequestUri(),
+                                                                         requestContext.getMethod());
+
+        if (holder != null) {
+            requestContext.setProperty(TRACE_SPAN, holder);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public void filter(final ContainerRequestContext requestContext,
+                       final ContainerResponseContext responseContext)
+        throws IOException {
+        super.stopTraceSpan(requestContext.getHeaders(), responseContext.getHeaders(),
+                            responseContext.getStatus(),
+                            (TraceScopeHolder<TraceScope>)requestContext.getProperty(TRACE_SPAN));
+    }
+
+    @Override
+    protected boolean isAsyncResponse() {
+        for (final Annotation[] annotations : resourceInfo.getResourceMethod().getParameterAnnotations()) {
+            for (final Annotation annotation : annotations) {
+                if (annotation.annotationType().equals(Suspended.class)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+}
diff --git a/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/jaxrs/OpenTelemetryTracerContextClassProvider.java b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/jaxrs/OpenTelemetryTracerContextClassProvider.java
new file mode 100644
index 0000000000..788fe9e3f1
--- /dev/null
+++ b/integration/tracing/tracing-opentelemetry/src/main/java/org/apache/cxf/tracing/opentelemetry/jaxrs/OpenTelemetryTracerContextClassProvider.java
@@ -0,0 +1,29 @@
+/**
+ * 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.apache.cxf.tracing.opentelemetry.jaxrs;
+
+import org.apache.cxf.jaxrs.ext.ContextClassProvider;
+import org.apache.cxf.tracing.TracerContext;
+
+public class OpenTelemetryTracerContextClassProvider implements ContextClassProvider {
+    @Override
+    public Class<?> getContextClass() {
+        return TracerContext.class;
+    }
+}
diff --git a/integration/tracing/tracing-opentelemetry/src/main/resources/META-INF/services/org.apache.cxf.jaxrs.ext.ContextClassProvider b/integration/tracing/tracing-opentelemetry/src/main/resources/META-INF/services/org.apache.cxf.jaxrs.ext.ContextClassProvider
new file mode 100644
index 0000000000..2fcb268970
--- /dev/null
+++ b/integration/tracing/tracing-opentelemetry/src/main/resources/META-INF/services/org.apache.cxf.jaxrs.ext.ContextClassProvider
@@ -0,0 +1 @@
+org.apache.cxf.tracing.opentelemetry.jaxrs.OpenTelemetryTracerContextClassProvider
\ No newline at end of file
diff --git a/integration/tracing/tracing-opentelemetry/src/test/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryContextTest.java b/integration/tracing/tracing-opentelemetry/src/test/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryContextTest.java
new file mode 100644
index 0000000000..853f4e3ee2
--- /dev/null
+++ b/integration/tracing/tracing-opentelemetry/src/test/java/org/apache/cxf/tracing/opentelemetry/OpenTelemetryContextTest.java
@@ -0,0 +1,55 @@
+/**
+ * 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.apache.cxf.tracing.opentelemetry;
+
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.sdk.testing.junit4.OpenTelemetryRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.sameInstance;
+import static org.junit.Assert.assertThat;
+
+public class OpenTelemetryContextTest {
+    @Rule
+    public OpenTelemetryRule otel = OpenTelemetryRule.create();
+
+    private OpenTelemetryContext context;
+    private Tracer tracer;
+
+    @Before
+    public void setUp() {
+        tracer = otel.getOpenTelemetry().getTracer("test");
+        context = new OpenTelemetryContext(tracer);
+    }
+
+    @Test
+    public void testUnwrapTracer() {
+        assertThat(context.unwrap(Tracer.class), sameInstance(tracer));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testUnwrapUnsupportedClass() {
+        context.unwrap(Scope.class);
+    }
+
+}
diff --git a/parent/pom.xml b/parent/pom.xml
index 9b5a15ad83..de18a09da5 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -184,6 +184,8 @@
         <cxf.openjpa.version>3.2.2</cxf.openjpa.version>
         <cxf.opensaml.osgi.version.range>[3.1,4)</cxf.opensaml.osgi.version.range>
         <cxf.opensaml.osgi.version>3.4.6_3</cxf.opensaml.osgi.version>
+        <cxf.opentelemetry.version>1.28.0</cxf.opentelemetry.version>
+        <cxf.opentelemetry.semconv.version>1.28.0-alpha</cxf.opentelemetry.semconv.version>
         <cxf.opentracing.version>0.33.0</cxf.opentracing.version>
         <cxf.openwebbeans.version>2.0.27</cxf.openwebbeans.version>
         <cxf.persistence-api.version>2.2.3</cxf.persistence-api.version>
@@ -1962,6 +1964,23 @@
                 <version>${cxf.opentracing.version}</version>
                 <scope>test</scope>
             </dependency>
+            <dependency>
+                <groupId>io.opentelemetry</groupId>
+                <artifactId>opentelemetry-bom</artifactId>
+                <version>${cxf.opentelemetry.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <dependency>
+                <groupId>io.opentelemetry</groupId>
+                <artifactId>opentelemetry-semconv</artifactId>
+                <version>${cxf.opentelemetry.semconv.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.opentelemetry</groupId>
+                <artifactId>opentelemetry-opentracing-shim</artifactId>
+                <version>${cxf.opentelemetry.version}</version>
+            </dependency>
             <dependency>
                 <groupId>io.jaegertracing</groupId>
                 <artifactId>jaeger-core</artifactId>
diff --git a/systests/tracing/pom.xml b/systests/tracing/pom.xml
index e4cc1a63bf..b37f4746d2 100644
--- a/systests/tracing/pom.xml
+++ b/systests/tracing/pom.xml
@@ -100,6 +100,11 @@
             <artifactId>cxf-integration-tracing-opentracing</artifactId>
             <version>${project.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.cxf</groupId>
+            <artifactId>cxf-integration-tracing-opentelemetry</artifactId>
+            <version>${project.version}</version>
+        </dependency>
         <dependency>
             <groupId>org.apache.cxf</groupId>
             <artifactId>cxf-testutils</artifactId>
@@ -145,6 +150,16 @@
             <artifactId>awaitility</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>io.opentelemetry</groupId>
+            <artifactId>opentelemetry-api</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.opentelemetry</groupId>
+            <artifactId>opentelemetry-sdk-testing</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
     <build>
         <plugins>
diff --git a/systests/tracing/src/test/java/org/apache/cxf/systest/jaxrs/tracing/BookStore.java b/systests/tracing/src/test/java/org/apache/cxf/systest/jaxrs/tracing/BookStore.java
index f02142fc55..8c56162bd5 100644
--- a/systests/tracing/src/test/java/org/apache/cxf/systest/jaxrs/tracing/BookStore.java
+++ b/systests/tracing/src/test/java/org/apache/cxf/systest/jaxrs/tracing/BookStore.java
@@ -18,8 +18,6 @@
  */
 package org.apache.cxf.systest.jaxrs.tracing;
 
-import java.io.Closeable;
-import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.UUID;
@@ -44,14 +42,14 @@ import org.apache.cxf.tracing.Traceable;
 import org.apache.cxf.tracing.TracerContext;
 
 @Path("/bookstore/")
-public class BookStore<T extends Closeable> {
+public class BookStore<T extends AutoCloseable> {
     @Context private TracerContext tracer;
     private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
 
     @GET
     @Path("/books")
     @Produces(MediaType.APPLICATION_JSON)
-    public Collection<Book> getBooks() throws IOException {
+    public Collection<Book> getBooks() throws Exception {
         try (T span = tracer.startSpan("Get Books")) {
             return books();
         }
diff --git a/systests/tracing/src/test/java/org/apache/cxf/systest/jaxrs/tracing/opentelemetry/HasAttribute.java b/systests/tracing/src/test/java/org/apache/cxf/systest/jaxrs/tracing/opentelemetry/HasAttribute.java
new file mode 100644
index 0000000000..1e0eef8b26
--- /dev/null
+++ b/systests/tracing/src/test/java/org/apache/cxf/systest/jaxrs/tracing/opentelemetry/HasAttribute.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.apache.cxf.systest.jaxrs.tracing.opentelemetry;
+
+import org.hamcrest.FeatureMatcher;
+import org.hamcrest.Matcher;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class HasAttribute<T> extends FeatureMatcher<Attributes, T> {
+    private final AttributeKey<T> key;
+
+    public HasAttribute(AttributeKey<T> key, Matcher<T> valueMatcher) {
+        super(valueMatcher, "attribute map containing key " + key.getKey() + " with value",
+              "attribute value");
+        this.key = key;
+    }
+
+    public static <V> HasAttribute<V> hasAttribute(AttributeKey<V> key, Matcher<V> valueMatcher) {
+        return new HasAttribute<V>(key, valueMatcher);
+    }
+
+    public static <V> HasAttribute<V> hasAttribute(AttributeKey<V> key, V value) {
+        return hasAttribute(key, equalTo(value));
+    }
+
+    public static HasAttribute<String> hasAttribute(String key, String value) {
+        return hasAttribute(AttributeKey.stringKey(key), value);
+    }
+
+    @Override
+    protected T featureValueOf(Attributes attributes) {
+        return attributes.get(key);
+    }
+}
diff --git a/systests/tracing/src/test/java/org/apache/cxf/systest/jaxrs/tracing/opentelemetry/HasSpan.java b/systests/tracing/src/test/java/org/apache/cxf/systest/jaxrs/tracing/opentelemetry/HasSpan.java
new file mode 100644
index 0000000000..5a80d96a06
--- /dev/null
+++ b/systests/tracing/src/test/java/org/apache/cxf/systest/jaxrs/tracing/opentelemetry/HasSpan.java
@@ -0,0 +1,68 @@
+/**
+ * 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.apache.cxf.systest.jaxrs.tracing.opentelemetry;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+import org.hamcrest.core.IsIterableContaining;
+
+import io.opentelemetry.sdk.trace.data.EventData;
+import io.opentelemetry.sdk.trace.data.SpanData;
+
+public class HasSpan extends IsIterableContaining<SpanData> {
+    public HasSpan(final String name) {
+        this(name, null);
+    }
+
+    public HasSpan(final String name, final Matcher<Iterable<? super EventData>> matcher) {
+        super(new TypeSafeMatcher<SpanData>() {
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("span with name ").appendValue(name).appendText(" ");
+
+                if (matcher != null) {
+                    description.appendText(" and ");
+                    matcher.describeTo(description);
+                }
+            }
+
+            @Override
+            protected boolean matchesSafely(SpanData item) {
+                if (!name.equals(item.getName())) {
+                    return false;
+                }
+
+                if (matcher != null) {
+                    return matcher.matches(item.getEvents());
+                }
+
+                return true;
+            }
+        });
+    }
+
+    public static HasSpan hasSpan(final String name) {
+        return new HasSpan(name);
+    }
+
+    public static HasSpan hasSpan(final String name, final Matcher<Iterable<? super EventData>> matcher) {
+        return new HasSpan(name, matcher);
+    }
+}
diff --git a/systests/tracing/src/test/java/org/apache/cxf/systest/jaxrs/tracing/opentelemetry/IsLogContaining.java b/systests/tracing/src/test/java/org/apache/cxf/systest/jaxrs/tracing/opentelemetry/IsLogContaining.java
new file mode 100644
index 0000000000..c0895f80d2
--- /dev/null
+++ b/systests/tracing/src/test/java/org/apache/cxf/systest/jaxrs/tracing/opentelemetry/IsLogContaining.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.apache.cxf.systest.jaxrs.tracing.opentelemetry;
+
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+import org.hamcrest.core.IsIterableContaining;
+
+import io.opentelemetry.sdk.trace.data.EventData;
+
+public class IsLogContaining extends IsIterableContaining<EventData> {
+    public IsLogContaining(final String value) {
+        super(new TypeSafeMatcher<EventData>() {
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("annotation with name ").appendValue(value);
+            }
+
+            @Override
+            protected boolean matchesSafely(EventData item) {
+                return value.equals(item.getName());
+            }
+        });
+    }
+
+    public static IsLogContaining hasItem(final String value) {
+        return new IsLogContaining(value);
+    }
+}
diff --git a/systests/tracing/src/test/java/org/apache/cxf/systest/jaxrs/tracing/opentelemetry/OpenTelemetryTracingTest.java b/systests/tracing/src/test/java/org/apache/cxf/systest/jaxrs/tracing/opentelemetry/OpenTelemetryTracingTest.java
new file mode 100644
index 0000000000..e86301e4ad
--- /dev/null
+++ b/systests/tracing/src/test/java/org/apache/cxf/systest/jaxrs/tracing/opentelemetry/OpenTelemetryTracingTest.java
@@ -0,0 +1,466 @@
+/**
+ * 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.apache.cxf.systest.jaxrs.tracing.opentelemetry;
+
+import java.net.MalformedURLException;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.IntStream;
+
+import javax.ws.rs.ProcessingException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
+
+import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
+import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;
+import org.apache.cxf.jaxrs.model.AbstractResourceInfo;
+import org.apache.cxf.systest.jaxrs.tracing.BookStore;
+import org.apache.cxf.systest.jaxrs.tracing.NullPointerExceptionMapper;
+import org.apache.cxf.testutil.common.AbstractClientServerTestBase;
+import org.apache.cxf.testutil.common.AbstractTestServerBase;
+import org.apache.cxf.tracing.opentelemetry.OpenTelemetryClientFeature;
+import org.apache.cxf.tracing.opentelemetry.jaxrs.OpenTelemetryClientProvider;
+import org.apache.cxf.tracing.opentelemetry.jaxrs.OpenTelemetryFeature;
+import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanContext;
+import io.opentelemetry.api.trace.SpanId;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.api.trace.StatusCode;
+import io.opentelemetry.api.trace.TraceFlags;
+import io.opentelemetry.api.trace.TraceId;
+import io.opentelemetry.api.trace.TraceState;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.context.propagation.TextMapSetter;
+import io.opentelemetry.sdk.testing.junit4.OpenTelemetryRule;
+import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
+
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.apache.cxf.systest.jaxrs.tracing.opentelemetry.HasAttribute.hasAttribute;
+import static org.apache.cxf.systest.jaxrs.tracing.opentelemetry.HasSpan.hasSpan;
+import static org.apache.cxf.systest.jaxrs.tracing.opentelemetry.IsLogContaining.hasItem;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class OpenTelemetryTracingTest extends AbstractClientServerTestBase {
+    public static final String PORT = allocatePort(OpenTelemetryTracingTest.class);
+
+    @ClassRule
+    public static OpenTelemetryRule otelRule = OpenTelemetryRule.create();
+
+    private static final AtomicLong RANDOM = new AtomicLong();
+
+    @Rule
+    public ExpectedException expectedException = ExpectedException.none();
+
+    @BeforeClass
+    public static void startServers() throws Exception {
+        AbstractResourceInfo.clearAllMaps();
+        // keep out of process due to stack traces testing failures
+        assertTrue("server did not launch correctly", launchServer(OpenTelemetryServer.class, true));
+    }
+
+    private static WebClient createWebClient(final String path, final Object... providers) {
+        return WebClient.create("http://localhost:" + PORT + path, Arrays.asList(providers))
+            .accept(MediaType.APPLICATION_JSON);
+    }
+
+    private static Context fromRandom() {
+        return Context.root()
+            .with(Span.wrap(SpanContext
+                .create(TraceId.fromLongs(RANDOM.getAndIncrement(), RANDOM.getAndIncrement()),
+                        SpanId.fromLong(RANDOM.getAndIncrement()), TraceFlags.getSampled(),
+                        TraceState.getDefault())));
+    }
+
+    @After
+    public void tearDown() {
+        otelRule.clearSpans();
+    }
+
+    @Test
+    public void testThatNewSpanIsCreatedWhenNotProvided() {
+        final Response r = createWebClient("/bookstore/books").get();
+        assertEquals(Status.OK.getStatusCode(), r.getStatus());
+
+        assertThat(otelRule.getSpans().size(), equalTo(2));
+        assertThat(otelRule.getSpans().get(0).getName(), equalTo("Get Books"));
+        assertThat(otelRule.getSpans().get(1).getName(), equalTo("GET /bookstore/books"));
+        assertThat(otelRule.getSpans().get(1).getAttributes(),
+                   hasAttribute(SemanticAttributes.HTTP_STATUS_CODE, 200L));
+        assertThat(otelRule.getSpans().get(1).getInstrumentationScopeInfo().getName(),
+            equalTo("jaxrs-server-test"));
+    }
+
+    @Test
+    public void testThatNewInnerSpanIsCreated() {
+        final Context parentContext = fromRandom();
+
+        try (Scope parentScope = parentContext.makeCurrent()) {
+            final Response r = withTrace(createWebClient("/bookstore/books")).get();
+            assertEquals(Status.OK.getStatusCode(), r.getStatus());
+
+            assertThat(otelRule.getSpans().size(), equalTo(2));
+            assertThat(otelRule.getSpans().get(0).getName(), equalTo("Get Books"));
+            assertThat(otelRule.getSpans().get(1).getName(), equalTo("GET /bookstore/books"));
+        }
+    }
+
+    @Test
+    public void testThatCurrentSpanIsAnnotatedWithKeyValue() {
+        final Context parentContext = fromRandom();
+
+        try (Scope parentScope = parentContext.makeCurrent()) {
+            final Response r = withTrace(createWebClient("/bookstore/book/1")).get();
+            assertEquals(Status.OK.getStatusCode(), r.getStatus());
+
+            assertThat(otelRule.getSpans().size(), equalTo(1));
+            assertThat(otelRule.getSpans().get(0).getName(), equalTo("GET /bookstore/book/1"));
+            assertThat(otelRule.getSpans().get(0).getAttributes(), hasAttribute("book-id", "1"));
+        }
+    }
+
+    @Test
+    public void testThatParallelSpanIsAnnotatedWithTimeline() {
+        final Context parentContext = fromRandom();
+
+        try (Scope parentScope = parentContext.makeCurrent()) {
+            final Response r = withTrace(createWebClient("/bookstore/process")).put("");
+            assertEquals(Status.OK.getStatusCode(), r.getStatus());
+
+            assertThat(otelRule.getSpans().size(), equalTo(2));
+            assertThat(otelRule.getSpans(), hasSpan("Processing books", hasItem("Processing started")));
+            assertThat(otelRule.getSpans(), hasSpan("PUT /bookstore/process"));
+        }
+    }
+
+    @Test
+    public void testThatNewChildSpanIsCreatedWhenParentIsProvided() {
+        final Response r = createWebClient("/bookstore/books",
+            new OpenTelemetryClientProvider(otelRule.getOpenTelemetry(), "jaxrs-client-test")).get();
+        assertEquals(Status.OK.getStatusCode(), r.getStatus());
+
+        assertThat(otelRule.getSpans().toString(), otelRule.getSpans().size(), equalTo(3));
+        assertThat(otelRule.getSpans().get(0).getName(), equalTo("Get Books"));
+        assertThat(otelRule.getSpans().get(0).getParentSpanContext().isValid(), equalTo(true));
+
+        // let's check that the parent client span has the custom instrumentation name in scope info
+        assertThat(otelRule.getSpans().get(2).getInstrumentationScopeInfo().getName(), equalTo("jaxrs-client-test"));
+    }
+
+    @Test
+    public void testThatNewInnerSpanIsCreatedUsingAsyncInvocation() throws InterruptedException {
+        final Context parentContext = fromRandom();
+
+        try (Scope parentScope = parentContext.makeCurrent()) {
+            final Response r = withTrace(createWebClient("/bookstore/books/async")).get();
+            assertEquals(Status.OK.getStatusCode(), r.getStatus());
+
+            await().atMost(Duration.ofSeconds(1L)).until(() -> otelRule.getSpans().size() == 2);
+
+            assertThat(otelRule.getSpans().size(), equalTo(2));
+            assertEquals("Processing books", otelRule.getSpans().get(0).getName());
+            assertEquals("GET /bookstore/books/async", otelRule.getSpans().get(1).getName());
+            assertThat(otelRule.getSpans().get(1).getParentSpanContext().isValid(), equalTo(true));
+            assertThat(otelRule.getSpans().get(1).getParentSpanId(),
+                       equalTo(Span.current().getSpanContext().getSpanId()));
+        }
+    }
+
+    @Test
+    public void testThatOuterSpanIsCreatedUsingAsyncInvocation() {
+        final Context parentContext = fromRandom();
+
+        try (Scope parentScope = parentContext.makeCurrent()) {
+            final Response r = withTrace(createWebClient("/bookstore/books/async/notrace")).get();
+            assertEquals(Status.OK.getStatusCode(), r.getStatus());
+
+            assertThat(otelRule.getSpans().size(), equalTo(1));
+            assertThat(otelRule.getSpans().get(0).getName(), equalTo("GET /bookstore/books/async/notrace"));
+        }
+    }
+
+    @Test
+    public void testThatNewSpanIsCreatedUsingAsyncInvocation() throws InterruptedException {
+        final Response r = createWebClient("/bookstore/books/async").get();
+        assertEquals(Status.OK.getStatusCode(), r.getStatus());
+
+        await().atMost(Duration.ofSeconds(1L)).until(() -> otelRule.getSpans().size() == 2);
+
+        assertThat(otelRule.getSpans().size(), equalTo(2));
+        assertThat(otelRule.getSpans().get(0).getName(), equalTo("Processing books"));
+        assertThat(otelRule.getSpans().get(1).getName(), equalTo("GET /bookstore/books/async"));
+    }
+
+    @Test
+    public void testThatNewSpanIsCreatedWhenNotProvidedUsingAsyncClient() throws Exception {
+        final WebClient client = createWebClient("/bookstore/books",
+            new OpenTelemetryClientProvider(otelRule.getOpenTelemetry(), "jaxrs-client-test"));
+
+        final Response r = client.async().get().get(1L, TimeUnit.SECONDS);
+        assertEquals(Status.OK.getStatusCode(), r.getStatus());
+
+        await().atMost(Duration.ofSeconds(1L)).until(() -> otelRule.getSpans().size() == 3);
+
+        assertThat(otelRule.getSpans().size(), equalTo(3));
+        assertThat(otelRule.getSpans().get(0).getName(), equalTo("Get Books"));
+        assertThat(otelRule.getSpans().get(1).getName(), equalTo("GET /bookstore/books"));
+        assertThat(otelRule.getSpans().get(1).getKind(), equalTo(SpanKind.SERVER));
+        assertThat(otelRule.getSpans().get(2).getName(), equalTo("GET " + client.getCurrentURI()));
+        assertThat(otelRule.getSpans().get(2).getKind(), equalTo(SpanKind.CLIENT));
+    }
+
+    @Test
+    public void testThatNewSpansAreCreatedWhenNotProvidedUsingMultipleAsyncClients() throws Exception {
+        final WebClient client = createWebClient("/bookstore/books",
+            new OpenTelemetryClientProvider(otelRule.getOpenTelemetry(), "jaxrs-client-test"));
+
+        // The intention is to make a calls one after another, not in parallel, to ensure the
+        // thread have trace contexts cleared out.
+        IntStream.range(0, 4).mapToObj(index -> client.async().get()).map(this::get)
+            .forEach(r -> assertEquals(Status.OK.getStatusCode(), r.getStatus()));
+
+        assertThat(otelRule.getSpans().size(), equalTo(12));
+
+        IntStream.range(0, 4).map(index -> index * 3).forEach(index -> {
+            assertThat(otelRule.getSpans().get(index).getName(), equalTo("Get Books"));
+            assertThat(otelRule.getSpans().get(index + 1).getName(), equalTo("GET /bookstore/books"));
+            assertThat(otelRule.getSpans().get(index + 2).getName(),
+                       equalTo("GET " + client.getCurrentURI()));
+        });
+    }
+
+    @Test
+    public void testThatNewSpansAreCreatedWhenNotProvidedUsingMultipleClients() throws Exception {
+        final WebClient client = createWebClient("/bookstore/books",
+            new OpenTelemetryClientProvider(otelRule.getOpenTelemetry(), "jaxrs-client-test"));
+
+        // The intention is to make a calls one after another, not in parallel, to ensure the
+        // thread have trace contexts cleared out.
+        IntStream.range(0, 4).mapToObj(index -> client.get())
+            .forEach(r -> assertEquals(Status.OK.getStatusCode(), r.getStatus()));
+
+        assertEquals(otelRule.getSpans().toString(), 12, otelRule.getSpans().size());
+
+        IntStream.range(0, 4).map(index -> index * 3).forEach(index -> {
+            assertThat(otelRule.getSpans().get(index).getName(), equalTo("Get Books"));
+            assertThat(otelRule.getSpans().get(index + 1).getName(), equalTo("GET /bookstore/books"));
+            assertThat(otelRule.getSpans().get(index + 2).getName(),
+                       equalTo("GET " + client.getCurrentURI()));
+        });
+    }
+
+    @Test
+    public void testThatProvidedSpanIsNotClosedWhenActive() throws MalformedURLException {
+        final WebClient client = createWebClient("/bookstore/books",
+            new OpenTelemetryClientProvider(otelRule.getOpenTelemetry(), "jaxrs-client-test"));
+
+        final Span span = otelRule.getOpenTelemetry().getTracer("test").spanBuilder("test span").startSpan();
+        try (Scope scope = span.makeCurrent()) {
+            final Response r = client.get();
+            assertEquals(Status.OK.getStatusCode(), r.getStatus());
+
+            assertEquals(otelRule.getSpans().toString(), 3, otelRule.getSpans().size());
+            assertThat(otelRule.getSpans().get(0).getName(), equalTo("Get Books"));
+            assertThat(otelRule.getSpans().get(0).getParentSpanContext().isValid(), equalTo(true));
+            assertThat(otelRule.getSpans().get(1).getName(), equalTo("GET /bookstore/books"));
+            assertThat(otelRule.getSpans().get(1).getParentSpanContext().isValid(), equalTo(true));
+            assertThat(otelRule.getSpans().get(2).getName(), equalTo("GET " + client.getCurrentURI()));
+            assertThat(otelRule.getSpans().get(2).getParentSpanContext().isValid(), equalTo(true));
+        } finally {
+            span.end();
+        }
+
+        // Await till flush happens, usually every second
+        await().atMost(Duration.ofSeconds(1L)).until(() -> otelRule.getSpans().size() == 4);
+
+        assertThat(otelRule.getSpans().size(), equalTo(4));
+        assertThat(otelRule.getSpans().get(3).getName(), equalTo("test span"));
+        assertThat(otelRule.getSpans().get(3).getParentSpanContext().isValid(), equalTo(false));
+    }
+
+    @Test
+    public void testThatProvidedSpanIsNotDetachedWhenActiveUsingAsyncClient() throws Exception {
+        final WebClient client = createWebClient("/bookstore/books",
+            new OpenTelemetryClientProvider(otelRule.getOpenTelemetry(), "jaxrs-client-test"));
+
+        final Span span = otelRule.getOpenTelemetry().getTracer("test").spanBuilder("test span").startSpan();
+        try (Scope scope = span.makeCurrent()) {
+            final Response r = client.async().get().get(1L, TimeUnit.MINUTES);
+            assertEquals(Status.OK.getStatusCode(), r.getStatus());
+            assertThat(Span.current().getSpanContext().getSpanId(),
+                       equalTo(span.getSpanContext().getSpanId()));
+
+            await().atMost(Duration.ofSeconds(1L)).until(() -> otelRule.getSpans().size() == 3);
+
+            assertThat(otelRule.getSpans().size(), equalTo(3));
+            assertThat(otelRule.getSpans().get(0).getName(), equalTo("Get Books"));
+            assertThat(otelRule.getSpans().get(0).getParentSpanContext(), notNullValue());
+            assertThat(otelRule.getSpans().get(1).getName(), equalTo("GET /bookstore/books"));
+            assertThat(otelRule.getSpans().get(1).getParentSpanContext().isValid(), equalTo(true));
+            assertThat(otelRule.getSpans().get(2).getName(), equalTo("GET " + client.getCurrentURI()));
+            assertThat(otelRule.getSpans().get(2).getParentSpanContext(), notNullValue());
+        } finally {
+            span.end();
+        }
+
+        // Await till flush happens, usually every second
+        await().atMost(Duration.ofSeconds(1L)).until(() -> otelRule.getSpans().size() == 4);
+
+        assertThat(otelRule.getSpans().size(), equalTo(4));
+        assertThat(otelRule.getSpans().get(3).getName(), equalTo("test span"));
+        assertThat(otelRule.getSpans().get(3).getParentSpanContext().isValid(), equalTo(false));
+    }
+
+    @Test
+    public void testThatInnerSpanIsCreatedUsingPseudoAsyncInvocation() {
+        final Context parentContext = fromRandom();
+
+        try (Scope parentScope = parentContext.makeCurrent()) {
+            final Response r = withTrace(createWebClient("/bookstore/books/pseudo-async")).get();
+            assertEquals(Status.OK.getStatusCode(), r.getStatus());
+
+            assertThat(otelRule.getSpans().size(), equalTo(2));
+            assertThat(otelRule.getSpans().get(1).getName(), equalTo("GET /bookstore/books/pseudo-async"));
+            assertThat(otelRule.getSpans().get(0).getName(), equalTo("Processing books"));
+        }
+    }
+
+    @Test
+    public void testThatNewSpanIsCreatedOnClientTimeout() {
+        final WebClient client = WebClient
+            .create("http://localhost:" + PORT + "/bookstore/books/long", Collections.emptyList(),
+                    Arrays.asList(new OpenTelemetryClientFeature(otelRule.getOpenTelemetry())), null)
+            .accept(MediaType.APPLICATION_JSON);
+
+        HTTPClientPolicy httpClientPolicy = new HTTPClientPolicy();
+        httpClientPolicy.setConnectionTimeout(100);
+        httpClientPolicy.setReceiveTimeout(100);
+        WebClient.getConfig(client).getHttpConduit().setClient(httpClientPolicy);
+
+        expectedException.expect(ProcessingException.class);
+        try {
+            client.get();
+        } finally {
+            await().atMost(Duration.ofSeconds(1L)).until(() -> otelRule.getSpans().size() == 2);
+            assertThat(otelRule.getSpans().toString(), otelRule.getSpans().size(), equalTo(2));
+            assertThat(otelRule.getSpans().get(0).getName(), equalTo("GET " + client.getCurrentURI()));
+            assertThat(otelRule.getSpans().get(0).getStatus().getStatusCode(), equalTo(StatusCode.ERROR));
+            assertThat(otelRule.getSpans().get(1).getName(), equalTo("GET /bookstore/books/long"));
+        }
+    }
+
+    @Test
+    public void testThatErrorSpanIsCreatedOnExceptionWhenNotProvided() {
+        final Response r = createWebClient("/bookstore/books/exception").get();
+        assertEquals(Status.INTERNAL_SERVER_ERROR.getStatusCode(), r.getStatus());
+
+        assertThat(otelRule.getSpans().toString(), otelRule.getSpans().size(), equalTo(1));
+        assertThat(otelRule.getSpans().get(0).getName(), equalTo("GET /bookstore/books/exception"));
+        assertThat(otelRule.getSpans().get(0).getAttributes(),
+                   hasAttribute(SemanticAttributes.HTTP_STATUS_CODE, 500L));
+    }
+
+    @Test
+    public void testThatErrorSpanIsCreatedOnErrorWhenNotProvided() {
+        final Response r = createWebClient("/bookstore/books/error").get();
+        assertEquals(Status.SERVICE_UNAVAILABLE.getStatusCode(), r.getStatus());
+
+        assertThat(otelRule.getSpans().toString(), otelRule.getSpans().size(), equalTo(1));
+        assertThat(otelRule.getSpans().get(0).getName(), equalTo("GET /bookstore/books/error"));
+        assertThat(otelRule.getSpans().get(0).getAttributes(),
+                   hasAttribute(SemanticAttributes.HTTP_STATUS_CODE, 503L));
+    }
+
+    @Test
+    public void testThatErrorSpanIsCreatedOnMappedExceptionWhenNotProvided() {
+        final Response r = createWebClient("/bookstore/books/mapper").get();
+        assertEquals(Status.NOT_FOUND.getStatusCode(), r.getStatus());
+
+        assertThat(otelRule.getSpans().toString(), otelRule.getSpans().size(), equalTo(1));
+        assertThat(otelRule.getSpans().get(0).getName(), equalTo("GET /bookstore/books/mapper"));
+        assertThat(otelRule.getSpans().get(0).getAttributes(),
+                   hasAttribute(SemanticAttributes.HTTP_STATUS_CODE, 404L));
+    }
+
+    private WebClient withTrace(final WebClient client) {
+        GlobalOpenTelemetry.getPropagators().getTextMapPropagator().inject(Context.current(), client,
+            new TextMapSetter<WebClient>() {
+                @Override
+                public void set(WebClient carrier, String key, String value) {
+                    carrier.header(key, value);
+                }
+            });
+
+        return client;
+    }
+
+    private <T> T get(final Future<T> future) {
+        try {
+            return future.get(1L, TimeUnit.MINUTES);
+        } catch (InterruptedException | TimeoutException | ExecutionException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    public static class OpenTelemetryServer extends AbstractTestServerBase {
+
+        private org.apache.cxf.endpoint.Server server;
+
+        @Override
+        protected void run() {
+            final JAXRSServerFactoryBean sf = new JAXRSServerFactoryBean();
+            sf.setResourceClasses(BookStore.class);
+            sf.setResourceProvider(BookStore.class, new SingletonResourceProvider(new BookStore<Scope>()));
+            sf.setAddress("http://localhost:" + PORT);
+            sf.setProvider(new JacksonJsonProvider());
+            sf.setProvider(new OpenTelemetryFeature(otelRule.getOpenTelemetry(), "jaxrs-server-test"));
+            sf.setProvider(new NullPointerExceptionMapper());
+            server = sf.create();
+        }
+
+        @Override
+        public void tearDown() throws Exception {
+            server.destroy();
+        }
+    }
+}
diff --git a/systests/tracing/src/test/java/org/apache/cxf/systest/jaxws/tracing/opentelemetry/BookStore.java b/systests/tracing/src/test/java/org/apache/cxf/systest/jaxws/tracing/opentelemetry/BookStore.java
new file mode 100644
index 0000000000..829aa0f8f7
--- /dev/null
+++ b/systests/tracing/src/test/java/org/apache/cxf/systest/jaxws/tracing/opentelemetry/BookStore.java
@@ -0,0 +1,71 @@
+/**
+ * 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.apache.cxf.systest.jaxws.tracing.opentelemetry;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.UUID;
+
+import javax.annotation.Resource;
+import javax.jws.WebMethod;
+import javax.jws.WebService;
+import javax.xml.ws.WebServiceContext;
+import javax.xml.ws.handler.MessageContext;
+
+import org.apache.cxf.systest.Book;
+import org.apache.cxf.systest.jaxws.tracing.BookStoreService;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.context.Scope;
+
+@WebService(endpointInterface = "org.apache.cxf.systest.jaxws.tracing.BookStoreService", serviceName = "BookStore")
+public class BookStore implements BookStoreService {
+    private final Tracer tracer;
+
+    @Resource
+    private WebServiceContext context;
+
+    public BookStore() {
+        tracer = GlobalOpenTelemetry.getTracer(BookStore.class.getName());
+    }
+
+    @WebMethod
+    public Collection<Book> getBooks() {
+        final Span span = tracer.spanBuilder("Get Books").startSpan();
+        try (Scope scope = span.makeCurrent()) {
+            return Arrays.asList(new Book("Apache CXF in Action", UUID.randomUUID().toString()),
+                                 new Book("Mastering Apache CXF", UUID.randomUUID().toString()));
+        } finally {
+            span.end();
+        }
+    }
+
+    @WebMethod
+    public int removeBooks() {
+        throw new RuntimeException("Unable to remove books");
+    }
+
+    @WebMethod
+    public void addBooks() {
+        final MessageContext ctx = context.getMessageContext();
+        ctx.put(MessageContext.HTTP_RESPONSE_CODE, 202);
+    }
+}
diff --git a/systests/tracing/src/test/java/org/apache/cxf/systest/jaxws/tracing/opentelemetry/OpenTelemetryTracingTest.java b/systests/tracing/src/test/java/org/apache/cxf/systest/jaxws/tracing/opentelemetry/OpenTelemetryTracingTest.java
new file mode 100644
index 0000000000..5b5e43e071
--- /dev/null
+++ b/systests/tracing/src/test/java/org/apache/cxf/systest/jaxws/tracing/opentelemetry/OpenTelemetryTracingTest.java
@@ -0,0 +1,281 @@
+/**
+ * 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.apache.cxf.systest.jaxws.tracing.opentelemetry;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
+
+import javax.xml.ws.soap.SOAPFaultException;
+
+import org.apache.cxf.endpoint.Client;
+import org.apache.cxf.ext.logging.LoggingInInterceptor;
+import org.apache.cxf.ext.logging.LoggingOutInterceptor;
+import org.apache.cxf.feature.Feature;
+import org.apache.cxf.frontend.ClientProxy;
+import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;
+import org.apache.cxf.jaxws.JaxWsServerFactoryBean;
+import org.apache.cxf.message.Message;
+import org.apache.cxf.systest.jaxws.tracing.BookStoreService;
+import org.apache.cxf.testutil.common.AbstractClientServerTestBase;
+import org.apache.cxf.testutil.common.AbstractTestServerBase;
+import org.apache.cxf.tracing.opentelemetry.OpenTelemetryClientFeature;
+import org.apache.cxf.tracing.opentelemetry.OpenTelemetryFeature;
+import org.apache.cxf.tracing.opentelemetry.internal.TextMapInjectAdapter;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanContext;
+import io.opentelemetry.api.trace.SpanId;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.api.trace.TraceFlags;
+import io.opentelemetry.api.trace.TraceId;
+import io.opentelemetry.api.trace.TraceState;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.sdk.testing.junit4.OpenTelemetryRule;
+import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
+
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import static org.apache.cxf.systest.jaxrs.tracing.opentelemetry.HasAttribute.hasAttribute;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+
+public class OpenTelemetryTracingTest extends AbstractClientServerTestBase {
+    public static final String PORT = allocatePort(OpenTelemetryTracingTest.class);
+
+    @ClassRule
+    public static OpenTelemetryRule otelRule = OpenTelemetryRule.create();
+
+    private static final AtomicLong RANDOM = new AtomicLong();
+
+    @BeforeClass
+    public static void startServers() throws Exception {
+        // keep out of process due to stack traces testing failures
+        assertTrue("server did not launch correctly", launchServer(BraveServer.class, true));
+    }
+
+    private static BookStoreService createJaxWsService() {
+        return createJaxWsService(Collections.emptyMap());
+    }
+
+    private static BookStoreService createJaxWsService(final Map<String, List<String>> headers) {
+        return createJaxWsService(headers, null);
+    }
+
+    private static BookStoreService createJaxWsService(final Map<String, List<String>> headers,
+                                                       final Feature feature) {
+
+        JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean();
+        factory.getOutInterceptors().add(new LoggingOutInterceptor());
+        factory.getInInterceptors().add(new LoggingInInterceptor());
+        factory.setServiceClass(BookStoreService.class);
+        factory.setAddress("http://localhost:" + PORT + "/BookStore");
+
+        if (feature != null) {
+            factory.getFeatures().add(feature);
+        }
+
+        final BookStoreService service = (BookStoreService)factory.create();
+        final Client proxy = ClientProxy.getClient(service);
+        proxy.getRequestContext().put(Message.PROTOCOL_HEADERS, headers);
+
+        return service;
+    }
+
+    private static Context fromRandom() {
+        return Context.root()
+            .with(Span.wrap(SpanContext
+                .create(TraceId.fromLongs(RANDOM.getAndIncrement(), RANDOM.getAndIncrement()),
+                        SpanId.fromLong(RANDOM.getAndIncrement()), TraceFlags.getSampled(),
+                        TraceState.getDefault())));
+    }
+
+    @After
+    public void tearDown() {
+        otelRule.clearSpans();
+    }
+
+    @Test
+    public void testThatNewSpanIsCreatedWhenNotProvided() throws Exception {
+        final BookStoreService service = createJaxWsService();
+        assertThat(service.getBooks().size(), equalTo(2));
+
+        assertThat(otelRule.getSpans().size(), equalTo(2));
+        assertThat(otelRule.getSpans().get(0).getName(), equalTo("Get Books"));
+        assertThat(otelRule.getSpans().get(1).getName(), equalTo("POST /BookStore"));
+    }
+
+    @Test
+    public void testThatNewInnerSpanIsCreated() throws Exception {
+        final Context parentContext = fromRandom();
+
+        try (Scope parentScope = parentContext.makeCurrent()) {
+            final Map<String, List<String>> headers = new HashMap<>();
+            GlobalOpenTelemetry.getPropagators().getTextMapPropagator().inject(Context.current(), headers,
+                                                                               TextMapInjectAdapter.get());
+
+            final BookStoreService service = createJaxWsService(headers);
+            assertThat(service.getBooks().size(), equalTo(2));
+
+            assertThat(otelRule.getSpans().size(), equalTo(2));
+            assertThat(otelRule.getSpans().get(0).getName(), equalTo("Get Books"));
+            assertThat(otelRule.getSpans().get(1).getName(), equalTo("POST /BookStore"));
+        }
+    }
+
+    @Test
+    public void testThatNewChildSpanIsCreatedWhenParentIsProvided() throws Exception {
+        final BookStoreService service = createJaxWsService(new OpenTelemetryClientFeature(otelRule
+            .getOpenTelemetry(), "jaxws-client-test"));
+        assertThat(service.getBooks().size(), equalTo(2));
+
+        assertThat(otelRule.getSpans().size(), equalTo(3));
+        assertThat(otelRule.getSpans().get(0).getName(), equalTo("Get Books"));
+        assertThat(otelRule.getSpans().get(0).getParentSpanContext().isValid(), equalTo(true));
+        assertThat(otelRule.getSpans().get(1).getName(), equalTo("POST /BookStore"));
+        assertThat(otelRule.getSpans().get(1).getKind(), equalTo(SpanKind.SERVER));
+        assertThat(otelRule.getSpans().get(2).getName(),
+                   equalTo("POST http://localhost:" + PORT + "/BookStore"));
+        assertThat(otelRule.getSpans().get(2).getKind(), equalTo(SpanKind.CLIENT));
+    }
+
+    @Test
+    public void testThatProvidedSpanIsNotClosedWhenActive() throws Exception {
+        final BookStoreService service = createJaxWsService(new OpenTelemetryClientFeature(otelRule
+            .getOpenTelemetry(), "jaxws-client-test"));
+
+        try (Scope parentScope = Context.root().makeCurrent()) {
+            final Span span = otelRule.getOpenTelemetry().getTracer("test").spanBuilder("test span")
+                .startSpan();
+            try (Scope scope = span.makeCurrent()) {
+                assertThat(service.getBooks().size(), equalTo(2));
+                assertThat(Span.current(), not(nullValue()));
+
+                assertThat(otelRule.getSpans().size(), equalTo(3));
+                assertThat(otelRule.getSpans().get(0).getName(), equalTo("Get Books"));
+                assertThat(otelRule.getSpans().get(0).getParentSpanContext().isValid(), equalTo(true));
+                assertThat(otelRule.getSpans().get(0).getInstrumentationScopeInfo().getName(),
+                           equalTo(BookStore.class.getName()));
+                assertThat(otelRule.getSpans().get(1).getName(), equalTo("POST /BookStore"));
+                assertThat(otelRule.getSpans().get(1).getParentSpanContext().isValid(), equalTo(true));
+                assertThat(otelRule.getSpans().get(1).getInstrumentationScopeInfo().getName(),
+                           equalTo("jaxws-server-test"));
+                assertThat(otelRule.getSpans().get(2).getName(),
+                           equalTo("POST http://localhost:" + PORT + "/BookStore"));
+                assertThat(otelRule.getSpans().get(2).getParentSpanContext().isValid(), equalTo(true));
+                assertThat(otelRule.getSpans().get(2).getInstrumentationScopeInfo().getName(),
+                           equalTo("jaxws-client-test"));
+            } finally {
+                span.end();
+            }
+
+            // Await till flush happens, usually every second
+            await().atMost(Duration.ofSeconds(1L)).until(() -> otelRule.getSpans().size() == 4);
+
+            assertThat(otelRule.getSpans().size(), equalTo(4));
+            assertThat(otelRule.getSpans().get(3).getName(), equalTo("test span"));
+            assertThat(otelRule.getSpans().get(3).getParentSpanContext().isValid(), equalTo(false));
+        }
+    }
+
+    @Test
+    public void testThatNewSpanIsCreatedInCaseOfFault() throws Exception {
+        final BookStoreService service = createJaxWsService();
+
+        try {
+            service.removeBooks();
+            fail("Expected SOAPFaultException to be raised");
+        } catch (final SOAPFaultException ex) {
+            /* expected exception */
+        }
+
+        assertThat(otelRule.getSpans().size(), equalTo(1));
+        assertThat(otelRule.getSpans().get(0).getName(), equalTo("POST /BookStore"));
+    }
+
+    @Test
+    public void testThatNewChildSpanIsCreatedWhenParentIsProvidedInCaseOfFault() throws Exception {
+        final BookStoreService service = createJaxWsService(new OpenTelemetryClientFeature(otelRule
+            .getOpenTelemetry(), "jaxws-client-test"));
+
+        try {
+            service.removeBooks();
+            fail("Expected SOAPFaultException to be raised");
+        } catch (final SOAPFaultException ex) {
+            /* expected exception */
+        }
+
+        assertThat(otelRule.getSpans().size(), equalTo(2));
+        assertThat(otelRule.getSpans().get(0).getName(), equalTo("POST /BookStore"));
+        assertThat(otelRule.getSpans().get(0).getAttributes(),
+                   hasAttribute(SemanticAttributes.HTTP_STATUS_CODE, 500L));
+        assertThat(otelRule.getSpans().get(1).getName(),
+                   equalTo("POST http://localhost:" + PORT + "/BookStore"));
+    }
+
+    @Test
+    public void testThatNewChildSpanIsCreatedWhenParentIsProvidedAndCustomStatusCodeReturned()
+        throws Exception {
+        final BookStoreService service = createJaxWsService(new OpenTelemetryClientFeature(otelRule
+            .getOpenTelemetry(), "jaxws-client-test"));
+        service.addBooks();
+
+        assertThat(otelRule.getSpans().size(), equalTo(1));
+        assertThat(otelRule.getSpans().get(0).getName(), equalTo("POST /BookStore"));
+        assertThat(otelRule.getSpans().get(0).getAttributes(),
+                   hasAttribute(SemanticAttributes.HTTP_STATUS_CODE, 202L));
+    }
+
+    private BookStoreService createJaxWsService(final Feature feature) {
+        return createJaxWsService(Collections.emptyMap(), feature);
+    }
+
+    public static class BraveServer extends AbstractTestServerBase {
+
+        private org.apache.cxf.endpoint.Server server;
+
+        @Override
+        protected void run() {
+            final JaxWsServerFactoryBean sf = new JaxWsServerFactoryBean();
+            sf.setServiceClass(BookStore.class);
+            sf.setAddress("http://localhost:" + PORT);
+            sf.getFeatures().add(new OpenTelemetryFeature(otelRule.getOpenTelemetry(), "jaxws-server-test"));
+            server = sf.create();
+        }
+
+        @Override
+        public void tearDown() throws Exception {
+            server.destroy();
+        }
+    }
+}