Skip to content

Commit 7fd3380

Browse files
authored
OpenTelemetry Tracing: Visualize JGroups communication (#39659)
Closes #39658 Signed-off-by: Alexander Schwartz <[email protected]>
1 parent 98612bb commit 7fd3380

File tree

18 files changed

+503
-7
lines changed

18 files changed

+503
-7
lines changed

docs/documentation/upgrading/topics/changes/changes-26_4_0.adoc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ In order to maintain backwards compatibility, {project_name}'s upgrade will modi
6363

6464
For more information about client configuration, please see link:{adminguide_link}#_client-saml-configuration[Creating a SAML client] chapter in the {adminguide_name}.
6565

66+
=== Tracing extended for embedded Infinispan caches
67+
68+
When tracing is enabled, now also calls to other nodes of a {project_name} cluster will create spans in the traces.
69+
70+
To disable this kind of tracing, set the option `tracing-infinispan-enabled` to `false`.
71+
72+
6673
// ------------------------ Deprecated features ------------------------ //
6774
== Deprecated features
6875

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.keycloak.infinispan.module.factory;
2+
3+
import io.opentelemetry.api.OpenTelemetry;
4+
import jakarta.enterprise.inject.Instance;
5+
import jakarta.enterprise.inject.spi.CDI;
6+
import org.infinispan.factories.AbstractComponentFactory;
7+
import org.infinispan.factories.AutoInstantiableFactory;
8+
import org.infinispan.factories.annotations.DefaultFactoryFor;
9+
import org.infinispan.factories.scopes.Scope;
10+
import org.infinispan.factories.scopes.Scopes;
11+
import org.infinispan.telemetry.InfinispanTelemetry;
12+
import org.infinispan.telemetry.impl.DisabledInfinispanTelemetry;
13+
14+
@Scope(Scopes.GLOBAL)
15+
@DefaultFactoryFor(classes = InfinispanTelemetry.class)
16+
public class InfinispanTelemetryFactory extends AbstractComponentFactory implements AutoInstantiableFactory {
17+
18+
@Override
19+
public Object construct(String componentName) {
20+
CDI<Object> current;
21+
try {
22+
current = CDI.current();
23+
} catch (IllegalStateException e) {
24+
// No CDI context, assume tracing is not available
25+
return new DisabledInfinispanTelemetry();
26+
}
27+
Instance<OpenTelemetry> selector = current.select(OpenTelemetry.class);
28+
if (!selector.isResolvable()) {
29+
return new DisabledInfinispanTelemetry();
30+
} else {
31+
return new OpenTelemetryService(selector.get());
32+
}
33+
}
34+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package org.keycloak.infinispan.module.factory;
2+
3+
import io.opentelemetry.api.OpenTelemetry;
4+
import io.opentelemetry.api.trace.Span;
5+
import io.opentelemetry.api.trace.SpanBuilder;
6+
import io.opentelemetry.api.trace.SpanKind;
7+
import io.opentelemetry.api.trace.Tracer;
8+
import io.opentelemetry.context.Context;
9+
import io.opentelemetry.context.propagation.TextMapGetter;
10+
import org.infinispan.telemetry.InfinispanSpan;
11+
import org.infinispan.telemetry.InfinispanSpanAttributes;
12+
import org.infinispan.telemetry.InfinispanSpanContext;
13+
import org.infinispan.telemetry.InfinispanTelemetry;
14+
15+
public class OpenTelemetryService implements InfinispanTelemetry, TextMapGetter<InfinispanSpanContext> {
16+
17+
private static final String INFINISPAN_SERVER_TRACING_NAME = "org.infinispan.server.tracing";
18+
private static final String INFINISPAN_SERVER_TRACING_VERSION = "1.0.0";
19+
20+
private final Tracer tracer;
21+
private volatile String nodeName = "n/a";
22+
23+
public OpenTelemetryService(OpenTelemetry openTelemetry) {
24+
this.tracer = openTelemetry.getTracer(INFINISPAN_SERVER_TRACING_NAME, INFINISPAN_SERVER_TRACING_VERSION);
25+
}
26+
27+
@Override
28+
public <T> InfinispanSpan<T> startTraceRequest(String operationName, InfinispanSpanAttributes attributes) {
29+
// The original Infininspan implementation allows for filtering via the trace attributes. We don't support this here and instead trace everything.
30+
31+
var builder = tracer.spanBuilder(operationName)
32+
.setSpanKind(SpanKind.SERVER);
33+
// the parent context is inherited automatically,
34+
// because the parent span is created in the same process
35+
36+
return createOpenTelemetrySpan(builder, attributes);
37+
}
38+
39+
@Override
40+
public <T> InfinispanSpan<T> startTraceRequest(String operationName, InfinispanSpanAttributes attributes, InfinispanSpanContext context) {
41+
// The original Infinispan implementation allows for filtering via the trace attributes. We don't support this here and instead trace everything
42+
43+
var builder = tracer.spanBuilder(operationName)
44+
.setSpanKind(SpanKind.SERVER)
45+
.setParent(Context.current().with(Span.current()));
46+
47+
return createOpenTelemetrySpan(builder, attributes);
48+
}
49+
50+
@Override
51+
public void setNodeName(String nodeName) {
52+
if (nodeName != null) {
53+
this.nodeName = nodeName;
54+
}
55+
}
56+
57+
private <T> InfinispanSpan<T> createOpenTelemetrySpan(SpanBuilder builder, InfinispanSpanAttributes attributes) {
58+
attributes.cacheName().ifPresent(cacheName -> builder.setAttribute("cache", cacheName));
59+
builder.setAttribute("category", attributes.category().toString());
60+
builder.setAttribute("server.address", nodeName);
61+
return new OpenTelemetrySpan<>(builder.startSpan());
62+
}
63+
64+
@Override
65+
public Iterable<String> keys(InfinispanSpanContext ctx) {
66+
return ctx.keys();
67+
}
68+
69+
@Override
70+
public String get(InfinispanSpanContext ctx, String key) {
71+
assert ctx != null;
72+
return ctx.getKey(key);
73+
}
74+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.keycloak.infinispan.module.factory;
2+
3+
import io.opentelemetry.api.trace.Span;
4+
import io.opentelemetry.api.trace.StatusCode;
5+
import io.opentelemetry.context.Scope;
6+
import org.infinispan.telemetry.InfinispanSpan;
7+
import org.infinispan.telemetry.SafeAutoClosable;
8+
9+
import java.util.Objects;
10+
11+
public class OpenTelemetrySpan<T> implements InfinispanSpan<T> {
12+
13+
private final Span span;
14+
private final Scope scope;
15+
16+
public OpenTelemetrySpan(Span span) {
17+
this.span = Objects.requireNonNull(span);
18+
// TODO: This is actually wrong if you are doing asynchronous calls, but it allows the JGroups calls to be nested
19+
// This should be fixed in ISPN 16+ so that it is no longer needed
20+
// https://github.com/infinispan/infinispan/issues/15287
21+
this.scope = span.makeCurrent();
22+
}
23+
24+
@Override
25+
public SafeAutoClosable makeCurrent() {
26+
//noinspection resource
27+
Scope scope = span.makeCurrent();
28+
return scope::close;
29+
}
30+
31+
@Override
32+
public void complete() {
33+
scope.close();
34+
span.end();
35+
}
36+
37+
@Override
38+
public void recordException(Throwable throwable) {
39+
span.setStatus(StatusCode.ERROR, "Error during the cache request processing");
40+
span.recordException(throwable);
41+
}
42+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2025 Red Hat, Inc. and/or its affiliates
3+
* and other contributors as indicated by the @author tags.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.keycloak.jgroups.header;
19+
20+
import org.jgroups.Header;
21+
import org.jgroups.util.Util;
22+
23+
import java.io.DataInput;
24+
import java.io.DataOutput;
25+
import java.io.IOException;
26+
import java.util.HashMap;
27+
import java.util.Map;
28+
import java.util.Set;
29+
import java.util.function.Supplier;
30+
31+
/**
32+
* Header which carries an OpenTelemetry {@link io.opentelemetry.api.trace.Span} between requests and responses
33+
*
34+
* @author Bela Ban
35+
* @since 1.0.0
36+
*/
37+
public class TracerHeader extends Header {
38+
public static final short ID = 1050;
39+
protected final Map<String, String> ctx = new HashMap<>();
40+
41+
public TracerHeader() {
42+
}
43+
44+
public short getMagicId() {
45+
return ID;
46+
}
47+
48+
public Supplier<? extends Header> create() {
49+
return TracerHeader::new;
50+
}
51+
52+
public void put(String key, String value) {
53+
ctx.put(key, value);
54+
}
55+
56+
public String get(String key) {
57+
return ctx.get(key);
58+
}
59+
60+
public Set<String> keys() {
61+
return ctx.keySet();
62+
}
63+
64+
public int serializedSize() {
65+
int size = Integer.BYTES;
66+
int num_attrs = ctx.size();
67+
if (num_attrs > 0) {
68+
for (Map.Entry<String, String> entry : ctx.entrySet()) {
69+
String key = entry.getKey();
70+
String val = entry.getValue();
71+
size += Util.size(key) + Util.size(val);
72+
}
73+
}
74+
return size;
75+
}
76+
77+
public void writeTo(DataOutput out) throws IOException {
78+
out.writeInt(ctx.size());
79+
if (!ctx.isEmpty()) {
80+
for (Map.Entry<String, String> e : ctx.entrySet()) {
81+
Util.writeString(e.getKey(), out);
82+
Util.writeString(e.getValue(), out);
83+
}
84+
}
85+
}
86+
87+
public void readFrom(DataInput in) throws IOException {
88+
int size = in.readInt();
89+
if (size > 0) {
90+
for (int i = 0; i < size; i++)
91+
ctx.put(Util.readString(in), Util.readString(in));
92+
}
93+
}
94+
95+
public String toString() {
96+
return ctx.toString();
97+
}
98+
}

0 commit comments

Comments
 (0)