| /* |
| 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. |
| */ |
| |
| solrAdminApp.controller('CloudController', |
| function($scope, $location, Zookeeper, Constants, Collections, System, Metrics, ZookeeperStatus) { |
| |
| $scope.showDebug = false; |
| |
| $scope.$on("cloud-dump", function(event) { |
| $scope.showDebug = true; |
| }); |
| |
| $scope.closeDebug = function() { |
| $scope.showDebug = false; |
| }; |
| |
| var view = $location.search().view ? $location.search().view : "nodes"; |
| if (view === "tree") { |
| $scope.resetMenu("cloud-tree", Constants.IS_ROOT_PAGE); |
| treeSubController($scope, Zookeeper); |
| } else if (view === "graph") { |
| $scope.resetMenu("cloud-graph", Constants.IS_ROOT_PAGE); |
| graphSubController($scope, Zookeeper, false); |
| } else if (view === "nodes") { |
| $scope.resetMenu("cloud-nodes", Constants.IS_ROOT_PAGE); |
| nodesSubController($scope, Collections, System, Metrics); |
| } else if (view === "zkstatus") { |
| $scope.resetMenu("cloud-zkstatus", Constants.IS_ROOT_PAGE); |
| zkStatusSubController($scope, ZookeeperStatus, false); |
| } |
| } |
| ); |
| |
| function getOrCreateObj(name, object) { |
| if (name in object) { |
| entry = object[name]; |
| } else { |
| entry = {}; |
| object[name] = entry; |
| } |
| return entry; |
| } |
| |
| function getOrCreateList(name, object) { |
| if (name in object) { |
| entry = object[name]; |
| } else { |
| entry = []; |
| object[name] = entry; |
| } |
| return entry; |
| } |
| |
| function ensureInList(string, list) { |
| if (list.indexOf(string) === -1) { |
| list.push(string); |
| } |
| } |
| |
| /* Puts a node name into the hosts structure */ |
| function ensureNodeInHosts(node_name, hosts) { |
| var hostName = node_name.split(":")[0]; |
| var host = getOrCreateObj(hostName, hosts); |
| var hostNodes = getOrCreateList("nodes", host); |
| ensureInList(node_name, hostNodes); |
| } |
| |
| // from http://scratch99.com/web-development/javascript/convert-bytes-to-mb-kb/ |
| function bytesToSize(bytes) { |
| var sizes = ['b', 'Kb', 'Mb', 'Gb', 'Tb']; |
| if (bytes === 0) return '0b'; |
| var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); |
| if (bytes === 0) return bytes + '' + sizes[i]; |
| return (bytes / Math.pow(1024, i)).toFixed(1) + '' + sizes[i]; |
| } |
| |
| function numDocsHuman(docs) { |
| var sizes = ['', 'k', 'mn', 'bn', 'tn']; |
| if (docs === 0) return '0'; |
| var i = parseInt(Math.floor(Math.log(docs) / Math.log(1000))); |
| if (i === 0) return docs + '' + sizes[i]; |
| return (docs / Math.pow(1000, i)).toFixed(1) + '' + sizes[i]; |
| } |
| |
| /* Returns a style class depending on percentage */ |
| var styleForPct = function (pct) { |
| if (pct < 60) return "pct-normal"; |
| if (pct < 80) return "pct-warn"; |
| return "pct-critical" |
| }; |
| |
| function isNumeric(n) { |
| return !isNaN(parseFloat(n)) && isFinite(n); |
| } |
| |
| function coreNameToLabel(name) { |
| return name.replace(/(.*?)_shard((\d+_?)+)_replica_?[ntp]?(\d+)/, '\$1_s\$2r\$4'); |
| } |
| |
| var nodesSubController = function($scope, Collections, System, Metrics) { |
| $scope.pageSize = 10; |
| $scope.showNodes = true; |
| $scope.showTree = false; |
| $scope.showGraph = false; |
| $scope.showData = false; |
| $scope.showAllDetails = false; |
| $scope.showDetails = {}; |
| $scope.from = 0; |
| $scope.to = $scope.pageSize - 1; |
| $scope.filterType = "node"; // Pre-initialize dropdown |
| |
| $scope.toggleAllDetails = function() { |
| $scope.showAllDetails = !$scope.showAllDetails; |
| for (var node in $scope.nodes) { |
| $scope.showDetails[node] = $scope.showAllDetails; |
| } |
| for (var host in $scope.hosts) { |
| $scope.showDetails[host] = $scope.showAllDetails; |
| } |
| }; |
| |
| $scope.toggleDetails = function(key) { |
| $scope.showDetails[key] = !$scope.showDetails[key] === true; |
| }; |
| |
| $scope.toggleHostDetails = function(key) { |
| $scope.showDetails[key] = !$scope.showDetails[key] === true; |
| for (var nodeId in $scope.hosts[key].nodes) { |
| var node = $scope.hosts[key].nodes[nodeId]; |
| $scope.showDetails[node] = $scope.showDetails[key]; |
| } |
| }; |
| |
| $scope.nextPage = function() { |
| $scope.from += parseInt($scope.pageSize); |
| $scope.reload(); |
| }; |
| |
| $scope.previousPage = function() { |
| $scope.from = Math.max(0, $scope.from - parseInt($scope.pageSize)); |
| $scope.reload(); |
| }; |
| |
| // Checks if this node is the first (alphabetically) for a given host. Used to decide rowspan in table |
| $scope.isFirstNodeForHost = function(node) { |
| var hostName = node.split(":")[0]; |
| var nodesInHost = $scope.filteredNodes.filter(function (node) { |
| return node.startsWith(hostName); |
| }); |
| return nodesInHost[0] === node; |
| }; |
| |
| // Returns the first live node for this host, to make sure we pick host-level metrics from a live node |
| $scope.firstLiveNodeForHost = function(key) { |
| var hostName = key.split(":")[0]; |
| var liveNodesInHost = $scope.filteredNodes.filter(function (key) { |
| return key.startsWith(hostName); |
| }).filter(function (key) { |
| return $scope.live_nodes.includes(key); |
| }); |
| return liveNodesInHost.length > 0 ? liveNodesInHost[0] : key; |
| }; |
| |
| // Initializes the cluster state, list of nodes, collections etc |
| $scope.initClusterState = function() { |
| var nodes = {}; |
| var hosts = {}; |
| var live_nodes = []; |
| |
| // We build a node-centric view of the cluster state which we can easily consume to render the table |
| Collections.status(function (data) { |
| // Fetch cluster state from collections API and invert to a nodes structure |
| for (var name in data.cluster.collections) { |
| var collection = data.cluster.collections[name]; |
| collection.name = name; |
| var shards = collection.shards; |
| collection.shards = []; |
| for (var shardName in shards) { |
| var shard = shards[shardName]; |
| shard.name = shardName; |
| shard.collection = collection.name; |
| var replicas = shard.replicas; |
| shard.replicas = []; |
| for (var replicaName in replicas) { |
| var core = replicas[replicaName]; |
| core.name = replicaName; |
| core.label = coreNameToLabel(core['core']); |
| core.collection = collection.name; |
| core.shard = shard.name; |
| core.shard_state = shard.state; |
| |
| var node_name = core['node_name']; |
| var node = getOrCreateObj(node_name, nodes); |
| var cores = getOrCreateList("cores", node); |
| cores.push(core); |
| node['base_url'] = core.base_url; |
| node['id'] = core.base_url.replace(/[^\w\d]/g, ''); |
| node['host'] = node_name.split(":")[0]; |
| var collections = getOrCreateList("collections", node); |
| ensureInList(core.collection, collections); |
| ensureNodeInHosts(node_name, hosts); |
| } |
| } |
| } |
| |
| live_nodes = data.cluster.live_nodes; |
| for (n in data.cluster.live_nodes) { |
| node = data.cluster.live_nodes[n]; |
| if (!(node in nodes)) { |
| var hostName = node.split(":")[0]; |
| nodes[node] = {}; |
| nodes[node]['host'] = hostName; |
| } |
| ensureNodeInHosts(node, hosts); |
| } |
| |
| // Make sure nodes are sorted alphabetically to align with rowspan in table |
| for (var host in hosts) { |
| hosts[host].nodes.sort(); |
| } |
| |
| $scope.nodes = nodes; |
| $scope.hosts = hosts; |
| $scope.live_nodes = live_nodes; |
| |
| $scope.Math = window.Math; |
| $scope.reload(); |
| }); |
| }; |
| |
| $scope.filterInput = function() { |
| $scope.from = 0; |
| $scope.to = $scope.pageSize - 1; |
| $scope.reload(); |
| }; |
| |
| /* |
| Reload will fetch data for the current page of the table and thus refresh numbers. |
| It is also called whenever a filter or paging action is executed |
| */ |
| $scope.reload = function() { |
| var nodes = $scope.nodes; |
| var node_keys = Object.keys(nodes); |
| var hosts = $scope.hosts; |
| var live_nodes = $scope.live_nodes; |
| var hostNames = Object.keys(hosts); |
| hostNames.sort(); |
| var pageSize = isNumeric($scope.pageSize) ? $scope.pageSize : 10; |
| |
| // Calculate what nodes that will show on this page |
| var nodesToShow = []; |
| var nodesParam; |
| var hostsToShow = []; |
| var filteredNodes; |
| var filteredHosts; |
| var isFiltered = false; |
| switch ($scope.filterType) { |
| case "node": // Find what nodes match the node filter |
| if ($scope.nodeFilter) { |
| filteredNodes = node_keys.filter(function (node) { |
| return node.indexOf($scope.nodeFilter) !== -1; |
| }); |
| } |
| break; |
| |
| case "collection": // Find what collections match the collection filter and what nodes that have these collections |
| if ($scope.collectionFilter) { |
| candidateNodes = {}; |
| nodesCollections = []; |
| for (var i = 0 ; i < node_keys.length ; i++) { |
| var node_name = node_keys[i]; |
| var node = nodes[node_name]; |
| nodeColl = {}; |
| nodeColl['node'] = node_name; |
| collections = {}; |
| node.cores.forEach(function(core) { |
| collections[core.collection] = true; |
| }); |
| nodeColl['collections'] = Object.keys(collections); |
| nodesCollections.push(nodeColl); |
| } |
| nodesCollections.forEach(function(nc) { |
| matchingColls = nc['collections'].filter(function (collection) { |
| return collection.indexOf($scope.collectionFilter) !== -1; |
| }); |
| if (matchingColls.length > 0) { |
| candidateNodes[nc.node] = true; |
| } |
| }); |
| filteredNodes = Object.keys(candidateNodes); |
| } |
| break; |
| |
| case "health": |
| |
| } |
| |
| if (filteredNodes) { |
| // If filtering is active, calculate what hosts contain the nodes that match the filters |
| isFiltered = true; |
| filteredHosts = filteredNodes.map(function (node) { |
| return node.split(":")[0]; |
| }).filter(function (item, index, self) { |
| return self.indexOf(item) === index; |
| }); |
| } else { |
| filteredNodes = node_keys; |
| filteredHosts = hostNames; |
| } |
| filteredNodes.sort(); |
| filteredHosts.sort(); |
| |
| // Find what hosts & nodes (from the filtered set) that should be displayed on current page |
| for (var id = $scope.from ; id < $scope.from + pageSize && filteredHosts[id] ; id++) { |
| var hostName = filteredHosts[id]; |
| hostsToShow.push(hostName); |
| if (isFiltered) { // Only show the nodes per host matching active filter |
| nodesToShow = nodesToShow.concat(filteredNodes.filter(function (node) { |
| return node.startsWith(hostName); |
| })); |
| } else { |
| nodesToShow = nodesToShow.concat(hosts[hostName]['nodes']); |
| } |
| } |
| nodesParam = nodesToShow.filter(function (node) { |
| return live_nodes.includes(node); |
| }).join(','); |
| var deadNodes = nodesToShow.filter(function (node) { |
| return !live_nodes.includes(node); |
| }); |
| deadNodes.forEach(function (node) { |
| nodes[node]['dead'] = true; |
| }); |
| $scope.nextEnabled = $scope.from + pageSize < filteredHosts.length; |
| $scope.prevEnabled = $scope.from - pageSize >= 0; |
| nodesToShow.sort(); |
| hostsToShow.sort(); |
| |
| /* |
| Fetch system info for all selected nodes |
| Pick the data we want to display and add it to the node-centric data structure |
| */ |
| System.get({"nodes": nodesParam}, function (systemResponse) { |
| for (var node in systemResponse) { |
| if (node in nodes) { |
| var s = systemResponse[node]; |
| nodes[node]['system'] = s; |
| var memTotal = s.system.totalPhysicalMemorySize; |
| var memFree = s.system.freePhysicalMemorySize; |
| var memPercentage = Math.floor((memTotal - memFree) / memTotal * 100); |
| nodes[node]['memUsedPct'] = memPercentage; |
| nodes[node]['memUsedPctStyle'] = styleForPct(memPercentage); |
| nodes[node]['memTotal'] = bytesToSize(memTotal); |
| nodes[node]['memFree'] = bytesToSize(memFree); |
| nodes[node]['memUsed'] = bytesToSize(memTotal - memFree); |
| |
| var heapTotal = s.jvm.memory.raw.total; |
| var heapFree = s.jvm.memory.raw.free; |
| var heapPercentage = Math.floor((heapTotal - heapFree) / heapTotal * 100); |
| nodes[node]['heapUsed'] = bytesToSize(heapTotal - heapFree); |
| nodes[node]['heapUsedPct'] = heapPercentage; |
| nodes[node]['heapUsedPctStyle'] = styleForPct(heapPercentage); |
| nodes[node]['heapTotal'] = bytesToSize(heapTotal); |
| nodes[node]['heapFree'] = bytesToSize(heapFree); |
| |
| var jvmUptime = s.jvm.jmx.upTimeMS / 1000; // Seconds |
| nodes[node]['jvmUptime'] = secondsForHumans(jvmUptime); |
| nodes[node]['jvmUptimeSec'] = jvmUptime; |
| |
| nodes[node]['uptime'] = (s.system.uptime || "unknown").replace(/.*up (.*?,.*?),.*/, "$1"); |
| nodes[node]['loadAvg'] = Math.round(s.system.systemLoadAverage * 100) / 100; |
| nodes[node]['cpuPct'] = Math.ceil(s.system.processCpuLoad); |
| nodes[node]['cpuPctStyle'] = styleForPct(Math.ceil(s.system.processCpuLoad)); |
| nodes[node]['maxFileDescriptorCount'] = s.system.maxFileDescriptorCount; |
| nodes[node]['openFileDescriptorCount'] = s.system.openFileDescriptorCount; |
| } |
| } |
| }); |
| |
| /* |
| Fetch metrics for all selected nodes. Only pull the metrics that we'll show to save bandwidth |
| Pick the data we want to display and add it to the node-centric data structure |
| */ |
| Metrics.get({ |
| "nodes": nodesParam, |
| "prefix": "CONTAINER.fs,org.eclipse.jetty.server.handler.DefaultHandler.get-requests,INDEX.sizeInBytes,SEARCHER.searcher.numDocs,SEARCHER.searcher.deletedDocs,SEARCHER.searcher.warmupTime" |
| }, |
| function (metricsResponse) { |
| for (var node in metricsResponse) { |
| if (node in nodes) { |
| var m = metricsResponse[node]; |
| nodes[node]['metrics'] = m; |
| var diskTotal = m.metrics['solr.node']['CONTAINER.fs.totalSpace']; |
| var diskFree = m.metrics['solr.node']['CONTAINER.fs.usableSpace']; |
| var diskPercentage = Math.floor((diskTotal - diskFree) / diskTotal * 100); |
| nodes[node]['diskUsedPct'] = diskPercentage; |
| nodes[node]['diskUsedPctStyle'] = styleForPct(diskPercentage); |
| nodes[node]['diskTotal'] = bytesToSize(diskTotal); |
| nodes[node]['diskFree'] = bytesToSize(diskFree); |
| |
| var r = m.metrics['solr.jetty']['org.eclipse.jetty.server.handler.DefaultHandler.get-requests']; |
| nodes[node]['req'] = r.count; |
| nodes[node]['req1minRate'] = Math.floor(r['1minRate'] * 100) / 100; |
| nodes[node]['req5minRate'] = Math.floor(r['5minRate'] * 100) / 100; |
| nodes[node]['req15minRate'] = Math.floor(r['15minRate'] * 100) / 100; |
| nodes[node]['reqp75_ms'] = Math.floor(r['p75_ms']); |
| nodes[node]['reqp95_ms'] = Math.floor(r['p95_ms']); |
| nodes[node]['reqp99_ms'] = Math.floor(r['p99_ms']); |
| |
| var cores = nodes[node]['cores']; |
| var indexSizeTotal = 0; |
| var docsTotal = 0; |
| var graphData = []; |
| if (cores) { |
| for (coreId in cores) { |
| var core = cores[coreId]; |
| var keyName = "solr.core." + core['core'].replace(/(.*?)_(shard(\d+_?)+)_(replica.*?)/, '\$1.\$2.\$4'); |
| var nodeMetric = m.metrics[keyName]; |
| var size = nodeMetric['INDEX.sizeInBytes']; |
| size = (typeof size !== 'undefined') ? size : 0; |
| core['sizeInBytes'] = size; |
| core['size'] = bytesToSize(size); |
| if (core['shard_state'] !== 'active' || core['state'] !== 'active') { |
| // If core state is not active, display the real state, or if shard is inactive, display that |
| var labelState = (core['state'] !== 'active') ? core['state'] : core['shard_state']; |
| core['label'] += "_(" + labelState + ")"; |
| } |
| indexSizeTotal += size; |
| var numDocs = nodeMetric['SEARCHER.searcher.numDocs']; |
| numDocs = (typeof numDocs !== 'undefined') ? numDocs : 0; |
| core['numDocs'] = numDocs; |
| core['numDocsHuman'] = numDocsHuman(numDocs); |
| core['avgSizePerDoc'] = bytesToSize(numDocs === 0 ? 0 : size / numDocs); |
| var deletedDocs = nodeMetric['SEARCHER.searcher.deletedDocs']; |
| deletedDocs = (typeof deletedDocs !== 'undefined') ? deletedDocs : 0; |
| core['deletedDocs'] = deletedDocs; |
| core['deletedDocsHuman'] = numDocsHuman(deletedDocs); |
| var warmupTime = nodeMetric['SEARCHER.searcher.warmupTime']; |
| warmupTime = (typeof warmupTime !== 'undefined') ? warmupTime : 0; |
| core['warmupTime'] = warmupTime; |
| docsTotal += core['numDocs']; |
| } |
| for (coreId in cores) { |
| core = cores[coreId]; |
| var graphObj = {}; |
| graphObj['label'] = core['label']; |
| graphObj['size'] = core['sizeInBytes']; |
| graphObj['sizeHuman'] = core['size']; |
| graphObj['pct'] = (core['sizeInBytes'] / indexSizeTotal) * 100; |
| graphData.push(graphObj); |
| } |
| cores.sort(function (a, b) { |
| return b.sizeInBytes - a.sizeInBytes |
| }); |
| } else { |
| cores = {}; |
| } |
| graphData.sort(function (a, b) { |
| return b.size - a.size |
| }); |
| nodes[node]['graphData'] = graphData; |
| nodes[node]['numDocs'] = numDocsHuman(docsTotal); |
| nodes[node]['sizeInBytes'] = indexSizeTotal; |
| nodes[node]['size'] = bytesToSize(indexSizeTotal); |
| nodes[node]['sizePerDoc'] = docsTotal === 0 ? '0b' : bytesToSize(indexSizeTotal / docsTotal); |
| |
| // Build the d3 powered bar chart |
| $('#chart' + nodes[node]['id']).empty(); |
| var chart = d3.select('#chart' + nodes[node]['id']).append('div').attr('class', 'chart'); |
| |
| // Add one div per bar which will group together both labels and bars |
| var g = chart.selectAll('div') |
| .data(nodes[node]['graphData']).enter() |
| .append('div'); |
| |
| // Add the bars |
| var bars = g.append("div") |
| .attr("class", "rect") |
| .text(function (d) { |
| return d.label + ':\u00A0\u00A0' + d.sizeHuman; |
| }); |
| |
| // Execute the transition to show the bars |
| bars.transition() |
| .ease('elastic') |
| .style('width', function (d) { |
| return d.pct + '%'; |
| }); |
| } |
| } |
| }); |
| $scope.nodes = nodes; |
| $scope.hosts = hosts; |
| $scope.live_nodes = live_nodes; |
| $scope.nodesToShow = nodesToShow; |
| $scope.hostsToShow = hostsToShow; |
| $scope.filteredNodes = filteredNodes; |
| $scope.filteredHosts = filteredHosts; |
| }; |
| $scope.initClusterState(); |
| }; |
| |
| var zkStatusSubController = function($scope, ZookeeperStatus) { |
| $scope.showZkStatus = true; |
| $scope.showNodes = false; |
| $scope.showTree = false; |
| $scope.showGraph = false; |
| $scope.tree = {}; |
| $scope.showData = false; |
| $scope.showDetails = false; |
| |
| $scope.toggleDetails = function() { |
| $scope.showDetails = !$scope.showDetails === true; |
| }; |
| |
| $scope.initZookeeper = function() { |
| ZookeeperStatus.monitor({}, function(data) { |
| $scope.zkState = data.zkStatus; |
| $scope.mainKeys = ["ok", "clientPort", "secureClientPort", "zk_server_state", "zk_version", |
| "zk_approximate_data_size", "zk_znode_count", "zk_num_alive_connections"]; |
| $scope.detailKeys = ["dataDir", "dataLogDir", |
| "zk_avg_latency", "zk_max_file_descriptor_count", "zk_watch_count", |
| "zk_packets_sent", "zk_packets_received", |
| "tickTime", "maxClientCnxns", "minSessionTimeout", "maxSessionTimeout"]; |
| $scope.ensembleMainKeys = ["serverId", "electionPort", "quorumPort", "role"]; |
| $scope.ensembleDetailKeys = ["peerType", "electionAlg", "initLimit", "syncLimit", |
| "zk_followers", "zk_synced_followers", "zk_pending_syncs"]; |
| $scope.notEmptyRow = function(key) { |
| for (hostId in $scope.zkState.details) { |
| if (key in $scope.zkState.details[hostId]) return true; |
| } |
| return false; |
| }; |
| }); |
| }; |
| |
| $scope.initZookeeper(); |
| }; |
| |
| var treeSubController = function($scope, Zookeeper) { |
| $scope.showZkStatus = false; |
| $scope.showTree = true; |
| $scope.showGraph = false; |
| $scope.tree = {}; |
| $scope.showData = false; |
| |
| $scope.showTreeLink = function(link) { |
| var path = decodeURIComponent(link.replace(/.*[\\?&]path=([^&#]*).*/, "$1")); |
| Zookeeper.detail({path: path}, function(data) { |
| $scope.znode = data.znode; |
| if (data.znode.path.endsWith("/managed-schema") || data.znode.path.endsWith(".xml.bak")) { |
| $scope.lang = "xml"; |
| } else { |
| var lastPathElement = data.znode.path.split( '/' ).pop(); |
| var lastDotAt = lastPathElement ? lastPathElement.lastIndexOf('.') : -1; |
| $scope.lang = lastDotAt != -1 ? lastPathElement.substring(lastDotAt+1) : "txt"; |
| } |
| $scope.showData = true; |
| }); |
| }; |
| |
| $scope.hideData = function() { |
| $scope.showData = false; |
| }; |
| |
| $scope.initTree = function() { |
| Zookeeper.simple(function(data) { |
| $scope.tree = data.tree; |
| }); |
| }; |
| |
| $scope.initTree(); |
| }; |
| |
| /** |
| * Translates seconds into human readable format of seconds, minutes, hours, days, and years |
| * |
| * @param {number} seconds The number of seconds to be processed |
| * @return {string} The phrase describing the the amount of time |
| */ |
| function secondsForHumans ( seconds ) { |
| var levels = [ |
| [Math.floor(seconds / 31536000), 'y'], |
| [Math.floor((seconds % 31536000) / 86400), 'd'], |
| [Math.floor(((seconds % 31536000) % 86400) / 3600), 'h'], |
| [Math.floor((((seconds % 31536000) % 86400) % 3600) / 60), 'm'] |
| ]; |
| var returntext = ''; |
| |
| for (var i = 0, max = levels.length; i < max; i++) { |
| if ( levels[i][0] === 0 ) continue; |
| returntext += ' ' + levels[i][0] + levels[i][1]; |
| } |
| return returntext.trim() === '' ? '0m' : returntext.trim(); |
| } |
| |
| var graphSubController = function ($scope, Zookeeper) { |
| $scope.showZkStatus = false; |
| $scope.showTree = false; |
| $scope.showGraph = true; |
| |
| $scope.filterType = "status"; |
| |
| $scope.helperData = { |
| protocol: [], |
| host: [], |
| hostname: [], |
| port: [], |
| pathname: [], |
| replicaType: [], |
| base_url: [], |
| core: [], |
| node_name: [], |
| state: [], |
| core_node: [] |
| }; |
| |
| $scope.next = function() { |
| $scope.pos += $scope.rows; |
| $scope.initGraph(); |
| }; |
| |
| $scope.previous = function() { |
| $scope.pos = Math.max(0, $scope.pos - $scope.rows); |
| $scope.initGraph(); |
| }; |
| |
| $scope.resetGraph = function() { |
| $scope.pos = 0; |
| $scope.initGraph(); |
| }; |
| |
| $scope.initGraph = function() { |
| Zookeeper.liveNodes(function (data) { |
| var live_nodes = {}; |
| for (var c in data.tree[0].children) { |
| live_nodes[data.tree[0].children[c].text] = true; |
| } |
| |
| var params = {view: "graph"}; |
| if ($scope.rows) { |
| params.start = $scope.pos; |
| params.rows = $scope.rows; |
| } |
| |
| var filter = ($scope.filterType=='status') ? $scope.pagingStatusFilter : $scope.pagingFilter; |
| |
| if (filter) { |
| params.filterType = $scope.filterType; |
| params.filter = filter; |
| } |
| |
| Zookeeper.clusterState(params, function (data) { |
| var state = $.parseJSON(data.znode.data); |
| |
| var leaf_count = 0; |
| var graph_data = { |
| name: null, |
| children: [] |
| }; |
| |
| for (var c in state) { |
| var shards = []; |
| for (var s in state[c].shards) { |
| var shard_status = state[c].shards[s].state; |
| shard_status = shard_status == 'inactive' ? 'shard-inactive' : shard_status; |
| var nodes = []; |
| for (var n in state[c].shards[s].replicas) { |
| leaf_count++; |
| var replica = state[c].shards[s].replicas[n] |
| |
| var uri = replica.base_url; |
| var parts = uri.match(/^(\w+:)\/\/(([\w\d\.-]+)(:(\d+))?)(.+)$/); |
| var uri_parts = { |
| protocol: parts[1], |
| host: parts[2], |
| hostname: parts[3], |
| port: parseInt(parts[5] || 80, 10), |
| pathname: parts[6], |
| replicaType: replica.type, |
| base_url: replica.base_url, |
| core: replica.core, |
| node_name: replica.node_name, |
| state: replica.state, |
| core_node: n |
| }; |
| |
| $scope.helperData.protocol.push(uri_parts.protocol); |
| $scope.helperData.host.push(uri_parts.host); |
| $scope.helperData.hostname.push(uri_parts.hostname); |
| $scope.helperData.port.push(uri_parts.port); |
| $scope.helperData.pathname.push(uri_parts.pathname); |
| $scope.helperData.replicaType.push(uri_parts.replicaType); |
| $scope.helperData.base_url.push(uri_parts.base_url); |
| $scope.helperData.core.push(uri_parts.core); |
| $scope.helperData.node_name.push(uri_parts.node_name); |
| $scope.helperData.state.push(uri_parts.state); |
| $scope.helperData.core_node.push(uri_parts.core_node); |
| |
| var replica_status = replica.state; |
| |
| if (!live_nodes[replica.node_name]) { |
| replica_status = 'gone'; |
| } else if(shard_status=='shard-inactive') { |
| replica_status += ' ' + shard_status; |
| } |
| |
| var node = { |
| name: uri, |
| data: { |
| type: 'node', |
| state: replica_status, |
| leader: 'true' === replica.leader, |
| uri: uri_parts |
| } |
| }; |
| nodes.push(node); |
| } |
| |
| var shard = { |
| name: shard_status == "shard-inactive" ? s + ' (inactive)' : s, |
| data: { |
| type: 'shard', |
| state: shard_status, |
| range: state[c].shards[s].range |
| |
| }, |
| children: nodes |
| }; |
| shards.push(shard); |
| } |
| |
| var collection = { |
| name: c, |
| data: { |
| type: 'collection', |
| pullReplicas: state[c].pullReplicas, |
| replicationFactor: state[c].replicationFactor, |
| router: state[c].router.name, |
| maxShardsPerNode: state[c].maxShardsPerNode, |
| autoAddReplicas: state[c].autoAddReplicas, |
| nrtReplicas: state[c].nrtReplicas, |
| tlogReplicas: state[c].tlogReplicas, |
| numShards: shards.length |
| }, |
| children: shards |
| }; |
| graph_data.children.push(collection); |
| } |
| $scope.helperData.protocol = $.unique($scope.helperData.protocol); |
| $scope.helperData.host = $.unique($scope.helperData.host); |
| $scope.helperData.hostname = $.unique($scope.helperData.hostname); |
| $scope.helperData.port = $.unique($scope.helperData.port); |
| $scope.helperData.pathname = $.unique($scope.helperData.pathname); |
| $scope.helperData.replicaType = $.unique($scope.helperData.replicaType); |
| $scope.helperData.base_url = $.unique($scope.helperData.base_url); |
| $scope.helperData.core = $.unique($scope.helperData.core); |
| $scope.helperData.node_name = $.unique($scope.helperData.node_name); |
| $scope.helperData.state = $.unique($scope.helperData.state); |
| $scope.helperData.core_node = $.unique($scope.helperData.core_node); |
| |
| if (data.znode && data.znode.paging) { |
| $scope.showPaging = true; |
| |
| var parr = data.znode.paging.split('|'); |
| if (parr.length < 3) { |
| $scope.showPaging = false; |
| } else { |
| $scope.start = Math.max(parseInt(parr[0]), 0); |
| $scope.prevEnabled = ($scope.start > 0); |
| $scope.rows = parseInt(parr[1]); |
| $scope.total = parseInt(parr[2]); |
| |
| if ($scope.rows == -1) { |
| $scope.showPaging = false; |
| } else { |
| var filterType = parr.length > 3 ? parr[3] : ''; |
| |
| if (filterType == '' || filterType == 'none') filterType = 'status'; |
| |
| +$('#cloudGraphPagingFilterType').val(filterType); |
| |
| var filter = parr.length > 4 ? parr[4] : ''; |
| var page = Math.floor($scope.start / $scope.rows) + 1; |
| var pages = Math.ceil($scope.total / $scope.rows); |
| $scope.last = Math.min($scope.start + $scope.rows, $scope.total); |
| $scope.nextEnabled = ($scope.last < $scope.total); |
| } |
| } |
| } |
| else { |
| $scope.showPaging = false; |
| } |
| $scope.graphData = graph_data; |
| $scope.leafCount = leaf_count; |
| }); |
| }); |
| }; |
| |
| $scope.initGraph(); |
| $scope.pos = 0; |
| }; |
| |
| solrAdminApp.directive('graph', function(Constants) { |
| return { |
| restrict: 'EA', |
| scope: { |
| data: "=", |
| leafCount: "=", |
| helperData: "=", |
| }, |
| link: function (scope, element, attrs) { |
| var helper_path_class = function (p) { |
| var classes = ['link']; |
| classes.push('lvl-' + p.target.depth); |
| |
| if (p.target.data && p.target.data.leader) { |
| classes.push('leader'); |
| } |
| |
| if (p.target.data && p.target.data.state) { |
| classes.push(p.target.data.state); |
| } |
| |
| return classes.join(' '); |
| }; |
| |
| var helper_node_class = function (d) { |
| var classes = ['node']; |
| classes.push('lvl-' + d.depth); |
| |
| if (d.data && d.data.leader) { |
| classes.push('leader'); |
| } |
| |
| if (d.data && d.data.state) { |
| if(!(d.data.type=='shard' && d.data.state=='active')){ |
| classes.push(d.data.state); |
| } |
| } |
| |
| return classes.join(' '); |
| }; |
| |
| var helper_tooltip_text = function (d) { |
| if (!d.data) { |
| return tooltip; |
| } |
| var tooltip; |
| |
| if (! d.data.type) { |
| return tooltip; |
| } |
| |
| |
| if (d.data.type == 'collection') { |
| tooltip = d.name + " {<br/> "; |
| tooltip += "numShards: [" + d.data.numShards + "],<br/>"; |
| tooltip += "maxShardsPerNode: [" + d.data.maxShardsPerNode + "],<br/>"; |
| tooltip += "router: [" + d.data.router + "],<br/>"; |
| tooltip += "autoAddReplicas: [" + d.data.autoAddReplicas + "],<br/>"; |
| tooltip += "replicationFactor: [" + d.data.replicationFactor + "],<br/>"; |
| tooltip += "nrtReplicas: [" + d.data.nrtReplicas + "],<br/>"; |
| tooltip += "pullReplicas: [" + d.data.pullReplicas + "],<br/>"; |
| tooltip += "tlogReplicas: [" + d.data.tlogReplicas + "],<br/>"; |
| tooltip += "}"; |
| } else if (d.data.type == 'shard') { |
| tooltip = d.name + " {<br/> "; |
| tooltip += "range: [" + d.data.range + "],<br/>"; |
| tooltip += "state: [" + d.data.state + "],<br/>"; |
| tooltip += "}"; |
| } else if (d.data.type == 'node') { |
| tooltip = d.data.uri.core_node + " {<br/>"; |
| |
| if (0 !== scope.helperData.core.length) { |
| tooltip += "core: [" + d.data.uri.core + "],<br/>"; |
| } |
| |
| if (0 !== scope.helperData.node_name.length) { |
| tooltip += "node_name: [" + d.data.uri.node_name + "],<br/>"; |
| } |
| tooltip += "}"; |
| } |
| |
| return tooltip; |
| }; |
| |
| var helper_node_text = function (d) { |
| if (!d.data || !d.data.uri) { |
| return d.name; |
| } |
| |
| var name = d.data.uri.hostname; |
| if (1 !== scope.helperData.protocol.length) { |
| name = d.data.uri.protocol + '//' + name; |
| } |
| |
| if (1 !== scope.helperData.port.length) { |
| name += ':' + d.data.uri.port; |
| } |
| |
| if (1 !== scope.helperData.pathname.length) { |
| name += d.data.uri.pathname; |
| } |
| |
| if(0 !== scope.helperData.replicaType.length) { |
| name += ' (' + d.data.uri.replicaType[0] + ')'; |
| } |
| |
| return name; |
| }; |
| |
| scope.$watch("data", function(newValue, oldValue) { |
| if (newValue) { |
| flatGraph(element, scope.data, scope.leafCount); |
| } |
| |
| $('text').tooltip({ |
| content: function() { |
| return $(this).attr('title'); |
| } |
| }); |
| }); |
| |
| |
| function setNodeNavigationBehavior(node, view){ |
| node |
| .attr('data-href', function (d) { |
| if (d.type == "node"){ |
| return getNodeUrl(d, view); |
| } |
| else{ |
| return ""; |
| } |
| }) |
| .on('click', function(d) { |
| if (d.data.type == "node"){ |
| location.href = getNodeUrl(d, view); |
| } |
| }); |
| } |
| |
| function getNodeUrl(d, view){ |
| var url = d.name + Constants.ROOT_URL + "#/~cloud"; |
| if (view != undefined){ |
| url += "?view=" + view; |
| } |
| return url; |
| } |
| |
| var flatGraph = function(element, graphData, leafCount) { |
| var w = element.width(), |
| h = leafCount * 20; |
| |
| var tree = d3.layout.tree().size([h, w - 400]); |
| |
| var diagonal = d3.svg.diagonal().projection(function (d) { |
| return [d.y, d.x]; |
| }); |
| |
| d3.select('#canvas', element).html(''); |
| var vis = d3.select('#canvas', element).append('svg') |
| .attr('width', w) |
| .attr('height', h) |
| .append('g') |
| .attr('transform', 'translate(100, 0)'); |
| |
| var nodes = tree.nodes(graphData); |
| |
| var link = vis.selectAll('path.link') |
| .data(tree.links(nodes)) |
| .enter().append('path') |
| .attr('class', helper_path_class) |
| .attr('d', diagonal); |
| |
| var node = vis.selectAll('g.node') |
| .data(nodes) |
| .enter().append('g') |
| .attr('class', helper_node_class) |
| .attr('transform', function (d) { |
| return 'translate(' + d.y + ',' + d.x + ')'; |
| }) |
| |
| node.append('circle') |
| .attr('r', 4.5); |
| |
| node.append('text') |
| .attr('dx', function (d) { |
| return 0 === d.depth ? -8 : 8; |
| }) |
| .attr('dy', function (d) { |
| return 5; |
| }) |
| .attr('text-anchor', function (d) { |
| return 0 === d.depth ? 'end' : 'start'; |
| }) |
| .attr("title", helper_tooltip_text) |
| .text(helper_node_text); |
| |
| setNodeNavigationBehavior(node); |
| }; |
| } |
| }; |
| }); |