Add latency metrics for relevant operations

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I0545d8012820834382d4b0cca8c18c93a0b4053e
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/BUILD b/java/com/google/gerrit/plugins/codeowners/backend/BUILD
index 5aefae5..83671a0 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/BUILD
+++ b/java/com/google/gerrit/plugins/codeowners/backend/BUILD
@@ -7,6 +7,7 @@
     visibility = ["//visibility:public"],
     deps = PLUGIN_DEPS_NEVERLINK + [
         "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/common",
+        "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/metrics",
         "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/util",
         "//plugins/code-owners/proto:owners_metadata_java_proto",
     ],
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java b/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java
index 78956a2..1e76b05 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java
@@ -23,9 +23,11 @@
 import com.google.gerrit.entities.Patch;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
+import com.google.gerrit.metrics.Timer0;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
 import com.google.gerrit.plugins.codeowners.common.ChangedFile;
 import com.google.gerrit.plugins.codeowners.common.MergeCommitStrategy;
+import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchListCache;
@@ -60,15 +62,18 @@
   private final GitRepositoryManager repoManager;
   private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
   private final PatchListCache patchListCache;
+  private final CodeOwnerMetrics codeOwnerMetrics;
 
   @Inject
   public ChangedFiles(
       GitRepositoryManager repoManager,
       CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
-      PatchListCache patchListCache) {
+      PatchListCache patchListCache,
+      CodeOwnerMetrics codeOwnerMetrics) {
     this.repoManager = repoManager;
     this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
     this.patchListCache = patchListCache;
+    this.codeOwnerMetrics = codeOwnerMetrics;
   }
 
   /**
@@ -139,12 +144,14 @@
     logger.atFine().log(
         "computing changed files for revision %s in project %s", revCommit.name(), project);
 
-    if (revCommit.getParentCount() > 1
-        && MergeCommitStrategy.FILES_WITH_CONFLICT_RESOLUTION.equals(mergeCommitStrategy)) {
-      return computeByComparingAgainstAutoMerge(project, revCommit);
-    }
+    try (Timer0.Context ctx = codeOwnerMetrics.computeChangedFiles.start()) {
+      if (revCommit.getParentCount() > 1
+          && MergeCommitStrategy.FILES_WITH_CONFLICT_RESOLUTION.equals(mergeCommitStrategy)) {
+        return computeByComparingAgainstAutoMerge(project, revCommit);
+      }
 
-    return computeByComparingAgainstFirstParent(repoConfig, revWalk, revCommit);
+      return computeByComparingAgainstFirstParent(repoConfig, revWalk, revCommit);
+    }
   }
 
   /**
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
index 49eddd7..937a118 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
@@ -33,15 +33,14 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.metrics.Timer0;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
 import com.google.gerrit.plugins.codeowners.backend.config.RequiredApproval;
 import com.google.gerrit.plugins.codeowners.common.ChangedFile;
 import com.google.gerrit.plugins.codeowners.common.CodeOwnerStatus;
+import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.logging.Metadata;
-import com.google.gerrit.server.logging.TraceContext;
-import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
@@ -91,6 +90,7 @@
   private final CodeOwnerConfigHierarchy codeOwnerConfigHierarchy;
   private final Provider<CodeOwnerResolver> codeOwnerResolver;
   private final ApprovalsUtil approvalsUtil;
+  private final CodeOwnerMetrics codeOwnerMetrics;
 
   @Inject
   CodeOwnerApprovalCheck(
@@ -101,7 +101,8 @@
       CodeOwnerConfigScanner.Factory codeOwnerConfigScannerFactory,
       CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
       Provider<CodeOwnerResolver> codeOwnerResolver,
-      ApprovalsUtil approvalsUtil) {
+      ApprovalsUtil approvalsUtil,
+      CodeOwnerMetrics codeOwnerMetrics) {
     this.permissionBackend = permissionBackend;
     this.repoManager = repoManager;
     this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
@@ -110,6 +111,7 @@
     this.codeOwnerConfigHierarchy = codeOwnerConfigHierarchy;
     this.codeOwnerResolver = codeOwnerResolver;
     this.approvalsUtil = approvalsUtil;
+    this.codeOwnerMetrics = codeOwnerMetrics;
   }
 
   /**
@@ -200,13 +202,11 @@
   public Stream<FileCodeOwnerStatus> getFileStatuses(ChangeNotes changeNotes)
       throws ResourceConflictException, IOException, PatchListNotAvailableException {
     requireNonNull(changeNotes, "changeNotes");
-    try (TraceTimer traceTimer =
-        TraceContext.newTimer(
-            "Compute file statuses",
-            Metadata.builder()
-                .projectName(changeNotes.getProjectName().get())
-                .changeId(changeNotes.getChangeId().get())
-                .build())) {
+    try (Timer0.Context ctx = codeOwnerMetrics.computeFileStatuses.start()) {
+      logger.atFine().log(
+          "compute file statuses (project = %s, change = %d)",
+          changeNotes.getProjectName(), changeNotes.getChangeId().get());
+
       boolean enableImplicitApprovalFromUploader =
           codeOwnersPluginConfiguration.areImplicitApprovalsEnabled(changeNotes.getProjectName());
       Account.Id patchSetUploader = changeNotes.getCurrentPatchSet().uploader();
@@ -287,14 +287,14 @@
     requireNonNull(changeNotes, "changeNotes");
     requireNonNull(patchSet, "patchSet");
     requireNonNull(accountId, "accountId");
-    try (TraceTimer traceTimer =
-        TraceContext.newTimer(
-            "Compute file statuses for account",
-            Metadata.builder()
-                .projectName(changeNotes.getProjectName().get())
-                .changeId(changeNotes.getChangeId().get())
-                .patchSetId(patchSet.id().get())
-                .build())) {
+    try (Timer0.Context ctx = codeOwnerMetrics.computeFileStatusesForAccount.start()) {
+      logger.atFine().log(
+          "compute file statuses for account %d (project = %s, change = %d, patch set = %d)",
+          accountId.get(),
+          changeNotes.getProjectName(),
+          changeNotes.getChangeId().get(),
+          patchSet.id().get());
+
       RequiredApproval requiredApproval =
           codeOwnersPluginConfiguration.getRequiredApproval(changeNotes.getProjectName());
       logger.atFine().log("requiredApproval = %s", requiredApproval);
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
index 2fb7607..31dda71 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
@@ -25,7 +25,9 @@
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.metrics.Timer0;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
@@ -33,9 +35,6 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
-import com.google.gerrit.server.logging.Metadata;
-import com.google.gerrit.server.logging.TraceContext;
-import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -62,6 +61,7 @@
   private final AccountCache accountCache;
   private final AccountControl.Factory accountControlFactory;
   private final PathCodeOwners.Factory pathCodeOwnersFactory;
+  private final CodeOwnerMetrics codeOwnerMetrics;
 
   // Enforce visibility by default.
   private boolean enforceVisibility = true;
@@ -79,7 +79,8 @@
       ExternalIds externalIds,
       AccountCache accountCache,
       AccountControl.Factory accountControlFactory,
-      PathCodeOwners.Factory pathCodeOwnersFactory) {
+      PathCodeOwners.Factory pathCodeOwnersFactory,
+      CodeOwnerMetrics codeOwnerMetrics) {
     this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
     this.permissionBackend = permissionBackend;
     this.currentUser = currentUser;
@@ -87,6 +88,7 @@
     this.accountCache = accountCache;
     this.accountControlFactory = accountControlFactory;
     this.pathCodeOwnersFactory = pathCodeOwnersFactory;
+    this.codeOwnerMetrics = codeOwnerMetrics;
   }
 
   /**
@@ -147,15 +149,10 @@
     requireNonNull(absolutePath, "absolutePath");
     checkState(absolutePath.isAbsolute(), "path %s must be absolute", absolutePath);
 
-    try (TraceTimer traceTimer =
-        TraceContext.newTimer(
-            "Resolve path code owners",
-            Metadata.builder()
-                .projectName(codeOwnerConfig.key().project().get())
-                .branchName(codeOwnerConfig.key().ref())
-                .filePath(codeOwnerConfig.key().fileName().orElse("<default>"))
-                .build())) {
-      logger.atFine().log("resolving path code owners for path %s", absolutePath);
+    try (Timer0.Context ctx = codeOwnerMetrics.resolvePathCodeOwners.start()) {
+      logger.atFine().log(
+          "resolve path code owners (code owner config = %s, path = %s)",
+          codeOwnerConfig.key(), absolutePath);
       PathCodeOwnersResult pathCodeOwnersResult =
           pathCodeOwnersFactory.create(codeOwnerConfig, absolutePath).resolveCodeOwnerConfig();
       return resolve(
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRule.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRule.java
index 6cf8ef3..4c23676 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRule.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRule.java
@@ -23,10 +23,9 @@
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.metrics.Timer0;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
-import com.google.gerrit.server.logging.Metadata;
-import com.google.gerrit.server.logging.TraceContext;
-import com.google.gerrit.server.logging.TraceContext.TraceTimer;
+import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -56,13 +55,16 @@
 
   private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
   private final CodeOwnerApprovalCheck codeOwnerApprovalCheck;
+  private final CodeOwnerMetrics codeOwnerMetrics;
 
   @Inject
   CodeOwnerSubmitRule(
       CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
-      CodeOwnerApprovalCheck codeOwnerApprovalCheck) {
+      CodeOwnerApprovalCheck codeOwnerApprovalCheck,
+      CodeOwnerMetrics codeOwnerMetrics) {
     this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
     this.codeOwnerApprovalCheck = codeOwnerApprovalCheck;
+    this.codeOwnerMetrics = codeOwnerMetrics;
   }
 
   @Override
@@ -70,13 +72,11 @@
     try {
       requireNonNull(changeData, "changeData");
 
-      try (TraceTimer traceTimer =
-          TraceContext.newTimer(
-              "Run code owner submit rule",
-              Metadata.builder()
-                  .projectName(changeData.project().get())
-                  .changeId(changeData.getId().get())
-                  .build())) {
+      try (Timer0.Context ctx = codeOwnerMetrics.runCodeOwnerSubmitRule.start()) {
+        logger.atFine().log(
+            "run code owner submit rule (project = %s, change = %d)",
+            changeData.project().get(), changeData.getId().get());
+
         if (codeOwnersPluginConfiguration.isDisabled(changeData.change().getDest())) {
           logger.atFine().log(
               "code owners functionality is disabled for branch %s", changeData.change().getDest());
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
index f787dc8..4f67254 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
@@ -24,10 +24,9 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.metrics.Timer0;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
-import com.google.gerrit.server.logging.Metadata;
-import com.google.gerrit.server.logging.TraceContext;
-import com.google.gerrit.server.logging.TraceContext.TraceTimer;
+import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
@@ -54,15 +53,18 @@
 
   @Singleton
   public static class Factory {
+    private final CodeOwnerMetrics codeOwnerMetrics;
     private final ProjectCache projectCache;
     private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
     private final CodeOwners codeOwners;
 
     @Inject
     Factory(
+        CodeOwnerMetrics codeOwnerMetrics,
         ProjectCache projectCache,
         CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
         CodeOwners codeOwners) {
+      this.codeOwnerMetrics = codeOwnerMetrics;
       this.projectCache = projectCache;
       this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
       this.codeOwners = codeOwners;
@@ -71,6 +73,7 @@
     public PathCodeOwners create(CodeOwnerConfig codeOwnerConfig, Path absolutePath) {
       requireNonNull(codeOwnerConfig, "codeOwnerConfig");
       return new PathCodeOwners(
+          codeOwnerMetrics,
           projectCache,
           codeOwners,
           codeOwnerConfig,
@@ -85,6 +88,7 @@
           .map(
               codeOwnerConfig ->
                   new PathCodeOwners(
+                      codeOwnerMetrics,
                       projectCache,
                       codeOwners,
                       codeOwnerConfig,
@@ -118,6 +122,7 @@
     }
   }
 
+  private final CodeOwnerMetrics codeOwnerMetrics;
   private final ProjectCache projectCache;
   private final CodeOwners codeOwners;
   private final CodeOwnerConfig codeOwnerConfig;
@@ -127,11 +132,13 @@
   private PathCodeOwnersResult pathCodeOwnersResult;
 
   private PathCodeOwners(
+      CodeOwnerMetrics codeOwnerMetrics,
       ProjectCache projectCache,
       CodeOwners codeOwners,
       CodeOwnerConfig codeOwnerConfig,
       Path path,
       PathExpressionMatcher pathExpressionMatcher) {
+    this.codeOwnerMetrics = requireNonNull(codeOwnerMetrics, "codeOwnerMetrics");
     this.projectCache = requireNonNull(projectCache, "projectCache");
     this.codeOwners = requireNonNull(codeOwners, "codeOwners");
     this.codeOwnerConfig = requireNonNull(codeOwnerConfig, "codeOwnerConfig");
@@ -187,13 +194,7 @@
       return this.pathCodeOwnersResult;
     }
 
-    try (TraceTimer traceTimer =
-        TraceContext.newTimer(
-            "Resolve code owner config",
-            Metadata.builder()
-                .projectName(codeOwnerConfig.key().project().get())
-                .filePath(path.toString())
-                .build())) {
+    try (Timer0.Context ctx = codeOwnerMetrics.resolveCodeOwnerConfig.start()) {
       logger.atFine().log(
           "resolve code owners for %s from code owner config %s", path, codeOwnerConfig.key());
 
@@ -249,14 +250,9 @@
       CodeOwnerConfig importingCodeOwnerConfig,
       CodeOwnerConfig.Builder resolvedCodeOwnerConfigBuilder) {
     ImmutableList.Builder<UnresolvedImport> unresolvedImports = ImmutableList.builder();
-    try (TraceTimer traceTimer =
-        TraceContext.newTimer(
-            "Resolve code owner config imports",
-            Metadata.builder()
-                .projectName(codeOwnerConfig.key().project().get())
-                .branchName(codeOwnerConfig.key().ref())
-                .filePath(codeOwnerConfig.key().filePath("<default>").toString())
-                .build())) {
+    try (Timer0.Context ctx = codeOwnerMetrics.resolveCodeOwnerConfigImports.start()) {
+      logger.atFine().log("resolve imports of codeOwnerConfig %s", importingCodeOwnerConfig.key());
+
       // To detect cyclic dependencies we keep track of all seen code owner configs.
       Set<CodeOwnerConfig.Key> seenCodeOwnerConfigs = new HashSet<>();
       seenCodeOwnerConfigs.add(codeOwnerConfig.key());
@@ -278,17 +274,10 @@
         CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
             createKeyForImportedCodeOwnerConfig(
                 importingCodeOwnerConfig.key(), codeOwnerConfigReference);
-        try (TraceTimer traceTimer2 =
-            TraceContext.newTimer(
-                "Resolve code owner config import",
-                Metadata.builder()
-                    .projectName(keyOfImportedCodeOwnerConfig.project().get())
-                    .branchName(keyOfImportedCodeOwnerConfig.ref())
-                    .filePath(
-                        keyOfImportedCodeOwnerConfig
-                            .filePath(codeOwnerConfigReference.fileName())
-                            .toString())
-                    .build())) {
+        try (Timer0.Context ctx2 = codeOwnerMetrics.resolveCodeOwnerConfigImport.start()) {
+          logger.atFine().log(
+              "resolve import of code owner config %s", keyOfImportedCodeOwnerConfig);
+
           Optional<ProjectState> projectState =
               projectCache.get(keyOfImportedCodeOwnerConfig.project());
           if (!projectState.isPresent()) {
diff --git a/java/com/google/gerrit/plugins/codeowners/metrics/BUILD b/java/com/google/gerrit/plugins/codeowners/metrics/BUILD
new file mode 100644
index 0000000..1f1ad14
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/metrics/BUILD
@@ -0,0 +1,9 @@
+load("@rules_java//java:defs.bzl", "java_library")
+load("//tools/bzl:plugin.bzl", "PLUGIN_DEPS_NEVERLINK")
+
+java_library(
+    name = "metrics",
+    srcs = glob(["*.java"]),
+    visibility = ["//visibility:public"],
+    deps = PLUGIN_DEPS_NEVERLINK,
+)
diff --git a/java/com/google/gerrit/plugins/codeowners/metrics/CodeOwnerMetrics.java b/java/com/google/gerrit/plugins/codeowners/metrics/CodeOwnerMetrics.java
new file mode 100644
index 0000000..e1ce3bf
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/metrics/CodeOwnerMetrics.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2021 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.plugins.codeowners.metrics;
+
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Description.Units;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer0;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/** Metrics of the code-owners plugin. */
+@Singleton
+public class CodeOwnerMetrics {
+  public final Timer0 computeChangedFiles;
+  public final Timer0 computeFileStatuses;
+  public final Timer0 computeFileStatusesForAccount;
+  public final Timer0 resolveCodeOwnerConfig;
+  public final Timer0 resolveCodeOwnerConfigImport;
+  public final Timer0 resolveCodeOwnerConfigImports;
+  public final Timer0 resolvePathCodeOwners;
+  public final Timer0 runCodeOwnerSubmitRule;
+
+  private final MetricMaker metricMaker;
+
+  @Inject
+  CodeOwnerMetrics(MetricMaker metricMaker) {
+    this.metricMaker = metricMaker;
+
+    this.computeChangedFiles =
+        createLatencyTimer("compute_changed_files", "Latency for computing changed files");
+    this.computeFileStatuses =
+        createLatencyTimer("compute_file_statuses", "Latency for computing file statuses");
+    this.computeFileStatusesForAccount =
+        createLatencyTimer(
+            "compute_file_statuses_for_account",
+            "Latency for computing file statuses for an account");
+    this.resolveCodeOwnerConfig =
+        createLatencyTimer(
+            "resolve_code_owner_config", "Latency for resolving a code owner config file");
+    this.resolveCodeOwnerConfigImport =
+        createLatencyTimer(
+            "resolve_code_owner_config_import",
+            "Latency for resolving an import of a code owner config file");
+    this.resolveCodeOwnerConfigImports =
+        createLatencyTimer(
+            "resolve_code_owner_config_imports",
+            "Latency for resolving all imports of a code owner config file");
+    this.resolvePathCodeOwners =
+        createLatencyTimer(
+            "resolve_path_code_owners", "Latency for resolving the code owners of a path");
+    this.runCodeOwnerSubmitRule =
+        createLatencyTimer(
+            "run_code_owner_submit_rule", "Latency for running the code owner submit rule");
+  }
+
+  private Timer0 createLatencyTimer(String name, String description) {
+    return metricMaker.newTimer(
+        "code_owners/" + name,
+        new Description(description).setCumulative().setUnit(Units.MILLISECONDS));
+  }
+}
diff --git a/resources/Documentation/metrics.md b/resources/Documentation/metrics.md
new file mode 100644
index 0000000..2e63c23
--- /dev/null
+++ b/resources/Documentation/metrics.md
@@ -0,0 +1,27 @@
+# Metrics
+
+The @PLUGIN@ plugin exports several metrics which can give insights into the
+usage and performance of the code owners functionality.
+
+## <a id="latencyMetrics"> Latency Metrics
+
+* `compute_changed_files`:
+  Latency for computing changed files.
+* `compute_file_statuses`:
+  Latency for computing file statuses.
+* `compute_file_statuses_for_account`:
+  Latency for computing file statuses for an account.
+* `resolve_code_owner_config`:
+  Latency for resolving a code owner config file.
+* `resolve_code_owner_config_import`:
+  Latency for resolving an import of a code owner config file.
+* `resolve_code_owner_config_imports`:
+  Latency for resolving all imports of a code owner config file.
+* `run_code_owner_submit_rule`:
+  Latency for running the code owner submit rule.
+
+---
+
+Back to [@PLUGIN@ documentation index](index.html)
+
+Part of [Gerrit Code Review](../../../Documentation/index.html)