blob: d8e6853fc18b07c2be6a61f82a7f57d3699ac6ea [file] [log] [blame]
<#--
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
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.
-->
<#include "*/generic.ftl">
<#-- Format comma-delimited string-->
<#macro format_string str>
<#if str?has_content>
${(str?split(","))?join(", ")}
<#else>
${"<empty>"}
</#if>
</#macro>
<#macro page_head>
<style>
.list-value {
text-align: right !important;
}
</style>
</#macro>
<#macro page_body>
<div class="page-header">
</div>
<#if (model.getMismatchedVersions()?size > 0)>
<div id="message" class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<strong>Drill does not support clusters containing a mix of Drillbit versions.
Current drillbit version is ${model.getCurrentVersion()}.
One or more drillbits in cluster have different version:
${model.getMismatchedVersions()?join(", ")}.
</strong>
</div>
</#if>
<#include "*/confirmationModals.ftl">
<div class="row">
<div class="col-md-12">
<h3><span id="sizeLabel">Drillbits <span class="label label-primary" id="size">${model.getDrillbits()?size}</span>
<button type="button" class="btn btn-warning" id='reloadBtn' style="display:none;font-size:50%" title='New Drillbits detected! Click to Refresh' onclick='location.reload();'>
<span class="glyphicon glyphicon-refresh"></span></button></h3>
<div class="table-responsive">
<table class="table table-hover" id="bitTable">
<thead>
<tr>
<th>#</th>
<th title="Drillbits in Cluster" style="cursor: help;">Address <span class="glyphicon glyphicon-info-sign" style="font-size:100%"/></th>
<th title="Heap Memory in use (as percent of Total Heap)" style="cursor: help;">Heap Memory Usage <span class="glyphicon glyphicon-info-sign" style="font-size:100%"/></th>
<th title="Estimated Direct Memory ACTIVELY in use (as percent of Peak Usage)" style="cursor: help;">Direct Memory Usage <span class="glyphicon glyphicon-info-sign" style="font-size:100%"/></th>
<th title="Current CPU usage by Drill" style="cursor: help;">CPU Usage<span class="glyphicon glyphicon-info-sign" style="font-size:100%"/></th>
<th title="Average load on the system in the last 1 minute" style="cursor: help;">Avg Sys Load <span class="glyphicon glyphicon-info-sign" style="font-size:100%"/></th>
<th>User Port</th>
<th>Control Port</th>
<th>Data Port</th>
<th>Version</th>
<th>Status</th>
<th>Uptime</th>
<th style="display:${(model.isAuthEnabled() && !model.shouldShowAdminInfo())?then("none","table-cell")}">Shutdown</th>
</tr>
</thead>
<tbody>
<#assign i = 1>
<#list model.getDrillbits() as drillbit>
<tr id="row-${i}">
<td>${i}</td>
<td id="address" >${drillbit.getAddress()}<#if drillbit.isCurrent()>
<span class="label label-info" id="current">Current</span>
<#else>
<a onclick="popOutRemoteDbitUI('${drillbit.getAddress()}','${drillbit.getHttpPort()}');" style="cursor:pointer;color:blue" title="Open in new window"><span class="glyphicon glyphicon-new-window"/></a>
</#if>
</td>
<td id="httpPort" style="display:none">${drillbit.getHttpPort()}</td>
<td class="heap">Not Available</td>
<td class="direct">Not Available</td>
<td class="bitload"><span class="label label-info" id="NA">Not Available</span></td>
<td class="avgload"><span class="label label-info" id="NA">Not Available</span></td>
<td id="port">${drillbit.getUserPort()}</td>
<td>${drillbit.getControlPort()}</td>
<td>${drillbit.getDataPort()}</td>
<td>
<span class="label
<#if drillbit.isVersionMatch()>label-success<#else>label-danger</#if>">
${drillbit.getVersion()}
</span>
</td>
<td id="status" >${drillbit.getState()}</td>
<td class="uptime" >Not Available</td>
<td>
<#if (model.isAuthEnabled() && !model.shouldShowAdminInfo())>
<button class='shutdownCtrl' type="button" id="shutdown" disabled="true" style="opacity:0.5;cursor:not-allowed;display:none" onClick="" comment="Placeholder. Hidden by default">
<#else>
<button class='shutdownCtrl' type="button" id="shutdown" disabled="true" style="opacity:0.5;cursor:not-allowed;display:inline" onClick="shutdown($(this));" title="Not available for remote Drillbits">
</#if>
<span class="glyphicon glyphicon-off"></span></button>
</td>
<td id="queriesCount"> </td>
</tr>
<#assign i = i + 1>
</#list>
</tbody>
</table>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h3>Encryption</h3>
<div class="table-responsive">
<table class="table table-hover" style="width: auto;">
<tbody>
<tr>
<td>Client to Bit Encryption</td>
<td class="list-value">${model.isUserEncryptionEnabled()?string("Enabled", "Disabled")}
<#if model.isBitEncryptionEnabled()><span class="glyphicon glyphicon-lock"/></#if>
</td>
</tr>
<tr>
<td>Bit to Bit Encryption</td>
<td class="list-value">${model.isBitEncryptionEnabled()?string("Enabled", "Disabled")}
<#if model.isBitEncryptionEnabled()><span class="glyphicon glyphicon-lock"/></#if>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<#if model.shouldShowAdminInfo()>
<div class="row">
<div class="col-md-12">
<h3>User Info </h3>
<div class="table-responsive">
<table class="table table-hover" style="width: auto;">
<tbody>
<tr>
<td>Admin Users</td>
<td class="list-value"><@format_string str=model.getAdminUsers()/></td>
</tr>
<tr>
<td>Admin User Groups</td>
<td class="list-value"><@format_string str=model.getAdminUserGroups()/></td>
</tr>
<tr>
<td>Process User</td>
<td class="list-value">${model.getProcessUser()}</td>
</tr>
<tr>
<td>Process User Groups</td>
<td class="list-value">${model.getProcessUserGroups()}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</#if>
<#assign queueInfo = model.queueInfo() />
<div class="row">
<div class="col-md-12">
<h3>Query Throttling</h3>
<div class="table-responsive">
<table class="table table-hover" style="width: auto;">
<tbody>
<tr>
<td>Queue Status</td>
<td class="list-value">${queueInfo.isEnabled()?string("Enabled", "Disabled")}</td>
</tr>
<#if queueInfo.isEnabled() >
<tr>
<td>Maximum Concurrent "Small" Queries</td>
<td class="list-value">${queueInfo.smallQueueSize()}</td>
</tr>
<tr>
<td>Maximum Concurrent "Large" Queries</td>
<td class="list-value">${queueInfo.largeQueueSize()}</td>
</tr>
<tr>
<td>Cost Threshhold for Large vs. Small Queries</td>
<td class="list-value">${queueInfo.threshold()}</td>
</tr>
<tr>
<td>Total Memory</td>
<td class="list-value">${queueInfo.totalMemory()}</td>
</tr>
<tr>
<td>Memory per Small Query</td>
<td class="list-value">${queueInfo.smallQueueMemory()}</td>
</tr>
<tr>
<td>Memory per Large Query</td>
<td class="list-value">${queueInfo.largeQueueMemory()}</td>
</tr>
</#if>
</tbody>
</table>
</div>
</div>
</div>
<script charset="utf-8">
var refreshTime = 10000;
var nAText = "Not Available";
var refresh = getRefreshTime();
var timeout;
var size = $("#size").html();
reloadMetrics();
setInterval(reloadMetrics, refreshTime);
//Gets a refresh time for graceful shutdown
function getRefreshTime() {
$.ajax({
type: 'GET',
url: '/gracePeriod',
dataType: "json",
complete: function (data) {
let gracePeriod = data.responseJSON["gracePeriod"];
if (gracePeriod > 0) {
refreshTime = gracePeriod / 3;
}
timeout = setTimeout(reloadStatus, refreshTime);
}
});
}
//Periodic reload of status of Drillbits
function reloadStatus () {
let result = $.ajax({
type: 'GET',
url: '/state',
dataType: "json",
complete: function(data) {
fillStatus(data,size);
}
});
timeout = setTimeout(reloadStatus, refreshTime);
}
//Fill the Status table for all the listed drillbits
function fillStatus(dataResponse,size) {
let status_map = (dataResponse.responseJSON);
//In case localhost has gone down (i.e. we don't know status from ZK)
if (typeof status_map == 'undefined') {
//Query a couple of other nodes for state details
//Pick 3 Random bits
let randomAltBitList = getRandomIndexList(size, 3);
let numAltBits = randomAltBitList.length;
for (j = 0; j < numAltBits; j++) {
let currentRow = $("#row-"+ randomAltBitList[j]);
if (currentRow.find("#current").html() == "Current") {
continue; //Skip LocalHost
}
let address = currentRow.find("#address").contents().get(0).nodeValue.trim();
let restPort = currentRow.find("#httpPort").contents().get(0).nodeValue.trim();
let altStateUrl = location.protocol + "//" + address+":"+restPort + "/state";
let altResponse = $.ajax({
url:altStateUrl,
dataType:'json',
timeout: 3000,
success : function(stateDataJson) {
//Update Status & Buttons for alternate stateData
if (typeof status_map == 'undefined') {
//Note: Race-condition would not exist due to JScript's single threaded model, only one thread can update the variable
//Ref: https://stackoverflow.com/questions/7238586/do-i-need-to-be-concerned-with-race-conditions-with-asynchronous-javascript
status_map = stateDataJson; //Marking map as 'defined'
updateStatusAndShutdown(stateDataJson);
}
}
});
//Don't loop any more (for small #bits, status_map will never update fast enough)
if (typeof status_map != 'undefined') {
break;
}
}
} else {
updateStatusAndShutdown(status_map);
}
}
//Generates a list of indices
function getRandomIndexList(inputSize, outputSize) {
//Generate inputList
let inputList = [];
let outputList = [];
//Skip 'row-1'
for (p = 2; p <= inputSize; p++) {
let currentRow = $("#row-"+ p);
if (currentRow.find("#status").text() == 'ONLINE' ) {
inputList.push(p);
}
}
//Define how many to pick (pick min if numDBits is too small)
let actualOPSize = (inputList.length > outputSize ? outputSize : inputList.length);
for (q = 0; q < actualOPSize; q++) {
let randomIndex = Math.floor(Math.random() * inputList.length);
outputList.push(inputList[randomIndex]); //Capture random pick
inputList.splice(randomIndex, 1); //Remove from list
}
return outputList;
}
//Updates the status map
function updateStatusAndShutdown(status_map) {
let bitMap = {};
if (typeof status_map != 'undefined') {
for (let k in status_map) {
bitMap[k] = status_map[k];
}
}
for (i = 1; i <= size; i++) {
let currentRow = $("#row-"+i);
let address = currentRow.find("#address").contents().get(0).nodeValue.trim();
let port = currentRow.find("#httpPort").html();
let key = address+"-"+port;
if (typeof status_map == 'undefined') {
currentRow.find("#status").text(nAText).css('font-style','').prop('title','');
currentRow.find("#shutdown").prop('disabled',true).css('opacity',0.5).css('cursor','not-allowed');
currentRow.find("#queriesCount").text("");
} else if (status_map[key] == null) {
currentRow.find("#status").text("OFFLINE*").css('font-style','italic').prop('title','Drillbit is De-Registered from ZooKeeper');
currentRow.find("#shutdown").prop('disabled',true).css('opacity',0.5).css('cursor','not-allowed');
currentRow.find("#queriesCount").text("");
} else {
if (status_map[key] === "ONLINE") {
currentRow.find("#status").text(status_map[key]).css('font-style','').prop('title','');
//EnableShutdown IFF => !isAuthEnabled OR isAuthEnabled-&&-current
if ( ( !${model.isAuthEnabled()?c} ) || ( ${model.shouldShowAdminInfo()?c} && currentRow.find("#current").html() === "Current" ) ) {
currentRow.find("#shutdown").prop('disabled',false).css('opacity',1.0).css('cursor','pointer').attr('title','');
}
} else {
if (currentRow.find("#current").html() === "Current") {
fillQueryCount(i);
}
currentRow.find("#status").text(status_map[key]).css('font-style', '').prop('title', '');
}
//Removing accounted key
delete bitMap[key];
}
}
//If bitMap is not empty, then new bits have been discovered!
listNewDrillbits(bitMap);
}
//Add new Bits for listing
function listNewDrillbits(newBits) {
let newBitList = Object.keys(newBits);
let tableRef = document.getElementById('bitTable').getElementsByTagName('tbody')[0];
let bitId = size;
for (i = 0; i < newBitList.length; i++) {
let splitPt = newBitList[i].lastIndexOf("-");
let displayNodeName = newBitList[i].substring(0, splitPt);
let newBitHttpPort = newBitList[i].substring(splitPt+1);
let newBitElemId = "neo-"+newBitList[i];
let newBitElem = document.getElementsByName(newBitElemId);
bitId++;
let bitState = newBits[newBitList[i]];
//Injecting new row for previously unseen Drillbit
$('#bitTable').find('tbody')
.append("<tr id='row-" + bitId + "' class='newbit' title='Recommend page refresh for more info'>"
+ "<td>"+bitId+"</td>"
+ "<td id='address' name='"+newBitElemId+"'>"+displayNodeName+" <span class='label label-primary' id='size' >new</span></td>"
+ "<td id='httpPort' style='display:none'>"+newBitHttpPort+"</td>"
+ "<td class='heap'>"+nAText+"</td>"
+ "<td class='direct'>"+nAText+"</td>"
+ "<td class='bitload'>"+nAText+"</td>"
+ "<td class='avgload'>"+nAText+"</td>"
+ "<td uiElem='userPort'>--</td>"
+ "<td uiElem='ctrlPort'>--</td>"
+ "<td uiElem='dataPort'>--</td>"
+ "<td uiElem='version'>"+nAText+"</td>"
+ "<td id='status'>"+bitState+"</td>"
+ "<td class='uptime'>"+nAText+"</td>"
+ "<td>"
+ "<button type='button' id='shutdown' onclick='shutdown($(this));' disabled='true' style='opacity:0.5;cursor:not-allowed;${(model.isAuthEnabled() && !model.shouldShowAdminInfo())?then('display:none','')}'>"
+ "<span class='glyphicon glyphicon-off'></span></button></td>"
+ "<td uiElem='queriesCount' />"
+ "</tr>"
);
}
//Update Drillbits count on top
if (newBitList.length > 0) {
//Setting Drillbit Count & Display Refresh Icon
$('#size').text(bitId);
$('#reloadBtn').css('display', 'inline');
size = bitId;
}
}
//Updates the outstanding queries in flight before shutdown
function fillQueryCount(row_id) {
let requestPath = "/queriesCount";
let url = getRequestUrl(requestPath);
let result = $.ajax({
type: 'GET',
url: url,
complete: function(data) {
queries = data.responseJSON["queriesCount"];
fragments = data.responseJSON["fragmentsCount"];
$("#row-"+row_id).find("#queriesCount").text(queries+" queries and "+fragments+" fragments remaining before shutting down");
}
});
}
<#if (model.shouldShowAdminInfo() || !model.isAuthEnabled()) >
function shutdown(shutdownBtn) {
let rowElem = $(shutdownBtn).parent().parent();
let hostAddr = $(rowElem).find('#address').contents().get(0).nodeValue.trim();
let url = rowElem.find("#current").html() === "Current" ? "/gracefulShutdown" : "/gracefulShutdown/" + hostAddr;
showConfirmationDialog("Are you sure you want to shutdown Drillbit running on " + hostAddr + " node?", function() {
let result = $.ajax({
type: 'POST',
url: url,
contentType : 'text/plain',
error: function (request, textStatus, errorThrown) {
alert(errorThrown);
},
success: function(data) {
alert(data.responseJSON["response"]);
shutdownBtn.prop('disabled',true).css('opacity',0.5);
}
});
});
}
</#if>
//Pops out the WebUI for a remote Drillbit
function popOutRemoteDbitUI(dbitHost, dbitPort) {
let dbitWebUIUrl = location.protocol+'//'+ dbitHost+':'+dbitPort;
let tgtWindow = 'dbit_'+dbitHost;
window.open(dbitWebUIUrl, tgtWindow);
}
//Construct the URL with the appropriate protocol and host
function getRequestUrl(requestPath) {
let protocol = location.protocol;
let host = location.host;
var url = protocol + "//" + host + requestPath;
return url;
}
//Iterates through all the nodes for update
function reloadMetrics() {
for (i = 1; i <= size; i++) {
let currentRow = $("#row-" + i);
let address = "";
let isCurrent = false;
//For 'current' bit, address is referred to by location.hostname instead of FQDN ()
if (i === 1) {
isCurrent = true;
} else {
address = currentRow.find("#address").contents().get(0).nodeValue.trim();
}
updateMetricsHtml(address, isCurrent, i);
}
}
//Update memory
function updateMetricsHtml(drillbit, isCurrent, idx) {
let drillbitURL = isCurrent ? "/status/metrics" : "/status/" + drillbit + "/metrics";
let result = $.ajax({
type: 'GET',
url: drillbitURL,
dataType: "json",
error: function (data) {
resetMetricsHtml(idx);
},
complete: function (data) {
if (typeof data.responseJSON == 'undefined') {
resetMetricsHtml(idx);
return;
}
let metrics = data.responseJSON['gauges'];
//Memory
let usedHeap = metrics['heap.used'].value;
let maxHeap = metrics['heap.max'].value;
let usedDirect = metrics['drill.allocator.root.used'].value;
let peakDirect = metrics['drill.allocator.root.peak'].value;
let heapUsage = computeMemUsage(usedHeap, maxHeap);
let directUsage = computeMemUsage(usedDirect, peakDirect);
let rowElem = document.getElementById("row-" + idx);
let heapElem = rowElem.getElementsByClassName("heap")[0];
heapElem.innerHTML = heapUsage;
let directElem = rowElem.getElementsByClassName("direct")[0];
directElem.innerHTML = directUsage;
//DrillbitLoad
let dbitLoad = metrics['drillbit.load.avg'].value;
let dbitLoadElem = rowElem.getElementsByClassName("bitload")[0];
if (dbitLoad >= 0) {
dbitLoadElem.innerHTML = parseFloat(Math.round(dbitLoad * 10000) / 100).toFixed(2) + "%";
} else {
dbitLoadElem.innerHTML = nAText;
}
//AvgSysLoad
let avgSysLoad = metrics['os.load.avg'].value;
let sysLoadElem = rowElem.getElementsByClassName("avgload")[0];
sysLoadElem.innerHTML = avgSysLoad;
//Uptime
let uptimeValue = metrics['drillbit.uptime'].value;
let uptimeElem = rowElem.getElementsByClassName("uptime")[0];
uptimeElem.innerHTML = elapsedTime(uptimeValue);
}
});
}
//Reset the metrics of a Drillbit in the table
function resetMetricsHtml(idx) {
$.each(["heap","direct","bitload","avgload","uptime"], function(i, key) {
let resetElem = document.getElementById("row-"+idx).getElementsByClassName(key)[0];
resetElem.innerHTML = nAText;
});
};
//Compute Usage
function computeMemUsage(used, max) {
let percent = 0;
if ( max > 0) {
percent = Math.round((100 * used / max), 2);
}
let usage = bytesInGB(used, 2) + "GB (" + Math.max(0, percent) + "% of "+ bytesInGB(max, 2) +"GB)";
return usage;
}
//Translate bytes into GB with decimal places
function bytesInGB(byteVal, decimal) {
let scale = Math.pow(10,decimal);
return Math.round(scale * byteVal / 1073741824)/scale;
}
//Calculate Uptime
function elapsedTime(valueInMsec) {
let elapsedTime = "";
let d, h, m, s;
s = Math.floor(valueInMsec / 1000);
m = Math.floor(s / 60);
s = s % 60;
h = Math.floor(m / 60);
m = m % 60;
d = Math.floor(h / 24);
h = h % 24;
let detailCount = 0;
if (!isNaN(d) && d > 0) {
elapsedTime = d+"d ";
detailCount += 1;
}
if (!isNaN(h) && h > 0) {
elapsedTime = elapsedTime + h+"h ";
detailCount += 1;
}
if (!isNaN(m) && m > 0 && detailCount < 2) {
elapsedTime = elapsedTime + m+"m ";
detailCount += 1;
}
if (s > 0 && detailCount < 2) {
elapsedTime = elapsedTime + s+"s";
}
return elapsedTime;
};
</script>
</#macro>
<@page_html/>