all() {
return ExtensionList.lookup(ConsoleAnnotatorFactory.class);
}
+
+ /**
+ * This action makes {@link hudson.console.ConsoleAnnotatorFactory} instances accessible via HTTP.
+ *
+ * @see hudson.Functions#generateConsoleAnnotationScriptAndStylesheet
+ * @see ConsoleAnnotatorFactory#hasStylesheet()
+ * @see ConsoleAnnotatorFactory#hasScript()
+ */
+ @Restricted(NoExternalUse.class)
+ @Extension
+ public static class RootAction extends InvisibleAction implements hudson.model.RootAction {
+ @Override
+ public String getUrlName() {
+ return ConsoleAnnotatorFactory.class.getName();
+ }
+
+ public ConsoleAnnotatorFactory> getDynamic(String className) {
+ return all().getDynamic(className);
+ }
+ }
}
diff --git a/core/src/main/java/jenkins/diagnosis/MemoryUsageMonitorAction.java b/core/src/main/java/jenkins/diagnosis/MemoryUsageMonitorAction.java
new file mode 100644
index 0000000000..7ecb034f9f
--- /dev/null
+++ b/core/src/main/java/jenkins/diagnosis/MemoryUsageMonitorAction.java
@@ -0,0 +1,53 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2025, CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package jenkins.diagnosis;
+
+import hudson.Extension;
+import hudson.ExtensionList;
+import hudson.diagnosis.MemoryUsageMonitor;
+import hudson.model.InvisibleAction;
+import hudson.model.RootAction;
+import jenkins.model.Jenkins;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
+/**
+ * Expose {@link hudson.diagnosis.MemoryUsageMonitor#heap} at the {@code /hudson.diagnosis.MemoryUsageMonitor/heap} URL.
+ *
+ * @since TODO
+ */
+@Extension
+@Restricted(NoExternalUse.class)
+public class MemoryUsageMonitorAction extends InvisibleAction implements RootAction {
+ @Override
+ public String getUrlName() {
+ return MemoryUsageMonitorAction.class.getName();
+ }
+
+ public MemoryUsageMonitor.MemoryGroup getHeap() {
+ Jenkins.get().checkAnyPermission(Jenkins.SYSTEM_READ, Jenkins.MANAGE);
+ return ExtensionList.lookupSingleton(MemoryUsageMonitor.class).heap;
+ }
+}
diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java
index 1f5bea1275..686d780482 100644
--- a/core/src/main/java/jenkins/model/Jenkins.java
+++ b/core/src/main/java/jenkins/model/Jenkins.java
@@ -2825,11 +2825,13 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
}
/**
- * Used to bind {@link ExtensionList}s to URLs.
+ * Formerly used to bind {@link ExtensionList}s to URLs.
+ *
+ * Currently handled by {@link jenkins.telemetry.impl.HttpExtensionList.ExtensionListRootAction}.
+ *
*
* @since 1.349
*/
- @StaplerDispatchable
public ExtensionList getExtensionList(String extensionType) throws ClassNotFoundException {
return getExtensionList(pluginManager.uberClassLoader.loadClass(extensionType));
}
diff --git a/core/src/main/java/jenkins/telemetry/impl/HttpExtensionList.java b/core/src/main/java/jenkins/telemetry/impl/HttpExtensionList.java
new file mode 100644
index 0000000000..ebab0f824c
--- /dev/null
+++ b/core/src/main/java/jenkins/telemetry/impl/HttpExtensionList.java
@@ -0,0 +1,124 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2025, CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package jenkins.telemetry.impl;
+
+import static java.util.logging.Level.FINE;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.Extension;
+import hudson.ExtensionList;
+import hudson.model.InvisibleAction;
+import hudson.model.RootAction;
+import java.time.LocalDate;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Logger;
+import jenkins.model.Jenkins;
+import jenkins.telemetry.Telemetry;
+import net.sf.json.JSONObject;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+import org.kohsuke.stapler.Stapler;
+import org.kohsuke.stapler.StaplerRequest2;
+
+/**
+ * Collect information about which URLs in {@code /extensionList/} are being accessed.
+ *
+ * @since TODO
+ */
+@Extension
+@Restricted(NoExternalUse.class)
+public class HttpExtensionList extends Telemetry {
+ private final Map calls = new ConcurrentHashMap<>();
+
+ @NonNull
+ @Override
+ public String getDisplayName() {
+ return "Extension List access via HTTP";
+ }
+
+ @NonNull
+ @Override
+ public LocalDate getStart() {
+ return LocalDate.of(2025, 4, 5);
+ }
+
+ @NonNull
+ @Override
+ public LocalDate getEnd() {
+ return LocalDate.of(2025, 7, 1);
+ }
+
+ @Override
+ public synchronized JSONObject createContent() {
+ JSONObject info = new JSONObject();
+ info.put("components", buildComponentInformation());
+
+ Map currentRequests = new TreeMap<>(calls);
+ calls.clear();
+
+ JSONObject payload = new JSONObject();
+ payload.putAll(currentRequests);
+
+ info.put("dispatches", payload);
+ return info;
+ }
+
+ public synchronized void record(String path) {
+ String[] parts = path.split("/");
+ if (parts.length > 1) {
+ // Record just extension point + implementation class
+ path = parts[0] + '/' + parts[1];
+ }
+ calls.compute(path, (p, v) -> v == null ? 1 : v + 1);
+ }
+
+ @Extension
+ @Restricted(NoExternalUse.class)
+ public static class ExtensionListRootAction extends InvisibleAction implements RootAction {
+ private static final Logger LOGGER = Logger.getLogger(ExtensionListRootAction.class.getName());
+
+ @Override
+ public String getUrlName() {
+ return "extensionList";
+ }
+
+ public ExtensionList getDynamic(String extensionType) throws ClassNotFoundException {
+ StaplerRequest2 req = Stapler.getCurrentRequest2();
+ if (req != null && extensionType != null) {
+ try {
+ final HttpExtensionList telemetry = ExtensionList.lookupSingleton(HttpExtensionList.class);
+ if (telemetry.isActivePeriod()) {
+ telemetry.record(extensionType + req.getRestOfPath());
+ }
+ } catch (Exception ex) {
+ LOGGER.log(FINE, "Failed to record telemetry for " + HttpExtensionList.class.getName(), ex);
+ }
+ }
+ return Jenkins.get().getExtensionList(extensionType);
+ }
+ }
+}
diff --git a/core/src/main/resources/jenkins/telemetry/impl/HttpExtensionList/description.jelly b/core/src/main/resources/jenkins/telemetry/impl/HttpExtensionList/description.jelly
new file mode 100644
index 0000000000..5b47f34563
--- /dev/null
+++ b/core/src/main/resources/jenkins/telemetry/impl/HttpExtensionList/description.jelly
@@ -0,0 +1,9 @@
+
+
+
+ ${%blurb}
+
+
+ ${%blurb2}
+
+
diff --git a/core/src/main/resources/jenkins/telemetry/impl/HttpExtensionList/description.properties b/core/src/main/resources/jenkins/telemetry/impl/HttpExtensionList/description.properties
new file mode 100644
index 0000000000..3e3b86c311
--- /dev/null
+++ b/core/src/main/resources/jenkins/telemetry/impl/HttpExtensionList/description.properties
@@ -0,0 +1,4 @@
+blurb = This trial records uses of an HTTP endpoint granting direct access to extensions, the building blocks of Jenkins's extensibility through plugins. \
+ It is expected that there are few legitimate uses of this endpoint, and this trial informs plans for its eventual removal.
+blurb2 = Additionally, this trial collects the list of installed plugins, their version, and the version of Jenkins. \
+ This data will be used to understand the environments that Jenkins is running in.
diff --git a/src/main/js/pages/manage-jenkins/system-information/index.js b/src/main/js/pages/manage-jenkins/system-information/index.js
index 946fb4b49c..ec67a0bc60 100644
--- a/src/main/js/pages/manage-jenkins/system-information/index.js
+++ b/src/main/js/pages/manage-jenkins/system-information/index.js
@@ -10,7 +10,7 @@ graphHost.style.aspectRatio = `${imageWidth} / ${imageHeight}`;
timespanSelect.addEventListener("change", () => {
const rootURL = document.head.dataset.rooturl;
const type = timespanSelect.value;
- graphHost.innerHTML = `
`;
+ graphHost.innerHTML = `
`;
});
// Dispatch a change event to insert a graph on page load