1
0
mirror of https://github.com/google/nomulus synced 2026-01-03 03:35:42 +00:00

Add time-limited logs to Epp Login flow to test increased latency (#2810)

This commit is contained in:
Pavlo Tkach
2025-08-22 16:30:53 -04:00
committed by GitHub
parent e8a475f48b
commit d9c46170dd
5 changed files with 82 additions and 3 deletions

View File

@@ -26,6 +26,7 @@ import com.google.common.net.MediaType;
import google.registry.model.eppoutput.EppOutput; import google.registry.model.eppoutput.EppOutput;
import google.registry.request.Response; import google.registry.request.Response;
import google.registry.util.ProxyHttpHeaders; import google.registry.util.ProxyHttpHeaders;
import google.registry.util.StopwatchLogger;
import jakarta.inject.Inject; import jakarta.inject.Inject;
/** Handle an EPP request and response. */ /** Handle an EPP request and response. */
@@ -55,7 +56,10 @@ public class EppRequestHandler {
eppController.handleEppCommand( eppController.handleEppCommand(
sessionMetadata, credentials, eppRequestSource, isDryRun, isSuperuser, inputXmlBytes); sessionMetadata, credentials, eppRequestSource, isDryRun, isSuperuser, inputXmlBytes);
response.setContentType(APPLICATION_EPP_XML); response.setContentType(APPLICATION_EPP_XML);
final StopwatchLogger stopwatch = new StopwatchLogger();
byte[] eppResponseXmlBytes = marshalWithLenientRetry(eppOutput); byte[] eppResponseXmlBytes = marshalWithLenientRetry(eppOutput);
stopwatch.tick("Completed EPP output marshaling.");
response.setPayload(new String(eppResponseXmlBytes, UTF_8)); response.setPayload(new String(eppResponseXmlBytes, UTF_8));
logger.atInfo().log( logger.atInfo().log(
"EPP response: %s", prettyPrint(EppXmlSanitizer.sanitizeEppXml(eppResponseXmlBytes))); "EPP response: %s", prettyPrint(EppXmlSanitizer.sanitizeEppXml(eppResponseXmlBytes)));

View File

@@ -29,6 +29,7 @@ import google.registry.model.eppoutput.EppOutput;
import google.registry.monitoring.whitebox.EppMetric; import google.registry.monitoring.whitebox.EppMetric;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel; import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.persistence.transaction.JpaTransactionManager; import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.util.StopwatchLogger;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.inject.Provider; import jakarta.inject.Provider;
import java.util.Optional; import java.util.Optional;
@@ -77,23 +78,32 @@ public class FlowRunner {
flowReporter.recordToLogs(); flowReporter.recordToLogs();
} }
eppMetricBuilder.setCommandNameFromFlow(flowClass.getSimpleName()); eppMetricBuilder.setCommandNameFromFlow(flowClass.getSimpleName());
final StopwatchLogger stopwatch = new StopwatchLogger();
// We may already be in a transaction, e.g., when invoked by DeleteExpiredDomainsAction. // We may already be in a transaction, e.g., when invoked by DeleteExpiredDomainsAction.
if (!isTransactional || jpaTransactionManager.inTransaction()) { if (!isTransactional || jpaTransactionManager.inTransaction()) {
stopwatch.tick("We're in transaction, running the flow now.");
return EppOutput.create(flowProvider.get().run()); return EppOutput.create(flowProvider.get().run());
} }
stopwatch.tick("We're not in transaction, calling transact.");
try { try {
return jpaTransactionManager.transact( return jpaTransactionManager.transact(
isolationLevelOverride.orElse(null), isolationLevelOverride.orElse(null),
() -> { () -> {
try { try {
stopwatch.tick("Running the flow in transaction.");
EppOutput output = EppOutput.create(flowProvider.get().run()); EppOutput output = EppOutput.create(flowProvider.get().run());
stopwatch.tick("Completed the flow in transaction.");
if (isDryRun) { if (isDryRun) {
throw new DryRunException(output); throw new DryRunException(output);
} }
if (flowClass.equals(LoginFlow.class)) { if (flowClass.equals(LoginFlow.class)) {
// In LoginFlow, registrarId isn't known until after the flow executes, so save // In LoginFlow, registrarId isn't known until after the flow executes, so save
// it then. // it then.
stopwatch.tick("Login flow started setting registrar id.");
eppMetricBuilder.setRegistrarId(sessionMetadata.getRegistrarId()); eppMetricBuilder.setRegistrarId(sessionMetadata.getRegistrarId());
stopwatch.tick("Login flow finished setting registrar id.");
} }
return output; return output;
} catch (EppException e) { } catch (EppException e) {

View File

@@ -47,6 +47,7 @@ import google.registry.model.eppinput.EppInput.Options;
import google.registry.model.eppinput.EppInput.Services; import google.registry.model.eppinput.EppInput.Services;
import google.registry.model.eppoutput.EppResponse; import google.registry.model.eppoutput.EppResponse;
import google.registry.model.registrar.Registrar; import google.registry.model.registrar.Registrar;
import google.registry.util.StopwatchLogger;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@@ -97,19 +98,25 @@ public class LoginFlow implements MutatingFlow {
/** Run the flow without bothering to log errors. The {@link #run} method will do that for us. */ /** Run the flow without bothering to log errors. The {@link #run} method will do that for us. */
private EppResponse runWithoutLogging() throws EppException { private EppResponse runWithoutLogging() throws EppException {
final StopwatchLogger stopwatch = new StopwatchLogger();
extensionManager.validate(); // There are no legal extensions for this flow. extensionManager.validate(); // There are no legal extensions for this flow.
stopwatch.tick("LoginFlow extension validate");
Login login = (Login) eppInput.getCommandWrapper().getCommand(); Login login = (Login) eppInput.getCommandWrapper().getCommand();
stopwatch.tick("LoginFlow getCommand");
if (!registrarId.isEmpty()) { if (!registrarId.isEmpty()) {
throw new AlreadyLoggedInException(); throw new AlreadyLoggedInException();
} }
Options options = login.getOptions(); Options options = login.getOptions();
stopwatch.tick("LoginFlow getOptions");
if (!ProtocolDefinition.LANGUAGE.equals(options.getLanguage())) { if (!ProtocolDefinition.LANGUAGE.equals(options.getLanguage())) {
throw new UnsupportedLanguageException(); throw new UnsupportedLanguageException();
} }
Services services = login.getServices(); Services services = login.getServices();
stopwatch.tick("LoginFlow getServices");
Set<String> unsupportedObjectServices = difference( Set<String> unsupportedObjectServices = difference(
nullToEmpty(services.getObjectServices()), nullToEmpty(services.getObjectServices()),
ProtocolDefinition.SUPPORTED_OBJECT_SERVICES); ProtocolDefinition.SUPPORTED_OBJECT_SERVICES);
stopwatch.tick("LoginFlow difference unsupportedObjectServices");
if (!unsupportedObjectServices.isEmpty()) { if (!unsupportedObjectServices.isEmpty()) {
throw new UnimplementedObjectServiceException(); throw new UnimplementedObjectServiceException();
} }
@@ -121,11 +128,12 @@ public class LoginFlow implements MutatingFlow {
} }
serviceExtensionUrisBuilder.add(uri); serviceExtensionUrisBuilder.add(uri);
} }
stopwatch.tick("LoginFlow serviceExtensionUrisBuilder");
Optional<Registrar> registrar = Registrar.loadByRegistrarIdCached(login.getClientId()); Optional<Registrar> registrar = Registrar.loadByRegistrarIdCached(login.getClientId());
if (registrar.isEmpty()) { if (registrar.isEmpty()) {
throw new BadRegistrarIdException(login.getClientId()); throw new BadRegistrarIdException(login.getClientId());
} }
stopwatch.tick("LoginFlow loadByRegistrarIdCached");
// AuthenticationErrorExceptions will propagate up through here. // AuthenticationErrorExceptions will propagate up through here.
try { try {
credentials.validate(registrar.get(), login.getPassword()); credentials.validate(registrar.get(), login.getPassword());
@@ -137,6 +145,7 @@ public class LoginFlow implements MutatingFlow {
throw e; throw e;
} }
} }
stopwatch.tick("LoginFlow credentials.validate");
if (!registrar.get().isLive()) { if (!registrar.get().isLive()) {
throw new RegistrarAccountNotActiveException(); throw new RegistrarAccountNotActiveException();
} }
@@ -145,17 +154,24 @@ public class LoginFlow implements MutatingFlow {
String newPassword = login.getNewPassword().get(); String newPassword = login.getNewPassword().get();
// Load fresh from database (bypassing the cache) to ensure we don't save stale data. // Load fresh from database (bypassing the cache) to ensure we don't save stale data.
Optional<Registrar> freshRegistrar = Registrar.loadByRegistrarId(login.getClientId()); Optional<Registrar> freshRegistrar = Registrar.loadByRegistrarId(login.getClientId());
stopwatch.tick("LoginFlow reload freshRegistrar");
if (freshRegistrar.isEmpty()) { if (freshRegistrar.isEmpty()) {
throw new BadRegistrarIdException(login.getClientId()); throw new BadRegistrarIdException(login.getClientId());
} }
tm().put(freshRegistrar.get().asBuilder().setPassword(newPassword).build()); tm().put(freshRegistrar.get().asBuilder().setPassword(newPassword).build());
stopwatch.tick("LoginFlow updated password");
} }
// We are in! // We are in!
sessionMetadata.resetFailedLoginAttempts(); sessionMetadata.resetFailedLoginAttempts();
stopwatch.tick("LoginFlow resetFailedLoginAttempts");
sessionMetadata.setRegistrarId(login.getClientId()); sessionMetadata.setRegistrarId(login.getClientId());
stopwatch.tick("LoginFlow setRegistrarId");
sessionMetadata.setServiceExtensionUris(serviceExtensionUrisBuilder.build()); sessionMetadata.setServiceExtensionUris(serviceExtensionUrisBuilder.build());
return responseBuilder.setIsLoginResponse().build(); stopwatch.tick("LoginFlow setServiceExtensionUris");
EppResponse eppResponse = responseBuilder.setIsLoginResponse().build();
stopwatch.tick("LoginFlow eppResponse build()");
return eppResponse;
} }
/** Registrar with this ID could not be found. */ /** Registrar with this ID could not be found. */

View File

@@ -30,6 +30,7 @@ import google.registry.request.auth.AuthModule.IapOidc;
import google.registry.request.auth.AuthModule.RegularOidc; import google.registry.request.auth.AuthModule.RegularOidc;
import google.registry.request.auth.AuthSettings.AuthLevel; import google.registry.request.auth.AuthSettings.AuthLevel;
import google.registry.util.RegistryEnvironment; import google.registry.util.RegistryEnvironment;
import google.registry.util.StopwatchLogger;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional; import java.util.Optional;
@@ -77,7 +78,9 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
logger.atWarning().log("Using AuthResult %s for testing.", authResultForTesting); logger.atWarning().log("Using AuthResult %s for testing.", authResultForTesting);
return authResultForTesting; return authResultForTesting;
} }
final StopwatchLogger stopwatch = new StopwatchLogger();
String rawIdToken = tokenExtractor.extract(request); String rawIdToken = tokenExtractor.extract(request);
stopwatch.tick("OidcTokenAuthenticationMechanism tokenExtractor extracted");
if (rawIdToken == null) { if (rawIdToken == null) {
return AuthResult.NOT_AUTHENTICATED; return AuthResult.NOT_AUTHENTICATED;
} }
@@ -99,7 +102,7 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
? "Raw token redacted in prod" ? "Raw token redacted in prod"
: rawIdToken); : rawIdToken);
} }
stopwatch.tick("OidcTokenAuthenticationMechanism token verified");
if (token == null) { if (token == null) {
return AuthResult.NOT_AUTHENTICATED; return AuthResult.NOT_AUTHENTICATED;
} }
@@ -111,6 +114,7 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
} }
Optional<User> maybeUser = Optional<User> maybeUser =
tm().transact(() -> tm().loadByKeyIfPresent(VKey.create(User.class, email))); tm().transact(() -> tm().loadByKeyIfPresent(VKey.create(User.class, email)));
stopwatch.tick("OidcTokenAuthenticationMechanism maybeUser loaded");
if (maybeUser.isPresent()) { if (maybeUser.isPresent()) {
return AuthResult.createUser(maybeUser.get()); return AuthResult.createUser(maybeUser.get());
} }

View File

@@ -0,0 +1,45 @@
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
//
// 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 google.registry.util;
import com.google.common.flogger.FluentLogger;
import java.time.Duration;
/**
* A helper class to log only if the time elapsed between calls is more than a specified threshold.
*/
public final class StopwatchLogger {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
Duration threshold = Duration.ofMillis(400);
private final long thresholdNanos;
private long lastTickNanos;
public StopwatchLogger() {
this.thresholdNanos = threshold.toNanos();
this.lastTickNanos = System.nanoTime();
}
public void tick(String message) {
long currentNanos = System.nanoTime();
long elapsedNanos = currentNanos - lastTickNanos;
// Only log if the elapsed time is over the threshold.
if (elapsedNanos > thresholdNanos) {
logger.atInfo().log("%s (took %d ms)", message, Duration.ofNanos(elapsedNanos).toMillis());
}
this.lastTickNanos = currentNanos;
}
}