Merge changes I4eb349fb,Ib51147ea,I14ae3f05

* changes:
  Record in reject count metric whether the push was done by a service user
  ReceiveCommits: Call ExceptionHook to get status for exceptions
  ReceiveCommits: Log RuntimeException on severe level
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 1ca750b..a9b82d6 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -124,8 +124,10 @@
    DELETE).
 * `receivecommits/reject_count`: number of rejected pushes
 ** `kind`:
-   The push kind ('magic' if it was a push for code review, 'direct' if it was
-   a direct push or 'n/a' if the push kind couldn't be detected).
+   The push kind ('magic push'/'magic push by service user' if it was a push for
+   code review, 'direct push'/'direct push by service user' if it was a direct
+   push, 'magic push by service, 'magic or direct push'/'magic or direct push by
+   service user' if the push kind couldn't be detected).
 ** `reason`:
    The rejection reason.
 ** `status`:
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 9f7b763..536e1fb 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -124,6 +124,7 @@
 import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
+import com.google.gerrit.server.account.ServiceUserClassifier;
 import com.google.gerrit.server.approval.ApprovalsUtil;
 import com.google.gerrit.server.cancellation.RequestCancelledException;
 import com.google.gerrit.server.cancellation.RequestStateContext;
@@ -234,6 +235,7 @@
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
+import java.util.logging.Level;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
@@ -426,6 +428,7 @@
   private final Sequences seq;
   private final SetHashtagsOp.Factory hashtagsFactory;
   private final SetTopicOp.Factory setTopicFactory;
+  private final ServiceUserClassifier serviceUserClassifier;
   private final ImmutableList<SubmissionListener> superprojectUpdateSubmissionListeners;
   private final TagCache tagCache;
   private final ProjectConfig.Factory projectConfigFactory;
@@ -516,6 +519,7 @@
       Sequences seq,
       SetHashtagsOp.Factory hashtagsFactory,
       SetTopicOp.Factory setTopicFactory,
+      ServiceUserClassifier serviceUserClassifier,
       @SuperprojectUpdateOnSubmission
           ImmutableList<SubmissionListener> superprojectUpdateSubmissionListeners,
       TagCache tagCache,
@@ -550,6 +554,7 @@
     this.exceptionHooks = exceptionHooks;
     this.hashtagsFactory = hashtagsFactory;
     this.setTopicFactory = setTopicFactory;
+    this.serviceUserClassifier = serviceUserClassifier;
     this.indexer = indexer;
     this.initializers = initializers;
     this.mergeOpProvider = mergeOpProvider;
@@ -762,8 +767,15 @@
       logger.atFine().log("Processing commands done.");
     } catch (RuntimeException e) {
       String formattedCause = getFormattedCause(e).orElse(e.getClass().getSimpleName());
-      logger.atFine().withCause(e).log("ReceiveCommits failed due to %s", formattedCause);
-      metrics.rejectCount.increment("n/a", formattedCause, SC_INTERNAL_SERVER_ERROR);
+      int statusCode =
+          getStatus(e).map(ExceptionHook.Status::statusCode).orElse(SC_INTERNAL_SERVER_ERROR);
+      logger.at(statusCode < SC_INTERNAL_SERVER_ERROR ? Level.INFO : Level.SEVERE).withCause(e).log(
+          "ReceiveCommits failed due to %s", formattedCause);
+      String pushKind = "magic or direct push";
+      if (serviceUserClassifier.isServiceUser(user.getAccountId())) {
+        pushKind += " by service user";
+      }
+      metrics.rejectCount.increment(pushKind, formattedCause, statusCode);
       throw e;
     }
     progress.end();
@@ -778,6 +790,14 @@
         .findFirst();
   }
 
+  private Optional<ExceptionHook.Status> getStatus(Throwable err) {
+    return exceptionHooks.stream()
+        .map(h -> h.getStatus(err))
+        .filter(Optional::isPresent)
+        .map(Optional::get)
+        .findFirst();
+  }
+
   // Process as many commands as possible, but may leave some commands in state NOT_ATTEMPTED.
   private void processCommandsUnsafe(
       Collection<ReceiveCommand> commands, MultiProgressMonitor progress) {
@@ -3934,10 +3954,13 @@
 
   private void reject(ReceiveCommand cmd, RejectionReason reason) {
     logger.atFine().log("Rejecting command '%s': %s", cmd, reason.why());
-    metrics.rejectCount.increment(
-        MagicBranch.isMagicBranch(cmd.getRefName()) ? "magic" : "direct",
-        reason.metricBucket(),
-        reason.statusCode());
+
+    String pushKind = (MagicBranch.isMagicBranch(cmd.getRefName()) ? "magic push" : "direct push");
+    if (serviceUserClassifier.isServiceUser(user.getAccountId())) {
+      pushKind += " by service user";
+    }
+    metrics.rejectCount.increment(pushKind, reason.metricBucket(), reason.statusCode());
+
     cmd.setResult(REJECTED_OTHER_REASON, reason.why());
   }