diff --git a/java/google/registry/proxy/handler/QuotaHandler.java b/java/google/registry/proxy/handler/QuotaHandler.java
index 04477400b..44b949cdc 100644
--- a/java/google/registry/proxy/handler/QuotaHandler.java
+++ b/java/google/registry/proxy/handler/QuotaHandler.java
@@ -15,11 +15,13 @@
package google.registry.proxy.handler;
import static com.google.common.base.Preconditions.checkNotNull;
+import static google.registry.proxy.Protocol.PROTOCOL_KEY;
import static google.registry.proxy.handler.EppServiceHandler.CLIENT_CERTIFICATE_HASH_KEY;
import static google.registry.proxy.handler.ProxyProtocolHandler.REMOTE_ADDRESS_KEY;
import google.registry.proxy.EppProtocolModule.EppProtocol;
import google.registry.proxy.WhoisProtocolModule.WhoisProtocol;
+import google.registry.proxy.metric.FrontendMetrics;
import google.registry.proxy.quota.QuotaManager;
import google.registry.proxy.quota.QuotaManager.QuotaRebate;
import google.registry.proxy.quota.QuotaManager.QuotaRequest;
@@ -40,9 +42,11 @@ public abstract class QuotaHandler extends ChannelInboundHandlerAdapter {
protected final QuotaManager quotaManager;
protected QuotaResponse quotaResponse;
+ protected final FrontendMetrics metrics;
- protected QuotaHandler(QuotaManager quotaManager) {
+ protected QuotaHandler(QuotaManager quotaManager, FrontendMetrics metrics) {
this.quotaManager = quotaManager;
+ this.metrics = metrics;
}
abstract String getUserId(ChannelHandlerContext ctx);
@@ -57,7 +61,9 @@ public abstract class QuotaHandler extends ChannelInboundHandlerAdapter {
checkNotNull(userId, "Cannot obtain User ID");
quotaResponse = quotaManager.acquireQuota(QuotaRequest.create(userId));
if (!quotaResponse.success()) {
- throw new OverQuotaException(isUserIdPii() ? "none" : userId);
+ String protocolName = ctx.channel().attr(PROTOCOL_KEY).get().name();
+ metrics.registerQuotaRejection(protocolName, isUserIdPii() ? "none" : userId);
+ throw new OverQuotaException(protocolName, isUserIdPii() ? "none" : userId);
}
}
ctx.fireChannelRead(msg);
@@ -72,8 +78,11 @@ public abstract class QuotaHandler extends ChannelInboundHandlerAdapter {
public abstract void channelInactive(ChannelHandlerContext ctx);
static class OverQuotaException extends Exception {
- OverQuotaException(String userId) {
- super(String.format("USER ID: %s\nQuota exceeded, terminating connection.", userId));
+ OverQuotaException(String protocol, String userId) {
+ super(
+ String.format(
+ "\nPROTOCOL: %s\nUSER ID: %s\nQuota exceeded, terminating connection.",
+ protocol, userId));
}
}
@@ -81,8 +90,8 @@ public abstract class QuotaHandler extends ChannelInboundHandlerAdapter {
public static class WhoisQuotaHandler extends QuotaHandler {
@Inject
- WhoisQuotaHandler(@WhoisProtocol QuotaManager quotaManager) {
- super(quotaManager);
+ WhoisQuotaHandler(@WhoisProtocol QuotaManager quotaManager, FrontendMetrics metrics) {
+ super(quotaManager, metrics);
}
/**
@@ -117,8 +126,8 @@ public abstract class QuotaHandler extends ChannelInboundHandlerAdapter {
public static class EppQuotaHandler extends QuotaHandler {
@Inject
- EppQuotaHandler(@EppProtocol QuotaManager quotaManager) {
- super(quotaManager);
+ EppQuotaHandler(@EppProtocol QuotaManager quotaManager, FrontendMetrics metrics) {
+ super(quotaManager, metrics);
}
/**
diff --git a/java/google/registry/proxy/metric/BackendMetrics.java b/java/google/registry/proxy/metric/BackendMetrics.java
index 124f0a632..624d8c14d 100644
--- a/java/google/registry/proxy/metric/BackendMetrics.java
+++ b/java/google/registry/proxy/metric/BackendMetrics.java
@@ -26,8 +26,10 @@ import google.registry.util.NonFinalForTesting;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import javax.inject.Inject;
+import javax.inject.Singleton;
/** Backend metrics instrumentation. */
+@Singleton
public class BackendMetrics {
// Maximum request size is defined in the config file, this is not realistic and we'd be out of
diff --git a/java/google/registry/proxy/metric/FrontendMetrics.java b/java/google/registry/proxy/metric/FrontendMetrics.java
index d7eebe2ff..6e15f3564 100644
--- a/java/google/registry/proxy/metric/FrontendMetrics.java
+++ b/java/google/registry/proxy/metric/FrontendMetrics.java
@@ -31,10 +31,23 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.inject.Inject;
+import javax.inject.Singleton;
/** Frontend metrics instrumentation. */
+@Singleton
public class FrontendMetrics {
+ /**
+ * Labels to register front metrics with.
+ *
+ *
The client certificate hash value is only used for EPP metrics. For WHOIS metrics, it will
+ * always be {@code "none"}. In order to get the actual registrar name, one can use the {@code
+ * nomulus} tool:
+ *
+ *
+ * nomulus -e production list_registrars -f clientCertificateHash | grep $HASH
+ *
+ */
private static final ImmutableSet LABELS =
ImmutableSet.of(
LabelDescriptor.create("protocol", "Name of the protocol."),
@@ -68,6 +81,14 @@ public class FrontendMetrics {
"Connections",
LABELS);
+ static final IncrementableMetric quotaRejectionsCounter =
+ MetricRegistryImpl.getDefault()
+ .newIncrementableMetric(
+ "/proxy/frontend/quota_rejections",
+ "Total number rejected quota request made by proxy for each connection.",
+ "Rejections",
+ LABELS);
+
@Inject
public FrontendMetrics() {}
@@ -96,4 +117,9 @@ public class FrontendMetrics {
}
channelGroup.add(channel);
}
+
+ @NonFinalForTesting
+ public void registerQuotaRejection(String protocol, String certHash) {
+ quotaRejectionsCounter.increment(protocol, certHash);
+ }
}
diff --git a/javatests/google/registry/proxy/handler/EppQuotaHandlerTest.java b/javatests/google/registry/proxy/handler/EppQuotaHandlerTest.java
index c558a85a9..610a81863 100644
--- a/javatests/google/registry/proxy/handler/EppQuotaHandlerTest.java
+++ b/javatests/google/registry/proxy/handler/EppQuotaHandlerTest.java
@@ -15,6 +15,7 @@
package google.registry.proxy.handler;
import static com.google.common.truth.Truth.assertThat;
+import static google.registry.proxy.Protocol.PROTOCOL_KEY;
import static google.registry.proxy.handler.EppServiceHandler.CLIENT_CERTIFICATE_HASH_KEY;
import static google.registry.testing.JUnitBackports.expectThrows;
import static org.mockito.Mockito.mock;
@@ -22,12 +23,16 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
+import com.google.common.collect.ImmutableList;
+import google.registry.proxy.Protocol;
import google.registry.proxy.handler.QuotaHandler.EppQuotaHandler;
import google.registry.proxy.handler.QuotaHandler.OverQuotaException;
+import google.registry.proxy.metric.FrontendMetrics;
import google.registry.proxy.quota.QuotaManager;
import google.registry.proxy.quota.QuotaManager.QuotaRebate;
import google.registry.proxy.quota.QuotaManager.QuotaRequest;
import google.registry.proxy.quota.QuotaManager.QuotaResponse;
+import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.embedded.EmbeddedChannel;
import org.joda.time.DateTime;
@@ -43,15 +48,35 @@ import org.junit.runners.JUnit4;
public class EppQuotaHandlerTest {
private final QuotaManager quotaManager = mock(QuotaManager.class);
- private final EppQuotaHandler handler = new EppQuotaHandler(quotaManager);
+ private final FrontendMetrics metrics = mock(FrontendMetrics.class);
+ private final EppQuotaHandler handler = new EppQuotaHandler(quotaManager, metrics);
private final EmbeddedChannel channel = new EmbeddedChannel(handler);
private final String clientCertHash = "blah/123!";
private final DateTime now = DateTime.now(DateTimeZone.UTC);
private final Object message = new Object();
+ private void setProtocol(Channel channel) {
+ channel
+ .attr(PROTOCOL_KEY)
+ .set(
+ Protocol.frontendBuilder()
+ .name("epp")
+ .port(12345)
+ .handlerProviders(ImmutableList.of())
+ .relayProtocol(
+ Protocol.backendBuilder()
+ .name("backend")
+ .host("host.tld")
+ .port(1234)
+ .handlerProviders(ImmutableList.of())
+ .build())
+ .build());
+ }
+
@Before
public void setUp() {
channel.attr(CLIENT_CERTIFICATE_HASH_KEY).set(clientCertHash);
+ setProtocol(channel);
}
@Test
@@ -85,15 +110,18 @@ public class EppQuotaHandlerTest {
OverQuotaException e =
expectThrows(OverQuotaException.class, () -> channel.writeInbound(message));
assertThat(e).hasMessageThat().contains(clientCertHash);
+ verify(metrics).registerQuotaRejection("epp", clientCertHash);
+ verifyNoMoreInteractions(metrics);
}
@Test
public void testSuccess_twoChannels_twoUserIds() {
// Set up another user.
- final EppQuotaHandler otherHandler = new EppQuotaHandler(quotaManager);
+ final EppQuotaHandler otherHandler = new EppQuotaHandler(quotaManager, metrics);
final EmbeddedChannel otherChannel = new EmbeddedChannel(otherHandler);
final String otherClientCertHash = "hola@9x";
otherChannel.attr(CLIENT_CERTIFICATE_HASH_KEY).set(otherClientCertHash);
+ setProtocol(otherChannel);
final DateTime later = now.plus(Duration.standardSeconds(1));
when(quotaManager.acquireQuota(QuotaRequest.create(clientCertHash)))
@@ -110,14 +138,17 @@ public class EppQuotaHandlerTest {
OverQuotaException e =
expectThrows(OverQuotaException.class, () -> otherChannel.writeInbound(message));
assertThat(e).hasMessageThat().contains(otherClientCertHash);
+ verify(metrics).registerQuotaRejection("epp", otherClientCertHash);
+ verifyNoMoreInteractions(metrics);
}
@Test
public void testSuccess_twoChannels_sameUserIds() {
// Set up another channel for the same user.
- final EppQuotaHandler otherHandler = new EppQuotaHandler(quotaManager);
+ final EppQuotaHandler otherHandler = new EppQuotaHandler(quotaManager, metrics);
final EmbeddedChannel otherChannel = new EmbeddedChannel(otherHandler);
otherChannel.attr(CLIENT_CERTIFICATE_HASH_KEY).set(clientCertHash);
+ setProtocol(otherChannel);
final DateTime later = now.plus(Duration.standardSeconds(1));
when(quotaManager.acquireQuota(QuotaRequest.create(clientCertHash)))
@@ -133,5 +164,7 @@ public class EppQuotaHandlerTest {
OverQuotaException e =
expectThrows(OverQuotaException.class, () -> otherChannel.writeInbound(message));
assertThat(e).hasMessageThat().contains(clientCertHash);
+ verify(metrics).registerQuotaRejection("epp", clientCertHash);
+ verifyNoMoreInteractions(metrics);
}
}
diff --git a/javatests/google/registry/proxy/handler/WhoisQuotaHandlerTest.java b/javatests/google/registry/proxy/handler/WhoisQuotaHandlerTest.java
index bb1d33ec8..d0ccfb69d 100644
--- a/javatests/google/registry/proxy/handler/WhoisQuotaHandlerTest.java
+++ b/javatests/google/registry/proxy/handler/WhoisQuotaHandlerTest.java
@@ -15,6 +15,7 @@
package google.registry.proxy.handler;
import static com.google.common.truth.Truth.assertThat;
+import static google.registry.proxy.Protocol.PROTOCOL_KEY;
import static google.registry.proxy.handler.ProxyProtocolHandler.REMOTE_ADDRESS_KEY;
import static google.registry.testing.JUnitBackports.expectThrows;
import static org.mockito.Mockito.mock;
@@ -22,11 +23,15 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
+import com.google.common.collect.ImmutableList;
+import google.registry.proxy.Protocol;
import google.registry.proxy.handler.QuotaHandler.OverQuotaException;
import google.registry.proxy.handler.QuotaHandler.WhoisQuotaHandler;
+import google.registry.proxy.metric.FrontendMetrics;
import google.registry.proxy.quota.QuotaManager;
import google.registry.proxy.quota.QuotaManager.QuotaRequest;
import google.registry.proxy.quota.QuotaManager.QuotaResponse;
+import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.embedded.EmbeddedChannel;
import org.joda.time.DateTime;
@@ -42,15 +47,35 @@ import org.junit.runners.JUnit4;
public class WhoisQuotaHandlerTest {
private final QuotaManager quotaManager = mock(QuotaManager.class);
- private final WhoisQuotaHandler handler = new WhoisQuotaHandler(quotaManager);
+ private final FrontendMetrics metrics = mock(FrontendMetrics.class);
+ private final WhoisQuotaHandler handler = new WhoisQuotaHandler(quotaManager, metrics);
private final EmbeddedChannel channel = new EmbeddedChannel(handler);
private final DateTime now = DateTime.now(DateTimeZone.UTC);
private final String remoteAddress = "127.0.0.1";
private final Object message = new Object();
+ private void setProtocol(Channel channel) {
+ channel
+ .attr(PROTOCOL_KEY)
+ .set(
+ Protocol.frontendBuilder()
+ .name("whois")
+ .port(12345)
+ .handlerProviders(ImmutableList.of())
+ .relayProtocol(
+ Protocol.backendBuilder()
+ .name("backend")
+ .host("host.tld")
+ .port(1234)
+ .handlerProviders(ImmutableList.of())
+ .build())
+ .build());
+ }
+
@Before
public void setUp() {
channel.attr(REMOTE_ADDRESS_KEY).set(remoteAddress);
+ setProtocol(channel);
}
@Test
@@ -80,15 +105,18 @@ public class WhoisQuotaHandlerTest {
OverQuotaException e =
expectThrows(OverQuotaException.class, () -> channel.writeInbound(message));
assertThat(e).hasMessageThat().contains("none");
+ verify(metrics).registerQuotaRejection("whois", "none");
+ verifyNoMoreInteractions(metrics);
}
@Test
public void testSuccess_twoChannels_twoUserIds() {
// Set up another user.
- final WhoisQuotaHandler otherHandler = new WhoisQuotaHandler(quotaManager);
+ final WhoisQuotaHandler otherHandler = new WhoisQuotaHandler(quotaManager, metrics);
final EmbeddedChannel otherChannel = new EmbeddedChannel(otherHandler);
final String otherRemoteAddress = "192.168.0.1";
otherChannel.attr(REMOTE_ADDRESS_KEY).set(otherRemoteAddress);
+ setProtocol(otherChannel);
final DateTime later = now.plus(Duration.standardSeconds(1));
when(quotaManager.acquireQuota(QuotaRequest.create(remoteAddress)))
@@ -105,18 +133,21 @@ public class WhoisQuotaHandlerTest {
OverQuotaException e =
expectThrows(OverQuotaException.class, () -> otherChannel.writeInbound(message));
assertThat(e).hasMessageThat().contains("none");
+ verify(metrics).registerQuotaRejection("whois", "none");
+ verifyNoMoreInteractions(metrics);
}
@Test
public void testSuccess_oneUser_rateLimited() {
// Set up another channel for the same user.
- final WhoisQuotaHandler otherHandler = new WhoisQuotaHandler(quotaManager);
+ final WhoisQuotaHandler otherHandler = new WhoisQuotaHandler(quotaManager, metrics);
final EmbeddedChannel otherChannel = new EmbeddedChannel(otherHandler);
otherChannel.attr(REMOTE_ADDRESS_KEY).set(remoteAddress);
+ setProtocol(otherChannel);
final DateTime later = now.plus(Duration.standardSeconds(1));
// Set up the third channel for the same user
- final WhoisQuotaHandler thirdHandler = new WhoisQuotaHandler(quotaManager);
+ final WhoisQuotaHandler thirdHandler = new WhoisQuotaHandler(quotaManager, metrics);
final EmbeddedChannel thirdChannel = new EmbeddedChannel(thirdHandler);
thirdChannel.attr(REMOTE_ADDRESS_KEY).set(remoteAddress);
final DateTime evenLater = now.plus(Duration.standardSeconds(60));
@@ -137,10 +168,12 @@ public class WhoisQuotaHandlerTest {
OverQuotaException e =
expectThrows(OverQuotaException.class, () -> otherChannel.writeInbound(message));
assertThat(e).hasMessageThat().contains("none");
+ verify(metrics).registerQuotaRejection("whois", "none");
// Allows the third channel.
assertThat(thirdChannel.writeInbound(message)).isTrue();
assertThat((Object) thirdChannel.readInbound()).isEqualTo(message);
assertThat(thirdChannel.isActive()).isTrue();
+ verifyNoMoreInteractions(metrics);
}
}
diff --git a/javatests/google/registry/proxy/metric/FrontendMetricsTest.java b/javatests/google/registry/proxy/metric/FrontendMetricsTest.java
index f21d96951..9d7064876 100644
--- a/javatests/google/registry/proxy/metric/FrontendMetricsTest.java
+++ b/javatests/google/registry/proxy/metric/FrontendMetricsTest.java
@@ -163,4 +163,23 @@ public class FrontendMetricsTest {
.and()
.hasNoOtherValues();
}
+
+ @Test
+ public void testSuccess_registerQuotaRejections() {
+ String otherCertHash = "foobar1234X";
+ String remoteAddress = "127.0.0.1";
+ String otherProtocol = "other protocol";
+ metrics.registerQuotaRejection(PROTOCOL, CERT_HASH);
+ metrics.registerQuotaRejection(PROTOCOL, otherCertHash);
+ metrics.registerQuotaRejection(PROTOCOL, otherCertHash);
+ metrics.registerQuotaRejection(otherProtocol, remoteAddress);
+ assertThat(FrontendMetrics.quotaRejectionsCounter)
+ .hasValueForLabels(1, PROTOCOL, CERT_HASH)
+ .and()
+ .hasValueForLabels(2, PROTOCOL, otherCertHash)
+ .and()
+ .hasValueForLabels(1, otherProtocol, remoteAddress)
+ .and()
+ .hasNoOtherValues();
+ }
}