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(); + } }