google-nomulus/java/google/registry/flows/FlowRunner.java
nickfelt a4091594d0 Add new JSON logging to FlowRunner for EPP activity reporting
We've been using the very fragile newline-delimited legacy logging
statement in FlowRunner for ICANN reporting for a long time.  While
this is bad in a few ways, the worst is that the parsing of this
logging statement is extremely fragile (e.g. adding/removing fields
can easily break the parsing).  This is in fact part of what broke the
ExportLogsServlet parsing last fall ([] and forced us to
recover by manually parsing the log statement (and its XML) in
BigQuery.  It also broke again in [] where we were relying
on matching the logging classname, since matching on 'EPP Command'
was considered insufficiently narrow.

This introduces a new JSON-format logging statement to FlowRunner
that fixes both of these problems:

 1) it replaces the newline-delimited "format" with a JSON-based
    format, so that we can add new fields much more easily and
    reliably support logging more structured data

 2) it replaces the short 'EPP Command' signature with a much more
    targeted 'EPP-REPORTING-LOG-SIGNATURE' signature so that we can
    use that alone for matching, rather than relying on the class
    name in the log message

What this doesn't fix is the fact that we still need to parse the
XML in BigQuery; we should fix this by logging the parts of the XML
that ICANN reporting needs explicitly, but that'll be a subsequent
change, since while the existing approach is gross, it's actually
much less fragile than just matching the log statement itself.
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=125902976
2016-06-27 16:45:55 -04:00

205 lines
7.8 KiB
Java

