Retrieve dubbo server.address/server.port according to latest SemConv#17244
Retrieve dubbo server.address/server.port according to latest SemConv#17244steverao wants to merge 17 commits into
Conversation
Implementation descriptionProblem StatementHow can we accurately capture the registry address associated with each RPC invocation on the consumer side in Dubbo multi-registry scenarios? Core Principle: Independent Directory and Invoker Chain per Service ReferenceKey InsightImportant: In Dubbo's design, each service reference (ReferenceConfig) creates an independent Directory for each registry. Multiple registries are not stored in the same Directory; instead, each registry has its own Directory. Complete Service Reference Flow in Dubbo1. Initialization Phase: Service Reference Setup// User code
@Reference
private UserService userService;Dubbo's Internal Processing:// 1. ReferenceConfig.refer() is invoked
// Location: org.apache.dubbo.config.ReferenceConfig#refer()
public synchronized T get() {
if (ref == null) {
init();
}
return ref;
}
private void init() {
// ...
ref = createProxy(map); // Create proxy object
}
// 2. Generate registry:// URL based on registry configuration
// Example: registry://127.0.0.1:8848/org.apache.dubbo.registry.RegistryService?refer=...
// 3. Protocol.refer() routes to RegistryProtocol based on "registry://" protocol
// Location: org.apache.dubbo.registry.integration.RegistryProtocol#refer()
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
// url = registry://127.0.0.1:8848/...
// Normalize registry address
url = URLBuilder.from(url)
.setProtocol(url.getParameter(REGISTRY_KEY, DEFAULT_REGISTRY)) // nacos
.removeParameter(REGISTRY_KEY)
.build();
// url becomes nacos://127.0.0.1:8848/...
// Get Registry instance (e.g., NacosRegistry)
Registry registry = registryFactory.getRegistry(url);
// ⭐️ Create RegistryDirectory (This is the key!)
RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
directory.setRegistry(registry); // ⭐️ Bind Directory to Registry
directory.setProtocol(protocol);
// Subscribe to provider information from registry
directory.subscribe(...);
// ⭐️ Call Cluster.join(directory) to create ClusterInvoker
// This triggers our RegistryCapturingClusterWrapper
Invoker<T> invoker = cluster.join(directory);
return invoker;
}2. Current solution interception Point: Cluster.join()// RegistryCapturingClusterWrapper.java
@Override
public <T> Invoker<T> join(Directory<T> directory) {
// 1. First call the original Cluster's join method (e.g., FailoverCluster)
// Returns the original ClusterInvoker (e.g., FailoverClusterInvoker)
Invoker<T> originalInvoker = delegateJoin(cluster, directory, true);
// 2. Wrap if needed
return wrapIfNeeded(directory, originalInvoker);
}
private static <T> Invoker<T> wrapIfNeeded(Directory<T> directory, Invoker<T> invoker) {
// Skip StaticDirectory (direct connection scenario, no registry information)
if (isStaticDirectory(directory)) {
return invoker;
}
// ⭐️ Extract registry address from Directory
// directory is RegistryDirectory, which has a registry field
String registryAddress = DubboRegistryUtil.tryExtractRegistryAddressFromDirectory(directory);
if (registryAddress == null) {
return invoker;
}
// ⭐️ Create RegistryCapturingInvoker wrapper
// Store registryAddress in this Invoker instance
return new RegistryCapturingInvoker<>(invoker, registryAddress);
}3. Key Point: Binding Between Directory and Registry// org.apache.dubbo.registry.integration.RegistryDirectory
public class RegistryDirectory<T> extends AbstractDirectory<T> {
private Registry registry; // ⭐️ Each RegistryDirectory holds one Registry instance
public void setRegistry(Registry registry) {
this.registry = registry;
}
public Registry getRegistry() {
return registry;
}
// ...
}Important: 4. Registry Address Extraction Implementation// DubboRegistryUtil.java - Reflection logic
private static String extractRegistryAddressFromDirectory(Directory<?> directory) {
// 1. Get directory.getRegistry() via reflection
MethodHandle getRegistryHandle = findAccessor(
directory.getClass(),
"getRegistry"
);
Registry registry = (Registry) getRegistryHandle.invoke(directory);
// 2. Get registry.getUrl() via reflection
MethodHandle getUrlHandle = findAccessor(
registry.getClass(),
"getUrl"
);
URL urlObj = (URL) getUrlHandle.invoke(registry);
// 3. Extract protocol and address
// urlObj = nacos://127.0.0.1:8848/org.apache.dubbo.registry.RegistryService?...
String protocol = urlObj.getProtocol(); // "nacos"
String address = urlObj.getAddress(); // "127.0.0.1:8848"
return protocol + "://" + address; // "nacos://127.0.0.1:8848"
}Invocation Phase: Identify the Correct RegistryInvocation Chain AnalysisRegistryCapturingInvoker Implementation (Key Component)// RegistryCapturingInvoker.java
final class RegistryCapturingInvoker<T> implements Invoker<T> {
private final Invoker<T> delegate;
private final String registryAddress; // ⭐️ Registry address stored here
RegistryCapturingInvoker(Invoker<T> delegate, String registryAddress) {
this.delegate = delegate;
this.registryAddress = registryAddress; // Set at construction time from Directory
}
@Override
public Result invoke(Invocation invocation) {
// ⭐️ Set registryAddress to ThreadLocal on each invocation
DubboRegistryUtil.pushCapturedRegistryAddress(registryAddress);
try {
// Continue to original Invoker chain
return delegate.invoke(invocation);
} finally {
// Clean up ThreadLocal after invocation
DubboRegistryUtil.clearCapturedRegistryAddress();
}
}
}ThreadLocal Usage// DubboRegistryUtil.java
public final class DubboRegistryUtil {
// ThreadLocal stores the registry address for the current thread's invocation
private static final ThreadLocal<String> CAPTURED_REGISTRY_ADDRESS = new ThreadLocal<>();
static void pushCapturedRegistryAddress(String address) {
CAPTURED_REGISTRY_ADDRESS.set(address);
}
public static void clearCapturedRegistryAddress() {
CAPTURED_REGISTRY_ADDRESS.remove();
}
@Nullable
public static String extractRegistryAddress(RpcInvocation invocation) {
// Filter calls this method to get registry address
return CAPTURED_REGISTRY_ADDRESS.get();
}
}Multi-Registry Scenario HandlingScenario 1: Single Service Reference with Multiple Registries<!-- dubbo-consumer.xml -->
<dubbo:registry id="nacos1" address="nacos://127.0.0.1:8848"/>
<dubbo:registry id="nacos2" address="nacos://192.168.1.100:8848"/>
<dubbo:reference interface="com.demo.UserService" registry="nacos1,nacos2"/>Dubbo's Internal Processing: // ReferenceConfig.refer() calls RegistryProtocol.refer() for each registry
List<Invoker<?>> invokers = new ArrayList<>();
// First call: nacos1
Invoker invoker1 = registryProtocol.refer(
UserService.class,
URL.valueOf("registry://127.0.0.1:8848/...")
);
// Internally creates:
// - RegistryDirectory1 (bound to NacosRegistry1)
// - Cluster.join(directory1) returns ClusterInvoker1
// - Our wrapper: RegistryCapturingInvoker1("nacos://127.0.0.1:8848", ClusterInvoker1)
invokers.add(invoker1);
// Second call: nacos2
Invoker invoker2 = registryProtocol.refer(
UserService.class,
URL.valueOf("registry://192.168.1.100:8848/...")
);
// Internally creates:
// - RegistryDirectory2 (bound to NacosRegistry2)
// - Cluster.join(directory2) returns ClusterInvoker2
// - Our wrapper: RegistryCapturingInvoker2("nacos://192.168.1.100:8848", ClusterInvoker2)
invokers.add(invoker2);
// Aggregate multiple Invokers (via ZoneAwareCluster or RegistryAwareCluster)
Invoker finalInvoker = cluster.join(new StaticDirectory(invokers));Key Points:
Scenario 2: Routing During Invocation// User invocation
userService.getUser(1);
// Dubbo's internal routing logic
// 1. RegistryAwareClusterInvoker (aggregates multiple registry Invokers)
// Selects one Invoker based on routing rules
// Example: selects invoker1 (nacos1)
// 2. Call invoker1.invoke()
// → RegistryCapturingInvoker1.invoke()
// ├─ pushCapturedRegistryAddress("nacos://127.0.0.1:8848") ← nacos1
// ├─ delegate.invoke() → ClusterInvoker1 → Filter → Provider
// └─ clearCapturedRegistryAddress()
// If next invocation selects invoker2 (nacos2)
userService.getUser(2);
// 3. Call invoker2.invoke()
// → RegistryCapturingInvoker2.invoke()
// ├─ pushCapturedRegistryAddress("nacos://192.168.1.100:8848") ← nacos2
// ├─ delegate.invoke() → ClusterInvoker2 → Filter → Provider
// └─ clearCapturedRegistryAddress()Why Does This Approach Work?1. Directory is Bound to Registry at Creation Time// Inside RegistryProtocol.refer()
RegistryDirectory<T> directory = new RegistryDirectory<>(type, url);
directory.setRegistry(registry); // ⭐️ Binding
2. Cluster.join() Creates Independent Invoker for Each Directory// Our interception
Invoker<T> invoker = cluster.join(directory); // Called once per directory
3. Invoker is Statefulnew RegistryCapturingInvoker<>(delegate, "nacos://127.0.0.1:8848");
4. Invocation Associates Registry Through Invoker Instance// User invocation
userService.getUser(1);
↓
// Dubbo routing selects an Invoker (e.g., invoker1)
invoker1.invoke(invocation)
↓
// invoker1 is a RegistryCapturingInvoker instance
// Its registryAddress field = "nacos://127.0.0.1:8848"
RegistryCapturingInvoker.invoke() {
// Write instance field registryAddress to ThreadLocal
pushCapturedRegistryAddress(this.registryAddress);
// ...
}Direct Connection Mode HandlingWhen using direct connection mode with Single Direct URL// ReferenceConfig.createProxy():
if (urls.size() == 1) {
// ⚡️ Directly call refprotocol.refer(), completely bypassing Cluster.join()!
invoker = refprotocol.refer(interfaceClass, urls.get(0));
}
Multiple Direct URLs// Configure: url = "dubbo://host1:20880;dubbo://host2:20881;dubbo://host3:20882"
if (urls.size() > 1) {
List<Invoker<?>> invokers = new ArrayList<>();
for (URL url : urls) {
invokers.add(refprotocol.refer(interfaceClass, url));
}
// Create StaticDirectory to aggregate multiple direct invokers
invoker = cluster.join(new StaticDirectory(invokers)); // ← Triggers Cluster.join()
}
Version CompatibilityDubbo 3.0.4+ ChangeStarting from Dubbo 3.0.4, the // Dubbo 2.7.x and 3.0.0-3.0.3
<T> Invoker<T> join(Directory<T> directory);
// Dubbo 3.0.4+
<T> Invoker<T> join(Directory<T> directory, boolean buildFilterChain);Our implementation uses reflection to support both versions: static {
try {
// Try Dubbo 3.0.4+ method
Method m = Cluster.class.getMethod("join", Directory.class, boolean.class);
JOIN_TWO_ARG = LOOKUP.unreflect(m);
} catch (ReflectiveOperationException ignored) {
// Fallback to Dubbo 2.7.x / 3.0.0-3.0.3 method
}
} |
b85cdbf to
e2c19d0
Compare
trask
left a comment
There was a problem hiding this comment.
Purely AI review, I take no credit or accountability 😂 (see #17889)...
Code review for the Dubbo registry-mode server.address/server.port instrumentation. A few correctness, API-surface, and style points worth addressing before merge.
Highlights:
- Possible nested-invocation bug in the
ThreadLocalcapture (overwritten then cleared, dropping the outer value). DubboRequest.invocation()becomes part of the public library-autoconfigure API surface.- Test code uses non-conventional
satisfies(...)lambda parameter names (k/ainstead ofval/v). - A few minor style nits (
@SuppressWarnings("all"), unused catch parameter naming, an unnecessary public delegating wrapper, an unusedthrowsclause).
43a33c4 to
26387cc
Compare
# Conflicts: # instrumentation/apache-dubbo-2.7/testing/build.gradle.kts
|
I sent steverao#13 with a few proposed simplifications |
Dubbo 3.0.4+ removed the one-argument Cluster.join(Directory) method, but RegistryCapturingClusterWrapper still referenced it directly. This caused latest-deps tests to throw NoSuchMethodError and muzzle to reject Dubbo 3.x. Resolve both Cluster.join signatures through method handles and dispatch to the signature available at runtime, avoiding hard bytecode references to removed methods while preserving Dubbo 2.7 behavior. Validation: .\gradlew.bat :instrumentation:apache-dubbo-2.7:library-autoconfigure:test -PtestLatestDeps=true --tests io.opentelemetry.instrumentation.apachedubbo.v2_7.internal.RegistryCapturingClusterWrapperTest .\gradlew.bat :instrumentation:apache-dubbo-2.7:javaagent:testDubbo :instrumentation:apache-dubbo-2.7:javaagent:testDubboStableSemconv :instrumentation:apache-dubbo-2.7:javaagent:testDubboBothSemconv :instrumentation:apache-dubbo-2.7:javaagent:muzzle -PtestLatestDeps=true Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR implements registry-mode server.address/server.port resolution for Apache Dubbo instrumentation, aligned with the latest OpenTelemetry semantic conventions. When a consumer discovers a service through a registry (e.g., ZooKeeper), server.address is set to the registry URL including the service target (e.g., zookeeper://host:port/interface:version:group) instead of the direct provider host, and server.port is omitted.
Changes:
- Introduced a Dubbo
ClusterSPI wrapper (RegistryCapturingClusterWrapper) that captures the registry URL via a ThreadLocal when a registry-backed directory is used, making it available to the tracing filter chain. - Extended
DubboRequestandDubboClientNetworkAttributesGetterto use the captured registry address for constructingserver.address(composite registry URL + service target) and returning null forserver.portin registry mode. - Added comprehensive tests: unit tests for
DubboRegistryUtilandRegistryCapturingClusterWrapper, and end-to-end integration tests (AbstractDubboRegistryTest) for both library and javaagent modes using an embedded ZooKeeper instance.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
DubboRegistryUtil.java |
Core utility for extracting registry address via ThreadLocal or reflection, building service target strings |
RegistryCapturingClusterWrapper.java |
Dubbo Cluster SPI wrapper that intercepts join() to detect registry-backed directories and wrap invokers |
RegistryCapturingInvoker.java |
Invoker wrapper that pushes/restores registry address on ThreadLocal during delegation |
DubboRequest.java |
Added registryAddress field (AutoValue), captured at request creation time |
DubboClientNetworkAttributesGetter.java |
Modified getServerAddress/getServerPort to use registry-based addressing when available |
RegistryCapturingClusterWrapperProxy.java |
Javaagent proxy to bridge classloader boundaries for the Cluster SPI |
DubboInstrumentationModule.java |
Registers the new Cluster SPI resource and exposes the proxy class |
META-INF/services/org.apache.dubbo.rpc.cluster.Cluster (×2) |
SPI registration files for library-autoconfigure and javaagent |
testing/build.gradle.kts |
Added ZooKeeper/Curator dependencies for registry integration tests |
AbstractDubboRegistryTest.java |
Base integration test verifying registry-mode attributes with embedded ZooKeeper |
DubboRegistryTest.java |
Library-mode registry test |
DubboAgentRegistryTest.java |
Javaagent-mode registry test |
DubboRegistryUtilTest.java |
Unit tests for buildServiceTarget and extractRegistryAddress (reflection fallback) |
RegistryCapturingClusterWrapperTest.java |
Unit test verifying StaticDirectory is not wrapped |
|
@steverao this looks good, but I think we need to update semantic conventions before merging this? |
Prototype for open-telemetry/semantic-conventions#3317, open-telemetry/semantic-conventions#3408 (comment)
Related to #15871