This is an automated email from the ASF dual-hosted git repository.
szetszwo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ratis.git
The following commit(s) were added to refs/heads/master by this push:
new 1b7a8ff61 RATIS-2553. Isolate OpenTelemetry tracing dependencies.
(#1478)
1b7a8ff61 is described below
commit 1b7a8ff614b06a94cf678763ee883c8edb3199c0
Author: Haonan <[email protected]>
AuthorDate: Sat Jun 13 07:33:51 2026 +0800
RATIS-2553. Isolate OpenTelemetry tracing dependencies. (#1478)
---
pom.xml | 11 --
ratis-common/pom.xml | 9 +-
.../org/apache/ratis/trace/NoOpTraceProvider.java | 51 ++++++
.../java/org/apache/ratis/trace/TraceClient.java | 28 +---
.../java/org/apache/ratis/trace/TraceProvider.java | 40 +++++
.../java/org/apache/ratis/trace/TraceServer.java | 50 +-----
.../java/org/apache/ratis/trace/TraceUtils.java | 160 ++----------------
.../opentelemetry/OpenTelemetryTraceProvider.java | 183 +++++++++++++++++++++
.../opentelemetry/OpenTelemetryTraceUtils.java | 66 ++++++++
ratis-server/pom.xml | 5 +
.../server/impl/RaftServerImplTracingTests.java | 6 +-
ratis-test/pom.xml | 5 +
12 files changed, 377 insertions(+), 237 deletions(-)
diff --git a/pom.xml b/pom.xml
index 44c9b449b..dd320dc72 100644
--- a/pom.xml
+++ b/pom.xml
@@ -183,7 +183,6 @@
<!-- OpenTelemetry versions -->
<opentelemetry.version>1.62.0</opentelemetry.version>
- <opentelemetry-semconv.version>1.41.1</opentelemetry-semconv.version>
<!-- Test properties -->
<maven.test.redirectTestOutputToFile>true</maven.test.redirectTestOutputToFile>
<test.exclude.pattern>_</test.exclude.pattern>
@@ -394,21 +393,11 @@
<artifactId>opentelemetry-api</artifactId>
<version>${opentelemetry.version}</version>
</dependency>
- <dependency>
- <groupId>io.opentelemetry</groupId>
- <artifactId>opentelemetry-sdk</artifactId>
- <version>${opentelemetry.version}</version>
- </dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk-testing</artifactId>
<version>${opentelemetry.version}</version>
</dependency>
- <dependency>
- <groupId>io.opentelemetry.semconv</groupId>
- <artifactId>opentelemetry-semconv</artifactId>
- <version>${opentelemetry-semconv.version}</version>
- </dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-context</artifactId>
diff --git a/ratis-common/pom.xml b/ratis-common/pom.xml
index ba19c73e3..1d7a69fa5 100644
--- a/ratis-common/pom.xml
+++ b/ratis-common/pom.xml
@@ -46,17 +46,10 @@
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-context</artifactId>
</dependency>
- <dependency>
- <groupId>io.opentelemetry</groupId>
- <artifactId>opentelemetry-sdk</artifactId>
- </dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk-testing</artifactId>
- </dependency>
- <dependency>
- <groupId>io.opentelemetry.semconv</groupId>
- <artifactId>opentelemetry-semconv</artifactId>
+ <scope>test</scope>
</dependency>
<dependency>
diff --git
a/ratis-common/src/main/java/org/apache/ratis/trace/NoOpTraceProvider.java
b/ratis-common/src/main/java/org/apache/ratis/trace/NoOpTraceProvider.java
new file mode 100644
index 000000000..66451d5c0
--- /dev/null
+++ b/ratis-common/src/main/java/org/apache/ratis/trace/NoOpTraceProvider.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.ratis.trace;
+
+import org.apache.ratis.proto.RaftProtos.AppendEntriesRequestProto;
+import org.apache.ratis.protocol.RaftClientRequest;
+import org.apache.ratis.protocol.RaftPeerId;
+import org.apache.ratis.util.function.CheckedSupplier;
+
+import java.io.IOException;
+import java.util.concurrent.CompletableFuture;
+
+enum NoOpTraceProvider implements TraceProvider {
+ INSTANCE;
+
+ @Override
+ public <T, THROWABLE extends Throwable> CompletableFuture<T> traceClientSend(
+ CheckedSupplier<CompletableFuture<T>, THROWABLE> action,
+ RaftClientRequest.Type type, RaftPeerId server) throws THROWABLE {
+ return action.get();
+ }
+
+ @Override
+ public <T, THROWABLE extends Throwable> CompletableFuture<T>
traceServerRequest(
+ CheckedSupplier<CompletableFuture<T>, THROWABLE> action,
+ RaftClientRequest request, String memberId, String spanName) throws
THROWABLE {
+ return action.get();
+ }
+
+ @Override
+ public <T> CompletableFuture<T> traceAppendEntries(
+ CheckedSupplier<CompletableFuture<T>, IOException> action,
+ AppendEntriesRequestProto request, String memberId) throws IOException {
+ return action.get();
+ }
+}
diff --git a/ratis-common/src/main/java/org/apache/ratis/trace/TraceClient.java
b/ratis-common/src/main/java/org/apache/ratis/trace/TraceClient.java
index e7145eb94..a4309f6a5 100644
--- a/ratis-common/src/main/java/org/apache/ratis/trace/TraceClient.java
+++ b/ratis-common/src/main/java/org/apache/ratis/trace/TraceClient.java
@@ -17,19 +17,14 @@
*/
package org.apache.ratis.trace;
-import io.opentelemetry.api.trace.Span;
-import io.opentelemetry.api.trace.SpanKind;
import org.apache.ratis.protocol.RaftClientRequest;
import org.apache.ratis.protocol.RaftPeerId;
-import org.apache.ratis.util.Preconditions;
import org.apache.ratis.util.function.CheckedSupplier;
import java.util.concurrent.CompletableFuture;
-/** Client-side OpenTelemetry helpers. */
+/** Client-side tracing helpers. */
public final class TraceClient {
- private static final String LEADER = "LEADER";
-
private TraceClient() {
}
@@ -39,25 +34,6 @@ public final class TraceClient {
public static <T, THROWABLE extends Throwable> CompletableFuture<T>
asyncSend(
CheckedSupplier<CompletableFuture<T>, THROWABLE> action,
RaftClientRequest.Type type, RaftPeerId server) throws THROWABLE {
- if (!TraceUtils.isEnabled()) {
- return action.get();
- }
- return TraceUtils.traceAsyncMethod(action,
- () -> createClientOperationSpan(type, server, SpanNames.ASYNC_SEND));
- }
-
- private static Span createClientOperationSpan(RaftClientRequest.Type type,
RaftPeerId server,
- String spanName) {
- Preconditions.assertNotNull(spanName, () -> "Span name cannot be null");
- Preconditions.assertTrue(!spanName.isEmpty(), "Span name should not be
empty");
- String peerId = server == null ? LEADER : String.valueOf(server);
- final Span span = TraceUtils.getGlobalTracer()
- .spanBuilder(spanName)
- .setSpanKind(SpanKind.CLIENT)
- .startSpan();
- span.setAttribute(RatisAttributes.PEER_ID, peerId);
- span.setAttribute(RatisAttributes.OPERATION_NAME, spanName);
- span.setAttribute(RatisAttributes.OPERATION_TYPE, String.valueOf(type));
- return span;
+ return TraceUtils.getProvider().traceClientSend(action, type, server);
}
}
diff --git
a/ratis-common/src/main/java/org/apache/ratis/trace/TraceProvider.java
b/ratis-common/src/main/java/org/apache/ratis/trace/TraceProvider.java
new file mode 100644
index 000000000..36b5b11d7
--- /dev/null
+++ b/ratis-common/src/main/java/org/apache/ratis/trace/TraceProvider.java
@@ -0,0 +1,40 @@
+/*
+ * 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.ratis.trace;
+
+import org.apache.ratis.proto.RaftProtos.AppendEntriesRequestProto;
+import org.apache.ratis.protocol.RaftClientRequest;
+import org.apache.ratis.protocol.RaftPeerId;
+import org.apache.ratis.util.function.CheckedSupplier;
+
+import java.io.IOException;
+import java.util.concurrent.CompletableFuture;
+
+public interface TraceProvider {
+ <T, THROWABLE extends Throwable> CompletableFuture<T> traceClientSend(
+ CheckedSupplier<CompletableFuture<T>, THROWABLE> action,
+ RaftClientRequest.Type type, RaftPeerId server) throws THROWABLE;
+
+ <T, THROWABLE extends Throwable> CompletableFuture<T> traceServerRequest(
+ CheckedSupplier<CompletableFuture<T>, THROWABLE> action,
+ RaftClientRequest request, String memberId, String spanName) throws
THROWABLE;
+
+ <T> CompletableFuture<T> traceAppendEntries(
+ CheckedSupplier<CompletableFuture<T>, IOException> action,
+ AppendEntriesRequestProto request, String memberId) throws IOException;
+}
diff --git a/ratis-common/src/main/java/org/apache/ratis/trace/TraceServer.java
b/ratis-common/src/main/java/org/apache/ratis/trace/TraceServer.java
index 35f8fef1a..9d127dbe9 100644
--- a/ratis-common/src/main/java/org/apache/ratis/trace/TraceServer.java
+++ b/ratis-common/src/main/java/org/apache/ratis/trace/TraceServer.java
@@ -17,20 +17,14 @@
*/
package org.apache.ratis.trace;
-import io.opentelemetry.api.trace.Span;
-import io.opentelemetry.api.trace.SpanKind;
-import io.opentelemetry.context.Context;
import org.apache.ratis.proto.RaftProtos.AppendEntriesRequestProto;
-import org.apache.ratis.proto.RaftProtos.RaftRpcRequestProto;
-import org.apache.ratis.proto.RaftProtos.SpanContextProto;
import org.apache.ratis.protocol.RaftClientRequest;
-import org.apache.ratis.protocol.RaftPeerId;
import org.apache.ratis.util.function.CheckedSupplier;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
-/** Server-side OpenTelemetry helpers. */
+/** Server-side tracing helpers. */
public final class TraceServer {
private TraceServer() {
}
@@ -41,25 +35,7 @@ public final class TraceServer {
public static <T, THROWABLE extends Throwable> CompletableFuture<T>
traceAsyncMethod(
CheckedSupplier<CompletableFuture<T>, THROWABLE> action,
RaftClientRequest request, String memberId, String spanName) throws
THROWABLE {
- if (!TraceUtils.isEnabled()) {
- return action.get();
- }
- return TraceUtils.traceAsyncMethod(action,
- () -> createServerSpanFromClientRequest(request, memberId, spanName));
- }
-
- private static Span createServerSpanFromClientRequest(RaftClientRequest
request, String memberId,
- String spanName) {
- final Context remoteContext =
TraceUtils.extractContextFromProto(request.getSpanContext());
- final Span span = TraceUtils.getGlobalTracer()
- .spanBuilder(spanName)
- .setParent(remoteContext)
- .setSpanKind(SpanKind.SERVER)
- .startSpan();
- span.setAttribute(RatisAttributes.CLIENT_ID,
String.valueOf(request.getClientId()));
- span.setAttribute(RatisAttributes.CALL_ID,
String.valueOf(request.getCallId()));
- span.setAttribute(RatisAttributes.MEMBER_ID, memberId);
- return span;
+ return TraceUtils.getProvider().traceServerRequest(action, request,
memberId, spanName);
}
/**
@@ -69,26 +45,6 @@ public final class TraceServer {
public static <T> CompletableFuture<T> traceAppendEntriesAsync(
CheckedSupplier<CompletableFuture<T>, IOException> action,
AppendEntriesRequestProto request, String memberId) throws IOException {
- if (!TraceUtils.isEnabled()) {
- return action.get();
- }
- final RaftRpcRequestProto rpc = request.getServerRequest();
- final SpanContextProto spanContext = rpc.getSpanContext();
- // If the leader sent no parent span context, still trace as a root span
- // rather than skipping tracing entirely.
- final Context remoteContext = (spanContext == null ||
spanContext.getContextMap().isEmpty())
- ? Context.root()
- : TraceUtils.extractContextFromProto(spanContext);
- return TraceUtils.traceAsyncMethod(action, () -> {
- final Span span = TraceUtils.getGlobalTracer()
- .spanBuilder(SpanNames.APPEND_ENTRIES_ASYNC)
- .setParent(remoteContext)
- .setSpanKind(SpanKind.INTERNAL)
- .startSpan();
- span.setAttribute(RatisAttributes.MEMBER_ID, memberId);
- span.setAttribute(RatisAttributes.PEER_ID,
String.valueOf(RaftPeerId.valueOf(rpc.getRequestorId())));
- span.setAttribute(RatisAttributes.APPEND_ENTRIES_COUNT, (long)
request.getEntriesCount());
- return span;
- });
+ return TraceUtils.getProvider().traceAppendEntries(action, request,
memberId);
}
}
diff --git a/ratis-common/src/main/java/org/apache/ratis/trace/TraceUtils.java
b/ratis-common/src/main/java/org/apache/ratis/trace/TraceUtils.java
index 3dc3e228f..a164184ea 100644
--- a/ratis-common/src/main/java/org/apache/ratis/trace/TraceUtils.java
+++ b/ratis-common/src/main/java/org/apache/ratis/trace/TraceUtils.java
@@ -17,46 +17,23 @@
*/
package org.apache.ratis.trace;
-import io.opentelemetry.api.GlobalOpenTelemetry;
-import io.opentelemetry.api.trace.Span;
-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.context.propagation.TextMapPropagator;
-import io.opentelemetry.context.propagation.TextMapGetter;
import org.apache.ratis.conf.RaftProperties;
-import org.apache.ratis.proto.RaftProtos.SpanContextProto;
-import org.apache.ratis.util.JavaUtils;
-import org.apache.ratis.util.function.CheckedSupplier;
-import org.apache.ratis.util.VersionInfo;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.apache.ratis.trace.opentelemetry.OpenTelemetryTraceProvider;
-import java.util.Map;
-import java.util.Optional;
-import java.util.TreeMap;
-import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.BiConsumer;
-import java.util.function.Supplier;
-/** Common OpenTelemetry utilities shared by {@link TraceClient} and {@link
TraceServer}. */
+/** Common tracing utilities shared by {@link TraceClient} and {@link
TraceServer}. */
public final class TraceUtils {
- private static final AtomicReference<Tracer> TRACER = new
AtomicReference<>();
- private static final Logger LOG = LoggerFactory.getLogger(TraceUtils.class);
+ private static final AtomicReference<TraceProvider> PROVIDER =
+ new AtomicReference<>(NoOpTraceProvider.INSTANCE);
private TraceUtils() {
}
- public static Tracer getGlobalTracer() {
- return TRACER.get();
- }
-
/**
- * Initializes the global tracer from configuration when tracing is enabled,
or clears it when
- * disabled. Call from {@link org.apache.ratis.server.RaftServer} and
+ * Initializes tracing from configuration when tracing is enabled, or clears
it when disabled.
+ * Call from {@link org.apache.ratis.server.RaftServer} and
* {@link org.apache.ratis.client.RaftClient} construction so tracing follows
* {@link TraceConfigKeys}.
*
@@ -67,130 +44,29 @@ public final class TraceUtils {
}
/**
- * Enables or disables the tracer without reading {@link RaftProperties}.
Intended for tests and
+ * Enables or disables tracing without reading {@link RaftProperties}.
Intended for tests and
* simple toggles; production code should prefer {@link
#setTracerWhenEnabled(RaftProperties)}.
*
- * @param enabled when true, lazily obtains the OpenTelemetry tracer; when
false, clears it
+ * @param enabled when true, enables the OpenTelemetry provider; when false,
clears it
*/
public static void setTracerWhenEnabled(boolean enabled) {
- if (enabled) {
- TRACER.updateAndGet(previous -> previous != null ? previous
- : GlobalOpenTelemetry.getTracer("org.apache.ratis",
VersionInfo.getSoftwareInfoVersion()));
- } else {
- TRACER.set(null);
- }
+ PROVIDER.set(enabled ? newOpenTelemetryTraceProvider() :
NoOpTraceProvider.INSTANCE);
}
public static boolean isEnabled() {
- return TRACER.get() != null;
+ return !(getProvider() instanceof NoOpTraceProvider);
}
- /**
- * Traces an asynchronous operation represented by a {@link
CompletableFuture}. The returned future
- * completes with the same outcome as the supplied future; the span is ended
when that future
- * completes.
- */
- static <T, THROWABLE extends Throwable> CompletableFuture<T>
traceAsyncMethod(
- CheckedSupplier<CompletableFuture<T>, THROWABLE> action, Supplier<Span>
spanSupplier) throws THROWABLE {
- final Span span = spanSupplier.get();
- try (Scope ignored = span.makeCurrent()) {
- final CompletableFuture<T> future;
- try {
- future = action.get();
- } catch (RuntimeException | Error e) {
- setError(span, e);
- span.end();
- throw e;
- } catch (Throwable t) {
- setError(span, t);
- span.end();
- throw JavaUtils.<THROWABLE>cast(t);
- }
- endSpan(future, span);
- return future;
- }
+ static TraceProvider getProvider() {
+ return PROVIDER.get();
}
- private static void endSpan(CompletableFuture<?> future, Span span) {
- if (span == null) {
- LOG.debug("Span is null, cannot trace the future {}", future);
- return;
+ private static TraceProvider newOpenTelemetryTraceProvider() {
+ try {
+ return new OpenTelemetryTraceProvider();
+ } catch (Throwable e) {
+ throw new IllegalStateException(
+ "OpenTelemetry tracing is enabled but OpenTelemetry is not
available; tracing is disabled", e);
}
- addListener(future, (resp, error) -> {
- try {
- if (error != null) {
- setError(span, error);
- } else {
- span.setStatus(StatusCode.OK);
- }
- } catch (Throwable t) {
- LOG.error("Error setting span status, ending span anyway", t);
- } finally {
- span.end();
- }
- });
}
-
- public static void setError(Span span, Throwable error) {
- span.recordException(error);
- span.setStatus(StatusCode.ERROR);
- }
-
- /**
- * This is method is used when you just want to add a listener to the given
future. We will call
- * {@link CompletableFuture#whenComplete(BiConsumer)} to register the {@code
action} to the
- * {@code future}. Ignoring the return value of a Future is considered as a
bad practice as it may
- * suppress exceptions thrown from the code that completes the future, and
this method will catch
- * all the exception thrown from the {@code action} to catch possible code
bugs.
- * <p/>
- * And the error phone check will always report FutureReturnValueIgnored
because every method in
- * the {@link CompletableFuture} class will return a new {@link
CompletableFuture}, so you always
- * have one future that has not been checked. So we introduce this method
and add a suppression
- * warnings annotation here.
- */
- @SuppressWarnings("FutureReturnValueIgnored")
- private static <T> void addListener(CompletableFuture<T> future,
- BiConsumer<? super T, ? super Throwable> action) {
- future.whenComplete((resp, error) -> {
- try {
- // https://s.apache.org/completionexception — unwrap
CompletionException for callers
- action.accept(resp, error == null ? null :
JavaUtils.unwrapCompletionException(error));
- } catch (Throwable t) {
- LOG.error("Unexpected error caught when processing CompletableFuture",
t);
- }
- });
- }
-
- private static final TextMapPropagator PROPAGATOR =
- GlobalOpenTelemetry.getPropagators().getTextMapPropagator();
-
- public static SpanContextProto injectContextToProto(Context context) {
- Map<String, String> carrier = new TreeMap<>();
- PROPAGATOR.inject(context, carrier, (map, key, value) -> map.put(key,
value));
- return SpanContextProto.newBuilder().putAllContext(carrier).build();
- }
-
- public static Context extractContextFromProto(SpanContextProto proto) {
- if (proto == null || proto.getContextMap().isEmpty()) {
- return Context.current();
- }
- final TextMapGetter<SpanContextProto> getter = SpanContextGetter.INSTANCE;
- return PROPAGATOR.extract(Context.current(), proto, getter);
- }
-}
-
-class SpanContextGetter implements TextMapGetter<SpanContextProto> {
- static final SpanContextGetter INSTANCE = new SpanContextGetter();
-
- @Override
- public Iterable<String> keys(SpanContextProto carrier) {
- return carrier.getContextMap().keySet();
- }
-
- @Override
- public String get(SpanContextProto carrier, String key) {
- return Optional.ofNullable(carrier).map(SpanContextProto::getContextMap)
- .map(map -> map.get(key)).orElse(null);
- }
-
}
diff --git
a/ratis-common/src/main/java/org/apache/ratis/trace/opentelemetry/OpenTelemetryTraceProvider.java
b/ratis-common/src/main/java/org/apache/ratis/trace/opentelemetry/OpenTelemetryTraceProvider.java
new file mode 100644
index 000000000..a1059d9bf
--- /dev/null
+++
b/ratis-common/src/main/java/org/apache/ratis/trace/opentelemetry/OpenTelemetryTraceProvider.java
@@ -0,0 +1,183 @@
+/*
+ * 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.ratis.trace.opentelemetry;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+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 org.apache.ratis.proto.RaftProtos.AppendEntriesRequestProto;
+import org.apache.ratis.proto.RaftProtos.RaftRpcRequestProto;
+import org.apache.ratis.proto.RaftProtos.SpanContextProto;
+import org.apache.ratis.protocol.RaftClientRequest;
+import org.apache.ratis.protocol.RaftPeerId;
+import org.apache.ratis.trace.RatisAttributes;
+import org.apache.ratis.trace.SpanNames;
+import org.apache.ratis.trace.TraceProvider;
+import org.apache.ratis.util.JavaUtils;
+import org.apache.ratis.util.Preconditions;
+import org.apache.ratis.util.VersionInfo;
+import org.apache.ratis.util.function.CheckedSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.BiConsumer;
+import java.util.function.Supplier;
+
+public final class OpenTelemetryTraceProvider implements TraceProvider {
+ private static final Logger LOG =
LoggerFactory.getLogger(OpenTelemetryTraceProvider.class);
+ private static final String LEADER = "LEADER";
+
+ private final Tracer tracer = Objects.requireNonNull(
+ GlobalOpenTelemetry.getTracer("org.apache.ratis",
VersionInfo.getSoftwareInfoVersion()),
+ "tracer == null");
+
+ @Override
+ public <T, THROWABLE extends Throwable> CompletableFuture<T> traceClientSend(
+ CheckedSupplier<CompletableFuture<T>, THROWABLE> action,
+ RaftClientRequest.Type type, RaftPeerId server) throws THROWABLE {
+ return traceAsyncMethod(action, () -> createClientOperationSpan(type,
server, SpanNames.ASYNC_SEND));
+ }
+
+ @Override
+ public <T, THROWABLE extends Throwable> CompletableFuture<T>
traceServerRequest(
+ CheckedSupplier<CompletableFuture<T>, THROWABLE> action,
+ RaftClientRequest request, String memberId, String spanName) throws
THROWABLE {
+ return traceAsyncMethod(action, () ->
createServerSpanFromClientRequest(request, memberId, spanName));
+ }
+
+ @Override
+ public <T> CompletableFuture<T> traceAppendEntries(
+ CheckedSupplier<CompletableFuture<T>, IOException> action,
+ AppendEntriesRequestProto request, String memberId) throws IOException {
+ final RaftRpcRequestProto rpc = request.getServerRequest();
+ final SpanContextProto spanContext = rpc.getSpanContext();
+ final Context remoteContext = (spanContext == null ||
spanContext.getContextMap().isEmpty())
+ ? Context.root()
+ : OpenTelemetryTraceUtils.extractContextFromProto(spanContext);
+ return traceAsyncMethod(action, () -> {
+ final Span span = tracer.spanBuilder(SpanNames.APPEND_ENTRIES_ASYNC)
+ .setParent(remoteContext)
+ .setSpanKind(SpanKind.INTERNAL)
+ .startSpan();
+ span.setAttribute(RatisAttributes.MEMBER_ID, memberId);
+ span.setAttribute(RatisAttributes.PEER_ID,
String.valueOf(RaftPeerId.valueOf(rpc.getRequestorId())));
+ span.setAttribute(RatisAttributes.APPEND_ENTRIES_COUNT, (long)
request.getEntriesCount());
+ return span;
+ });
+ }
+
+ private Span createClientOperationSpan(RaftClientRequest.Type type,
RaftPeerId server, String spanName) {
+ Preconditions.assertNotNull(spanName, () -> "Span name cannot be null");
+ Preconditions.assertTrue(!spanName.isEmpty(), "Span name should not be
empty");
+ final String peerId = server == null ? LEADER : String.valueOf(server);
+ final Span span = tracer.spanBuilder(spanName)
+ .setSpanKind(SpanKind.CLIENT)
+ .startSpan();
+ span.setAttribute(RatisAttributes.PEER_ID, peerId);
+ span.setAttribute(RatisAttributes.OPERATION_NAME, spanName);
+ span.setAttribute(RatisAttributes.OPERATION_TYPE, String.valueOf(type));
+ return span;
+ }
+
+ private Span createServerSpanFromClientRequest(RaftClientRequest request,
String memberId, String spanName) {
+ final Context remoteContext =
OpenTelemetryTraceUtils.extractContextFromProto(request.getSpanContext());
+ final Span span = tracer.spanBuilder(spanName)
+ .setParent(remoteContext)
+ .setSpanKind(SpanKind.SERVER)
+ .startSpan();
+ span.setAttribute(RatisAttributes.CLIENT_ID,
String.valueOf(request.getClientId()));
+ span.setAttribute(RatisAttributes.CALL_ID,
String.valueOf(request.getCallId()));
+ span.setAttribute(RatisAttributes.MEMBER_ID, memberId);
+ return span;
+ }
+
+ @SuppressWarnings("try")
+ private static <T, THROWABLE extends Throwable> CompletableFuture<T>
traceAsyncMethod(
+ CheckedSupplier<CompletableFuture<T>, THROWABLE> action, Supplier<Span>
spanSupplier) throws THROWABLE {
+ final Span span = spanSupplier.get();
+ try (Scope ignored = span.makeCurrent()) {
+ final CompletableFuture<T> future;
+ try {
+ future = action.get();
+ } catch (RuntimeException | Error e) {
+ setError(span, e);
+ span.end();
+ throw e;
+ } catch (Throwable t) {
+ setError(span, t);
+ span.end();
+ throw JavaUtils.<THROWABLE>cast(t);
+ }
+ endSpan(future, span);
+ return future;
+ }
+ }
+
+ private static void endSpan(CompletableFuture<?> future, Span span) {
+ addListener(future, (resp, error) -> {
+ try {
+ if (error != null) {
+ setError(span, error);
+ } else {
+ span.setStatus(StatusCode.OK);
+ }
+ } catch (Throwable t) {
+ LOG.error("Error setting span status, ending span anyway", t);
+ } finally {
+ span.end();
+ }
+ });
+ }
+
+ private static void setError(Span span, Throwable error) {
+ span.recordException(error);
+ span.setStatus(StatusCode.ERROR);
+ }
+
+ /**
+ * This is method is used when you just want to add a listener to the given
future. We will call
+ * {@link CompletableFuture#whenComplete(BiConsumer)} to register the {@code
action} to the
+ * {@code future}. Ignoring the return value of a Future is considered as a
bad practice as it may
+ * suppress exceptions thrown from the code that completes the future, and
this method will catch
+ * all the exception thrown from the {@code action} to catch possible code
bugs.
+ * <p/>
+ * And the error phone check will always report FutureReturnValueIgnored
because every method in
+ * the {@link CompletableFuture} class will return a new {@link
CompletableFuture}, so you always
+ * have one future that has not been checked. So we introduce this method
and add a suppression
+ * warnings annotation here.
+ */
+ @SuppressWarnings("FutureReturnValueIgnored")
+ private static <T> void addListener(CompletableFuture<T> future,
+ BiConsumer<? super T, ? super Throwable> action) {
+ future.whenComplete((resp, error) -> {
+ try {
+ action.accept(resp, error == null ? null :
JavaUtils.unwrapCompletionException(error));
+ } catch (Throwable t) {
+ LOG.error("Unexpected error caught when processing CompletableFuture",
t);
+ }
+ });
+ }
+
+}
diff --git
a/ratis-common/src/main/java/org/apache/ratis/trace/opentelemetry/OpenTelemetryTraceUtils.java
b/ratis-common/src/main/java/org/apache/ratis/trace/opentelemetry/OpenTelemetryTraceUtils.java
new file mode 100644
index 000000000..b5f0c8c24
--- /dev/null
+++
b/ratis-common/src/main/java/org/apache/ratis/trace/opentelemetry/OpenTelemetryTraceUtils.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.ratis.trace.opentelemetry;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.propagation.TextMapGetter;
+import io.opentelemetry.context.propagation.TextMapPropagator;
+import org.apache.ratis.proto.RaftProtos.SpanContextProto;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.TreeMap;
+
+/** OpenTelemetry-specific helpers. Callers using this class must provide
OpenTelemetry jars. */
+public final class OpenTelemetryTraceUtils {
+ private OpenTelemetryTraceUtils() {
+ }
+
+ public static SpanContextProto injectContextToProto(Context context) {
+ final Map<String, String> carrier = new TreeMap<>();
+ getTextMapPropagator().inject(context, carrier, (map, key, value) ->
map.put(key, value));
+ return SpanContextProto.newBuilder().putAllContext(carrier).build();
+ }
+
+ static Context extractContextFromProto(SpanContextProto proto) {
+ if (proto == null || proto.getContextMap().isEmpty()) {
+ return Context.current();
+ }
+ return getTextMapPropagator().extract(Context.current(), proto,
SpanContextGetter.INSTANCE);
+ }
+
+ private static TextMapPropagator getTextMapPropagator() {
+ return GlobalOpenTelemetry.getPropagators().getTextMapPropagator();
+ }
+
+ private static final class SpanContextGetter implements
TextMapGetter<SpanContextProto> {
+ private static final SpanContextGetter INSTANCE = new SpanContextGetter();
+
+ @Override
+ public Iterable<String> keys(SpanContextProto carrier) {
+ return carrier.getContextMap().keySet();
+ }
+
+ @Override
+ public String get(SpanContextProto carrier, String key) {
+ return Optional.ofNullable(carrier).map(SpanContextProto::getContextMap)
+ .map(map -> map.get(key)).orElse(null);
+ }
+ }
+}
diff --git a/ratis-server/pom.xml b/ratis-server/pom.xml
index 2c0bc93a2..74cd7c546 100644
--- a/ratis-server/pom.xml
+++ b/ratis-server/pom.xml
@@ -84,6 +84,11 @@
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>io.opentelemetry</groupId>
+ <artifactId>opentelemetry-sdk-testing</artifactId>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.apache.ratis</groupId>
<artifactId>ratis-metrics-api</artifactId>
diff --git
a/ratis-server/src/test/java/org/apache/ratis/server/impl/RaftServerImplTracingTests.java
b/ratis-server/src/test/java/org/apache/ratis/server/impl/RaftServerImplTracingTests.java
index 81e9289f4..db0c466b7 100644
---
a/ratis-server/src/test/java/org/apache/ratis/server/impl/RaftServerImplTracingTests.java
+++
b/ratis-server/src/test/java/org/apache/ratis/server/impl/RaftServerImplTracingTests.java
@@ -43,6 +43,7 @@ import org.apache.ratis.trace.SpanNames;
import org.apache.ratis.trace.TraceConfigKeys;
import org.apache.ratis.trace.TraceServer;
import org.apache.ratis.trace.TraceUtils;
+import org.apache.ratis.trace.opentelemetry.OpenTelemetryTraceUtils;
import org.apache.ratis.util.JavaUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
@@ -182,7 +183,7 @@ public class RaftServerImplTracingTests {
.setSpanKind(SpanKind.CLIENT)
.startSpan();
try {
- return
TraceUtils.injectContextToProto(Context.current().with(remoteParent));
+ return
OpenTelemetryTraceUtils.injectContextToProto(Context.current().with(remoteParent));
} finally {
remoteParent.end();
}
@@ -253,7 +254,7 @@ public class RaftServerImplTracingTests {
.setGroupId(RaftGroupId.randomId())
.setCallId(1L)
.setType(type)
- .setSpanContext(TraceUtils.injectContextToProto(clientContext))
+
.setSpanContext(OpenTelemetryTraceUtils.injectContextToProto(clientContext))
.build();
} finally {
clientSpan.end();
@@ -265,4 +266,3 @@ public class RaftServerImplTracingTests {
}
}
-
diff --git a/ratis-test/pom.xml b/ratis-test/pom.xml
index 577262d84..54f076ac1 100644
--- a/ratis-test/pom.xml
+++ b/ratis-test/pom.xml
@@ -156,5 +156,10 @@
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>io.opentelemetry</groupId>
+ <artifactId>opentelemetry-sdk-testing</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>