Skip to content
5 changes: 5 additions & 0 deletions api/src/main/java/io/grpc/EquivalentAddressGroup.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ public final class EquivalentAddressGroup {
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/6138")
public static final Attributes.Key<String> ATTR_AUTHORITY_OVERRIDE =
Attributes.Key.create("io.grpc.EquivalentAddressGroup.ATTR_AUTHORITY_OVERRIDE");
/**
* The name of the locality that this EquivalentAddressGroup is in.
*/
public static final Attributes.Key<String> ATTR_LOCALITY_NAME =
Attributes.Key.create("io.grpc.lb.locality");
private final List<SocketAddress> addrs;
private final Attributes attrs;

Expand Down
6 changes: 0 additions & 6 deletions api/src/main/java/io/grpc/LoadBalancer.java
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,6 @@ public abstract class LoadBalancer {
public static final Attributes.Key<Boolean> IS_PETIOLE_POLICY =
Attributes.Key.create("io.grpc.IS_PETIOLE_POLICY");

/**
* The name of the locality that this EquivalentAddressGroup is in.
*/
public static final Attributes.Key<String> ATTR_LOCALITY_NAME =
Attributes.Key.create("io.grpc.lb.locality");

/**
* A picker that always returns an erring pick.
*
Expand Down
63 changes: 26 additions & 37 deletions core/src/main/java/io/grpc/internal/InternalSubchannel.java
Original file line number Diff line number Diff line change
Expand Up @@ -602,15 +602,15 @@ public void run() {
pendingTransport = null;
connectedAddressAttributes = addressIndex.getCurrentEagAttributes();
gotoNonErrorState(READY);
subchannelMetrics.recordConnectionAttemptSucceeded(buildLabelSet(
getAttributeOrDefault(
addressIndex.getCurrentEagAttributes(), NameResolver.ATTR_BACKEND_SERVICE),
getAttributeOrDefault(
addressIndex.getCurrentEagAttributes(), LoadBalancer.ATTR_LOCALITY_NAME),
null,
extractSecurityLevel(
addressIndex.getCurrentEagAttributes().get(GrpcAttributes.ATTR_SECURITY_LEVEL))
));
subchannelMetrics.recordConnectionAttemptSucceeded(MetricsAttributes.newBuilder(target)
.backendService(getAttributeOrDefault(
addressIndex.getCurrentEagAttributes(), NameResolver.ATTR_BACKEND_SERVICE))
.locality(getAttributeOrDefault(
addressIndex.getCurrentEagAttributes(),
EquivalentAddressGroup.ATTR_LOCALITY_NAME))
.securityLevel(extractSecurityLevel(
addressIndex.getCurrentEagAttributes().get(GrpcAttributes.ATTR_SECURITY_LEVEL)))
.build());
}
}
});
Expand All @@ -636,23 +636,24 @@ public void run() {
activeTransport = null;
addressIndex.reset();
gotoNonErrorState(IDLE);
subchannelMetrics.recordDisconnection(buildLabelSet(
getAttributeOrDefault(
addressIndex.getCurrentEagAttributes(), NameResolver.ATTR_BACKEND_SERVICE),
getAttributeOrDefault(
addressIndex.getCurrentEagAttributes(), LoadBalancer.ATTR_LOCALITY_NAME),
"Peer Pressure",
extractSecurityLevel(
addressIndex.getCurrentEagAttributes().get(GrpcAttributes.ATTR_SECURITY_LEVEL))
));
subchannelMetrics.recordDisconnection(MetricsAttributes.newBuilder(target)
.backendService(getAttributeOrDefault(
addressIndex.getCurrentEagAttributes(), NameResolver.ATTR_BACKEND_SERVICE))
.locality(getAttributeOrDefault(
addressIndex.getCurrentEagAttributes(),
EquivalentAddressGroup.ATTR_LOCALITY_NAME))
.disconnectError(SubchannelMetrics.DisconnectError.UNKNOWN.getErrorString(null))
.securityLevel(extractSecurityLevel(
addressIndex.getCurrentEagAttributes().get(GrpcAttributes.ATTR_SECURITY_LEVEL)))
.build());
} else if (pendingTransport == transport) {
subchannelMetrics.recordConnectionAttemptFailed(buildLabelSet(
getAttributeOrDefault(
addressIndex.getCurrentEagAttributes(), NameResolver.ATTR_BACKEND_SERVICE),
getAttributeOrDefault(
addressIndex.getCurrentEagAttributes(), LoadBalancer.ATTR_LOCALITY_NAME),
null, null
));
subchannelMetrics.recordConnectionAttemptFailed(MetricsAttributes.newBuilder(target)
.backendService(getAttributeOrDefault(
addressIndex.getCurrentEagAttributes(), NameResolver.ATTR_BACKEND_SERVICE))
.locality(getAttributeOrDefault(
addressIndex.getCurrentEagAttributes(),
EquivalentAddressGroup.ATTR_LOCALITY_NAME))
.build());
Preconditions.checkState(state.getState() == CONNECTING,
"Expected state is CONNECTING, actual state is %s", state.getState());
addressIndex.increment();
Expand Down Expand Up @@ -872,18 +873,6 @@ private String printShortStatus(Status status) {
return buffer.toString();
}

private OtelMetricsAttributes buildLabelSet(String backendService, String locality,
String disconnectError, String securityLevel) {
return new OtelMetricsAttributes(
target,
backendService,
locality,
disconnectError,
securityLevel
);
}


@VisibleForTesting
static final class TransportLogger extends ChannelLogger {
// Changed just after construction to break a cyclic dependency.
Expand Down
78 changes: 78 additions & 0 deletions core/src/main/java/io/grpc/internal/MetricsAttributes.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2025 The gRPC Authors
*
* Licensed 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 io.grpc.internal;

class MetricsAttributes {
final String target;
final String backendService;
final String locality;
final String disconnectError;
final String securityLevel;

// Constructor is private, only the Builder can call it
private MetricsAttributes(Builder builder) {
this.target = builder.target;
this.backendService = builder.backendService;
this.locality = builder.locality;
this.disconnectError = builder.disconnectError;
this.securityLevel = builder.securityLevel;
}

// Public static method to get a new builder instance
public static Builder newBuilder(String target) {
return new Builder(target);
}

public static class Builder {
// Required parameter
private final String target;

// Optional parameters - initialized to default values
private String backendService = null;
private String locality = null;
private String disconnectError = null;
private String securityLevel = null;

public Builder(String target) {
this.target = target;
}

public Builder backendService(String val) {
this.backendService = val;
return this;
}

public Builder locality(String val) {
this.locality = val;
return this;
}

public Builder disconnectError(String val) {
this.disconnectError = val;
return this;
}

public Builder securityLevel(String val) {
this.securityLevel = val;
return this;
}

public MetricsAttributes build() {
return new MetricsAttributes(this);
}
}
}
69 changes: 0 additions & 69 deletions core/src/main/java/io/grpc/internal/OtelMetricsAttributes.java

This file was deleted.

89 changes: 85 additions & 4 deletions core/src/main/java/io/grpc/internal/SubchannelMetrics.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@
import io.grpc.LongUpDownCounterMetricInstrument;
import io.grpc.MetricInstrumentRegistry;
import io.grpc.MetricRecorder;
import javax.annotation.Nullable;

public final class SubchannelMetrics {
final class SubchannelMetrics {

private static final LongCounterMetricInstrument disconnections;
private static final LongCounterMetricInstrument connectionAttemptsSucceeded;
Expand Down Expand Up @@ -75,7 +76,7 @@ public SubchannelMetrics(MetricRecorder metricRecorder) {
);
}

public void recordConnectionAttemptSucceeded(OtelMetricsAttributes labelSet) {
public void recordConnectionAttemptSucceeded(MetricsAttributes labelSet) {
metricRecorder
.addLongCounter(connectionAttemptsSucceeded, 1,
ImmutableList.of(labelSet.target),
Expand All @@ -86,14 +87,14 @@ public void recordConnectionAttemptSucceeded(OtelMetricsAttributes labelSet) {
ImmutableList.of(labelSet.securityLevel, labelSet.backendService, labelSet.locality));
}

public void recordConnectionAttemptFailed(OtelMetricsAttributes labelSet) {
public void recordConnectionAttemptFailed(MetricsAttributes labelSet) {
metricRecorder
.addLongCounter(connectionAttemptsFailed, 1,
ImmutableList.of(labelSet.target),
ImmutableList.of(labelSet.backendService, labelSet.locality));
}

public void recordDisconnection(OtelMetricsAttributes labelSet) {
public void recordDisconnection(MetricsAttributes labelSet) {
metricRecorder
.addLongCounter(disconnections, 1,
ImmutableList.of(labelSet.target),
Expand All @@ -103,4 +104,84 @@ public void recordDisconnection(OtelMetricsAttributes labelSet) {
ImmutableList.of(labelSet.target),
ImmutableList.of(labelSet.securityLevel, labelSet.backendService, labelSet.locality));
}

/**
* Represents the reason for a subchannel failure.
*/
public enum DisconnectError {

/**
* Represents an HTTP/2 GOAWAY frame. The specific error code
* (e.g., "NO_ERROR", "PROTOCOL_ERROR") should be handled separately
* as it is a dynamic part of the error.
* See RFC 9113 for error codes: https://www.rfc-editor.org/rfc/rfc9113.html#name-error-codes
*/
GOAWAY("goaway"),

/**
* The subchannel was shut down for various reasons like parent channel shutdown,
* idleness, or load balancing policy changes.
*/
SUBCHANNEL_SHUTDOWN("subchannel shutdown"),

/**
* Connection was reset (e.g., ECONNRESET, WSAECONNERESET).
*/
CONNECTION_RESET("connection reset"),

/**
* Connection timed out (e.g., ETIMEDOUT, WSAETIMEDOUT), including closures
* from gRPC keepalives.
*/
CONNECTION_TIMED_OUT("connection timed out"),

/**
* Connection was aborted (e.g., ECONNABORTED, WSAECONNABORTED).
*/
CONNECTION_ABORTED("connection aborted"),

/**
* Any socket error not covered by other specific disconnect errors.
*/
SOCKET_ERROR("socket error"),

/**
* A catch-all for any other unclassified reason.
*/
UNKNOWN("unknown");

private final String errorTag;

/**
* Private constructor to associate a description with each enum constant.
*
* @param errorTag The detailed explanation of the error.
*/
DisconnectError(String errorTag) {
this.errorTag = errorTag;
}

/**
* Gets the error string suitable for use as a metric tag.
*
* <p>If the reason is {@code GOAWAY}, this method requires the specific
* HTTP/2 error code to create the complete tag (e.g., "goaway PROTOCOL_ERROR").
* For all other reasons, the parameter is ignored.</p>
*
* @param goawayErrorCode The specific HTTP/2 error code. This is only
* used if the reason is GOAWAY and should not be null in that case.
* @return The formatted error string.
*/
public String getErrorString(@Nullable String goawayErrorCode) {
if (this == GOAWAY) {
if (goawayErrorCode == null || goawayErrorCode.isEmpty()) {
// Return the base tag if the code is missing, or consider throwing an exception
// throw new IllegalArgumentException("goawayErrorCode is required for GOAWAY reason.");
return this.errorTag;
}
return this.errorTag + " " + goawayErrorCode;
}
return this.errorTag;
}
}
}
Loading