Add metrics to record latency for loading/reading/parsing OWNERS files

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: Ib6927fa623e5b07fa79828f8851a018bc6766cec
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java b/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java
index 2824246..20a44c2 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java
@@ -52,6 +52,7 @@
   private final MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory;
   private final RetryHelper retryHelper;
   private final String defaultFileName;
+  private final CodeOwnerConfigFile.Factory codeOwnerConfigFileFactory;
   private final CodeOwnerConfigParser codeOwnerConfigParser;
 
   protected AbstractFileBasedCodeOwnerBackend(
@@ -61,6 +62,7 @@
       MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
       RetryHelper retryHelper,
       String defaultFileName,
+      CodeOwnerConfigFile.Factory codeOwnerConfigFileFactory,
       CodeOwnerConfigParser codeOwnerConfigParser) {
     this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
     this.repoManager = repoManager;
@@ -68,6 +70,7 @@
     this.metaDataUpdateInternalFactory = metaDataUpdateInternalFactory;
     this.retryHelper = retryHelper;
     this.defaultFileName = defaultFileName;
+    this.codeOwnerConfigFileFactory = codeOwnerConfigFileFactory;
     this.codeOwnerConfigParser = codeOwnerConfigParser;
   }
 
@@ -103,7 +106,7 @@
       @Nullable ObjectId revision) {
     try (Repository repository = repoManager.openRepository(codeOwnerConfigKey.project())) {
       if (revision == null) {
-        return CodeOwnerConfigFile.loadCurrent(
+        return codeOwnerConfigFileFactory.loadCurrent(
             fileName, codeOwnerConfigParser, repository, codeOwnerConfigKey);
       }
 
@@ -113,7 +116,7 @@
         revWalk = new RevWalk(repository);
       }
       try {
-        return CodeOwnerConfigFile.load(
+        return codeOwnerConfigFileFactory.load(
             fileName, codeOwnerConfigParser, revWalk, revision, codeOwnerConfigKey);
       } finally {
         if (closeRevWalk) {
@@ -222,7 +225,8 @@
       CodeOwnerConfigUpdate codeOwnerConfigUpdate) {
     try (Repository repository = repoManager.openRepository(codeOwnerConfigKey.project())) {
       CodeOwnerConfigFile codeOwnerConfigFile =
-          CodeOwnerConfigFile.loadCurrent(
+          codeOwnerConfigFileFactory
+              .loadCurrent(
                   getFileName(codeOwnerConfigKey.project()),
                   codeOwnerConfigParser,
                   repository,
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java
index 4cceb13..b704f2b 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java
@@ -19,9 +19,13 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
+import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.metrics.Timer1;
+import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
 import com.google.gerrit.plugins.codeowners.util.JgitPath;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
+import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -36,9 +40,9 @@
  * A representation of a code owner config that is stored as an {@code OWNERS} file in a source
  * branch.
  *
- * <p>For reading code owner configs or creating/updating them, refer to {@link #load(String,
- * CodeOwnerConfigParser, RevWalk, ObjectId, CodeOwnerConfig.Key)} and {@link #loadCurrent(String,
- * CodeOwnerConfigParser, Repository, CodeOwnerConfig.Key)}.
+ * <p>For reading code owner configs or creating/updating them, refer to {@link Factory#load(String,
+ * CodeOwnerConfigParser, RevWalk, ObjectId, CodeOwnerConfig.Key)} and {@link
+ * Factory#loadCurrent(String, CodeOwnerConfigParser, Repository, CodeOwnerConfig.Key)}.
  *
  * <p><strong>Note:</strong> Any modification (code owner config creation or update) only becomes
  * permanent (and hence written to repository) if {@link
@@ -46,83 +50,95 @@
  */
 @VisibleForTesting
 public class CodeOwnerConfigFile extends VersionedMetaData {
-  /**
-   * Creates a {@link CodeOwnerConfigFile} for a code owner config.
-   *
-   * <p>The code owner config is automatically loaded within this method and can be accessed via
-   * {@link #getLoadedCodeOwnerConfig()}.
-   *
-   * <p>It's safe to call this method for non-existing code owner configs. In that case, {@link
-   * #getLoadedCodeOwnerConfig()} won't return any code owner config. Thus, the existence of a code
-   * owner config can be easily tested.
-   *
-   * <p>The code owner config represented by the returned {@link CodeOwnerConfigFile} can be
-   * created/updated by setting an {@link CodeOwnerConfigUpdate} via {@link
-   * #setCodeOwnerConfigUpdate(CodeOwnerConfigUpdate)} and committing the {@link
-   * CodeOwnerConfigUpdate} via {@link #commit(com.google.gerrit.server.git.meta.MetaDataUpdate)}.
-   *
-   * @param defaultFileName the name of the code owner configuration files that should be used if
-   *     none is specified in the code owner config key
-   * @param codeOwnerConfigParser the parser that should be used to parse code owner config files
-   * @param revWalk the revWalk that should be used to load the revision
-   * @param revision the branch revision from which the code owner config file should be loaded
-   * @param codeOwnerConfigKey the key of the code owner config
-   * @return a {@link CodeOwnerConfigFile} for the code owner config with the specified key
-   * @throws IOException if the repository can't be accessed for some reason
-   * @throws ConfigInvalidException if the code owner config exists but can't be read due to an
-   *     invalid format
-   */
-  public static CodeOwnerConfigFile load(
-      String defaultFileName,
-      CodeOwnerConfigParser codeOwnerConfigParser,
-      RevWalk revWalk,
-      ObjectId revision,
-      CodeOwnerConfig.Key codeOwnerConfigKey)
-      throws IOException, ConfigInvalidException {
-    requireNonNull(defaultFileName, "defaultFileName");
-    requireNonNull(codeOwnerConfigParser, "codeOwnerConfigParser");
-    requireNonNull(revWalk, "revWalk");
-    requireNonNull(revision, "revision");
-    requireNonNull(codeOwnerConfigKey, "codeOwnerConfigKey");
+  public static class Factory {
+    private final CodeOwnerMetrics codeOwnerMetrics;
 
-    CodeOwnerConfigFile codeOwnerConfigFile =
-        new CodeOwnerConfigFile(defaultFileName, codeOwnerConfigParser, codeOwnerConfigKey);
-    codeOwnerConfigFile.load(codeOwnerConfigKey.project(), revWalk, revision);
-    return codeOwnerConfigFile;
+    @Inject
+    Factory(CodeOwnerMetrics codeOwnerMetrics) {
+      this.codeOwnerMetrics = codeOwnerMetrics;
+    }
+
+    /**
+     * Creates a {@link CodeOwnerConfigFile} for a code owner config.
+     *
+     * <p>The code owner config is automatically loaded within this method and can be accessed via
+     * {@link #getLoadedCodeOwnerConfig()}.
+     *
+     * <p>It's safe to call this method for non-existing code owner configs. In that case, {@link
+     * #getLoadedCodeOwnerConfig()} won't return any code owner config. Thus, the existence of a
+     * code owner config can be easily tested.
+     *
+     * <p>The code owner config represented by the returned {@link CodeOwnerConfigFile} can be
+     * created/updated by setting an {@link CodeOwnerConfigUpdate} via {@link
+     * #setCodeOwnerConfigUpdate(CodeOwnerConfigUpdate)} and committing the {@link
+     * CodeOwnerConfigUpdate} via {@link #commit(com.google.gerrit.server.git.meta.MetaDataUpdate)}.
+     *
+     * @param defaultFileName the name of the code owner configuration files that should be used if
+     *     none is specified in the code owner config key
+     * @param codeOwnerConfigParser the parser that should be used to parse code owner config files
+     * @param revWalk the revWalk that should be used to load the revision
+     * @param revision the branch revision from which the code owner config file should be loaded
+     * @param codeOwnerConfigKey the key of the code owner config
+     * @return a {@link CodeOwnerConfigFile} for the code owner config with the specified key
+     * @throws IOException if the repository can't be accessed for some reason
+     * @throws ConfigInvalidException if the code owner config exists but can't be read due to an
+     *     invalid format
+     */
+    public CodeOwnerConfigFile load(
+        String defaultFileName,
+        CodeOwnerConfigParser codeOwnerConfigParser,
+        RevWalk revWalk,
+        ObjectId revision,
+        CodeOwnerConfig.Key codeOwnerConfigKey)
+        throws IOException, ConfigInvalidException {
+      requireNonNull(defaultFileName, "defaultFileName");
+      requireNonNull(codeOwnerConfigParser, "codeOwnerConfigParser");
+      requireNonNull(revWalk, "revWalk");
+      requireNonNull(revision, "revision");
+      requireNonNull(codeOwnerConfigKey, "codeOwnerConfigKey");
+
+      CodeOwnerConfigFile codeOwnerConfigFile =
+          new CodeOwnerConfigFile(
+              codeOwnerMetrics, defaultFileName, codeOwnerConfigParser, codeOwnerConfigKey);
+      codeOwnerConfigFile.load(codeOwnerConfigKey.project(), revWalk, revision);
+      return codeOwnerConfigFile;
+    }
+
+    /**
+     * Creates a {@link CodeOwnerConfigFile} for a code owner config from the current revision in
+     * the branch.
+     *
+     * @param defaultFileName the name of the code owner configuration files that should be used if
+     *     none is specified in the code owner config key
+     * @param codeOwnerConfigParser the parser that should be used to parse code owner config files
+     * @param repository the repository in which the code owner config is stored
+     * @param codeOwnerConfigKey the key of the code owner config
+     * @return a {@link CodeOwnerConfigFile} for the code owner config with the specified key
+     * @throws IOException if the repository can't be accessed for some reason
+     * @throws ConfigInvalidException if the code owner config exists but can't be read due to an
+     *     invalid format
+     * @see #load(String, CodeOwnerConfigParser, RevWalk, ObjectId, CodeOwnerConfig.Key)
+     */
+    public CodeOwnerConfigFile loadCurrent(
+        String defaultFileName,
+        CodeOwnerConfigParser codeOwnerConfigParser,
+        Repository repository,
+        CodeOwnerConfig.Key codeOwnerConfigKey)
+        throws IOException, ConfigInvalidException {
+      requireNonNull(defaultFileName, "defaultFileName");
+      requireNonNull(codeOwnerConfigParser, "codeOwnerConfigParser");
+      requireNonNull(repository, "repository");
+      requireNonNull(codeOwnerConfigKey, "codeOwnerConfigKey");
+
+      CodeOwnerConfigFile codeOwnerConfigFile =
+          new CodeOwnerConfigFile(
+              codeOwnerMetrics, defaultFileName, codeOwnerConfigParser, codeOwnerConfigKey);
+      codeOwnerConfigFile.load(codeOwnerConfigKey.project(), repository);
+      return codeOwnerConfigFile;
+    }
   }
 
-  /**
-   * Creates a {@link CodeOwnerConfigFile} for a code owner config from the current revision in the
-   * branch.
-   *
-   * @param defaultFileName the name of the code owner configuration files that should be used if
-   *     none is specified in the code owner config key
-   * @param codeOwnerConfigParser the parser that should be used to parse code owner config files
-   * @param repository the repository in which the code owner config is stored
-   * @param codeOwnerConfigKey the key of the code owner config
-   * @return a {@link CodeOwnerConfigFile} for the code owner config with the specified key
-   * @throws IOException if the repository can't be accessed for some reason
-   * @throws ConfigInvalidException if the code owner config exists but can't be read due to an
-   *     invalid format
-   * @see #load(String, CodeOwnerConfigParser, RevWalk, ObjectId, CodeOwnerConfig.Key)
-   */
-  public static CodeOwnerConfigFile loadCurrent(
-      String defaultFileName,
-      CodeOwnerConfigParser codeOwnerConfigParser,
-      Repository repository,
-      CodeOwnerConfig.Key codeOwnerConfigKey)
-      throws IOException, ConfigInvalidException {
-    requireNonNull(defaultFileName, "defaultFileName");
-    requireNonNull(codeOwnerConfigParser, "codeOwnerConfigParser");
-    requireNonNull(repository, "repository");
-    requireNonNull(codeOwnerConfigKey, "codeOwnerConfigKey");
-
-    CodeOwnerConfigFile codeOwnerConfigFile =
-        new CodeOwnerConfigFile(defaultFileName, codeOwnerConfigParser, codeOwnerConfigKey);
-    codeOwnerConfigFile.load(codeOwnerConfigKey.project(), repository);
-    return codeOwnerConfigFile;
-  }
-
+  private final CodeOwnerMetrics codeOwnerMetrics;
   private final String defaultFileName;
   private final CodeOwnerConfigParser codeOwnerConfigParser;
   private final CodeOwnerConfig.Key codeOwnerConfigKey;
@@ -132,9 +148,11 @@
   private Optional<CodeOwnerConfigUpdate> codeOwnerConfigUpdate = Optional.empty();
 
   private CodeOwnerConfigFile(
+      CodeOwnerMetrics codeOwnerMetrics,
       String defaultFileName,
       CodeOwnerConfigParser codeOwnerConfigParser,
       CodeOwnerConfig.Key codeOwnerConfigKey) {
+    this.codeOwnerMetrics = codeOwnerMetrics;
     this.defaultFileName = defaultFileName;
     this.codeOwnerConfigParser = codeOwnerConfigParser;
     this.codeOwnerConfigKey = codeOwnerConfigKey;
@@ -186,12 +204,21 @@
   }
 
   @Override
+  protected byte[] readFile(String fileName) throws IOException {
+    try (Timer0.Context ctx = codeOwnerMetrics.readCodeOwnerConfig.start()) {
+      return super.readFile(fileName);
+    }
+  }
+
+  @Override
   protected void onLoad() throws IOException, ConfigInvalidException {
     if (revision != null) {
       Optional<String> codeOwnerConfigFileContent =
           getFileIfItExists(JgitPath.of(codeOwnerConfigKey.filePath(defaultFileName)).get());
       if (codeOwnerConfigFileContent.isPresent()) {
-        try {
+        try (Timer1.Context<String> ctx =
+            codeOwnerMetrics.parseCodeOwnerConfig.start(
+                codeOwnerConfigParser.getClass().getSimpleName())) {
           loadedCodeOwnersConfig =
               Optional.of(
                   codeOwnerConfigParser.parse(
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java
index 4247849..32ad7b5 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java
@@ -17,6 +17,7 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.base.Throwables;
+import com.google.gerrit.metrics.Timer1;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
 import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
 import com.google.inject.Inject;
@@ -57,7 +58,10 @@
         codeOwnersPluginConfiguration
             .getProjectConfig(codeOwnerConfigKey.project())
             .getBackend(codeOwnerConfigKey.branchNameKey().branch());
-    return codeOwnerBackend.getCodeOwnerConfig(codeOwnerConfigKey, revision);
+    try (Timer1.Context<String> ctx =
+        codeOwnerMetrics.loadCodeOwnerConfig.start(codeOwnerBackend.getClass().getSimpleName())) {
+      return codeOwnerBackend.getCodeOwnerConfig(codeOwnerConfigKey, revision);
+    }
   }
 
   @Override
@@ -68,7 +72,10 @@
         codeOwnersPluginConfiguration
             .getProjectConfig(codeOwnerConfigKey.project())
             .getBackend(codeOwnerConfigKey.branchNameKey().branch());
-    return codeOwnerBackend.getCodeOwnerConfig(codeOwnerConfigKey, /* revision= */ null);
+    try (Timer1.Context<String> ctx =
+        codeOwnerMetrics.loadCodeOwnerConfig.start(codeOwnerBackend.getClass().getSimpleName())) {
+      return codeOwnerBackend.getCodeOwnerConfig(codeOwnerConfigKey, /* revision= */ null);
+    }
   }
 
   /**
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackend.java b/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackend.java
index 257c1d4..0e923d7 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackend.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackend.java
@@ -17,6 +17,7 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.plugins.codeowners.backend.AbstractFileBasedCodeOwnerBackend;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigFile;
 import com.google.gerrit.plugins.codeowners.backend.FindOwnersGlobMatcher;
 import com.google.gerrit.plugins.codeowners.backend.PathExpressionMatcher;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
@@ -44,6 +45,7 @@
   @Inject
   FindOwnersBackend(
       CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
+      CodeOwnerConfigFile.Factory codeOwnerConfigFileFactory,
       FindOwnersCodeOwnerConfigParser codeOwnerConfigParser,
       GitRepositoryManager repoManager,
       @GerritPersonIdent PersonIdent serverIdent,
@@ -56,6 +58,7 @@
         metaDataUpdateInternalFactory,
         retryHelper,
         CODE_OWNER_CONFIG_FILE_NAME,
+        codeOwnerConfigFileFactory,
         codeOwnerConfigParser);
   }
 
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/proto/ProtoBackend.java b/java/com/google/gerrit/plugins/codeowners/backend/proto/ProtoBackend.java
index e8e0694..b273ab2 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/proto/ProtoBackend.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/proto/ProtoBackend.java
@@ -17,6 +17,7 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.plugins.codeowners.backend.AbstractFileBasedCodeOwnerBackend;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigFile;
 import com.google.gerrit.plugins.codeowners.backend.PathExpressionMatcher;
 import com.google.gerrit.plugins.codeowners.backend.SimplePathExpressionMatcher;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
@@ -52,6 +53,7 @@
       @GerritPersonIdent PersonIdent serverIdent,
       MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
       RetryHelper retryHelper,
+      CodeOwnerConfigFile.Factory codeOwnerConfigFileFactory,
       ProtoCodeOwnerConfigParser codeOwnerConfigParser) {
     super(
         codeOwnersPluginConfiguration,
@@ -60,6 +62,7 @@
         metaDataUpdateInternalFactory,
         retryHelper,
         CODE_OWNER_CONFIG_FILE_NAME,
+        codeOwnerConfigFileFactory,
         codeOwnerConfigParser);
   }
 
diff --git a/java/com/google/gerrit/plugins/codeowners/metrics/CodeOwnerMetrics.java b/java/com/google/gerrit/plugins/codeowners/metrics/CodeOwnerMetrics.java
index e5a3c58..8cfc74d 100644
--- a/java/com/google/gerrit/plugins/codeowners/metrics/CodeOwnerMetrics.java
+++ b/java/com/google/gerrit/plugins/codeowners/metrics/CodeOwnerMetrics.java
@@ -17,8 +17,10 @@
 import com.google.gerrit.metrics.Counter0;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Description.Units;
+import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.metrics.Timer1;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -38,6 +40,11 @@
   public final Timer0 resolvePathCodeOwners;
   public final Timer0 runCodeOwnerSubmitRule;
 
+  // code owner config latency metrics
+  public final Timer1<String> loadCodeOwnerConfig;
+  public final Timer0 readCodeOwnerConfig;
+  public final Timer1<String> parseCodeOwnerConfig;
+
   // counter metrics
   public final Counter0 countCodeOwnerConfigReads;
   public final Counter0 countCodeOwnerConfigCacheReads;
@@ -87,6 +94,19 @@
         createLatencyTimer(
             "run_code_owner_submit_rule", "Latency for running the code owner submit rule");
 
+    // code owner config latency metrics
+    this.loadCodeOwnerConfig =
+        createTimerWithClassField(
+            "load_code_owner_config",
+            "Latency for loading a code owner config file (read + parse)",
+            "backend");
+    this.parseCodeOwnerConfig =
+        createTimerWithClassField(
+            "parse_code_owner_config", "Latency for parsing a code owner config file", "parser");
+    this.readCodeOwnerConfig =
+        createLatencyTimer(
+            "read_code_owner_config", "Latency for reading a code owner config file");
+
     // counter metrics
     this.countCodeOwnerConfigReads =
         createCounter(
@@ -104,6 +124,19 @@
         new Description(description).setCumulative().setUnit(Units.MILLISECONDS));
   }
 
+  private Timer1<String> createTimerWithClassField(
+      String name, String description, String fieldName) {
+    Field<String> CODE_OWNER_BACKEND_FIELD =
+        Field.ofString(
+                fieldName, (metadataBuilder, fieldValue) -> metadataBuilder.className(fieldValue))
+            .build();
+
+    return metricMaker.newTimer(
+        "code_owners/" + name,
+        new Description(description).setCumulative().setUnit(Description.Units.MILLISECONDS),
+        CODE_OWNER_BACKEND_FIELD);
+  }
+
   private Counter0 createCounter(String name, String description) {
     return metricMaker.newCounter("code_owners/" + name, new Description(description).setRate());
   }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileTest.java
index 61bbdb7..e6b776d 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileTest.java
@@ -47,10 +47,13 @@
   @Inject private ProjectOperations projectOperations;
   @Inject private MetaDataUpdate.Server metaDataUpdateServer;
 
+  private CodeOwnerConfigFile.Factory codeOwnerConfigFileFactory;
   private TestCodeOwnerConfigStorage testCodeOwnerConfigStorage;
 
   @Before
   public void setUpCodeOwnersPlugin() throws Exception {
+    codeOwnerConfigFileFactory =
+        plugin.getSysInjector().getInstance(CodeOwnerConfigFile.Factory.class);
     testCodeOwnerConfigStorage =
         plugin
             .getSysInjector()
@@ -534,7 +537,7 @@
   private CodeOwnerConfigFile loadCodeOwnerConfig(CodeOwnerConfig.Key codeOwnerConfigKey)
       throws IOException, ConfigInvalidException {
     try (Repository repository = repoManager.openRepository(codeOwnerConfigKey.project())) {
-      return CodeOwnerConfigFile.loadCurrent(
+      return codeOwnerConfigFileFactory.loadCurrent(
           CODE_OWNER_CONFIG_FILE_NAME, CODE_OWNER_CONFIG_PARSER, repository, codeOwnerConfigKey);
     }
   }
@@ -544,7 +547,7 @@
       throws IOException, ConfigInvalidException {
     try (Repository repository = repoManager.openRepository(codeOwnerConfigKey.project());
         RevWalk revWalk = new RevWalk(repository)) {
-      return CodeOwnerConfigFile.load(
+      return codeOwnerConfigFileFactory.load(
           CODE_OWNER_CONFIG_FILE_NAME,
           CODE_OWNER_CONFIG_PARSER,
           revWalk,
diff --git a/resources/Documentation/metrics.md b/resources/Documentation/metrics.md
index d236b3e..bc80415 100644
--- a/resources/Documentation/metrics.md
+++ b/resources/Documentation/metrics.md
@@ -28,6 +28,15 @@
 * `run_code_owner_submit_rule`:
   Latency for running the code owner submit rule.
 
+### <a id="codeOwnerConfigLatencyMetrics"> Code Owner Config Latency Metrics
+
+* `load_code_owner_config`:
+  Latency for loading a code owner config file (read + parse).
+* `parse_code_owner_config`:
+  Latency for parsing a code owner config file.
+* `read_code_owner_config`:
+  Latency for reading a code owner config file.
+
 ## <a id="counterMetrics"> Counter Metrics
 
 * `count_code_owner_config_reads`: