diff --git a/java/google/registry/tools/BUILD b/java/google/registry/tools/BUILD index 125690100..1574bd85d 100644 --- a/java/google/registry/tools/BUILD +++ b/java/google/registry/tools/BUILD @@ -70,6 +70,7 @@ java_library( "@com_google_api_client", "@com_google_apis_google_api_services_bigquery", "@com_google_apis_google_api_services_dns", + "@com_google_apis_google_api_services_monitoring", "@com_google_appengine_api_1_0_sdk", "@com_google_appengine_remote_api", "@com_google_appengine_remote_api//:link", @@ -81,6 +82,8 @@ java_library( "@com_google_guava", "@com_google_http_client", "@com_google_http_client_jackson2", + "@com_google_monitoring_client_metrics", + "@com_google_monitoring_client_stackdriver", "@com_google_oauth_client", "@com_google_oauth_client_java6", "@com_google_oauth_client_jetty", diff --git a/java/google/registry/tools/MetricToolModule.java b/java/google/registry/tools/MetricToolModule.java new file mode 100644 index 000000000..6ed06998b --- /dev/null +++ b/java/google/registry/tools/MetricToolModule.java @@ -0,0 +1,53 @@ +// Copyright 2018 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.tools; + +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; +import com.google.api.services.monitoring.v3.Monitoring; +import com.google.api.services.monitoring.v3.model.MonitoredResource; +import com.google.monitoring.metrics.MetricWriter; +import com.google.monitoring.metrics.stackdriver.StackdriverWriter; +import dagger.Module; +import dagger.Provides; +import google.registry.config.CredentialModule.DefaultCredential; +import google.registry.config.RegistryConfig.Config; + +/** Dagger module for metrics on the client tool. */ +@Module +public final class MetricToolModule { + + @Provides + static Monitoring provideMonitoring( + @DefaultCredential GoogleCredential credential, @Config("projectId") String projectId) { + return new Monitoring.Builder( + credential.getTransport(), credential.getJsonFactory(), credential) + .setApplicationName(projectId) + .build(); + } + + @Provides + static MetricWriter provideMetricWriter( + Monitoring monitoringClient, + @Config("projectId") String projectId, + @Config("stackdriverMaxQps") int maxQps, + @Config("stackdriverMaxPointsPerRequest") int maxPointsPerRequest) { + return new StackdriverWriter( + monitoringClient, + projectId, + new MonitoredResource().setType("global"), + maxQps, + maxPointsPerRequest); + } +} diff --git a/java/google/registry/tools/RegistryCli.java b/java/google/registry/tools/RegistryCli.java index f0c3b5798..8a9061975 100644 --- a/java/google/registry/tools/RegistryCli.java +++ b/java/google/registry/tools/RegistryCli.java @@ -26,10 +26,19 @@ import com.beust.jcommander.Parameter; import com.beust.jcommander.ParameterException; import com.beust.jcommander.Parameters; import com.beust.jcommander.ParametersDelegate; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; +import com.google.monitoring.metrics.IncrementableMetric; +import com.google.monitoring.metrics.LabelDescriptor; +import com.google.monitoring.metrics.Metric; +import com.google.monitoring.metrics.MetricPoint; +import com.google.monitoring.metrics.MetricRegistryImpl; +import com.google.monitoring.metrics.MetricWriter; import google.registry.model.ofy.ObjectifyService; import google.registry.tools.params.ParameterFactory; +import java.io.IOException; import java.security.Security; import java.util.Map; import org.bouncycastle.jce.provider.BouncyCastleProvider; @@ -38,6 +47,11 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider; @Parameters(separators = " =", commandDescription = "Command-line interface to the registry") final class RegistryCli implements AutoCloseable, CommandRunner { + // The environment parameter is parsed twice: once here, and once with {@link + // RegistryToolEnvironment#parseFromArgs} in the {@link RegistryTool#main} or {@link + // GtechTool#main} functions. + // + // The flag names must be in sync between the two, and also - this is ugly and we should feel bad. @Parameter( names = {"-e", "--environment"}, description = "Sets the default environment to run the command.") @@ -48,6 +62,9 @@ final class RegistryCli implements AutoCloseable, CommandRunner { description = "Returns all command names.") private boolean showAllCommands; + @VisibleForTesting + boolean uploadMetrics = true; + // Do not make this final - compile-time constant inlining may interfere with JCommander. @ParametersDelegate private AppEngineConnectionFlags appEngineConnectionFlags = @@ -68,6 +85,24 @@ final class RegistryCli implements AutoCloseable, CommandRunner { // "shell". private boolean isFirstUse = true; + private static final ImmutableSet LABEL_DESCRIPTORS_FOR_COMMANDS = + ImmutableSet.of( + LabelDescriptor.create("program", "The program used - e.g. nomulus or gtech_tool"), + LabelDescriptor.create("environment", "The environment used - e.g. sandbox"), + LabelDescriptor.create("command", "The command used"), + LabelDescriptor.create("success", "Whether the command succeeded"), + LabelDescriptor.create("shell", "Whether the command was called from the nomulus shell")); + + private static final IncrementableMetric commandsCalledCount = + MetricRegistryImpl.getDefault() + .newIncrementableMetric( + "/tools/commands_called", + "Count of tool commands called", + "count", + LABEL_DESCRIPTORS_FOR_COMMANDS); + + private MetricWriter metricWriter = null; + Map> commands; String programName; @@ -89,9 +124,13 @@ final class RegistryCli implements AutoCloseable, CommandRunner { // http://www.angelikalanger.com/GenericsFAQ/FAQSections/TypeArguments.html#FAQ104 @Override public void run(String[] args) throws Exception { + boolean inShell = !isFirstUse; + isFirstUse = false; // Create the JCommander instance. - JCommander jcommander = new JCommander(this); + // If we're in the shell, we don't want to update the RegistryCli's parameters (so we give a + // dummy object to update) + JCommander jcommander = new JCommander(inShell ? new Object() : this); jcommander.addConverterFactory(new ParameterFactory()); jcommander.setProgramName(programName); @@ -110,8 +149,8 @@ final class RegistryCli implements AutoCloseable, CommandRunner { // Create the "help" and "shell" commands (these are special in that they don't have a default // constructor). jcommander.addCommand("help", new HelpCommand(jcommander)); - if (isFirstUse) { - isFirstUse = false; + if (!inShell) { + // If we aren't inside a shell, then we want to add the shell command. ShellCommand shellCommand = new ShellCommand(this); // We have to build the completions based on the jcommander *before* we add the "shell" // command - to avoid completion for the "shell" command itself. @@ -153,17 +192,31 @@ final class RegistryCli implements AutoCloseable, CommandRunner { jcommander.getCommands().get(jcommander.getParsedCommand()).getObjects()); loggingParams.configureLogging(); // Must be called after parameters are parsed. + boolean success = false; try { runCommand(command); + success = true; } catch (AuthModule.LoginRequiredException ex) { System.err.println("==================================================================="); System.err.println("You must login using 'nomulus login' prior to running this command."); System.err.println("==================================================================="); + } finally { + commandsCalledCount.increment( + programName, + environment.toString(), + command.getClass().getSimpleName(), + String.valueOf(success), + String.valueOf(inShell)); + exportMetrics(); } } @Override public void close() { + exportMetrics(); + if (metricWriter != null) { + metricWriter = null; + } if (installer != null) { installer.uninstall(); installer = null; @@ -180,6 +233,9 @@ final class RegistryCli implements AutoCloseable, CommandRunner { private void runCommand(Command command) throws Exception { injectReflectively(RegistryToolComponent.class, component, command); + if (metricWriter == null && uploadMetrics) { + metricWriter = component.metricWriter(); + } if (command instanceof CommandWithConnection) { ((CommandWithConnection) command).setConnection(getConnection()); @@ -211,6 +267,25 @@ final class RegistryCli implements AutoCloseable, CommandRunner { command.run(); } + private void exportMetrics() { + if (metricWriter == null) { + return; + } + try { + for (Metric metric : MetricRegistryImpl.getDefault().getRegisteredMetrics()) { + for (MetricPoint point : metric.getTimestampedValues()) { + metricWriter.write(point); + } + } + metricWriter.flush(); + } catch (IOException e) { + System.err.format("Failed to export metrics. Got error:\n%s\n\n", e); + System.err.println("Maybe you need to login? Try calling:"); + System.err.println(" gcloud auth application-default login"); + } + } + + @VisibleForTesting void setEnvironment(RegistryToolEnvironment environment) { this.environment = environment; } diff --git a/java/google/registry/tools/RegistryToolComponent.java b/java/google/registry/tools/RegistryToolComponent.java index f3b89be95..ae08d817e 100644 --- a/java/google/registry/tools/RegistryToolComponent.java +++ b/java/google/registry/tools/RegistryToolComponent.java @@ -14,6 +14,7 @@ package google.registry.tools; +import com.google.monitoring.metrics.MetricWriter; import dagger.Component; import google.registry.config.CredentialModule; import google.registry.config.RegistryConfig.ConfigModule; @@ -27,6 +28,7 @@ import google.registry.request.Modules.AppIdentityCredentialModule; import google.registry.request.Modules.DatastoreServiceModule; import google.registry.request.Modules.GoogleCredentialModule; import google.registry.request.Modules.Jackson2Module; +import google.registry.request.Modules.NetHttpTransportModule; import google.registry.request.Modules.URLFetchServiceModule; import google.registry.request.Modules.UrlFetchTransportModule; import google.registry.request.Modules.UseAppIdentityCredentialForGoogleApisModule; @@ -63,6 +65,7 @@ import javax.inject.Singleton; Jackson2Module.class, KeyModule.class, KmsModule.class, + NetHttpTransportModule.class, RdeModule.class, RegistryToolModule.class, SystemClockModule.class, @@ -74,6 +77,7 @@ import javax.inject.Singleton; UserServiceModule.class, VoidDnsWriterModule.class, WhoisModule.class, + MetricToolModule.class, }) interface RegistryToolComponent { void inject(CheckDomainClaimsCommand command); @@ -111,4 +115,6 @@ interface RegistryToolComponent { void inject(WhoisQueryCommand command); AppEngineConnection appEngineConnection(); + + MetricWriter metricWriter(); } diff --git a/java/google/registry/tools/logging.properties b/java/google/registry/tools/logging.properties index f724c987e..d66a2fcff 100644 --- a/java/google/registry/tools/logging.properties +++ b/java/google/registry/tools/logging.properties @@ -2,4 +2,7 @@ handlers = java.util.logging.ConsoleHandler .level = INFO com.google.wrappers.base.GoogleInit.level = WARNING com.google.monitoring.metrics.MetricRegistryImpl.level = WARNING +com.google.monitoring.metrics.MetricReporter.level = WARNING +com.google.monitoring.metrics.MetricExporter.level = WARNING +com.google.monitoring.metrics.stackdriver.StackdriverWriter.level = WARNING diff --git a/javatests/google/registry/tools/ShellCommandTest.java b/javatests/google/registry/tools/ShellCommandTest.java index af9a9ebca..bc6a1447d 100644 --- a/javatests/google/registry/tools/ShellCommandTest.java +++ b/javatests/google/registry/tools/ShellCommandTest.java @@ -152,6 +152,7 @@ public class ShellCommandTest { public void testMultipleCommandInvocations() throws Exception { try (RegistryCli cli = new RegistryCli("unittest", ImmutableMap.of("test_command", TestCommand.class))) { + cli.uploadMetrics = false; RegistryToolEnvironment.UNITTEST.setup(); cli.setEnvironment(RegistryToolEnvironment.UNITTEST); cli.run(new String[] {"test_command", "-x", "xval", "arg1", "arg2"}); @@ -169,7 +170,7 @@ public class ShellCommandTest { public void testNonExistentCommand() { try (RegistryCli cli = new RegistryCli("unittest", ImmutableMap.of("test_command", TestCommand.class))) { - + cli.uploadMetrics = false; cli.setEnvironment(RegistryToolEnvironment.UNITTEST); assertThrows(MissingCommandException.class, () -> cli.run(new String[] {"bad_command"})); }