Merge changes I264c7525,I0c2fe374

* changes:
  Allow to trace REST requests that match a URI regex by config
  Allow to configure automatic tracing by project or calling user
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index ecc2ba0..1601d7a 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -4552,6 +4552,52 @@
 +
 By default, true.
 
+[[tracing.traceid]]
+==== Subsection tracing.<trace-id>
+
+There can be multiple `tracing.<trace-id>` subsections to configure
+automatic tracing of requests. To be traced a request must match all
+conditions of one `tracing.<trace-id>` subsection. The subsection name
+is used as trace ID. Using this trace ID administrators can find
+matching log entries.
+
+[[tracing.traceid.requestType]]tracing.<trace-id>.requestType::
++
+Type of request for which request tracing should be always enabled (can
+be `GIT_RECEIVE`, `GIT_UPLOAD`, `REST` and `SSH`).
++
+May be specified multiple times.
++
+By default, unset (all request types are matched).
+
+[[tracing.traceid.requestUriPattern]]tracing.<trace-id>.requestUriPattern::
++
+Regular expression to match request URIs for which request tracing
+should be always enabled. Request URIs are only available for REST
+requests. Request URIs never include the '/a' prefix.
++
+May be specified multiple times.
++
+By default, unset (all request URIs are matched).
+
+[[tracing.traceid.account]]tracing.<trace-id>.account::
++
+Account ID of an account for which request tracing should be always
+enabled.
++
+May be specified multiple times.
++
+By default, unset (all accounts are matched).
+
+[[tracing.traceid.projectPattern]]tracing.<trace-id>.projectPattern::
++
+Regular expression to match project names for which request tracing
+should be always enabled.
++
+May be specified multiple times.
++
+By default, unset (all projects are matched).
+
 [[trackingid]]
 === Section trackingid
 
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 736018a..2128777b 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -317,7 +317,7 @@
     try (TraceContext traceContext = enableTracing(req, res)) {
       List<IdString> path = splitPath(req);
 
-      RequestInfo requestInfo = createRequestInfo(traceContext, path);
+      RequestInfo requestInfo = createRequestInfo(traceContext, requestUri(req), path);
       globals.requestListeners.runEach(l -> l.onRequest(requestInfo));
 
       try (PerThreadCache ignored = PerThreadCache.create()) {
@@ -1424,9 +1424,11 @@
     return traceContext;
   }
 
-  private RequestInfo createRequestInfo(TraceContext traceContext, List<IdString> path) {
+  private RequestInfo createRequestInfo(
+      TraceContext traceContext, String requestUri, List<IdString> path) {
     RequestInfo.Builder requestInfo =
-        RequestInfo.builder(RequestInfo.RequestType.REST, globals.currentUser.get(), traceContext);
+        RequestInfo.builder(RequestInfo.RequestType.REST, globals.currentUser.get(), traceContext)
+            .requestUri(requestUri);
 
     if (path.size() < 1) {
       return requestInfo.build();
diff --git a/java/com/google/gerrit/server/RequestInfo.java b/java/com/google/gerrit/server/RequestInfo.java
index c86d34d..f5749fc 100644
--- a/java/com/google/gerrit/server/RequestInfo.java
+++ b/java/com/google/gerrit/server/RequestInfo.java
@@ -45,6 +45,15 @@
    */
   public abstract String requestType();
 
+  /**
+   * Request URI.
+   *
+   * <p>Only set if request type is {@link RequestType#REST}.
+   *
+   * <p>Never includes the "/a" prefix.
+   */
+  public abstract Optional<String> requestUri();
+
   /** The user that has sent the request. */
   public abstract CurrentUser callingUser();
 
@@ -74,6 +83,8 @@
       return requestType(requestType.name());
     }
 
+    public abstract Builder requestUri(String requestUri);
+
     public abstract Builder callingUser(CurrentUser callingUser);
 
     public abstract Builder traceContext(TraceContext traceContext);
diff --git a/java/com/google/gerrit/server/TraceRequestListener.java b/java/com/google/gerrit/server/TraceRequestListener.java
index 1ae39e1..773f712 100644
--- a/java/com/google/gerrit/server/TraceRequestListener.java
+++ b/java/com/google/gerrit/server/TraceRequestListener.java
@@ -14,12 +14,215 @@
 
 package com.google.gerrit.server;
 
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.logging.RequestId;
+import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import java.util.Optional;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
 
+/**
+ * Request listener that sets additional logging tags and enables tracing automatically if the
+ * request matches any tracing configuration in gerrit.config (see description of
+ * 'tracing.<trace-id>' subsection in config-gerrit.txt).
+ */
 @Singleton
 public class TraceRequestListener implements RequestListener {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final Config cfg;
+  private final ImmutableList<TraceConfig> traceConfigs;
+
+  @Inject
+  TraceRequestListener(@GerritServerConfig Config cfg) {
+    this.cfg = cfg;
+    this.traceConfigs = parseTraceConfigs();
+  }
+
   @Override
   public void onRequest(RequestInfo requestInfo) {
     requestInfo.project().ifPresent(p -> requestInfo.traceContext().addTag("project", p));
+    traceConfigs.stream()
+        .filter(traceConfig -> traceConfig.matches(requestInfo))
+        .forEach(
+            traceConfig ->
+                requestInfo
+                    .traceContext()
+                    .forceLogging()
+                    .addTag(RequestId.Type.TRACE_ID, traceConfig.traceId()));
+  }
+
+  private ImmutableList<TraceConfig> parseTraceConfigs() {
+    ImmutableList.Builder<TraceConfig> traceConfigs = ImmutableList.builder();
+
+    for (String traceId : cfg.getSubsections("tracing")) {
+      try {
+        TraceConfig.Builder traceConfig = TraceConfig.builder();
+        traceConfig.traceId(traceId);
+        traceConfig.requestTypes(parseRequestTypes(traceId));
+        traceConfig.requestUriPatterns(parseRequestUriPatterns(traceId));
+        traceConfig.accountIds(parseAccounts(traceId));
+        traceConfig.projectPatterns(parseProjectPatterns(traceId));
+        traceConfigs.add(traceConfig.build());
+      } catch (ConfigInvalidException e) {
+        logger.atWarning().log("Ignoring invalid tracing configuration:\n %s", e.getMessage());
+      }
+    }
+
+    return traceConfigs.build();
+  }
+
+  private ImmutableSet<String> parseRequestTypes(String traceId) {
+    return ImmutableSet.copyOf(cfg.getStringList("tracing", traceId, "requestType"));
+  }
+
+  private ImmutableSet<Pattern> parseRequestUriPatterns(String traceId)
+      throws ConfigInvalidException {
+    return parsePatterns(traceId, "requestUriPattern");
+  }
+
+  private ImmutableSet<Account.Id> parseAccounts(String traceId) throws ConfigInvalidException {
+    ImmutableSet.Builder<Account.Id> accountIds = ImmutableSet.builder();
+    String[] accounts = cfg.getStringList("tracing", traceId, "account");
+    for (String account : accounts) {
+      Optional<Account.Id> accountId = Account.Id.tryParse(account);
+      if (!accountId.isPresent()) {
+        throw new ConfigInvalidException(
+            String.format(
+                "Invalid tracing config ('tracing.%s.account = %s'): invalid account ID",
+                traceId, account));
+      }
+      accountIds.add(accountId.get());
+    }
+    return accountIds.build();
+  }
+
+  private ImmutableSet<Pattern> parseProjectPatterns(String traceId) throws ConfigInvalidException {
+    return parsePatterns(traceId, "projectPattern");
+  }
+
+  private ImmutableSet<Pattern> parsePatterns(String traceId, String name)
+      throws ConfigInvalidException {
+    ImmutableSet.Builder<Pattern> patterns = ImmutableSet.builder();
+    String[] patternRegExs = cfg.getStringList("tracing", traceId, name);
+    for (String patternRegEx : patternRegExs) {
+      try {
+        patterns.add(Pattern.compile(patternRegEx));
+      } catch (PatternSyntaxException e) {
+        throw new ConfigInvalidException(
+            String.format(
+                "Invalid tracing config ('tracing.%s.%s = %s'): %s",
+                traceId, name, patternRegEx, e.getMessage()));
+      }
+    }
+    return patterns.build();
+  }
+
+  @AutoValue
+  abstract static class TraceConfig {
+    /** ID for the trace */
+    abstract String traceId();
+
+    /** request types that should be traced */
+    abstract ImmutableSet<String> requestTypes();
+
+    /** pattern matching request URIs */
+    abstract ImmutableSet<Pattern> requestUriPatterns();
+
+    /** accounts IDs matching calling user */
+    abstract ImmutableSet<Account.Id> accountIds();
+
+    /** pattern matching projects names */
+    abstract ImmutableSet<Pattern> projectPatterns();
+
+    static Builder builder() {
+      return new AutoValue_TraceRequestListener_TraceConfig.Builder();
+    }
+
+    /**
+     * Whether this trace config matches a given request.
+     *
+     * @param requestInfo request info
+     * @return whether this trace config matches
+     */
+    boolean matches(RequestInfo requestInfo) {
+      // If in the trace config request types are set and none of them matches, then the request is
+      // not matched.
+      if (!requestTypes().isEmpty()
+          && requestTypes().stream()
+              .noneMatch(type -> type.equalsIgnoreCase(requestInfo.requestType()))) {
+        return false;
+      }
+
+      // If in the trace config request URI patterns are set and none of them matches, then the
+      // request is not matched.
+      if (!requestUriPatterns().isEmpty()) {
+        if (!requestInfo.requestUri().isPresent()) {
+          // The request has no request URI, hence it cannot match a request URI pattern.
+          return false;
+        }
+
+        if (requestUriPatterns().stream()
+            .noneMatch(p -> p.matcher(requestInfo.requestUri().get()).matches())) {
+          return false;
+        }
+      }
+
+      // If in the trace config accounts are set and none of them matches, then the request is not
+      // matched.
+      if (!accountIds().isEmpty()) {
+        try {
+          if (accountIds().stream()
+              .noneMatch(id -> id.equals(requestInfo.callingUser().getAccountId()))) {
+            return false;
+          }
+        } catch (UnsupportedOperationException e) {
+          // The calling user is not logged in, hence it cannot match an account.
+          return false;
+        }
+      }
+
+      // If in the trace config project patterns are set and none of them matches, then the request
+      // is not matched.
+      if (!projectPatterns().isEmpty()) {
+        if (!requestInfo.project().isPresent()) {
+          // The request is not for a project, hence it cannot match a project pattern.
+          return false;
+        }
+
+        if (projectPatterns().stream()
+            .noneMatch(p -> p.matcher(requestInfo.project().get().get()).matches())) {
+          return false;
+        }
+      }
+
+      // For any match criteria (request type, request URI pattern, account, project pattern) that
+      // was specified in the trace config, at least one of the configured value matched the
+      // request.
+      return true;
+    }
+
+    @AutoValue.Builder
+    abstract static class Builder {
+      abstract Builder traceId(String traceId);
+
+      abstract Builder requestTypes(ImmutableSet<String> requestTypes);
+
+      abstract Builder requestUriPatterns(ImmutableSet<Pattern> requestUriPatterns);
+
+      abstract Builder accountIds(ImmutableSet<Account.Id> accountIds);
+
+      abstract Builder projectPatterns(ImmutableSet<Pattern> projectPatterns);
+
+      abstract TraceConfig build();
+    }
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
index 50d6dff..3a01517 100644
--- a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
@@ -367,6 +367,218 @@
     assertThat(testPerformanceLogger.logEntries()).isEmpty();
   }
 
+  @Test
+  @GerritConfig(name = "tracing.issue123.projectPattern", value = "new12")
+  public void traceProject() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new12");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+    assertThat(projectCreationListener.isLoggingForced).isTrue();
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new12");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.projectPattern", value = "new.*")
+  public void traceProjectMatchRegEx() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new13");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+    assertThat(projectCreationListener.isLoggingForced).isTrue();
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new13");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.projectPattern", value = "foo.*")
+  public void traceProjectNoMatch() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new13");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isNull();
+    assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new13");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.projectPattern", value = "][")
+  public void traceProjectInvalidRegEx() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new14");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isNull();
+    assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new14");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.account", value = "1000000")
+  public void traceAccount() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new15");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+    assertThat(projectCreationListener.isLoggingForced).isTrue();
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new15");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.account", value = "1000001")
+  public void traceAccountNoMatch() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new16");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isNull();
+    assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new16");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.account", value = "999")
+  public void traceAccountNotFound() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new17");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isNull();
+    assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new17");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.account", value = "invalid")
+  public void traceAccountInvalidId() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new18");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isNull();
+    assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new18");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.requestType", value = "REST")
+  public void traceRequestType() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new19");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+    assertThat(projectCreationListener.isLoggingForced).isTrue();
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new19");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.requestType", value = "SSH")
+  public void traceRequestTypeNoMatch() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new20");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isNull();
+    assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new20");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.requestType", value = "FOO")
+  public void traceProjectInvalidRequestType() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new21");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isNull();
+    assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new21");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.account", value = "1000000")
+  @GerritConfig(name = "tracing.issue123.projectPattern", value = "new.*")
+  public void traceProjectForAccount() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new22");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+    assertThat(projectCreationListener.isLoggingForced).isTrue();
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new22");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.account", value = "1000000")
+  @GerritConfig(name = "tracing.issue123.projectPattern", value = "foo.*")
+  public void traceProjectForAccountNoProjectMatch() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new23");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isNull();
+    assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new23");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.account", value = "1000001")
+  @GerritConfig(name = "tracing.issue123.projectPattern", value = "new.*")
+  public void traceProjectForAccountNoAccountMatch() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new24");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isNull();
+    assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new24");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.requestUriPattern", value = "/projects/.*")
+  public void traceRequestUri() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new23");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+    assertThat(projectCreationListener.isLoggingForced).isTrue();
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new23");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.requestUriPattern", value = "/projects/.*/foo")
+  public void traceRequestUriNoMatch() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new23");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isNull();
+    assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new23");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.requestUriPattern", value = "][")
+  public void traceRequestUriInvalidRegEx() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new24");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isNull();
+    assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new24");
+  }
+
   private void assertForceLogging(boolean expected) {
     assertThat(LoggingContext.getInstance().shouldForceLogging(null, null, false))
         .isEqualTo(expected);