You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@accumulo.apache.org by dd...@apache.org on 2025/07/18 06:15:05 UTC

(accumulo) branch main updated (9c3497bfdc -> 2529d83754)

This is an automated email from the ASF dual-hosted git repository.

ddanielr pushed a change to branch main
in repository https://gitbox.apache.org/repos/asf/accumulo.git


    from 9c3497bfdc Constructed API methods for creating Mutations from key/value pairs (#5702)
     add ab4bafb1ea Refactor common code into Before/AfterEach blocks in FateIT (#5741)
     add 8779467e73 Allows the monitor to be hosted behind a proxy (#5729)
     new 2529d83754 Merge branch '2.1'

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 assemble/pom.xml                                   |  5 ++
 .../org/apache/accumulo/core/conf/Property.java    |  5 ++
 pom.xml                                            |  5 ++
 server/monitor/pom.xml                             |  4 +
 .../apache/accumulo/monitor/EmbeddedWebServer.java | 21 ++++-
 .../java/org/apache/accumulo/monitor/Monitor.java  | 11 ++-
 .../org/apache/accumulo/monitor/view/WebViews.java |  8 +-
 .../accumulo/monitor/resources/js/bulkImport.js    |  2 +-
 .../accumulo/monitor/resources/js/compactions.js   |  4 +-
 .../org/apache/accumulo/monitor/resources/js/ec.js |  8 +-
 .../accumulo/monitor/resources/js/functions.js     | 28 +++----
 .../org/apache/accumulo/monitor/resources/js/gc.js |  2 +-
 .../apache/accumulo/monitor/resources/js/global.js |  2 +
 .../accumulo/monitor/resources/js/manager.js       |  6 +-
 .../apache/accumulo/monitor/resources/js/scans.js  |  4 +-
 .../apache/accumulo/monitor/resources/js/server.js |  4 +-
 .../apache/accumulo/monitor/resources/js/table.js  |  6 +-
 .../accumulo/monitor/resources/js/tservers.js      |  8 +-
 .../apache/accumulo/monitor/templates/default.ftl  | 31 +++----
 .../apache/accumulo/monitor/templates/modals.ftl   |  2 +-
 .../apache/accumulo/monitor/templates/navbar.ftl   | 24 +++---
 .../apache/accumulo/monitor/templates/overview.ftl |  8 +-
 .../apache/accumulo/monitor/templates/tables.ftl   |  4 +-
 .../accumulo/monitor/EmbeddedWebServerTest.java    | 98 ++++++++++++++++++++++
 24 files changed, 227 insertions(+), 73 deletions(-)
 create mode 100644 server/monitor/src/test/java/org/apache/accumulo/monitor/EmbeddedWebServerTest.java


(accumulo) 01/01: Merge branch '2.1'

Posted by dd...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ddanielr pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/accumulo.git

commit 2529d83754a7d4f6036f101fd692d289560f0294
Merge: 9c3497bfdc 8779467e73
Author: Daniel Roberts ddanielr <dd...@gmail.com>
AuthorDate: Fri Jul 18 02:37:09 2025 +0000

    Merge branch '2.1'

 assemble/pom.xml                                   |  5 ++
 .../org/apache/accumulo/core/conf/Property.java    |  5 ++
 pom.xml                                            |  5 ++
 server/monitor/pom.xml                             |  4 +
 .../apache/accumulo/monitor/EmbeddedWebServer.java | 21 ++++-
 .../java/org/apache/accumulo/monitor/Monitor.java  | 11 ++-
 .../org/apache/accumulo/monitor/view/WebViews.java |  8 +-
 .../accumulo/monitor/resources/js/bulkImport.js    |  2 +-
 .../accumulo/monitor/resources/js/compactions.js   |  4 +-
 .../org/apache/accumulo/monitor/resources/js/ec.js |  8 +-
 .../accumulo/monitor/resources/js/functions.js     | 28 +++----
 .../org/apache/accumulo/monitor/resources/js/gc.js |  2 +-
 .../apache/accumulo/monitor/resources/js/global.js |  2 +
 .../accumulo/monitor/resources/js/manager.js       |  6 +-
 .../apache/accumulo/monitor/resources/js/scans.js  |  4 +-
 .../apache/accumulo/monitor/resources/js/server.js |  4 +-
 .../apache/accumulo/monitor/resources/js/table.js  |  6 +-
 .../accumulo/monitor/resources/js/tservers.js      |  8 +-
 .../apache/accumulo/monitor/templates/default.ftl  | 31 +++----
 .../apache/accumulo/monitor/templates/modals.ftl   |  2 +-
 .../apache/accumulo/monitor/templates/navbar.ftl   | 24 +++---
 .../apache/accumulo/monitor/templates/overview.ftl |  8 +-
 .../apache/accumulo/monitor/templates/tables.ftl   |  4 +-
 .../accumulo/monitor/EmbeddedWebServerTest.java    | 98 ++++++++++++++++++++++
 24 files changed, 227 insertions(+), 73 deletions(-)

diff --cc core/src/main/java/org/apache/accumulo/core/conf/Property.java
index 0f47eee39e,625cb0ed1f..6526d04565
--- a/core/src/main/java/org/apache/accumulo/core/conf/Property.java
+++ b/core/src/main/java/org/apache/accumulo/core/conf/Property.java
@@@ -885,18 -1037,46 +885,23 @@@ public enum Property 
            + " The resources that are used by default can be seen in"
            + " `accumulo/server/monitor/src/main/resources/templates/default.ftl`.",
        "2.0.0"),
 +  MONITOR_FETCH_TIMEOUT("monitor.fetch.timeout", "5m", PropertyType.TIMEDURATION,
 +      "The Monitor fetches information for display in a set of background threads. This property"
 +          + " controls the amount of time that process should wait before cancelling any remaining"
 +          + " tasks to fetch information. These background threads could end up waiting on servers"
 +          + " to respond or for scans to complete.",
 +      "4.0.0"),
 +  MONITOR_DEAD_LIST_RG_EXCLUSIONS("monitor.dead.server.rg.exclusions", "", PropertyType.STRING,
 +      "The Monitor displays information about servers that it believes have died recently."
 +          + " This property accepts a comma separated list of resource group names. If"
 +          + " the dead servers resource group matches a resource group in this list,"
 +          + " then it will be suppressed from the dead servers list in the monitor.",
 +      "4.0.0"),
+   MONITOR_ROOT_CONTEXT("monitor.root.context", "/", PropertyType.STRING,
 -      "The root context path of the monitor application. If this value is set, all paths for the"
++      "The root context path of the monit   application. If this value is set, all paths for the"
+           + " monitor application will be hosted using this context. As an example, setting this to `/accumulo/`"
+           + " would cause all `/rest/` endpoints to be hosted at `/accumulo/rest/*`.",
+       "2.1.4"),
 -  @Deprecated(since = "2.1.0")
 -  TRACE_PREFIX("trace.", null, PropertyType.PREFIX,
 -      "Properties in this category affect the behavior of distributed tracing.", "1.3.5"),
 -  @Deprecated(since = "2.1.0")
 -  TRACE_SPAN_RECEIVERS("trace.span.receivers", "org.apache.accumulo.tracer.ZooTraceClient",
 -      PropertyType.CLASSNAMELIST, "A list of span receiver classes to send trace spans.", "1.7.0"),
 -  @Deprecated(since = "2.1.0")
 -  TRACE_SPAN_RECEIVER_PREFIX("trace.span.receiver.", null, PropertyType.PREFIX,
 -      "Prefix for span receiver configuration properties.", "1.7.0"),
 -  @Deprecated(since = "2.1.0")
 -  TRACE_ZK_PATH("trace.zookeeper.path", Constants.ZTRACERS, PropertyType.STRING,
 -      "The zookeeper node where tracers are registered.", "1.7.0"),
 -  @Deprecated(since = "2.1.0")
 -  TRACE_PORT("trace.port.client", "12234", PropertyType.PORT,
 -      "The listening port for the trace server.", "1.3.5"),
 -  @Deprecated(since = "2.1.0")
 -  TRACE_TABLE("trace.table", "trace", PropertyType.STRING,
 -      "The name of the table to store distributed traces.", "1.3.5"),
 -  @Deprecated(since = "2.1.0")
 -  TRACE_USER("trace.user", "root", PropertyType.STRING,
 -      "The name of the user to store distributed traces.", "1.3.5"),
 -  @Sensitive
 -  @Deprecated(since = "2.1.0")
 -  TRACE_PASSWORD("trace.password", "secret", PropertyType.STRING,
 -      "The password for the user used to store distributed traces.", "1.3.5"),
 -  @Sensitive
 -  @Deprecated(since = "2.1.0")
 -  TRACE_TOKEN_PROPERTY_PREFIX("trace.token.property.", null, PropertyType.PREFIX,
 -      "The prefix used to create a token for storing distributed traces. For"
 -          + " each property required by trace.token.type, place this prefix in front of it.",
 -      "1.5.0"),
 -  @Deprecated(since = "2.1.0")
 -  TRACE_TOKEN_TYPE("trace.token.type", PasswordToken.class.getName(), PropertyType.CLASSNAME,
 -      "An AuthenticationToken type supported by the authorizer.", "1.5.0"),
 -
    // per table properties
    TABLE_PREFIX("table.", null, PropertyType.PREFIX,
        "Properties in this category affect tablet server treatment of tablets,"
diff --cc pom.xml
index 837e5d7cbe,fdcab6190e..e7c235c160
--- a/pom.xml
+++ b/pom.xml
@@@ -322,10 -322,21 +322,15 @@@
          <artifactId>commons-logging</artifactId>
          <version>1.3.5</version>
        </dependency>
+       <dependency>
+         <groupId>commons-validator</groupId>
+         <artifactId>commons-validator</artifactId>
+         <version>1.10.0</version>
+       </dependency>
 -      <dependency>
 -        <!-- legacy junit version specified here for dependency convergence -->
 -        <groupId>junit</groupId>
 -        <artifactId>junit</artifactId>
 -        <version>4.13.2</version>
 -      </dependency>
        <dependency>
          <groupId>org.apache.accumulo</groupId>
 -        <artifactId>accumulo-compaction-coordinator</artifactId>
 -        <version>${project.version}</version>
 +        <artifactId>accumulo-access</artifactId>
 +        <version>${version.accumulo-access}</version>
        </dependency>
        <dependency>
          <groupId>org.apache.accumulo</groupId>
diff --cc server/monitor/pom.xml
index 2fc8822fc0,23ecd0d691..a9cf60ede1
--- a/server/monitor/pom.xml
+++ b/server/monitor/pom.xml
@@@ -56,10 -52,10 +56,14 @@@
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
      </dependency>
+     <dependency>
+       <groupId>commons-validator</groupId>
+       <artifactId>commons-validator</artifactId>
+     </dependency>
 +    <dependency>
 +      <groupId>io.micrometer</groupId>
 +      <artifactId>micrometer-core</artifactId>
 +    </dependency>
      <dependency>
        <groupId>jakarta.inject</groupId>
        <artifactId>jakarta.inject-api</artifactId>
diff --cc server/monitor/src/main/java/org/apache/accumulo/monitor/Monitor.java
index ba5a3a858e,f9672ed196..e77a9171dd
--- a/server/monitor/src/main/java/org/apache/accumulo/monitor/Monitor.java
+++ b/server/monitor/src/main/java/org/apache/accumulo/monitor/Monitor.java
@@@ -102,8 -105,8 +102,9 @@@ import org.glassfish.jersey.servlet.Ser
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
+ import com.google.common.base.Preconditions;
  import com.google.common.base.Suppliers;
 +import com.google.common.net.HostAndPort;
  
  /**
   * Serve manager statistics with an embedded web server.
@@@ -397,27 -503,28 +402,31 @@@ public class Monitor extends AbstractSe
      }
      HostAndPort monitorHostAndPort = getAdvertiseAddress();
      log.debug("Using {} to advertise monitor location in ZooKeeper", monitorHostAndPort);
 +
      try {
 -      monitorLock.replaceLockData(monitorHostAndPort.toString().getBytes(UTF_8));
 -    } catch (KeeperException | InterruptedException e) {
 -      throw new IllegalStateException("Exception updating monitor lock with host and port", e);
 +      getMonitorLock(monitorHostAndPort);
 +    } catch (Exception e) {
 +      log.error("Failed to get Monitor ZooKeeper lock");
 +      throw new RuntimeException(e);
      }
 +    getContext().setServiceLock(monitorLock);
  
      MetricsInfo metricsInfo = getContext().getMetricsInfo();
 +    metricsInfo.addMetricsProducers(this);
      metricsInfo.init(MetricsInfo.serviceTags(getContext().getInstanceName(), getApplicationName(),
 -        monitorHostAndPort, ""));
 +        monitorHostAndPort, getResourceGroup()));
  
+     // Needed to support the existing zk monitor address format
+     if (!rootContext.endsWith("/")) {
+       rootContext = rootContext + "/";
+     }
      try {
        URL url = new URL(server.isSecure() ? "https" : "http", monitorHostAndPort.getHost(),
-           server.getPort(), "/");
+           server.getPort(), rootContext);
 -      final String path = context.getZooKeeperRoot() + Constants.ZMONITOR_HTTP_ADDR;
 -      final ZooReaderWriter zoo = context.getZooReaderWriter();
 +      final ZooReaderWriter zoo = context.getZooSession().asReaderWriter();
        // Delete before we try to re-create in case the previous session hasn't yet expired
 -      zoo.delete(path);
 -      zoo.putEphemeralData(path, url.toString().getBytes(UTF_8));
 +      zoo.delete(Constants.ZMONITOR_HTTP_ADDR);
 +      zoo.putEphemeralData(Constants.ZMONITOR_HTTP_ADDR, url.toString().getBytes(UTF_8));
        log.info("Set monitor address in zookeeper to {}", url);
      } catch (Exception ex) {
        log.error("Unable to advertise monitor HTTP address in zookeeper", ex);
diff --cc server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/bulkImport.js
index b5c0e18912,a89bc95ad4..28a1883c90
--- a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/bulkImport.js
+++ b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/bulkImport.js
@@@ -37,9 -38,9 +37,9 @@@ function refresh() 
  /**
   * Initializes the bulk import DataTables
   */
 -$(document).ready(function () {
 +$(function () {
  
-   const url = '/rest/bulkImports';
+   const url = contextPath + 'rest/bulkImports';
    console.debug('REST url used to fetch data for the DataTables in bulkImport.js: ' + url);
  
    // Generates the manager bulk import status table
diff --cc server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/functions.js
index 639d935cf8,30705be653..8a4db14119
--- a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/functions.js
+++ b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/functions.js
@@@ -444,32 -445,17 +444,32 @@@ function getTableServers(tableID) 
  }
  
  /**
 - * REST GET call for the logs, stores it on a sessionStorage variable
 + * REST GET call for the server status, stores it on a sessionStorage variable
   */
 -function getLogs() {
 -  return getJSONForTable(contextPath + 'rest/logs', 'logs');
 +function getStatus() {
-   return getJSONForTable('/rest/status', 'status');
++  return getJSONForTable(contextPath + 'rest/status', 'status');
  }
  
 +/*
 + * Jquery call to clear all data from cells of a table
 + */
 +function clearAllTableCells(tableId) {
 +  console.log("Clearing all table cell data for " + tableId);
 +  $("#" + tableId + " > tbody > tr > td").each(function () {
 +    $(this).text("");
 +  });
 +}
 +
 +// NEW REST CALLS
 +
- const REST_V2_PREFIX = '/rest-v2';
++const REST_V2_PREFIX = contextPath + 'rest-v2';
 +
  /**
 - * REST POST call to clear logs
 + * REST GET call for /problems,
 + * stores it on a sessionStorage variable
   */
 -function clearLogs() {
 -  doLoggedPostCall(contextPath + 'rest/logs/clear', refresh, false);
 +function getProblems() {
 +  return getJSONForTable(REST_V2_PREFIX + '/problems', 'problems');
  }
  
  /**
diff --cc server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/table.js
index 0734b573f6,9a37094149..0dd150c89e
--- a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/table.js
+++ b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/table.js
@@@ -35,17 -34,24 +35,17 @@@ function refresh() 
    refreshTable();
  }
  
 -function getQueuedAndRunning(data) {
 -  return `${data.running}(${data.queued})`;
 -}
 -
  /**
 - * Initialize the table
 - * 
 - * @param {String} tableID the accumulo table ID
 + * Makes the REST call to fetch tablet details and render them.
   */
 -function initTableServerTable(tableID) {
 -
 -  const url = contextPath + 'rest/tables/' + tableID;
 -  console.debug('REST url used to fetch data for table.js DataTable: ' + url);
 +function initTabletsTable(tableId) {
-   var tabletsUrl = '/rest-v2/tables/' + tableId + '/tablets';
++  var tabletsUrl = contextPath + 'rest-v2/tables/' + tableId + '/tablets';
 +  console.debug('Fetching tablets info from: ' + tabletsUrl);
  
 -  tableServersTable = $('#participatingTServers').DataTable({
 +  tabletsTable = $('#tabletsList').DataTable({
      "ajax": {
 -      "url": url,
 -      "dataSrc": "servers"
 +      "url": tabletsUrl,
 +      "dataSrc": ""
      },
      "stateSave": true,
      "columnDefs": [{
@@@ -68,128 -74,111 +68,128 @@@
        }
      ],
      "columns": [{
 -        "data": "hostname",
 -        "type": "html",
 -        "render": function (data, type, row) {
 -          if (type === 'display') {
 -            data = `<a href="tservers?s=${row.id}">${data}</a>`;
 -          }
 -          return data;
 -        }
 +        "data": "tabletId",
 +        "title": "Tablet ID"
        },
        {
 -        "data": "tablets"
 +        "data": "estimatedSize",
 +        "title": "Estimated Size"
        },
        {
 -        "data": "lastContact"
 +        "data": "estimatedEntries",
 +        "title": "Estimated Entries"
        },
        {
 -        "data": "entries"
 +        "data": "tabletAvailability",
 +        "title": "Availability"
        },
        {
 -        "data": "ingest"
 +        "data": "numFiles",
 +        "title": "Files"
        },
        {
 -        "data": "query"
 +        "data": "numWalLogs",
 +        "title": "WALs"
        },
        {
 -        "data": "holdtime"
 +        "data": "location",
 +        "title": "Location"
 +      }
 +    ]
 +  });
 +}
 +
 +/**
 + * Initialize the table
-  * 
++ *
 + * @param {String} tableId the accumulo table ID
 + */
 +function initTableServerTable(tableId) {
-   const url = '/rest-v2/tables/' + tableId;
++  const url = contextPath + 'rest-v2/tables/' + tableId;
 +  console.debug('REST url used to fetch summary data: ' + url);
 +
 +  tableServersTable = $('#participatingTServers').DataTable({
 +    "ajax": {
 +      "url": url,
 +      "dataSrc": function (json) {
 +        // Convert the JSON object into an array for DataTables consumption.
 +        return [json];
 +      }
 +    },
 +    "stateSave": true,
 +    "searching": false,
 +    "paging": false,
 +    "info": false,
 +    "columnDefs": [{
 +        "targets": "big-num",
 +        "render": function (data, type) {
 +          if (type === 'display') {
 +            data = bigNumberForQuantity(data);
 +          }
 +          return data;
 +        }
        },
        {
 -        "data": function (row) {
 -          return getQueuedAndRunning(row.compactions.scans);
 +        "targets": "big-size",
 +        "render": function (data, type) {
 +          if (type === 'display') {
 +            data = bigNumberForSize(data);
 +          }
 +          return data;
          }
 +      }
 +    ],
 +    "columns": [{
 +        "data": "totalEntries",
 +        "title": "Entry Count"
        },
        {
 -        "data": function (row) {
 -          return getQueuedAndRunning(row.compactions.minor);
 -        }
 +        "data": "totalSizeOnDisk",
 +        "title": "Size on disk"
        },
        {
 -        "data": function (row) {
 -          return getQueuedAndRunning(row.compactions.major);
 -        }
 +        "data": "totalFiles",
 +        "title": "File Count"
        },
        {
 -        "data": "indexCacheHitRate"
 +        "data": "totalWals",
 +        "title": "WAL Count"
        },
        {
 -        "data": "dataCacheHitRate"
 +        "data": "totalTablets",
 +        "title": "Total Tablet Count"
        },
        {
 -        "data": "osload"
 +        "data": "availableAlways",
 +        "title": "Always Hosted Count"
        },
        {
 -        "data": "scansRunning",
 -        "visible": false
 +        "data": "availableOnDemand",
 +        "title": "On Demand Count"
        },
        {
 -        "data": "scansQueued",
 -        "visible": false
 +        "data": "availableNever",
 +        "title": "Never Hosted Count"
        },
        {
 -        "data": "minorRunning",
 -        "visible": false
 +        "data": "totalAssignedTablets",
 +        "title": "Assigned Tablet Count"
        },
        {
 -        "data": "minorQueued",
 -        "visible": false
 +        "data": "totalAssignedToDeadServerTablets",
 +        "title": "Tablets Assigned to Dead Servers Count"
        },
        {
 -        "data": "majorRunning",
 -        "visible": false
 +        "data": "totalHostedTablets",
 +        "title": "Hosted Tablet Count"
        },
        {
 -        "data": "majorQueued",
 -        "visible": false
 +        "data": "totalSuspendedTablets",
 +        "title": "Suspended Tablet Count"
 +      },
 +      {
 +        "data": "totalUnassignedTablets",
 +        "title": "Unassigned Tablet Count"
        }
      ]
    });
diff --cc server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/default.ftl
index 92b70794de,037001569e..3d802c533c
--- a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/default.ftl
+++ b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/default.ftl
@@@ -30,13 -31,23 +31,13 @@@
          ${val}
        </#list>
      <#else>
-       <script src="/resources/external/jquery/jquery-3.7.1.js"></script>
-       <script src="/resources/external/bootstrap/js/bootstrap.bundle.js"></script>
-       <script src="/resources/external/datatables/js/jquery.dataTables.js"></script>
-       <script src="/resources/external/datatables/js/dataTables.bootstrap5.js"></script>
-       <link rel="stylesheet" href="/resources/external/bootstrap/css/bootstrap.css" />
-       <link rel="stylesheet" href="/resources/external/bootstrap/css/bootstrap-icons.css" />
-       <link rel="stylesheet" href="/resources/external/datatables/css/dataTables.bootstrap5.css" />
+       <script src="resources/external/jquery/jquery-3.7.1.js"></script>
+       <script src="resources/external/bootstrap/js/bootstrap.bundle.js"></script>
+       <script src="resources/external/datatables/js/jquery.dataTables.js"></script>
+       <script src="resources/external/datatables/js/dataTables.bootstrap5.js"></script>
 -      <script src="resources/external/flot/jquery.canvaswrapper.js"></script>
 -      <script src="resources/external/flot/jquery.colorhelpers.js"></script>
 -      <script src="resources/external/flot/jquery.flot.js"></script>
 -      <script src="resources/external/flot/jquery.flot.saturated.js"></script>
 -      <script src="resources/external/flot/jquery.flot.browser.js"></script>
 -      <script src="resources/external/flot/jquery.flot.drawSeries.js"></script>
 -      <script src="resources/external/flot/jquery.flot.uiConstants.js"></script>
 -      <script src="resources/external/flot/jquery.flot.legend.js"></script>
 -      <script src="resources/external/flot/jquery.flot.time.js"></script>
 -      <script src="resources/external/flot/jquery.flot.resize.js"></script>
+       <link rel="stylesheet" href="resources/external/bootstrap/css/bootstrap.css" />
+       <link rel="stylesheet" href="resources/external/bootstrap/css/bootstrap-icons.css" />
+       <link rel="stylesheet" href="resources/external/datatables/css/dataTables.bootstrap5.css" />
      </#if>
  
      <!-- accumulo resources -->
@@@ -54,11 -65,10 +55,11 @@@
        });
      </script>
      <#if js??>
-       <script src="/resources/js/${js}"></script>
+       <script src="resources/js/${js}"></script>
      </#if>
-     <script src="/resources/js/navbar.js"></script>
-     <script src="/resources/js/systemAlert.js"></script>
-     <script src="/resources/js/modals.js"></script>
+     <script src="resources/js/navbar.js"></script>
+     <script src="resources/js/systemAlert.js"></script>
++    <script src="resources/js/modals.js"></script>
    </head>
  
    <body>
diff --cc server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/navbar.ftl
index 7135936d88,7e77c4c3fe..b6424668ee
--- a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/navbar.ftl
+++ b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/navbar.ftl
@@@ -52,10 -52,20 +52,10 @@@
                  Activity
                </a>
                <ul class="dropdown-menu col-xs-12" aria-labelledby="navbarDropdown">
-                 <li><a class="dropdown-item" href="/compactions">Active&nbsp;Compactions</a></li>
-                 <li><a class="dropdown-item" href="/scans">Active&nbsp;Scans</a></li>
-                 <li><a class="dropdown-item" href="/bulkImports">Bulk&nbsp;Imports</a></li>
-                 <li><a class="dropdown-item" href="/ec">External&nbsp;Compactions</a></li>
+                 <li><a class="dropdown-item" href="compactions">Active&nbsp;Compactions</a></li>
+                 <li><a class="dropdown-item" href="scans">Active&nbsp;Scans</a></li>
+                 <li><a class="dropdown-item" href="bulkImports">Bulk&nbsp;Imports</a></li>
+                 <li><a class="dropdown-item" href="ec">External&nbsp;Compactions</a></li>
 -                <li><a class="dropdown-item" href="replication">Replication</a></li>
 -              </ul>
 -            </li>
 -            <li class="dropdown">
 -              <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown"
 -              role="button" data-bs-toggle="dropdown" aria-expanded="false">Debug&nbsp;<span id="errorsNotification" class="badge"></span><span class="caret"></span>
 -              </a>
 -              <ul class="dropdown-menu">
 -                <li><a class="dropdown-item" href="log">Recent&nbsp;Logs&nbsp;<span id="recentLogsNotifications" class="badge"></span></a></li>
 -                <li><a class="dropdown-item" href="problems">Table&nbsp;Problems&nbsp;<span id="tableProblemsNotifications" class="badge"></span></a></li>
                </ul>
              </li>
              <li class="dropdown">
diff --cc server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/tables.ftl
index dcbfc71900,c010c68dae..8f481388cc
--- a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/tables.ftl
+++ b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/tables.ftl
@@@ -29,13 -29,8 +29,13 @@@
  
            tableList = $('#tableList').DataTable({
              "ajax": {
-               "url": "/rest-v2/tables",
 -              "url": '${rootContext}rest/tables',
 -              "dataSrc": "table"
++              "url": '${rootContext}rest-v2/tables',
 +              "dataSrc": function (json) {
 +                return Object.keys(json).map(function (key) {
 +                  json[key].tableId = key;
 +                  return json[key];
 +                });
 +              }
              },
              "stateSave": true,
              "columnDefs": [
@@@ -64,7 -79,7 +64,7 @@@
                  "type": "html",
                  "render": function (data, type, row, meta) {
                    if (type === 'display') {
-                     data = '<a href="/tables/' + row.tableId + '">' + row.tableName + '</a>';
 -                    data = '<a href="tables/' + row.tableId + '">' + row.tablename + '</a>';
++                    data = '<a href="tables/' + row.tableId + '">' + row.tableName + '</a>';
                    }
                    return data;
                  }
diff --cc server/monitor/src/test/java/org/apache/accumulo/monitor/EmbeddedWebServerTest.java
index 0000000000,1b1cd90b0c..188db48225
mode 000000,100644..100644
--- a/server/monitor/src/test/java/org/apache/accumulo/monitor/EmbeddedWebServerTest.java
+++ b/server/monitor/src/test/java/org/apache/accumulo/monitor/EmbeddedWebServerTest.java
@@@ -1,0 -1,93 +1,98 @@@
+ /*
+  * Licensed to the Apache Software Foundation (ASF) under one
+  * or more contributor license agreements.  See the NOTICE file
+  * distributed with this work for additional information
+  * regarding copyright ownership.  The ASF licenses this file
+  * to you 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
+  *
+  *   https://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 org.apache.accumulo.monitor;
+ 
+ import static org.easymock.EasyMock.createMock;
+ import static org.easymock.EasyMock.expect;
+ import static org.easymock.EasyMock.replay;
+ import static org.easymock.EasyMock.verify;
+ import static org.junit.jupiter.api.Assertions.assertEquals;
+ import static org.junit.jupiter.api.Assertions.assertThrows;
+ 
+ import java.util.concurrent.atomic.AtomicReference;
+ 
+ import org.apache.accumulo.core.conf.ConfigurationCopy;
+ import org.apache.accumulo.core.conf.DefaultConfiguration;
+ import org.apache.accumulo.core.conf.Property;
+ import org.apache.accumulo.server.ServerContext;
++import org.eclipse.jetty.io.ConnectionStatistics;
+ import org.junit.jupiter.api.AfterAll;
+ import org.junit.jupiter.api.BeforeAll;
+ import org.junit.jupiter.api.Test;
+ 
+ /**
+  * Basic tests for EmbeddedWebServer
+  */
+ public class EmbeddedWebServerTest {
+ 
+   private static final AtomicReference<Monitor> monitor = new AtomicReference<>(null);
+ 
+   private static final AtomicReference<ConfigurationCopy> configuration = new AtomicReference<>();
+ 
+   @BeforeAll
+   public static void createMocks() {
+ 
+     // Mock a configuration with the new context Path
+     ConfigurationCopy config = new ConfigurationCopy(DefaultConfiguration.getInstance());
+     config.set(Property.MONITOR_ROOT_CONTEXT, "/test/");
+     configuration.set(config);
+ 
+     ServerContext contextMock = createMock(ServerContext.class);
+     expect(contextMock.getConfiguration()).andReturn(config).atLeastOnce();
+ 
+     Monitor monitorMock = createMock(Monitor.class);
+     expect(monitorMock.getContext()).andReturn(contextMock).atLeastOnce();
++    ConnectionStatistics connectionStatisticMock = createMock(ConnectionStatistics.class);
++    expect(connectionStatisticMock.isRunning()).andReturn(true).atLeastOnce();
++    expect(monitorMock.getConnectionStatisticsBean()).andReturn(connectionStatisticMock)
++        .atLeastOnce();
+     expect(monitorMock.getConfiguration()).andReturn(config).atLeastOnce();
+     expect(monitorMock.getBindAddress()).andReturn("localhost:9995").atLeastOnce();
+ 
 -    replay(contextMock, monitorMock);
++    replay(contextMock, monitorMock, connectionStatisticMock);
+     monitor.set(monitorMock);
+   }
+ 
+   @AfterAll
+   public static void finishMocks() {
+     Monitor m = monitor.get();
 -    verify(m.getContext(), m);
++    verify(m.getContext(), m, m.getConnectionStatisticsBean());
+   }
+ 
+   @Test
+   public void testContextPath() {
+     // Test removal of trailing slash
+     EmbeddedWebServer ews = new EmbeddedWebServer(monitor.get(),
+         Integer.parseInt(Property.MONITOR_PORT.getDefaultValue()));
+     assertEquals("/test", ews.getContextPath(),
+         "Context path of " + ews.getContextPath() + " does not match");
+     // Test redirect URL
+     configuration.get().set(Property.MONITOR_ROOT_CONTEXT, "/../test");
+     IllegalArgumentException exception =
+         assertThrows(IllegalArgumentException.class, () -> new EmbeddedWebServer(monitor.get(),
+             Integer.parseInt(Property.MONITOR_PORT.getDefaultValue())));
+     assertEquals("Root context: \"/../test\" is not a valid URL", exception.getMessage());
+     // Test whitespace in URL
+     configuration.get().set(Property.MONITOR_ROOT_CONTEXT, "/whitespace /test");
+     exception =
+         assertThrows(IllegalArgumentException.class, () -> new EmbeddedWebServer(monitor.get(),
+             Integer.parseInt(Property.MONITOR_PORT.getDefaultValue())));
+     assertEquals("Root context: \"/whitespace /test\" is not a valid URL", exception.getMessage());
+   }
+ }