Add metrics for ssh requests
- break them down by ssh command name
- include aggregate counts and errors by exception type
Release-Notes: Add metrics for ssh requests
Change-Id: I92ad23c67284f8215b21f57345ba2051f9eb86ab
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index a510e25..903c635 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -321,6 +321,17 @@
** `view`:
view implementation class
+=== SSH
+
+* `ssh/success_count`: Rate of successful SSH requests
+** `command_name`:
+ Name of the SSH command
+* `ssh/error_count`: Rate of SSH error responses
+** `command_name`:
+ Name of the SSH command
+** `exception`:
+ Name of the exception which has caused the request to fail.
+
=== Query
* `query/query_latency`: Successful query latency, accumulated over the life
diff --git a/java/com/google/gerrit/acceptance/InProcessProtocol.java b/java/com/google/gerrit/acceptance/InProcessProtocol.java
index abcc108..8907fae 100644
--- a/java/com/google/gerrit/acceptance/InProcessProtocol.java
+++ b/java/com/google/gerrit/acceptance/InProcessProtocol.java
@@ -324,7 +324,7 @@
.orElseThrow(
() -> new RuntimeException(String.format("project %s not found", req.project)));
- AsyncReceiveCommits arc = factory.create(projectState, identifiedUser, db, null);
+ AsyncReceiveCommits arc = factory.create(projectState, identifiedUser, db, null, null);
if (arc.canUpload() != Capable.OK) {
throw new ServiceNotAuthorizedException();
}
diff --git a/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index 1537655..b90ef35 100644
--- a/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -534,7 +534,7 @@
}
AsyncReceiveCommits arc =
- factory.create(state, userProvider.get().asIdentifiedUser(), db, null);
+ factory.create(state, userProvider.get().asIdentifiedUser(), db, null, null);
ReceivePack rp = arc.getReceivePack();
req.setAttribute(ATT_ARC, arc);
return rp;
diff --git a/java/com/google/gerrit/server/RequestCounter.java b/java/com/google/gerrit/server/RequestCounter.java
new file mode 100644
index 0000000..444521a
--- /dev/null
+++ b/java/com/google/gerrit/server/RequestCounter.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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 com.google.gerrit.server;
+
+import com.google.gerrit.common.Nullable;
+
+public interface RequestCounter {
+ /**
+ * Count a request.
+ *
+ * @param requestInfo information about the request
+ * @param error The exception which caused the request to fail, or null if the request was
+ * successful.
+ */
+ void countRequest(RequestInfo requestInfo, @Nullable Throwable error);
+}
diff --git a/java/com/google/gerrit/server/RequestInfo.java b/java/com/google/gerrit/server/RequestInfo.java
index 927985d8..6457b38 100644
--- a/java/com/google/gerrit/server/RequestInfo.java
+++ b/java/com/google/gerrit/server/RequestInfo.java
@@ -85,6 +85,13 @@
return requestUri().map(RequestInfo::redactRequestUri);
}
+ /**
+ * The command name of the SSH command.
+ *
+ * <p>Only set if request type is {@link RequestType#SSH}.
+ */
+ public abstract Optional<String> commandName();
+
/** The user that has sent the request. */
public abstract CurrentUser callingUser();
@@ -164,6 +171,18 @@
return builder().requestType(requestType).callingUser(callingUser).traceContext(traceContext);
}
+ public static RequestInfo.Builder builder(
+ RequestType requestType,
+ String commandName,
+ CurrentUser callingUser,
+ TraceContext traceContext) {
+ return builder()
+ .requestType(requestType)
+ .commandName(commandName)
+ .callingUser(callingUser)
+ .traceContext(traceContext);
+ }
+
@UsedAt(UsedAt.Project.GOOGLE)
public static RequestInfo.Builder builder() {
return new AutoValue_RequestInfo.Builder();
@@ -191,6 +210,8 @@
return this;
}
+ public abstract Builder commandName(String commandName);
+
public abstract Builder callingUser(CurrentUser callingUser);
public abstract Builder traceContext(TraceContext traceContext);
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index cb1af07..1d8aa05 100644
--- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -36,6 +36,7 @@
import com.google.gerrit.metrics.Timer1;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PublishCommentsOp;
+import com.google.gerrit.server.RequestCounter;
import com.google.gerrit.server.cache.PerThreadCache;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.ConfigUtil;
@@ -102,7 +103,8 @@
ProjectState projectState,
IdentifiedUser user,
Repository repository,
- @Nullable MessageSender messageSender);
+ @Nullable MessageSender messageSender,
+ @Nullable RequestCounter requestCounter);
}
public static class AsyncReceiveCommitsModule extends PrivateModule {
@@ -259,7 +261,8 @@
@Assisted ProjectState projectState,
@Assisted IdentifiedUser user,
@Assisted Repository repo,
- @Assisted @Nullable MessageSender messageSender)
+ @Assisted @Nullable MessageSender messageSender,
+ @Assisted @Nullable RequestCounter requestCounter)
throws PermissionBackendException {
this.multiProgressMonitorFactory = multiProgressMonitorFactory;
this.executor = executor;
@@ -305,7 +308,8 @@
projectName,
user.getAccountId()));
receiveCommits =
- factory.create(projectState, user, receivePack, repo, allRefsWatcher, messageSender);
+ factory.create(
+ projectState, user, receivePack, repo, allRefsWatcher, messageSender, requestCounter);
receiveCommits.init();
QuotaResponse.Aggregated availableTokens =
quotaBackend.user(user).project(projectName).availableTokens(REPOSITORY_SIZE_GROUP);
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 7544f95..19e2253 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -119,6 +119,7 @@
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.PublishCommentUtil;
import com.google.gerrit.server.PublishCommentsOp;
+import com.google.gerrit.server.RequestCounter;
import com.google.gerrit.server.RequestInfo;
import com.google.gerrit.server.RequestListener;
import com.google.gerrit.server.Sequences;
@@ -288,7 +289,8 @@
ReceivePack receivePack,
Repository repository,
AllRefsWatcher allRefsWatcher,
- MessageSender messageSender);
+ @Nullable MessageSender messageSender,
+ @Nullable RequestCounter requestCounter);
}
private class ReceivePackMessageSender implements MessageSender {
@@ -430,6 +432,7 @@
private final SetHashtagsOp.Factory hashtagsFactory;
private final SetTopicOp.Factory setTopicFactory;
private final ServiceUserClassifier serviceUserClassifier;
+ private final RequestCounter requestCounter;
private final ImmutableList<SubmissionListener> superprojectUpdateSubmissionListeners;
private final TagCache tagCache;
private final ProjectConfig.Factory projectConfigFactory;
@@ -535,7 +538,8 @@
@Assisted ReceivePack rp,
@Assisted Repository repository,
@Assisted AllRefsWatcher allRefsWatcher,
- @Nullable @Assisted MessageSender messageSender)
+ @Assisted @Nullable MessageSender messageSender,
+ @Assisted @Nullable RequestCounter requestCounter)
throws IOException {
// Injected fields.
this.accountResolver = accountResolver;
@@ -579,6 +583,7 @@
this.receiveConfig = receiveConfig;
this.refValidatorsFactory = refValidatorsFactory;
this.replaceOpFactory = replaceOpFactory;
+ this.requestCounter = requestCounter;
this.requestListeners = requestListeners;
this.retryHelper = retryHelper;
this.requestScopePropagator = requestScopePropagator;
@@ -705,7 +710,8 @@
TraceTimer traceTimer =
newTimer("processCommands", Metadata.builder().resourceCount(commandCount))) {
RequestInfo requestInfo =
- RequestInfo.builder(RequestInfo.RequestType.GIT_RECEIVE, user, traceContext)
+ RequestInfo.builder(
+ RequestInfo.RequestType.GIT_RECEIVE, "git-receive-pack", user, traceContext)
.project(project.getNameKey())
.build();
requestListeners.runEach(l -> l.onRequest(requestInfo));
@@ -721,6 +727,7 @@
commands =
commands.stream().map(c -> wrapReceiveCommand(c, commandProgress)).collect(toList());
+ Throwable error = null;
try (RequestStateContext requestStateContext =
RequestStateContext.open()
.addRequestStateProvider(progress)
@@ -731,9 +738,11 @@
commands,
RejectionReason.create(MetricBucket.INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR));
} catch (InvalidDeadlineException e) {
+ error = e;
rejectRemaining(
commands, RejectionReason.create(MetricBucket.INVALID_DEADLINE, e.getMessage()));
} catch (RuntimeException e) {
+ error = e;
Optional<RequestCancelledException> requestCancelledException =
RequestCancelledException.getFromCausalChain(e);
if (!requestCancelledException.isPresent()) {
@@ -763,6 +772,10 @@
}
rejectRemaining(commands, RejectionReason.create(metricBucket, msg.toString()));
+ } finally {
+ if (requestCounter != null) {
+ requestCounter.countRequest(requestInfo, error);
+ }
}
// This sends error messages before the 'done' string of the progress monitor is sent.
diff --git a/java/com/google/gerrit/server/logging/Metadata.java b/java/com/google/gerrit/server/logging/Metadata.java
index 33a49ae..60464e5 100644
--- a/java/com/google/gerrit/server/logging/Metadata.java
+++ b/java/com/google/gerrit/server/logging/Metadata.java
@@ -76,6 +76,9 @@
/** The cause of an error. */
public abstract Optional<String> cause();
+ /** The command name of an SSH request. */
+ public abstract Optional<String> commandName();
+
/** Side where the comment is written: <= 0 for parent, 1 for revision. */
public abstract Optional<Integer> commentSide();
@@ -88,6 +91,9 @@
/** The type of an event. */
public abstract Optional<String> eventType();
+ /** The name of an exception which failed an SSH request. */
+ public abstract Optional<String> exception();
+
/** The value of the @Export annotation which was used to register a plugin extension. */
public abstract Optional<String> exportValue();
@@ -192,8 +198,8 @@
* authDomainName=Optional.empty, branchName=Optional.empty, cacheKey=Optional.empty,
* cacheName=Optional.empty, caller=Optional.empty, className=Optional.empty,
* cancellationReason=Optional.empty, changeId=Optional[9212550], changeIdType=Optional.empty,
- * cause=Optional.empty, diffAlgorithm=Optional.empty, eventType=Optional.empty,
- * exportValue=Optional.empty, filePath=Optional.empty, garbageCollectorName=Optional.empty,
+ * cause=Optional.empty, commandName=Optional.empty, diffAlgorithm=Optional.empty, eventType=Optional.empty,
+ * exception=Optional.empty, exportValue=Optional.empty, filePath=Optional.empty, garbageCollectorName=Optional.empty,
* gitOperation=Optional.empty, groupId=Optional.empty, groupName=Optional.empty,
* groupUuid=Optional.empty, httpStatus=Optional.empty, indexName=Optional.empty,
* indexVersion=Optional[0], methodName=Optional.empty, multiple=Optional.empty,
@@ -235,10 +241,12 @@
.add("changeId", changeId().orElse(null))
.add("changeIdType", changeIdType().orElse(null))
.add("cause", cause().orElse(null))
+ .add("commandName", commandName().orElse(null))
.add("commentSide", commentSide().orElse(null))
.add("commit", commit().orElse(null))
.add("diffAlgorithm", diffAlgorithm().orElse(null))
.add("eventType", eventType().orElse(null))
+ .add("exception", exception().orElse(null))
.add("exportValue", exportValue().orElse(null))
.add("filePath", filePath().orElse(null))
.add("garbageCollectorName", garbageCollectorName().orElse(null))
@@ -314,6 +322,8 @@
public abstract Builder cause(@Nullable String cause);
+ public abstract Builder commandName(@Nullable String commandName);
+
public abstract Builder commentSide(int side);
public abstract Builder commit(@Nullable String commit);
@@ -322,6 +332,8 @@
public abstract Builder eventType(@Nullable String eventType);
+ public abstract Builder exception(@Nullable String exception);
+
public abstract Builder exportValue(@Nullable String exportValue);
public abstract Builder filePath(@Nullable String filePath);
diff --git a/java/com/google/gerrit/sshd/SshCommand.java b/java/com/google/gerrit/sshd/SshCommand.java
index a4e427d..359adae 100644
--- a/java/com/google/gerrit/sshd/SshCommand.java
+++ b/java/com/google/gerrit/sshd/SshCommand.java
@@ -46,6 +46,7 @@
@Inject @GerritServerConfig private Config config;
@Inject private DeadlineChecker.Factory deadlineCheckerFactory;
@Inject private CancellationMetrics cancellationMetrics;
+ @Inject private SshMetrics sshMetrics;
@Option(name = "--trace", usage = "enable request tracing")
private boolean trace;
@@ -72,7 +73,9 @@
PerformanceLogContext performanceLogContext =
new PerformanceLogContext(config, performanceLoggers)) {
RequestInfo requestInfo =
- RequestInfo.builder(RequestInfo.RequestType.SSH, user, traceContext).build();
+ RequestInfo.builder(RequestInfo.RequestType.SSH, getName(), user, traceContext)
+ .build();
+ Throwable error = null;
try (RequestStateContext requestStateContext =
RequestStateContext.open()
.addRequestStateProvider(
@@ -80,8 +83,10 @@
requestListeners.runEach(l -> l.onRequest(requestInfo));
SshCommand.this.run();
} catch (InvalidDeadlineException e) {
+ error = e;
stderr.println(e.getMessage());
} catch (RuntimeException e) {
+ error = e;
Optional<RequestCancelledException> requestCancelledException =
RequestCancelledException.getFromCausalChain(e);
if (!requestCancelledException.isPresent()) {
@@ -97,6 +102,8 @@
" (%s)", requestCancelledException.get().getCancellationMessage().get()));
}
stderr.println(msg.toString());
+ } finally {
+ sshMetrics.countRequest(requestInfo, error);
}
} finally {
stdout.flush();
diff --git a/java/com/google/gerrit/sshd/SshMetrics.java b/java/com/google/gerrit/sshd/SshMetrics.java
new file mode 100644
index 0000000..2e1ade3
--- /dev/null
+++ b/java/com/google/gerrit/sshd/SshMetrics.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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 com.google.gerrit.sshd;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Counter2;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.RequestCounter;
+import com.google.gerrit.server.RequestInfo;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class SshMetrics implements RequestCounter {
+ private final Counter1<String> count;
+ private final Counter2<String, String> errorCount;
+
+ @Inject
+ SshMetrics(MetricMaker metrics) {
+ this.count =
+ metrics.newCounter(
+ "ssh/success_count",
+ new Description("Count of successful ssh requests").setRate(),
+ Field.ofString("command_name", Metadata.Builder::commandName)
+ .description("The command name of the request.")
+ .build());
+
+ this.errorCount =
+ metrics.newCounter(
+ "ssh/error_count",
+ new Description("Number of failed requests").setRate(),
+ Field.ofString("command_name", Metadata.Builder::commandName)
+ .description("The command name of the request.")
+ .build(),
+ Field.ofString("exception", Metadata.Builder::exception)
+ .description("Exception that failed the request.")
+ .build());
+ }
+
+ @Override
+ public void countRequest(RequestInfo requestInfo, @Nullable Throwable error) {
+ if (error == null) {
+ count.increment(requestInfo.commandName().get());
+ } else {
+ errorCount.increment(requestInfo.commandName().get(), error.getClass().getSimpleName());
+ }
+ }
+}
diff --git a/java/com/google/gerrit/sshd/commands/Receive.java b/java/com/google/gerrit/sshd/commands/Receive.java
index 3f2e2ad..f34b08b 100644
--- a/java/com/google/gerrit/sshd/commands/Receive.java
+++ b/java/com/google/gerrit/sshd/commands/Receive.java
@@ -28,6 +28,7 @@
import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.sshd.AbstractGitCommand;
import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshMetrics;
import com.google.inject.Inject;
import java.io.IOException;
import org.eclipse.jgit.errors.TooLargeObjectInPackException;
@@ -45,6 +46,7 @@
@Inject private AsyncReceiveCommits.Factory factory;
@Inject private PermissionBackend permissionBackend;
+ @Inject private SshMetrics sshMetrics;
private final SetMultimap<ReviewerStateInternal, Account.Id> reviewers =
MultimapBuilder.hashKeys(2).hashSetValues().build();
@@ -82,7 +84,7 @@
}
AsyncReceiveCommits arc =
- factory.create(projectState, currentUser.asIdentifiedUser(), repo, null);
+ factory.create(projectState, currentUser.asIdentifiedUser(), repo, null, sshMetrics);
try {
Capable r = arc.canUpload();
diff --git a/java/com/google/gerrit/sshd/commands/Upload.java b/java/com/google/gerrit/sshd/commands/Upload.java
index b80b879..7977895 100644
--- a/java/com/google/gerrit/sshd/commands/Upload.java
+++ b/java/com/google/gerrit/sshd/commands/Upload.java
@@ -33,6 +33,7 @@
import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.gerrit.sshd.AbstractGitCommand;
+import com.google.gerrit.sshd.SshMetrics;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.List;
@@ -54,6 +55,7 @@
@Inject private UploadValidators.Factory uploadValidatorsFactory;
@Inject private PermissionBackend permissionBackend;
@Inject private UsersSelfAdvertiseRefsHook usersSelfAdvertiseRefsHook;
+ @Inject private SshMetrics sshMetrics;
private PackStatistics stats;
@@ -93,21 +95,30 @@
try (TraceContext traceContext = TraceContext.open();
TracingHook tracingHook = new TracingHook()) {
RequestInfo requestInfo =
- RequestInfo.builder(RequestInfo.RequestType.GIT_UPLOAD, user, traceContext)
+ RequestInfo.builder(RequestInfo.RequestType.GIT_UPLOAD, getName(), user, traceContext)
.project(projectState.getNameKey())
.build();
- requestListeners.runEach(l -> l.onRequest(requestInfo));
- up.setProtocolV2Hook(tracingHook);
- up.upload(in, out, err);
- session.setPeerAgent(up.getPeerUserAgent());
- stats = up.getStatistics();
- } catch (UploadValidationException e) {
- // UploadValidationException is used by the UploadValidators to
- // stop the uploadPack. We do not want this exception to go beyond this
- // point otherwise it would print a stacktrace in the logs and return an
- // internal server error to the client.
- if (!e.isOutput()) {
- up.sendMessage(e.getMessage());
+ Throwable error = null;
+ try {
+ requestListeners.runEach(l -> l.onRequest(requestInfo));
+ up.setProtocolV2Hook(tracingHook);
+ up.upload(in, out, err);
+ session.setPeerAgent(up.getPeerUserAgent());
+ stats = up.getStatistics();
+ } catch (UploadValidationException e) {
+ error = e;
+ // UploadValidationException is used by the UploadValidators to
+ // stop the uploadPack. We do not want this exception to go beyond this
+ // point otherwise it would print a stacktrace in the logs and return an
+ // internal server error to the client.
+ if (!e.isOutput()) {
+ up.sendMessage(e.getMessage());
+ }
+ } catch (RuntimeException e) {
+ error = e;
+ throw e;
+ } finally {
+ sshMetrics.countRequest(requestInfo, error);
}
}
}