cloud.js 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024
  1. /*
  2. Licensed to the Apache Software Foundation (ASF) under one or more
  3. contributor license agreements. See the NOTICE file distributed with
  4. this work for additional information regarding copyright ownership.
  5. The ASF licenses this file to You under the Apache License, Version 2.0
  6. (the "License"); you may not use this file except in compliance with
  7. the License. You may obtain a copy of the License at
  8. http://www.apache.org/licenses/LICENSE-2.0
  9. Unless required by applicable law or agreed to in writing, software
  10. distributed under the License is distributed on an "AS IS" BASIS,
  11. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. See the License for the specific language governing permissions and
  13. limitations under the License.
  14. */
  15. solrAdminApp.controller('CloudController',
  16. function($scope, $location, Zookeeper, Constants, Collections, System, Metrics, ZookeeperStatus) {
  17. $scope.showDebug = false;
  18. $scope.$on("cloud-dump", function(event) {
  19. $scope.showDebug = true;
  20. });
  21. $scope.closeDebug = function() {
  22. $scope.showDebug = false;
  23. };
  24. var view = $location.search().view ? $location.search().view : "graph";
  25. if (view === "tree") {
  26. $scope.resetMenu("cloud-tree", Constants.IS_ROOT_PAGE);
  27. treeSubController($scope, Zookeeper);
  28. } else if (view === "rgraph") {
  29. $scope.resetMenu("cloud-rgraph", Constants.IS_ROOT_PAGE);
  30. graphSubController($scope, Zookeeper, true);
  31. } else if (view === "graph") {
  32. $scope.resetMenu("cloud-graph", Constants.IS_ROOT_PAGE);
  33. graphSubController($scope, Zookeeper, false);
  34. } else if (view === "nodes") {
  35. $scope.resetMenu("cloud-nodes", Constants.IS_ROOT_PAGE);
  36. nodesSubController($scope, Collections, System, Metrics);
  37. } else if (view === "zkstatus") {
  38. $scope.resetMenu("cloud-zkstatus", Constants.IS_ROOT_PAGE);
  39. zkStatusSubController($scope, ZookeeperStatus, false);
  40. }
  41. }
  42. );
  43. function getOrCreateObj(name, object) {
  44. if (name in object) {
  45. entry = object[name];
  46. } else {
  47. entry = {};
  48. object[name] = entry;
  49. }
  50. return entry;
  51. }
  52. function getOrCreateList(name, object) {
  53. if (name in object) {
  54. entry = object[name];
  55. } else {
  56. entry = [];
  57. object[name] = entry;
  58. }
  59. return entry;
  60. }
  61. function ensureInList(string, list) {
  62. if (list.indexOf(string) === -1) {
  63. list.push(string);
  64. }
  65. }
  66. /* Puts a node name into the hosts structure */
  67. function ensureNodeInHosts(node_name, hosts) {
  68. var hostName = node_name.split(":")[0];
  69. var host = getOrCreateObj(hostName, hosts);
  70. var hostNodes = getOrCreateList("nodes", host);
  71. ensureInList(node_name, hostNodes);
  72. }
  73. // from http://scratch99.com/web-development/javascript/convert-bytes-to-mb-kb/
  74. function bytesToSize(bytes) {
  75. var sizes = ['b', 'Kb', 'Mb', 'Gb', 'Tb'];
  76. if (bytes === 0) return '0b';
  77. var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
  78. if (bytes === 0) return bytes + '' + sizes[i];
  79. return (bytes / Math.pow(1024, i)).toFixed(1) + '' + sizes[i];
  80. }
  81. function numDocsHuman(docs) {
  82. var sizes = ['', 'k', 'mn', 'bn', 'tn'];
  83. if (docs === 0) return '0';
  84. var i = parseInt(Math.floor(Math.log(docs) / Math.log(1000)));
  85. if (i === 0) return docs + '' + sizes[i];
  86. return (docs / Math.pow(1000, i)).toFixed(1) + '' + sizes[i];
  87. }
  88. /* Returns a style class depending on percentage */
  89. var styleForPct = function (pct) {
  90. if (pct < 60) return "pct-normal";
  91. if (pct < 80) return "pct-warn";
  92. return "pct-critical"
  93. };
  94. function isNumeric(n) {
  95. return !isNaN(parseFloat(n)) && isFinite(n);
  96. }
  97. var nodesSubController = function($scope, Collections, System, Metrics) {
  98. $scope.pageSize = 10;
  99. $scope.showNodes = true;
  100. $scope.showTree = false;
  101. $scope.showGraph = false;
  102. $scope.showData = false;
  103. $scope.showAllDetails = false;
  104. $scope.showDetails = {};
  105. $scope.from = 0;
  106. $scope.to = $scope.pageSize - 1;
  107. $scope.filterType = "node"; // Pre-initialize dropdown
  108. $scope.toggleAllDetails = function() {
  109. $scope.showAllDetails = !$scope.showAllDetails;
  110. for (var node in $scope.nodes) {
  111. $scope.showDetails[node] = $scope.showAllDetails;
  112. }
  113. for (var host in $scope.hosts) {
  114. $scope.showDetails[host] = $scope.showAllDetails;
  115. }
  116. };
  117. $scope.toggleDetails = function(key) {
  118. $scope.showDetails[key] = !$scope.showDetails[key] === true;
  119. };
  120. $scope.toggleHostDetails = function(key) {
  121. $scope.showDetails[key] = !$scope.showDetails[key] === true;
  122. for (var nodeId in $scope.hosts[key].nodes) {
  123. var node = $scope.hosts[key].nodes[nodeId];
  124. $scope.showDetails[node] = $scope.showDetails[key];
  125. }
  126. };
  127. $scope.nextPage = function() {
  128. $scope.from += parseInt($scope.pageSize);
  129. $scope.reload();
  130. };
  131. $scope.previousPage = function() {
  132. $scope.from = Math.max(0, $scope.from - parseInt($scope.pageSize));
  133. $scope.reload();
  134. };
  135. // Checks if this node is the first (alphabetically) for a given host. Used to decide rowspan in table
  136. $scope.isFirstNodeForHost = function(node) {
  137. var hostName = node.split(":")[0];
  138. var nodesInHost = $scope.filteredNodes.filter(function (node) {
  139. return node.startsWith(hostName);
  140. });
  141. return nodesInHost[0] === node;
  142. };
  143. // Initializes the cluster state, list of nodes, collections etc
  144. $scope.initClusterState = function() {
  145. var nodes = {};
  146. var hosts = {};
  147. var live_nodes = [];
  148. // We build a node-centric view of the cluster state which we can easily consume to render the table
  149. Collections.status(function (data) {
  150. // Fetch cluster state from collections API and invert to a nodes structure
  151. for (var name in data.cluster.collections) {
  152. var collection = data.cluster.collections[name];
  153. collection.name = name;
  154. var shards = collection.shards;
  155. collection.shards = [];
  156. for (var shardName in shards) {
  157. var shard = shards[shardName];
  158. shard.name = shardName;
  159. shard.collection = collection.name;
  160. var replicas = shard.replicas;
  161. shard.replicas = [];
  162. for (var replicaName in replicas) {
  163. var core = replicas[replicaName];
  164. core.name = replicaName;
  165. core.collection = collection.name;
  166. core.shard = shard.name;
  167. core.shard_state = shard.state;
  168. var node_name = core['node_name'];
  169. var node = getOrCreateObj(node_name, nodes);
  170. var cores = getOrCreateList("cores", node);
  171. cores.push(core);
  172. node['base_url'] = core.base_url;
  173. node['id'] = core.base_url.replace(/[^\w\d]/g, '');
  174. node['host'] = node_name.split(":")[0];
  175. var collections = getOrCreateList("collections", node);
  176. ensureInList(core.collection, collections);
  177. ensureNodeInHosts(node_name, hosts);
  178. }
  179. }
  180. }
  181. live_nodes = data.cluster.live_nodes;
  182. for (n in data.cluster.live_nodes) {
  183. node = data.cluster.live_nodes[n];
  184. if (!(node in nodes)) {
  185. var hostName = node.split(":")[0];
  186. nodes[node] = {};
  187. nodes[node]['host'] = hostName;
  188. }
  189. ensureNodeInHosts(node, hosts);
  190. }
  191. // Make sure nodes are sorted alphabetically to align with rowspan in table
  192. for (var host in hosts) {
  193. hosts[host].nodes.sort();
  194. }
  195. $scope.nodes = nodes;
  196. $scope.hosts = hosts;
  197. $scope.live_nodes = live_nodes;
  198. $scope.Math = window.Math;
  199. $scope.reload();
  200. });
  201. };
  202. $scope.filterInput = function() {
  203. $scope.from = 0;
  204. $scope.to = $scope.pageSize - 1;
  205. $scope.reload();
  206. };
  207. /*
  208. Reload will fetch data for the current page of the table and thus refresh numbers.
  209. It is also called whenever a filter or paging action is executed
  210. */
  211. $scope.reload = function() {
  212. var nodes = $scope.nodes;
  213. var node_keys = Object.keys(nodes);
  214. var hosts = $scope.hosts;
  215. var live_nodes = $scope.live_nodes;
  216. var hostNames = Object.keys(hosts);
  217. hostNames.sort();
  218. var pageSize = isNumeric($scope.pageSize) ? $scope.pageSize : 10;
  219. // Calculate what nodes that will show on this page
  220. var nodesToShow = [];
  221. var nodesParam;
  222. var hostsToShow = [];
  223. var filteredNodes;
  224. var filteredHosts;
  225. var isFiltered = false;
  226. switch ($scope.filterType) {
  227. case "node": // Find what nodes match the node filter
  228. if ($scope.nodeFilter) {
  229. filteredNodes = node_keys.filter(function (node) {
  230. return node.indexOf($scope.nodeFilter) !== -1;
  231. });
  232. }
  233. break;
  234. case "collection": // Find what collections match the collection filter and what nodes that have these collections
  235. if ($scope.collectionFilter) {
  236. candidateNodes = {};
  237. nodesCollections = [];
  238. for (var i = 0 ; i < node_keys.length ; i++) {
  239. var node_name = node_keys[i];
  240. var node = nodes[node_name];
  241. nodeColl = {};
  242. nodeColl['node'] = node_name;
  243. collections = {};
  244. node.cores.forEach(function(core) {
  245. collections[core.collection] = true;
  246. });
  247. nodeColl['collections'] = Object.keys(collections);
  248. nodesCollections.push(nodeColl);
  249. }
  250. nodesCollections.forEach(function(nc) {
  251. matchingColls = nc['collections'].filter(function (collection) {
  252. return collection.indexOf($scope.collectionFilter) !== -1;
  253. });
  254. if (matchingColls.length > 0) {
  255. candidateNodes[nc.node] = true;
  256. }
  257. });
  258. filteredNodes = Object.keys(candidateNodes);
  259. }
  260. break;
  261. case "health":
  262. }
  263. if (filteredNodes) {
  264. // If filtering is active, calculate what hosts contain the nodes that match the filters
  265. isFiltered = true;
  266. filteredHosts = filteredNodes.map(function (node) {
  267. return node.split(":")[0];
  268. }).filter(function (item, index, self) {
  269. return self.indexOf(item) === index;
  270. });
  271. } else {
  272. filteredNodes = node_keys;
  273. filteredHosts = hostNames;
  274. }
  275. filteredNodes.sort();
  276. filteredHosts.sort();
  277. // Find what hosts & nodes (from the filtered set) that should be displayed on current page
  278. for (var id = $scope.from ; id < $scope.from + pageSize && filteredHosts[id] ; id++) {
  279. var hostName = filteredHosts[id];
  280. hostsToShow.push(hostName);
  281. if (isFiltered) { // Only show the nodes per host matching active filter
  282. nodesToShow = nodesToShow.concat(filteredNodes.filter(function (node) {
  283. return node.startsWith(hostName);
  284. }));
  285. } else {
  286. nodesToShow = nodesToShow.concat(hosts[hostName]['nodes']);
  287. }
  288. }
  289. nodesParam = nodesToShow.join(',');
  290. $scope.nextEnabled = $scope.from + pageSize < filteredHosts.length;
  291. $scope.prevEnabled = $scope.from - pageSize >= 0;
  292. nodesToShow.sort();
  293. hostsToShow.sort();
  294. /*
  295. Fetch system info for all selected nodes
  296. Pick the data we want to display and add it to the node-centric data structure
  297. */
  298. System.get({"nodes": nodesParam}, function (systemResponse) {
  299. for (var node in systemResponse) {
  300. if (node in nodes) {
  301. var s = systemResponse[node];
  302. nodes[node]['system'] = s;
  303. var memTotal = s.system.totalPhysicalMemorySize;
  304. var memFree = s.system.freePhysicalMemorySize;
  305. var memPercentage = Math.floor((memTotal - memFree) / memTotal * 100);
  306. nodes[node]['memUsedPct'] = memPercentage;
  307. nodes[node]['memUsedPctStyle'] = styleForPct(memPercentage);
  308. nodes[node]['memTotal'] = bytesToSize(memTotal);
  309. nodes[node]['memFree'] = bytesToSize(memFree);
  310. nodes[node]['memUsed'] = bytesToSize(memTotal - memFree);
  311. var heapTotal = s.jvm.memory.raw.total;
  312. var heapFree = s.jvm.memory.raw.free;
  313. var heapPercentage = Math.floor((heapTotal - heapFree) / heapTotal * 100);
  314. nodes[node]['heapUsed'] = bytesToSize(heapTotal - heapFree);
  315. nodes[node]['heapUsedPct'] = heapPercentage;
  316. nodes[node]['heapUsedPctStyle'] = styleForPct(heapPercentage);
  317. nodes[node]['heapTotal'] = bytesToSize(heapTotal);
  318. nodes[node]['heapFree'] = bytesToSize(heapFree);
  319. var jvmUptime = s.jvm.jmx.upTimeMS / 1000; // Seconds
  320. nodes[node]['jvmUptime'] = secondsForHumans(jvmUptime);
  321. nodes[node]['jvmUptimeSec'] = jvmUptime;
  322. nodes[node]['uptime'] = s.system.uptime.replace(/.*up (.*?,.*?),.*/, "$1");
  323. nodes[node]['loadAvg'] = Math.round(s.system.systemLoadAverage * 100) / 100;
  324. nodes[node]['cpuPct'] = Math.ceil(s.system.processCpuLoad);
  325. nodes[node]['cpuPctStyle'] = styleForPct(Math.ceil(s.system.processCpuLoad));
  326. nodes[node]['maxFileDescriptorCount'] = s.system.maxFileDescriptorCount;
  327. nodes[node]['openFileDescriptorCount'] = s.system.openFileDescriptorCount;
  328. }
  329. }
  330. });
  331. /*
  332. Fetch metrics for all selected nodes. Only pull the metrics that we'll show to save bandwidth
  333. Pick the data we want to display and add it to the node-centric data structure
  334. */
  335. Metrics.get({
  336. "nodes": nodesParam,
  337. "prefix": "CONTAINER.fs,org.eclipse.jetty.server.handler.DefaultHandler.get-requests,INDEX.sizeInBytes,SEARCHER.searcher.numDocs,SEARCHER.searcher.deletedDocs,SEARCHER.searcher.warmupTime"
  338. },
  339. function (metricsResponse) {
  340. for (var node in metricsResponse) {
  341. if (node in nodes) {
  342. var m = metricsResponse[node];
  343. nodes[node]['metrics'] = m;
  344. var diskTotal = m.metrics['solr.node']['CONTAINER.fs.totalSpace'];
  345. var diskFree = m.metrics['solr.node']['CONTAINER.fs.usableSpace'];
  346. var diskPercentage = Math.floor((diskTotal - diskFree) / diskTotal * 100);
  347. nodes[node]['diskUsedPct'] = diskPercentage;
  348. nodes[node]['diskUsedPctStyle'] = styleForPct(diskPercentage);
  349. nodes[node]['diskTotal'] = bytesToSize(diskTotal);
  350. nodes[node]['diskFree'] = bytesToSize(diskFree);
  351. var r = m.metrics['solr.jetty']['org.eclipse.jetty.server.handler.DefaultHandler.get-requests'];
  352. nodes[node]['req'] = r.count;
  353. nodes[node]['req1minRate'] = Math.floor(r['1minRate'] * 100) / 100;
  354. nodes[node]['req5minRate'] = Math.floor(r['5minRate'] * 100) / 100;
  355. nodes[node]['req15minRate'] = Math.floor(r['15minRate'] * 100) / 100;
  356. nodes[node]['reqp75_ms'] = Math.floor(r['p75_ms']);
  357. nodes[node]['reqp95_ms'] = Math.floor(r['p95_ms']);
  358. nodes[node]['reqp99_ms'] = Math.floor(r['p99_ms']);
  359. var cores = nodes[node]['cores'];
  360. var indexSizeTotal = 0;
  361. var docsTotal = 0;
  362. var graphData = [];
  363. if (cores) {
  364. for (coreId in cores) {
  365. var core = cores[coreId];
  366. var keyName = "solr.core." + core['core'].replace(/(.*?)_(shard(\d+_?)+)_(replica.*?)/, '\$1.\$2.\$4');
  367. var nodeMetric = m.metrics[keyName];
  368. var size = nodeMetric['INDEX.sizeInBytes'];
  369. size = (typeof size !== 'undefined') ? size : 0;
  370. core['sizeInBytes'] = size;
  371. core['size'] = bytesToSize(size);
  372. core['label'] = core['core'].replace(/(.*?)_shard((\d+_?)+)_replica_?[ntp]?(\d+)/, '\$1_s\$2r\$4');
  373. if (core['shard_state'] !== 'active' || core['state'] !== 'active') {
  374. // If core state is not active, display the real state, or if shard is inactive, display that
  375. var labelState = (core['state'] !== 'active') ? core['state'] : core['shard_state'];
  376. core['label'] += "_(" + labelState + ")";
  377. }
  378. indexSizeTotal += size;
  379. var numDocs = nodeMetric['SEARCHER.searcher.numDocs'];
  380. numDocs = (typeof numDocs !== 'undefined') ? numDocs : 0;
  381. core['numDocs'] = numDocs;
  382. core['numDocsHuman'] = numDocsHuman(numDocs);
  383. core['avgSizePerDoc'] = bytesToSize(numDocs === 0 ? 0 : size / numDocs);
  384. var deletedDocs = nodeMetric['SEARCHER.searcher.deletedDocs'];
  385. deletedDocs = (typeof deletedDocs !== 'undefined') ? deletedDocs : 0;
  386. core['deletedDocs'] = deletedDocs;
  387. core['deletedDocsHuman'] = numDocsHuman(deletedDocs);
  388. var warmupTime = nodeMetric['SEARCHER.searcher.warmupTime'];
  389. warmupTime = (typeof warmupTime !== 'undefined') ? warmupTime : 0;
  390. core['warmupTime'] = warmupTime;
  391. docsTotal += core['numDocs'];
  392. }
  393. for (coreId in cores) {
  394. core = cores[coreId];
  395. var graphObj = {};
  396. graphObj['label'] = core['label'];
  397. graphObj['size'] = core['sizeInBytes'];
  398. graphObj['sizeHuman'] = core['size'];
  399. graphObj['pct'] = (core['sizeInBytes'] / indexSizeTotal) * 100;
  400. graphData.push(graphObj);
  401. }
  402. cores.sort(function (a, b) {
  403. return b.sizeInBytes - a.sizeInBytes
  404. });
  405. } else {
  406. cores = {};
  407. }
  408. graphData.sort(function (a, b) {
  409. return b.size - a.size
  410. });
  411. nodes[node]['graphData'] = graphData;
  412. nodes[node]['numDocs'] = numDocsHuman(docsTotal);
  413. nodes[node]['sizeInBytes'] = indexSizeTotal;
  414. nodes[node]['size'] = bytesToSize(indexSizeTotal);
  415. nodes[node]['sizePerDoc'] = docsTotal === 0 ? '0b' : bytesToSize(indexSizeTotal / docsTotal);
  416. // Build the d3 powered bar chart
  417. $('#chart' + nodes[node]['id']).empty();
  418. var chart = d3.select('#chart' + nodes[node]['id']).append('div').attr('class', 'chart');
  419. // Add one div per bar which will group together both labels and bars
  420. var g = chart.selectAll('div')
  421. .data(nodes[node]['graphData']).enter()
  422. .append('div');
  423. // Add the bars
  424. var bars = g.append("div")
  425. .attr("class", "rect")
  426. .text(function (d) {
  427. return d.label + ':\u00A0\u00A0' + d.sizeHuman;
  428. });
  429. // Execute the transition to show the bars
  430. bars.transition()
  431. .ease('elastic')
  432. .style('width', function (d) {
  433. return d.pct + '%';
  434. });
  435. }
  436. }
  437. });
  438. $scope.nodes = nodes;
  439. $scope.hosts = hosts;
  440. $scope.live_nodes = live_nodes;
  441. $scope.nodesToShow = nodesToShow;
  442. $scope.hostsToShow = hostsToShow;
  443. $scope.filteredNodes = filteredNodes;
  444. $scope.filteredHosts = filteredHosts;
  445. };
  446. $scope.initClusterState();
  447. };
  448. var zkStatusSubController = function($scope, ZookeeperStatus) {
  449. $scope.showZkStatus = true;
  450. $scope.showNodes = false;
  451. $scope.showTree = false;
  452. $scope.showGraph = false;
  453. $scope.tree = {};
  454. $scope.showData = false;
  455. $scope.showDetails = false;
  456. $scope.toggleDetails = function() {
  457. $scope.showDetails = !$scope.showDetails === true;
  458. };
  459. $scope.initZookeeper = function() {
  460. ZookeeperStatus.monitor({}, function(data) {
  461. $scope.zkState = data.zkStatus;
  462. $scope.mainKeys = ["ok", "clientPort", "zk_server_state", "zk_version",
  463. "zk_approximate_data_size", "zk_znode_count", "zk_num_alive_connections"];
  464. $scope.detailKeys = ["dataDir", "dataLogDir",
  465. "zk_avg_latency", "zk_max_file_descriptor_count", "zk_watch_count",
  466. "zk_packets_sent", "zk_packets_received",
  467. "tickTime", "maxClientCnxns", "minSessionTimeout", "maxSessionTimeout"];
  468. $scope.ensembleMainKeys = ["serverId", "electionPort", "quorumPort"];
  469. $scope.ensembleDetailKeys = ["peerType", "electionAlg", "initLimit", "syncLimit",
  470. "zk_followers", "zk_synced_followers", "zk_pending_syncs"];
  471. });
  472. };
  473. $scope.initZookeeper();
  474. };
  475. var treeSubController = function($scope, Zookeeper) {
  476. $scope.showZkStatus = false;
  477. $scope.showTree = true;
  478. $scope.showGraph = false;
  479. $scope.tree = {};
  480. $scope.showData = false;
  481. $scope.showTreeLink = function(link) {
  482. var path = decodeURIComponent(link.replace(/.*[\\?&]path=([^&#]*).*/, "$1"));
  483. Zookeeper.detail({path: path}, function(data) {
  484. $scope.znode = data.znode;
  485. var path = data.znode.path.split( '.' );
  486. if(path.length >1) {
  487. $scope.lang = path.pop();
  488. } else {
  489. var lastPathElement = data.znode.path.split( '/' ).pop();
  490. if (lastPathElement == "managed-schema") {
  491. $scope.lang = "xml";
  492. }
  493. }
  494. $scope.showData = true;
  495. });
  496. };
  497. $scope.hideData = function() {
  498. $scope.showData = false;
  499. };
  500. $scope.initTree = function() {
  501. Zookeeper.simple(function(data) {
  502. $scope.tree = data.tree;
  503. });
  504. };
  505. $scope.initTree();
  506. };
  507. /**
  508. * Translates seconds into human readable format of seconds, minutes, hours, days, and years
  509. *
  510. * @param {number} seconds The number of seconds to be processed
  511. * @return {string} The phrase describing the the amount of time
  512. */
  513. function secondsForHumans ( seconds ) {
  514. var levels = [
  515. [Math.floor(seconds / 31536000), 'y'],
  516. [Math.floor((seconds % 31536000) / 86400), 'd'],
  517. [Math.floor(((seconds % 31536000) % 86400) / 3600), 'h'],
  518. [Math.floor((((seconds % 31536000) % 86400) % 3600) / 60), 'm']
  519. ];
  520. var returntext = '';
  521. for (var i = 0, max = levels.length; i < max; i++) {
  522. if ( levels[i][0] === 0 ) continue;
  523. returntext += ' ' + levels[i][0] + levels[i][1];
  524. }
  525. return returntext.trim() === '' ? '0m' : returntext.trim();
  526. }
  527. var graphSubController = function ($scope, Zookeeper, isRadial) {
  528. $scope.showZkStatus = false;
  529. $scope.showTree = false;
  530. $scope.showGraph = true;
  531. $scope.filterType = "status";
  532. $scope.helperData = {
  533. protocol: [],
  534. host: [],
  535. hostname: [],
  536. port: [],
  537. pathname: [],
  538. replicaType: [],
  539. base_url: [],
  540. core: [],
  541. node_name: [],
  542. state: [],
  543. core_node: []
  544. };
  545. $scope.next = function() {
  546. $scope.pos += $scope.rows;
  547. $scope.initGraph();
  548. };
  549. $scope.previous = function() {
  550. $scope.pos = Math.max(0, $scope.pos - $scope.rows);
  551. $scope.initGraph();
  552. };
  553. $scope.resetGraph = function() {
  554. $scope.pos = 0;
  555. $scope.initGraph();
  556. };
  557. $scope.initGraph = function() {
  558. Zookeeper.liveNodes(function (data) {
  559. var live_nodes = {};
  560. for (var c in data.tree[0].children) {
  561. live_nodes[data.tree[0].children[c].data.title] = true;
  562. }
  563. var params = {view: "graph"};
  564. if ($scope.rows) {
  565. params.start = $scope.pos;
  566. params.rows = $scope.rows;
  567. }
  568. var filter = ($scope.filterType=='status') ? $scope.pagingStatusFilter : $scope.pagingFilter;
  569. if (filter) {
  570. params.filterType = $scope.filterType;
  571. params.filter = filter;
  572. }
  573. Zookeeper.clusterState(params, function (data) {
  574. eval("var state=" + data.znode.data); // @todo fix horrid means to parse JSON
  575. var leaf_count = 0;
  576. var graph_data = {
  577. name: null,
  578. children: []
  579. };
  580. for (var c in state) {
  581. var shards = [];
  582. for (var s in state[c].shards) {
  583. var shard_status = state[c].shards[s].state;
  584. shard_status = shard_status == 'inactive' ? 'shard-inactive' : shard_status;
  585. var nodes = [];
  586. for (var n in state[c].shards[s].replicas) {
  587. leaf_count++;
  588. var replica = state[c].shards[s].replicas[n]
  589. var uri = replica.base_url;
  590. var parts = uri.match(/^(\w+:)\/\/(([\w\d\.-]+)(:(\d+))?)(.+)$/);
  591. var uri_parts = {
  592. protocol: parts[1],
  593. host: parts[2],
  594. hostname: parts[3],
  595. port: parseInt(parts[5] || 80, 10),
  596. pathname: parts[6],
  597. replicaType: replica.type,
  598. base_url: replica.base_url,
  599. core: replica.core,
  600. node_name: replica.node_name,
  601. state: replica.state,
  602. core_node: n
  603. };
  604. $scope.helperData.protocol.push(uri_parts.protocol);
  605. $scope.helperData.host.push(uri_parts.host);
  606. $scope.helperData.hostname.push(uri_parts.hostname);
  607. $scope.helperData.port.push(uri_parts.port);
  608. $scope.helperData.pathname.push(uri_parts.pathname);
  609. $scope.helperData.replicaType.push(uri_parts.replicaType);
  610. $scope.helperData.base_url.push(uri_parts.base_url);
  611. $scope.helperData.core.push(uri_parts.core);
  612. $scope.helperData.node_name.push(uri_parts.node_name);
  613. $scope.helperData.state.push(uri_parts.state);
  614. $scope.helperData.core_node.push(uri_parts.core_node);
  615. var replica_status = replica.state;
  616. if (!live_nodes[replica.node_name]) {
  617. replica_status = 'gone';
  618. } else if(shard_status=='shard-inactive') {
  619. replica_status += ' ' + shard_status;
  620. }
  621. var node = {
  622. name: uri,
  623. data: {
  624. type: 'node',
  625. state: replica_status,
  626. leader: 'true' === replica.leader,
  627. uri: uri_parts
  628. }
  629. };
  630. nodes.push(node);
  631. }
  632. var shard = {
  633. name: shard_status == "shard-inactive" ? s + ' (inactive)' : s,
  634. data: {
  635. type: 'shard',
  636. state: shard_status
  637. },
  638. children: nodes
  639. };
  640. shards.push(shard);
  641. }
  642. var collection = {
  643. name: c,
  644. data: {
  645. type: 'collection'
  646. },
  647. children: shards
  648. };
  649. graph_data.children.push(collection);
  650. }
  651. $scope.helperData.protocol = $.unique($scope.helperData.protocol);
  652. $scope.helperData.host = $.unique($scope.helperData.host);
  653. $scope.helperData.hostname = $.unique($scope.helperData.hostname);
  654. $scope.helperData.port = $.unique($scope.helperData.port);
  655. $scope.helperData.pathname = $.unique($scope.helperData.pathname);
  656. $scope.helperData.replicaType = $.unique($scope.helperData.replicaType);
  657. $scope.helperData.base_url = $.unique($scope.helperData.base_url);
  658. $scope.helperData.core = $.unique($scope.helperData.core);
  659. $scope.helperData.node_name = $.unique($scope.helperData.node_name);
  660. $scope.helperData.state = $.unique($scope.helperData.state);
  661. $scope.helperData.core_node = $.unique($scope.helperData.core_node);
  662. if (!isRadial && data.znode && data.znode.paging) {
  663. $scope.showPaging = true;
  664. var parr = data.znode.paging.split('|');
  665. if (parr.length < 3) {
  666. $scope.showPaging = false;
  667. } else {
  668. $scope.start = Math.max(parseInt(parr[0]), 0);
  669. $scope.prevEnabled = ($scope.start > 0);
  670. $scope.rows = parseInt(parr[1]);
  671. $scope.total = parseInt(parr[2]);
  672. if ($scope.rows == -1) {
  673. $scope.showPaging = false;
  674. } else {
  675. var filterType = parr.length > 3 ? parr[3] : '';
  676. if (filterType == '' || filterType == 'none') filterType = 'status';
  677. +$('#cloudGraphPagingFilterType').val(filterType);
  678. var filter = parr.length > 4 ? parr[4] : '';
  679. var page = Math.floor($scope.start / $scope.rows) + 1;
  680. var pages = Math.ceil($scope.total / $scope.rows);
  681. $scope.last = Math.min($scope.start + $scope.rows, $scope.total);
  682. $scope.nextEnabled = ($scope.last < $scope.total);
  683. }
  684. }
  685. }
  686. else {
  687. $scope.showPaging = false;
  688. }
  689. $scope.graphData = graph_data;
  690. $scope.leafCount = leaf_count;
  691. $scope.isRadial = isRadial;
  692. });
  693. });
  694. };
  695. $scope.initGraph();
  696. $scope.pos = 0;
  697. };
  698. solrAdminApp.directive('graph', function(Constants) {
  699. return {
  700. restrict: 'EA',
  701. scope: {
  702. data: "=",
  703. leafCount: "=",
  704. helperData: "=",
  705. isRadial: "="
  706. },
  707. link: function (scope, element, attrs) {
  708. var helper_path_class = function (p) {
  709. var classes = ['link'];
  710. classes.push('lvl-' + p.target.depth);
  711. if (p.target.data && p.target.data.leader) {
  712. classes.push('leader');
  713. }
  714. if (p.target.data && p.target.data.state) {
  715. classes.push(p.target.data.state);
  716. }
  717. return classes.join(' ');
  718. };
  719. var helper_node_class = function (d) {
  720. var classes = ['node'];
  721. classes.push('lvl-' + d.depth);
  722. if (d.data && d.data.leader) {
  723. classes.push('leader');
  724. }
  725. if (d.data && d.data.state) {
  726. if(!(d.data.type=='shard' && d.data.state=='active')){
  727. classes.push(d.data.state);
  728. }
  729. }
  730. return classes.join(' ');
  731. };
  732. var helper_tooltip_text = function (d) {
  733. if (!d.data || !d.data.uri) {
  734. return tooltip;
  735. }
  736. var tooltip = d.data.uri.core_node + " {<br/>";
  737. if (0 !== scope.helperData.core.length) {
  738. tooltip += "core: [" + d.data.uri.core + "],<br/>";
  739. }
  740. if (0 !== scope.helperData.node_name.length) {
  741. tooltip += "node_name: [" + d.data.uri.node_name + "],<br/>";
  742. }
  743. tooltip += "}";
  744. return tooltip;
  745. };
  746. var helper_node_text = function (d) {
  747. if (!d.data || !d.data.uri) {
  748. return d.name;
  749. }
  750. var name = d.data.uri.hostname;
  751. if (1 !== scope.helperData.protocol.length) {
  752. name = d.data.uri.protocol + '//' + name;
  753. }
  754. if (1 !== scope.helperData.port.length) {
  755. name += ':' + d.data.uri.port;
  756. }
  757. if (1 !== scope.helperData.pathname.length) {
  758. name += d.data.uri.pathname;
  759. }
  760. if(0 !== scope.helperData.replicaType.length) {
  761. name += ' (' + d.data.uri.replicaType[0] + ')';
  762. }
  763. return name;
  764. };
  765. scope.$watch("data", function(newValue, oldValue) {
  766. if (newValue) {
  767. if (scope.isRadial) {
  768. radialGraph(element, scope.data, scope.leafCount);
  769. } else {
  770. flatGraph(element, scope.data, scope.leafCount);
  771. }
  772. }
  773. $('text').tooltip({
  774. content: function() {
  775. return $(this).attr('title');
  776. }
  777. });
  778. });
  779. function setNodeNavigationBehavior(node, view){
  780. node
  781. .attr('data-href', function (d) {
  782. if (d.type == "node"){
  783. return getNodeUrl(d, view);
  784. }
  785. else{
  786. return "";
  787. }
  788. })
  789. .on('click', function(d) {
  790. if (d.data.type == "node"){
  791. location.href = getNodeUrl(d, view);
  792. }
  793. });
  794. }
  795. function getNodeUrl(d, view){
  796. var url = d.name + Constants.ROOT_URL + "#/~cloud";
  797. if (view != undefined){
  798. url += "?view=" + view;
  799. }
  800. return url;
  801. }
  802. var flatGraph = function(element, graphData, leafCount) {
  803. var w = element.width(),
  804. h = leafCount * 20;
  805. var tree = d3.layout.tree().size([h, w - 400]);
  806. var diagonal = d3.svg.diagonal().projection(function (d) {
  807. return [d.y, d.x];
  808. });
  809. d3.select('#canvas', element).html('');
  810. var vis = d3.select('#canvas', element).append('svg')
  811. .attr('width', w)
  812. .attr('height', h)
  813. .append('g')
  814. .attr('transform', 'translate(100, 0)');
  815. var nodes = tree.nodes(graphData);
  816. var link = vis.selectAll('path.link')
  817. .data(tree.links(nodes))
  818. .enter().append('path')
  819. .attr('class', helper_path_class)
  820. .attr('d', diagonal);
  821. var node = vis.selectAll('g.node')
  822. .data(nodes)
  823. .enter().append('g')
  824. .attr('class', helper_node_class)
  825. .attr('transform', function (d) {
  826. return 'translate(' + d.y + ',' + d.x + ')';
  827. })
  828. node.append('circle')
  829. .attr('r', 4.5);
  830. node.append('text')
  831. .attr('dx', function (d) {
  832. return 0 === d.depth ? -8 : 8;
  833. })
  834. .attr('dy', function (d) {
  835. return 5;
  836. })
  837. .attr('text-anchor', function (d) {
  838. return 0 === d.depth ? 'end' : 'start';
  839. })
  840. .attr("title", helper_tooltip_text)
  841. .text(helper_node_text);
  842. setNodeNavigationBehavior(node);
  843. };
  844. var radialGraph = function(element, graphData, leafCount) {
  845. var max_val = Math.min(element.width(), $('body').height())
  846. var r = max_val / 2;
  847. var cluster = d3.layout.cluster()
  848. .size([360, r - 160]);
  849. var diagonal = d3.svg.diagonal.radial()
  850. .projection(function (d) {
  851. return [d.y, d.x / 180 * Math.PI];
  852. });
  853. d3.select('#canvas', element).html('');
  854. var vis = d3.select('#canvas').append('svg')
  855. .attr('width', r * 2)
  856. .attr('height', r * 2)
  857. .append('g')
  858. .attr('transform', 'translate(' + r + ',' + r + ')');
  859. var nodes = cluster.nodes(graphData);
  860. var link = vis.selectAll('path.link')
  861. .data(cluster.links(nodes))
  862. .enter().append('path')
  863. .attr('class', helper_path_class)
  864. .attr('d', diagonal);
  865. var node = vis.selectAll('g.node')
  866. .data(nodes)
  867. .enter().append('g')
  868. .attr('class', helper_node_class)
  869. .attr('transform', function (d) {
  870. return 'rotate(' + (d.x - 90) + ')translate(' + d.y + ')';
  871. })
  872. node.append('circle')
  873. .attr('r', 4.5);
  874. node.append('text')
  875. .attr('dx', function (d) {
  876. return d.x < 180 ? 8 : -8;
  877. })
  878. .attr('dy', '.31em')
  879. .attr('text-anchor', function (d) {
  880. return d.x < 180 ? 'start' : 'end';
  881. })
  882. .attr('transform', function (d) {
  883. return d.x < 180 ? null : 'rotate(180)';
  884. })
  885. .attr("title", helper_tooltip_text)
  886. .text(helper_node_text);
  887. setNodeNavigationBehavior(node, "rgraph");
  888. }
  889. }
  890. };
  891. });