blob: ac86a5c594f0c5a8fe386929b7130de7d5ec3680 [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.
*/
"use strict";
const millisecondsInSecond = 1000;
const kilobitsInGigabit = 1000000;
const kilobitsInMegabit = 1000;
/**
* This is the index of the latest event already catalogued by the UI. TM doesn't
* provide a way to fetch event logs "older than x" etc., so this is how we keep
* track of what we've seen.
*/
var lastEvent = 0;
/**
* Adds a comma for every 3rd power of ten in a number represented as a string.
* e.g. numberStrWithCommas("100000") outputs "100,000".
* (source: http://stackoverflow.com/a/2901298/292623)
* @param x A string containing a number which will be formatted.
*/
function numberStrWithCommas(x) {
return x.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
/**
* ajax performs an XMLHttpRequest and passes the result to a function - *only if the response code is EXACTLY 200*.
* @param endpoint An API endpoint relative to the root of the TM webserver e.g. /publish/DsStats
* @param f A function that takes a single argument which will be the entire response body as a string.
*/
function ajax(endpoint, f) {
const xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = () => {
if (xhttp.readyState === 4 && xhttp.status === 200) {
f(xhttp.responseText);
}
};
xhttp.open("GET", endpoint, true);
xhttp.send();
}
function getCacheCount() {
ajax("/api/cache-count", function(r) {
document.getElementById("cache-count").textContent = r;
});
}
function getCacheAvailableCount() {
ajax("/api/cache-available-count", function(r) {
document.getElementById("cache-available").textContent = r;
});
}
function getBandwidth() {
ajax("/api/bandwidth-kbps", function(r) {
document.getElementById("bandwidth").textContent = numberStrWithCommas((parseFloat(r) / kilobitsInGigabit).toFixed(2));
});
}
function getBandwidthCapacity() {
ajax("/api/bandwidth-capacity-kbps", function(r) {
document.getElementById("bandwidth-capacity").textContent = numberStrWithCommas((r / kilobitsInGigabit).toString());
});
}
function getCacheDownCount() {
ajax("/api/cache-down-count", function(r) {
document.getElementById("cache-down").textContent = r;
});
}
function getVersion() {
ajax("/api/version", function(r) {
document.getElementById("version").textContent = r;
});
}
function getTrafficOpsUri() {
ajax("/api/traffic-ops-uri", function(r) {
// This used to be done by setting the element's `innerHTML`, but that doesn't remove
// the child nodes. They're orphaned, but continue to take up memory.
const link = document.createElement('A');
link.href = r;
link.textContent = r;
const sourceURISpan = document.getElementById('source-uri');
while (sourceURISpan.lastChild) {
sourceURISpan.removeChild(sourceURISpan.lastChild);
}
sourceURISpan.appendChild(link);
});
}
function getTrafficOpsCdn() {
const cdnName = document.getElementById("cdn-name");
const discIconContainer = document.getElementById("icon-disc-holder");
ajax("/publish/ConfigDoc", function(r) {
let opsConfig = JSON.parse(r);
cdnName.textContent = opsConfig.cdnName || "unknown";
if (opsConfig.usingDummyTO === false) {
discIconContainer.hidden = true;
} else {
discIconContainer.hidden = false;
}
});
}
/**
* Fetches the event log from TM and updates the "Event Log" table with the new
* results.
*/
function getEvents() {
/// \todo add /api/events-since/{index} (and change Traffic Monitor to keep latest
ajax("/publish/EventLog", function(r) {
const events = JSON.parse(r).events || [];
for (const event of events.slice(lastEvent+1)) {
lastEvent = event.index;
const row = document.getElementById("event-log").insertRow(0);
row.insertCell(0).textContent = event.name;
row.insertCell(1).textContent = event.type;
const cell = row.insertCell(2);
if(event.isAvailable) {
cell.textContent = "available";
} else {
cell.textContent = "offline";
row.classList.add("error");
}
row.insertCell(3).textContent = event.description;
row.insertCell(4).textContent = new Date(event.time * 1000).toISOString();
}
});
}
/**
* Fetches the current cache server states and statistics from TM and updates
* the "Cache States" table with the results - replacing the current content.
*/
function getCacheStates() {
function parseIPAvailable(server, ipField) {
return server.status.indexOf("ONLINE") !== 0 ? server[ipField] : "N/A";
}
function parseBandwidth(server) {
if (Object.prototype.hasOwnProperty.call(server, "bandwidth_kbps") &&
Object.prototype.hasOwnProperty.call(server, "bandwidth_capacity_kbps")) {
const kbps = (server.bandwidth_kbps / kilobitsInMegabit).toFixed(2);
const max = numberStrWithCommas((server.bandwidth_capacity_kbps / kilobitsInMegabit).toFixed(0));
return `${kbps} / ${max}`;
} else {
return "N/A";
}
}
ajax("/api/cache-statuses", function(r) {
let serversArray = Object.entries(JSON.parse(r)).sort((serverTupleA, serverTupleB) => {
return -1*serverTupleA[0].localeCompare(serverTupleB[0]);
});
const servers = new Map(serversArray);
const oldtable = document.getElementById("cache-states");
const table = document.createElement('TBODY');
const interfaceTableTemplate = document.getElementById("interface-template").content.children[0];
const interfaceRowTemplate = document.getElementById("interface-row-template").content.children[0];
const cacheStatusRowTemplate = document.getElementById("cache-status-row-template").content.children[0];
table.id = oldtable.id;
// Match visibility of interface tables based on previous table
const interfaceRows = oldtable.querySelectorAll(".encompassing-row");
let openCachesByName = new Set();
for(const row of interfaceRows) {
if(row.classList.contains("visible")){
openCachesByName.add(row.querySelector(".sub-table").getAttribute("server-name"));
}
}
for (const [serverName, server] of servers) {
const row = cacheStatusRowTemplate.cloneNode(true);
const cacheRowChildren = row.children;
const indicatorDiv = cacheRowChildren[0].children[0];
if (Object.prototype.hasOwnProperty.call(server, "status") &&
Object.prototype.hasOwnProperty.call(server, "combined_available")) {
if (server.status.indexOf("ADMIN_DOWN") !== -1 || server.status.indexOf("OFFLINE") !== -1) {
row.classList.add("warning");
} else if (!server.combined_available && server.status.indexOf("ONLINE") !== 0) {
row.classList.add("error");
} else if (server.status.indexOf(" availableBandwidth") !== -1) {
row.classList.add("error");
}
}
cacheRowChildren[1].textContent = serverName;
cacheRowChildren[2].textContent = server.type || "UNKNOWN";
cacheRowChildren[3].textContent = parseIPAvailable(server, "ipv4_available");
cacheRowChildren[4].textContent = parseIPAvailable(server, "ipv6_available");
cacheRowChildren[5].textContent = server.status || "";
cacheRowChildren[6].textContent = server.load_average || "";
cacheRowChildren[7].textContent = server.query_time_ms || "";
cacheRowChildren[8].textContent = server.health_time_ms || "";
cacheRowChildren[9].textContent = server.stat_time_ms || "";
cacheRowChildren[10].textContent = server.health_span_ms || "";
cacheRowChildren[11].textContent = server.stat_span_ms || "";
cacheRowChildren[12].textContent = parseBandwidth(server);
cacheRowChildren[13].textContent = server.connection_count || "N/A";
table.prepend(row);
const encompassingRow = table.insertRow(1);
encompassingRow.classList.add("encompassing-row");
const encompassingCell = encompassingRow.insertCell(0);
const interfaceTable = interfaceTableTemplate.cloneNode(true);
encompassingCell.colSpan = 14;
// Add interfaces
if (Object.prototype.hasOwnProperty.call(server, "interfaces")) {
let interfacesArray = Object.entries(server.interfaces).sort((interfaceTupleA, interfaceTupleB) => {
return -1 * interfaceTupleA[0].localeCompare(interfaceTupleB[0]);
});
server.interfaces = new Map(interfacesArray);
interfaceTable.removeAttribute("id");
// To determine what cache this interface table belongs to
// used to ensure servers that were expanded remain expanded when refreshing the data.
interfaceTable.setAttribute("server-name", serverName);
const interfaceBody = interfaceTable.querySelector(".interface-content");
for (const [interfaceName, stat] of server.interfaces) {
const interfaceRow = interfaceRowTemplate.cloneNode(true);
const cells = interfaceRow.children;
cells[0].textContent = interfaceName;
cells[1].textContent = parseIPAvailable(stat, "ipv4_available");
cells[2].textContent = parseIPAvailable(stat, "ipv6_available");
cells[3].textContent = stat.status || "";
cells[4].textContent = parseBandwidth(stat);
cells[5].textContent = stat.connection_count || "N/A";
if (Object.prototype.hasOwnProperty.call(stat, "available")) {
if (stat.available === false) {
interfaceRow.classList.add("error");
}
}
interfaceBody.prepend(interfaceRow);
}
row.onclick = function() {
if(encompassingRow.classList.contains("visible")) {
encompassingRow.classList.remove("visible");
indicatorDiv.classList.remove("down");
}
else {
encompassingRow.classList.add("visible");
indicatorDiv.classList.add("down");
}
};
}
else {
indicatorDiv.classList.add("hidden");
}
encompassingCell.appendChild(interfaceTable);
// Row was unhidden previously
if(openCachesByName.has(serverName)) {
row.click();
}
}
oldtable.parentNode.replaceChild(table, oldtable);
});
}
/**
* dsDisplayFloat takes a float, and returns the string to display. For nonzero values, it returns two decimal places.
* For zero values, it returns an empty string, to make nonzero values more visible.
* @param f The floating point number to format
*/
const dsDisplayFloat = (f) => { return f === 0 ? "" : f.toFixed(2); };
/**
* Attempts to extract data from a deliveryService object, but falls back on "N/A" if it doesn't have that
* property.
* @param ds The Delivery Service from which to extract data
* @param prop The property being extracted. Technically, the extracted property is dsDisplayFloat(parseFloat(ds[prop][0].value)).
*/
function getDSProperty(ds, prop) {
try {
return dsDisplayFloat(parseFloat(ds[prop][0].value));
} catch (e) {
console.error(e);
}
return "N/A";
}
/**
* Fetches the current Delivery Service stats from TM and updates the "Delivery Service States"
* table with the results - replacing the current content.
*/
function getDsStats() {
/// \todo add /api/delivery-service-stats which only returns the data needed by the UI, for efficiency
ajax("/publish/DsStats", function(r) {
const deliveryServices = new Map(Object.entries(JSON.parse(r).deliveryService).sort((dsTupleA, dsTupleB) => {
return -1 * dsTupleA[0].localeCompare(dsTupleB[0]);
}));
const table = document.createElement('TBODY');
table.id = "deliveryservice-stats";
for (const [dsName, deliveryService] of deliveryServices) {
const row = table.insertRow(0);
const available = deliveryService.isAvailable && deliveryService.isAvailable[0] && deliveryService.isAvailable[0].value === "true";
if (!available) {
row.classList.add("error");
}
row.insertCell(0).textContent = dsName;
row.insertCell(1).textContent = available ? "available" : `unavailable - ${deliveryService["error-string"][0].value}`;
row.insertCell(2).textContent = (Object.prototype.hasOwnProperty.call(deliveryService, "caches-reporting") &&
Object.prototype.hasOwnProperty.call(deliveryService, "caches-available") &&
Object.prototype.hasOwnProperty.call(deliveryService, "caches-configured")) ?
`${deliveryService['caches-reporting'][0].value} / ${deliveryService['caches-available'][0].value} / ${deliveryService['caches-configured'][0].value}` : "N/A";
row.insertCell(3).textContent = Object.prototype.hasOwnProperty.call(deliveryService, "total.kbps") ? (deliveryService['total.kbps'][0].value / kilobitsInMegabit).toFixed(2) : "N/A";
row.insertCell(4).textContent = getDSProperty(deliveryService, "total.tps_total");
row.insertCell(5).textContent = getDSProperty(deliveryService, "total.tps_2xx");
row.insertCell(6).textContent = getDSProperty(deliveryService, "total.tps_3xx");
row.insertCell(7).textContent = getDSProperty(deliveryService, "total.tps_4xx");
row.insertCell(8).textContent = getDSProperty(deliveryService, "total.tps_5xx");
row.insertCell(9);
// \todo implement disabled locations
}
const oldtable = document.getElementById("deliveryservice-stats");
oldtable.parentNode.replaceChild(table, oldtable);
});
}
/**
* Fetches not only the "Cache States" but also the aggregate cache server statistics used in the
* informational section at the top of the page.
*/
function getCacheStatuses() {
getCacheCount();
getCacheAvailableCount();
getCacheDownCount();
getCacheStates();
}
/**
* Fetches the metadata information used at the very top of the page.
*/
function getTopBar() {
getVersion();
getTrafficOpsUri();
getTrafficOpsCdn();
getCacheStatuses();
}
/**
* Runs immediately after content is loaded, fetching initial information and setting intervals for
* for gathering other data.
*/
function init() {
getTopBar();
setInterval(getCacheCount, 4755);
setInterval(getCacheAvailableCount, 4800);
setInterval(getBandwidth, 4621);
setInterval(getBandwidthCapacity, 4591);
setInterval(getCacheDownCount, 4832);
setInterval(getVersion, 10007); // change to retry on failure, and only do on startup
setInterval(getTrafficOpsUri, 10019); // change to retry on failure, and only do on startup
setInterval(getTrafficOpsCdn, 10500); // change to retry on failure, and only do on startup
setInterval(getEvents, 2004); // change to retry on failure, and only do on startup
setInterval(getCacheStatuses, 5009);
setInterval(getDsStats, 4003);
}
window.addEventListener('load', init);