Merge "New script utility to extract local-refdb metrics"
diff --git a/multi-primary/localrefdb.groovy b/multi-primary/localrefdb.groovy
new file mode 100644
index 0000000..b5d1632
--- /dev/null
+++ b/multi-primary/localrefdb.groovy
@@ -0,0 +1,166 @@
+// Copyright (C) 2023 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.
+
+import com.google.gerrit.common.data.*
+import com.google.gerrit.extensions.annotations.*
+import com.google.inject.*
+import com.google.gerrit.extensions.events.*
+import com.google.gerrit.metrics.*
+import org.kohsuke.args4j.*
+import com.google.gerrit.server.git.*
+import com.google.gerrit.entities.*
+import org.eclipse.jgit.errors.*
+import com.gerritforge.gerrit.globalrefdb.*
+import org.eclipse.jgit.lib.*
+import com.google.gerrit.sshd.*
+import com.google.gerrit.lifecycle.*
+import java.util.function.BiConsumer
+import java.util.concurrent.*
+import com.google.common.flogger.*
+import java.util.regex.Pattern
+
+abstract class BaseSshCommand extends SshCommand {
+ void println(String msg) {
+ stdout.println msg
+ stdout.flush()
+ }
+
+ void error(String msg) { println("[ERROR] $msg") }
+ void warning(String msg) { println("[WARNING] $msg") }
+}
+
+@Singleton
+class RefDbMetrics implements LifecycleListener {
+ FluentLogger log = FluentLogger.forEnclosingClass()
+
+ @Inject
+ MetricMaker metrics
+
+ CallbackMetric1<String, Integer> numRefsMetric
+ final Map<String, Integer> projectsAndNumRefs = new ConcurrentHashMap()
+
+ void start() {
+ numRefsMetric =
+ metrics.newCallbackMetric(
+ "localrefdb/num_refs_per_project",
+ Integer.class,
+ new Description("Number of local refs").setGauge(),
+ Field.ofString("repository_name", { it.projectName } as BiConsumer)
+ .description("The name of the repository.")
+ .build())
+
+ metrics.newTrigger(
+ numRefsMetric, { ->
+ if (projectsAndNumRefs.isEmpty()) {
+ numRefsMetric.forceCreate("")
+ } else {
+ projectsAndNumRefs.each { e ->
+ numRefsMetric.set(e.key, e.value)
+ }
+ numRefsMetric.prune()
+ }
+ })
+ }
+
+ void stop() {
+ numRefsMetric.remove()
+ }
+}
+
+@CommandMetaData(name = "count-refs", description = "Count the local number of refs, excluding user edits, and publish the value as 'num_refs' metric")
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+class CountRefs extends BaseSshCommand {
+
+ @Argument(index = 0, usage = "Project name", metaVar = "PROJECTS", required = true)
+ String[] projects
+
+ @Inject
+ GitRepositoryManager repoMgr
+
+ @Inject
+ RefDbMetrics refdbMetrics
+
+ SanitizeProjectName sanitizeProjectName = new SanitizeProjectName()
+
+ public void run() {
+ refdbMetrics.projectsAndNumRefs.clear()
+ projects.each { project ->
+ try {
+ def projectName = Project.nameKey(project)
+
+ repoMgr.openRepository(projectName).with { repo ->
+ println "Counting refs of project $project ..."
+ def filteredRefs = repo.refDatabase.refs.findAll { ref -> !(ref.name.startsWith("refs/users/.*")) && !ref.symbolic }
+ println "Result: $project has ${filteredRefs.size()} refs"
+ refdbMetrics.projectsAndNumRefs.put(sanitizeProjectName.sanitize(project), filteredRefs.size())
+ }
+ } catch (RepositoryNotFoundException e) {
+ error "Project $project not found"
+ }
+ }
+ }
+}
+
+class MetricsModule extends LifecycleModule {
+ protected void configure() {
+ listener().to(RefDbMetrics)
+ }
+}
+
+class LocalRefDbCommandModule extends PluginCommandModule {
+ protected void configureCommands() {
+ command(CountRefs)
+ }
+}
+
+class SanitizeProjectName {
+ private static final Pattern METRIC_NAME_PATTERN = ~"[a-zA-Z0-9_-]+(/[a-zA-Z0-9_-]+)*"
+ private static final Pattern INVALID_CHAR_PATTERN = ~"[^\\w-/]"
+ private static final String REPLACEMENT_PREFIX = "_0x"
+
+ static String sanitize(String name) {
+ if (METRIC_NAME_PATTERN.matcher(name).matches() && !name.contains(REPLACEMENT_PREFIX)) {
+ return name;
+ }
+
+ StringBuilder sanitizedName =
+ new StringBuilder(name.substring(0, 1).replaceFirst("[^\\w-]", "_"))
+ if (name.length() == 1) {
+ return sanitizedName.toString()
+ }
+
+ String slashSanitizedName = name.substring(1).replaceAll("/[/]+", "/")
+ if (slashSanitizedName.endsWith("/")) {
+ slashSanitizedName = slashSanitizedName.substring(0, slashSanitizedName.length() - 1)
+ }
+
+ String replacementPrefixSanitizedName =
+ slashSanitizedName.replaceAll(REPLACEMENT_PREFIX, REPLACEMENT_PREFIX + REPLACEMENT_PREFIX)
+
+ for (int i = 0; i < replacementPrefixSanitizedName.length(); i++) {
+ Character c = replacementPrefixSanitizedName.charAt(i)
+ if (c.toString() ==~ INVALID_CHAR_PATTERN) {
+ sanitizedName.append(REPLACEMENT_PREFIX)
+ sanitizedName.append(c.toString().getBytes("UTF-8").encodeHex().toString().toUpperCase())
+ sanitizedName.append('_')
+ } else {
+ sanitizedName.append(c)
+ }
+ }
+
+ return sanitizedName.toString()
+ }
+}
+
+modules = [MetricsModule, LocalRefDbCommandModule]