// Copyright 2016 The Domain Registry 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.flows;
import static com.google.common.base.Strings.nullToEmpty;
import static com.google.common.base.Throwables.getStackTraceAsString;
import static com.google.common.io.BaseEncoding.base64;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.xml.XmlTransformer.prettyPrint;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.googlecode.objectify.Work;
import google.registry.flows.FlowModule.ClientId;
import google.registry.flows.FlowModule.DryRun;
import google.registry.flows.FlowModule.InputXml;
import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.Transactional;
import google.registry.model.eppcommon.Trid;
import google.registry.model.eppinput.EppInput;
import google.registry.model.eppoutput.EppOutput;
import google.registry.monitoring.whitebox.EppMetrics;
import google.registry.util.Clock;
import google.registry.util.FormattingLogger;
import org.joda.time.DateTime;
import org.json.simple.JSONValue;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Provider;
/** Run a flow, either transactionally or not, with logging and retrying as needed. */
public class FlowRunner {
/** Log format used by legacy ICANN reporting parsing - DO NOT CHANGE. */
// TODO(b/20725722): remove this log format entirely once we've transitioned to using the
// JSON log line below instead, or change this one to be for human consumption only.
private static final String COMMAND_LOG_FORMAT = "EPP Command" + Strings.repeat("\n\t%s", 7);
/**
* Log signature used by reporting pipelines to extract matching log lines.
*
* <p><b>WARNING:<b/> DO NOT CHANGE this value unless you want to break reporting.
*/
private static final String REPORTING_LOG_SIGNATURE = "EPP-REPORTING-LOG-SIGNATURE";
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
@Inject @Nullable @ClientId String clientId;
@Inject Clock clock;
@Inject TransportCredentials credentials;
@Inject EppInput eppInput;
@Inject EppRequestSource eppRequestSource;
@Inject Provider<Flow> flowProvider;
@Inject @InputXml byte[] inputXmlBytes;
@Inject @DryRun boolean isDryRun;
@Inject @Superuser boolean isSuperuser;
@Inject @Transactional boolean isTransactional;
@Inject EppMetrics metrics;
@Inject SessionMetadata sessionMetadata;
@Inject Trid trid;
@Inject FlowRunner() {}
public EppOutput run() throws EppException {
String prettyXml = prettyPrint(inputXmlBytes);
String xmlBase64 = base64().encode(inputXmlBytes);
// This log line is very fragile since it's used for ICANN reporting - DO NOT CHANGE.
// New data to be logged should be added only to the JSON log statement below.
// TODO(b/20725722): remove this log statement entirely once we've transitioned to using the
// log line below instead, or change this one to be for human consumption only.
logger.infofmt(
COMMAND_LOG_FORMAT,
trid.getServerTransactionId(),
clientId,
sessionMetadata,
prettyXml.replaceAll("\n", "\n\t"),
credentials,
eppRequestSource,
isDryRun ? "DRY_RUN" : "LIVE",
isSuperuser ? "SUPERUSER" : "NORMAL");
// WARNING: This JSON log statement is parsed by reporting pipelines - be careful when changing.
// It should be safe to add new keys, but be very cautious in changing existing keys.
logger.infofmt(
"%s: %s",
REPORTING_LOG_SIGNATURE,
JSONValue.toJSONString(ImmutableMap.<String, Object>of(
"trid", trid.getServerTransactionId(),
"clientId", nullToEmpty(clientId),
"xml", prettyXml,
"xmlBytes", xmlBase64)));
if (!isTransactional) {
metrics.incrementAttempts();
return createAndInitFlow(clock.nowUtc()).run();
}
// We log the command in a structured format. Note that we do this before the transaction;
// if we did it after, we might miss a transaction that committed successfully but then crashed
// before it could log.
logger.info("EPP_Mutation " + new JsonLogStatement(trid)
.add("client", clientId)
.add("privileges", isSuperuser ? "SUPERUSER" : "NORMAL")
.add("xmlBytes", xmlBase64));
try {
EppOutput flowResult = ofy().transact(new Work<EppOutput>() {
@Override
public EppOutput run() {
metrics.incrementAttempts();
try {
EppOutput output = createAndInitFlow(ofy().getTransactionTime()).run();
if (isDryRun) {
throw new DryRunException(output);
}
return output;
} catch (EppException e) {
throw new RuntimeException(e);
}
}});
logger.info("EPP_Mutation_Committed " + new JsonLogStatement(trid)
.add("createdRepoId", flowResult.getResponse().getCreatedRepoId())
.add("executionTime", flowResult.getResponse().getExecutionTime().getMillis()));
return flowResult;
} catch (DryRunException e) {
return e.output;
} catch (RuntimeException e) {
logger.warning("EPP_Mutation_Failed " + new JsonLogStatement(trid));
logger.warning(getStackTraceAsString(e));
if (e.getCause() instanceof EppException) {
throw (EppException) e.getCause();
}
throw e;
}
}
private Flow createAndInitFlow(DateTime now) throws EppException {
return flowProvider.get().init(
eppInput,
trid,
sessionMetadata,
credentials,
isSuperuser,
now);
}
/**
* Helper for logging in json format.
*
* <p>This is needed because the usual json outputters perform normalizations that we don't want
* or need, since we know that our values never need to be escaped - there are only strings and
* numbers, and the strings are not allowed to contain quote characters.
*
* <p>An example output for an EPP_Mutation: {"trid":"abc-123", "client":"some_registrar",
* "tld":"com", "xmlBytes":"abc123DEF"}
*
* <p>An example output for an EPP_Mutation_Committed that doesn't create a new resource:
* {"trid":"abc-123", "executionTime":123456789}
*
* <p>An example output for an EPP_Mutation_Committed that creates a new resource:
* {"trid":"abc-123", "executionRepoId":123, "executionTime":123456789}
*/
private static class JsonLogStatement {
StringBuilder message;
JsonLogStatement(Trid trid) {
message =
new StringBuilder("{\"trid\":\"").append(trid.getServerTransactionId()).append('\"');
}
JsonLogStatement add(String key, Object value) {
if (value != null) {
String quote = value instanceof String ? "\"" : "";
message.append(String.format(", \"%s\":%s%s%s", key, quote, value, quote));
}
return this;
}
@Override
public String toString() {
return message + "}";
}
}
/** Exception for canceling a transaction while capturing what the output would have been. */
private static class DryRunException extends RuntimeException {
final EppOutput output;
DryRunException(EppOutput output) {
this.output = output;
}
}
}