diff --git a/server/monitor/src/main/java/org/apache/accumulo/monitor/next/Endpoints.java b/server/monitor/src/main/java/org/apache/accumulo/monitor/next/Endpoints.java index 68d4d5627d0..c72a55cc042 100644 --- a/server/monitor/src/main/java/org/apache/accumulo/monitor/next/Endpoints.java +++ b/server/monitor/src/main/java/org/apache/accumulo/monitor/next/Endpoints.java @@ -50,6 +50,7 @@ import org.apache.accumulo.core.client.admin.TabletInformation; import org.apache.accumulo.core.client.admin.servers.ServerId; import org.apache.accumulo.core.data.TableId; +import org.apache.accumulo.core.process.thrift.MetricResponse; import org.apache.accumulo.core.util.compaction.RunningCompactionInfo; import org.apache.accumulo.monitor.Monitor; import org.apache.accumulo.monitor.next.InformationFetcher.InstanceSummary; @@ -185,6 +186,36 @@ public TableData getServerProcessView(@MatrixParam("table") TableDataFactory.Tab return view; } + @GET + @Path("servers/detail/{type}/{resourceGroup}/{server}") + @Produces(MediaType.APPLICATION_JSON) + @Description("Returns a UI-ready metric table for one server process") + public TableData getServerDetail(@PathParam("type") ServerId.Type type, + @PathParam("resourceGroup") String resourceGroup, @PathParam("server") String server) { + if (type == null) { + throw new BadRequestException("A 'type' parameter is required"); + } + if (resourceGroup == null || resourceGroup.isBlank()) { + throw new BadRequestException("A 'resourceGroup' parameter is required"); + } + if (server == null || server.isBlank()) { + throw new BadRequestException("A 'server' parameter is required"); + } + + MetricResponse response; + try { + response = monitor.getInformationFetcher().getSummaryForEndpoint() + .getServerMetricResponse(type, resourceGroup, server); + } catch (IllegalArgumentException e) { + throw new BadRequestException("Invalid server metrics parameters", e); + } + if (response == null) { + throw new NotFoundException("Server " + type.name() + " " + server + " in resource group " + + resourceGroup + " not found"); + } + return TableDataFactory.forServer(response); + } + @GET @Path("compactions/running") @Produces(MediaType.APPLICATION_JSON) diff --git a/server/monitor/src/main/java/org/apache/accumulo/monitor/next/SystemInformation.java b/server/monitor/src/main/java/org/apache/accumulo/monitor/next/SystemInformation.java index 06854319679..e0e9624284b 100644 --- a/server/monitor/src/main/java/org/apache/accumulo/monitor/next/SystemInformation.java +++ b/server/monitor/src/main/java/org/apache/accumulo/monitor/next/SystemInformation.java @@ -889,7 +889,9 @@ public Object getRowData(ServerId sid, MetricResponse mr, if (!qm.isEmpty()) { // Create a ServersView object from the MetricResponse for each queue - return TableDataFactory.forColumns(qm.keySet(), qm, cols); + TableData tableData = TableDataFactory.forColumns(qm.keySet(), qm, cols); + tableData.data().forEach(row -> row.put(TableDataFactory.LINK_RG_COL_KEY, "")); + return tableData; } return TableDataFactory.forColumns(Set.of(), Map.of(), cols); } @@ -1639,6 +1641,16 @@ public TableData getServerProcessView(TableDataFactory.TableName table) { return null; } + public MetricResponse getServerMetricResponse(ServerId.Type type, String resourceGroup, + String serverAddress) { + HostAndPort address = HostAndPort.fromString(serverAddress); + if (!address.hasPort()) { + throw new IllegalArgumentException("Server address must be host:port"); + } + return allMetrics.getIfPresent(new ServerId(type, ResourceGroupId.of(resourceGroup), + address.getHost(), address.getPort())); + } + public static Number getMetricValue(FMetric metric) { if (metric.ivalue() != 0) { return metric.ivalue(); diff --git a/server/monitor/src/main/java/org/apache/accumulo/monitor/next/views/TableDataFactory.java b/server/monitor/src/main/java/org/apache/accumulo/monitor/next/views/TableDataFactory.java index a16cb13063e..859bca36cf3 100644 --- a/server/monitor/src/main/java/org/apache/accumulo/monitor/next/views/TableDataFactory.java +++ b/server/monitor/src/main/java/org/apache/accumulo/monitor/next/views/TableDataFactory.java @@ -39,6 +39,7 @@ import org.apache.accumulo.core.metrics.flatbuffers.FTag; import org.apache.accumulo.core.process.thrift.MetricResponse; import org.apache.accumulo.core.util.threads.ThreadPoolNames; +import org.apache.accumulo.monitor.next.SystemInformation; import org.apache.accumulo.monitor.next.views.TableData.Column; import org.apache.accumulo.server.metrics.MetricResponseWrapper; @@ -53,14 +54,30 @@ public class TableDataFactory { public static final String RG_COL_KEY = "resourceGroup"; public static final String ADDR_COL_KEY = "serverAddress"; + public static final String TYPE_COL_KEY = "serverType"; + public static final String LINK_RG_COL_KEY = "serverLinkResourceGroup"; public static final String TIME_COL_KEY = "lastContact"; + public static final String METRIC_NAME_COL_KEY = "metricName"; + public static final String METRIC_VALUE_COL_KEY = "metricValue"; + public static final String METRIC_TYPE_COL_KEY = "metricType"; + public static final String METRIC_TAGS_COL_KEY = "metricTags"; + public static final String METRIC_DESCRIPTION_COL_KEY = "metricDescription"; + public static final String METRIC_SECTION_COL_KEY = "metricSection"; public static final Column LAST_CONTACT_COLUMN = new Column(TIME_COL_KEY, "Last Contact", "Time since the server last responded to the monitor", "duration"); public static final Column RG_COLUMN = new Column(RG_COL_KEY, "Resource Group", "Resource Group", ""); public static final Column ADDR_COLUMN = - new Column(ADDR_COL_KEY, "Server Address", "Server address", ""); + new Column(ADDR_COL_KEY, "Server Address", "Server address", "server-address"); + + private static final List SERVER_DETAIL_COLUMNS = + List.of(new Column(METRIC_NAME_COL_KEY, "Metric", "Metric name", ""), + new Column(METRIC_VALUE_COL_KEY, "Value", "Metric value", ""), + new Column(METRIC_TYPE_COL_KEY, "Type", "Metric type", ""), + new Column(METRIC_TAGS_COL_KEY, "Tags", "Metric tags", ""), + new Column(METRIC_SECTION_COL_KEY, "Section", "Metric documentation section", ""), + new Column(METRIC_DESCRIPTION_COL_KEY, "Description", "Metric description", "")); /** * Server-process table identifiers accepted by /rest-v2/servers/view. These enum names are used @@ -146,6 +163,8 @@ public static TableData forColumns(final Set servers, List> data = new ArrayList<>(); serverMetricRows.forEach(serverMetricRow -> { Map row = new LinkedHashMap<>(); + row.put(TYPE_COL_KEY, serverMetricRow.server().getType().name()); + row.put(LINK_RG_COL_KEY, serverMetricRow.server().getResourceGroup().canonical()); for (ColumnFactory colf : requestedColumns) { row.put(colf.getColumn().key(), colf.getRowData(serverMetricRow.server(), serverMetricRow.response(), serverMetricRow.metrics())); @@ -157,6 +176,45 @@ public static TableData forColumns(final Set servers, } + public static TableData forServer(final MetricResponse metricResponse) { + if (!hasMetricData(metricResponse)) { + return new TableData(SERVER_DETAIL_COLUMNS, List.of()); + } + + List> data = new ArrayList<>(); + FMetric metric = new FMetric(); + for (var binary : metricResponse.getMetrics()) { + metric = FMetric.getRootAsFMetric(binary, metric); + Map row = new LinkedHashMap<>(); + row.put(METRIC_NAME_COL_KEY, metric.name()); + row.put(METRIC_VALUE_COL_KEY, SystemInformation.getMetricValue(metric)); + row.put(METRIC_TYPE_COL_KEY, metric.type()); + row.put(METRIC_TAGS_COL_KEY, formatTags(metric)); + + try { + Metric metricDefinition = Metric.fromName(metric.name()); + row.put(METRIC_SECTION_COL_KEY, metricDefinition.getDocSection().getSectionTitle()); + row.put(METRIC_DESCRIPTION_COL_KEY, metricDefinition.getDescription()); + } catch (IllegalArgumentException e) { + row.put(METRIC_SECTION_COL_KEY, ""); + row.put(METRIC_DESCRIPTION_COL_KEY, ""); + } + data.add(row); + } + + return new TableData(SERVER_DETAIL_COLUMNS, data); + } + + private static String formatTags(FMetric metric) { + List tags = new ArrayList<>(); + FTag tag = new FTag(); + for (int i = 0; i < metric.tagsLength(); i++) { + tag = metric.tags(tag, i); + tags.add(tag.key() + "=" + tag.value()); + } + return String.join(", ", tags); + } + public static TableData forTable(final TableName table, final Set servers, final Map allMetrics) { diff --git a/server/monitor/src/main/java/org/apache/accumulo/monitor/view/WebViews.java b/server/monitor/src/main/java/org/apache/accumulo/monitor/view/WebViews.java index 55e1e90efa8..2dafa85d677 100644 --- a/server/monitor/src/main/java/org/apache/accumulo/monitor/view/WebViews.java +++ b/server/monitor/src/main/java/org/apache/accumulo/monitor/view/WebViews.java @@ -77,6 +77,10 @@ public class WebViews { */ private static final String TSERVER_PARAM_KEY = "s"; + private static final String SERVER_TYPE_PARAM_KEY = "type"; + + private static final String RESOURCE_GROUP_PARAM_KEY = "resourceGroup"; + private static final Logger log = LoggerFactory.getLogger(WebViews.class); @Inject @@ -195,9 +199,12 @@ public Map getTabletServers( Map model = getModel(); model.put("title", "Tablet Server Status"); if (server != null && !server.isBlank()) { + model.put("title", "Server Metrics"); model.put("template", "server.ftl"); model.put("js", "server.js"); model.put("server", server); + model.put("serverType", "TABLET_SERVER"); + model.put("resourceGroup", ""); return model; } model.put("template", "tservers.ftl"); @@ -205,6 +212,31 @@ public Map getTabletServers( return model; } + /** + * Returns the server metrics template + * + * @param serverType Accumulo server process type + * @param resourceGroup server resource group + * @param server server address + * @return server metrics model + */ + @GET + @Path("server") + @Template(name = "/default.ftl") + public Map getServerMetrics(@QueryParam(SERVER_TYPE_PARAM_KEY) String serverType, + @QueryParam(RESOURCE_GROUP_PARAM_KEY) String resourceGroup, + @QueryParam(TSERVER_PARAM_KEY) @Pattern(regexp = HOSTNAME_PORT_REGEX) String server) { + + Map model = getModel(); + model.put("title", "Server Metrics"); + model.put("template", "server.ftl"); + model.put("js", "server.js"); + model.put("server", server); + model.put("serverType", serverType); + model.put("resourceGroup", resourceGroup == null ? "" : resourceGroup); + return model; + } + /** * Returns the scan servers template * diff --git a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/compactions.js b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/compactions.js index 37ec0853717..62d1a45bcfa 100644 --- a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/compactions.js +++ b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/compactions.js @@ -51,7 +51,7 @@ $(function () { "type": "html", "render": function (data, type, row, meta) { if (type === 'display') { - data = '' + row.server + ''; + data = renderServerMetricsLink('COMPACTOR', '', row.server); } return data; } diff --git 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 index 597144a2110..bbe619a5c01 100644 --- 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 @@ -44,6 +44,7 @@ const ALERT_COUNTS = 'alertCounts'; const RECOVERY = 'recovery'; const INSTANCE_OVERVIEW = 'instanceOverview'; const SCANS = 'scans'; +const SERVER_METRICS = 'serverMetrics'; const LAST_UPDATE = 'lastUpdate'; var STATUS_REQUEST = null; @@ -311,6 +312,31 @@ function sanitize(url) { return url.split('+').join('%2B'); } +function escapeHtml(value) { + if (value === null || value === undefined) { + return ''; + } + return $('
').text(value).html(); +} + +function serverMetricsHref(serverType, resourceGroup, serverAddress) { + var href = 'server?type=' + encodeURIComponent(serverType) + + '&s=' + encodeURIComponent(serverAddress); + if (resourceGroup !== null && resourceGroup !== undefined && resourceGroup !== '') { + href += '&resourceGroup=' + encodeURIComponent(resourceGroup); + } + return href; +} + +function renderServerMetricsLink(serverType, resourceGroup, serverAddress) { + if (!serverType || !resourceGroup || !serverAddress) { + return escapeHtml(serverAddress); + } + return '' + + escapeHtml(serverAddress) + ''; +} + /** * Creates a string with the value to sort and the value to display * Options are 0 = firstcell left, 1 = right, 2 = center, 3 = left @@ -606,6 +632,12 @@ function getServerProcessView(table, storageKey) { return getJSONForTable(url, storageKey); } +function getServerMetrics(serverType, resourceGroup, serverAddress) { + var url = REST_V2_PREFIX + '/servers/detail/' + encodeURIComponent(serverType) + '/' + + encodeURIComponent(resourceGroup) + '/' + encodeURIComponent(serverAddress); + return getJSONForTable(url, SERVER_METRICS); +} + function getCompactorsView() { return getServerProcessView('COMPACTORS', COMPACTOR_SERVER_PROCESS_VIEW); } diff --git a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/server.js b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/server.js index f995a2fc850..2d17d137128 100644 --- a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/server.js +++ b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/server.js @@ -18,264 +18,45 @@ */ "use strict"; -var detailTable, historyTable, currentTable, resultsTable; - -/** - * Makes the REST calls, generates the tables with the new information - */ -function refreshServer() { - ajaxReloadTable(detailTable); - ajaxReloadTable(historyTable); - ajaxReloadTable(currentTable); - ajaxReloadTable(resultsTable); +const htmlServerMetricsTable = '#serverMetrics'; +const htmlServerMetricsBanner = '#serverMetricsStatusBanner'; +const htmlServerMetricsBannerMessage = '#server-metrics-banner-message'; + +function refreshServerMetrics() { + if (!serverMetricsType || !serverMetricsResourceGroup || !serverMetricsAddress) { + sessionStorage[SERVER_METRICS] = JSON.stringify({ + data: [], + columns: [] + }); + refreshTable(htmlServerMetricsTable, SERVER_METRICS); + $(htmlServerMetricsBannerMessage).text('ERROR: missing server metrics parameters.'); + $(htmlServerMetricsBanner).show(); + return; + } + + getServerMetrics(serverMetricsType, serverMetricsResourceGroup, serverMetricsAddress).then( + function () { + refreshTable(htmlServerMetricsTable, SERVER_METRICS); + $(htmlServerMetricsBanner).hide(); + }).fail(function () { + sessionStorage[SERVER_METRICS] = JSON.stringify({ + data: [], + columns: [] + }); + refreshTable(htmlServerMetricsTable, SERVER_METRICS); + $(htmlServerMetricsBannerMessage).text('ERROR: unable to retrieve server metrics.'); + $(htmlServerMetricsBanner).show(); + }); } -/** - * Used to redraw the page - */ function refresh() { - refreshServer(); + refreshServerMetrics(); } -/** - * Initializes all of the DataTables for the given hostname - * - * @param {String} serv the tserver hostname - */ -function initServerTables(serv) { - - const url = contextPath + 'rest/tservers/' + serv; - console.debug('REST url used to fetch data for server.js DataTables: ' + url); - - // Create a table for details on the current server - detailTable = $('#tServerDetail').DataTable({ - "ajax": { - "url": url, - "dataSrc": function (data) { - // the data needs to be in an array to work with DataTables - var arr = []; - if (data.details === undefined) { - console.warn('the value of "details" is undefined'); - } else { - arr = [data.details]; - } - - return arr; - } - }, - "stateSave": true, - "searching": false, - "paging": false, - "info": false, - "columnDefs": [{ - "targets": "big-num", - "render": function (data, type) { - if (type === 'display') { - data = bigNumberForQuantity(data); - } - return data; - } - }], - "columns": [{ - "data": "hostedTablets" - }, - { - "data": "entries" - }, - { - "data": "minors" - } - ] +$(function () { + sessionStorage[SERVER_METRICS] = JSON.stringify({ + data: [], + columns: [] }); - - // Create a table for all time tablet operations - historyTable = $('#opHistoryDetails').DataTable({ - "ajax": { - "url": url, - "dataSrc": "allTimeTabletResults" - }, - "stateSave": true, - "searching": false, - "paging": false, - "info": false, - "columnDefs": [{ - "targets": "big-num", - "render": function (data, type) { - if (type === 'display') { - data = bigNumberForQuantity(data); - } - return data; - } - }, - { - "targets": "duration", - "render": function (data, type) { - if (type === 'display') { - if (data === null) { - data = '—'; - } else { - data = timeDuration(data * 1000.0); - } - } - return data; - } - } - ], - "columns": [{ - "data": "operation" - }, - { - "data": "success" - }, - { - "data": "failure" - }, - { - "data": "avgQueueTime" - }, - { - "data": "queueStdDev" - }, - { - "data": "avgTime" - }, - { - "data": "stdDev" - }, - { - "data": "timeSpent" // placeholder for percent column, replaced below - } - ], - // calculate and fill percent column each time table is drawn - "drawCallback": function () { - var totalTime = 0; - var api = this.api(); - - // calculate total duration of all tablet operations - api.rows().every(function () { - totalTime += this.data().timeSpent; - }); - - const percentColumnIndex = 7; - api.rows().every(function (rowIdx) { - // calculate the percentage of time taken for each row (each tablet op) - var currentPercent = (this.data().timeSpent / totalTime) * 100; - currentPercent = Math.round(currentPercent); - if (isNaN(currentPercent)) { - currentPercent = 0; - } - // insert the percentage bar into the current row and percent column - var newData = `
${currentPercent}%
` - api.cell(rowIdx, percentColumnIndex).data(newData); - }); - } - }); - - // Create a table for tablet operations on the current server - currentTable = $('#currentTabletOps').DataTable({ - "ajax": { - "url": url, - "dataSrc": function (data) { - // the data needs to be in an array to work with DataTables - var arr = []; - if (data.currentTabletOperationResults === undefined) { - console.warn('the value of "currentTabletOperationResults" is undefined'); - } else { - arr = [data.currentTabletOperationResults]; - } - - return arr; - } - }, - "stateSave": true, - "searching": false, - "paging": false, - "info": false, - "columnDefs": [{ - "targets": "duration", - "render": function (data, type) { - if (type === 'display') { - data = timeDuration(data * 1000.0); - } - return data; - } - }], - "columns": [{ - "data": "currentMinorAvg" - }, - { - "data": "currentMinorStdDev" - } - ] - }); - - // Create a table for detailed tablet operations - resultsTable = $('#perTabletResults').DataTable({ - "ajax": { - "url": url, - "dataSrc": "currentOperations" - }, - "stateSave": true, - "dom": 't<"align-left"l>p', - "columnDefs": [{ - "targets": "big-num", - "render": function (data, type) { - if (type === 'display') { - data = bigNumberForQuantity(data); - } - return data; - } - }, - { - "targets": "duration", - "render": function (data, type) { - if (type === 'display') { - data = timeDuration(data); - } - return data; - } - } - ], - "columns": [{ - "data": "name", - "type": "html", - "render": function (data, type, row) { - if (type === 'display') { - data = `${data}`; - } - return data; - } - }, - { - "data": "tablet", - "type": "html", - "render": function (data, type) { - if (type === 'display') { - data = `${data}`; - } - return data; - } - }, - { - "data": "entries" - }, - { - "data": "ingest" - }, - { - "data": "query" - }, - { - "data": "minorAvg" - }, - { - "data": "minorStdDev" - }, - { - "data": "minorAvgES" - } - ] - }); - - refreshServer(); -} + refreshServerMetrics(); +}); diff --git a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/server_process_common.js b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/server_process_common.js index bdb93611459..2952dfed5dd 100644 --- a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/server_process_common.js +++ b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/server_process_common.js @@ -257,7 +257,7 @@ function createDataTable(table, storageKey) { "text": '', "titleAttr": 'Columns' }], - "dom": '<"row"<"col-sm-12 col-md-4"l><"col-sm-12 col-md-6"f><"col-sm-12 col-md-2"B>>' + + "dom": '<"row"<"col-sm-12 col-md-4"l><"col-sm-12 col-md-6 text-md-end"f><"col-sm-12 col-md-2 text-md-end"B>>' + '<"row dt-row"<"col-sm-12"rt>>' + '<"row"<"col-sm-12 col-md-5"i><"col-sm-12 col-md-7"p>>', "stateSave": true, @@ -368,6 +368,15 @@ function createDataTable(table, storageKey) { { "targets": "memory-state", "render": renderMemoryState + }, + { + "targets": "server-address", + "render": function (data, type, row) { + if (type === 'display') { + return renderServerMetricsLink(row.serverType, row.serverLinkResourceGroup, data); + } + return data; + } } ], "columns": getDataTableCols(storageKey) diff --git a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/server.ftl b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/server.ftl index b412b564b36..8204af61ba0 100644 --- a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/server.ftl +++ b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/server.ftl @@ -18,86 +18,25 @@ under the License. --> - -
-
-

${title}

-
-
-
-
- - - - - - - - - - -
${server}
Hosted Tablets Entries Minor Compacting 
-
-
-
-
-
- - - - - - - - - - - - - - - -
All-Time Tablet Operation Results
Operation Success Failure Average
Queue Time 
Std. Dev.
Queue Time 
Average
Time 
Std. Dev.
Time 
Percentage Time Spent 
-
-
-
-
-
- - - - - - - - - -
Current Tablet Operation Results
Minor Average Minor Std Dev 
-
-
-
-
-
- - - - - - - - - - - - - - - -
Detailed Tablet Operations
Table Tablet Entries Ingest Query Minor Avg Minor Std Dev Minor Avg e/s 
-
+ + +
+
+ Server Metrics +
+ + ${(serverType!)?html} ${(server!)?html}<#if resourceGroup?has_content> (${resourceGroup?html}) + +
+
+ + <#include "table_loading.ftl" > +
+