diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..15d56783 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM openjdk:11-jre-slim + +ENV CEREBRO_VERSION 0.8.4 + +ADD target/universal/cerebro-${CEREBRO_VERSION}.tgz /opt + +RUN mv /opt/cerebro-$CEREBRO_VERSION /opt/cerebro \ + && apt-get update \ + && apt-get install -y wget \ + && rm -rf /var/lib/apt/lists/* \ + && mkdir -p /opt/cerebro/logs \ + && sed -i '//d' /opt/cerebro/conf/logback.xml \ + && addgroup -gid 1000 cerebro \ + && adduser -gid 1000 -uid 1000 cerebro \ + && chown -R cerebro:cerebro /opt/cerebro + +WORKDIR /opt/cerebro +EXPOSE 9000 +USER cerebro +ENTRYPOINT [ "/opt/cerebro/bin/cerebro" ] \ No newline at end of file diff --git a/app/models/commons/NodeInfo.scala b/app/models/commons/NodeInfo.scala index 77279b11..4e576707 100644 --- a/app/models/commons/NodeInfo.scala +++ b/app/models/commons/NodeInfo.scala @@ -8,7 +8,8 @@ trait NodeInfo { "ml.machine_memory", "xpack.installed", "transform.node", - "ml.max_open_jobs" + "ml.max_open_jobs", + "ml.max_jvm_size" ) def attrs(info: JsValue) = { diff --git a/app/models/commons/NodeRoles.scala b/app/models/commons/NodeRoles.scala index 0719e13a..484f571f 100644 --- a/app/models/commons/NodeRoles.scala +++ b/app/models/commons/NodeRoles.scala @@ -16,7 +16,7 @@ object NodeRoles { def apply(nodeInfo: JsValue): NodeRoles = { // >= 7.10 - val dataRoles = Seq("data", "data_content", "data_hot", "data_warm", "data_cold").map(JsString) + val dataRoles = Seq("data", "data_content", "data_hot", "data_warm", "data_cold", "data_frozen").map(JsString) (nodeInfo \ "roles").asOpt[JsArray] match { case Some(JsArray(roles)) => // >= 5.X diff --git a/app/models/nodes/Node.scala b/app/models/nodes/Node.scala index f0d75b20..6641ebe5 100644 --- a/app/models/nodes/Node.scala +++ b/app/models/nodes/Node.scala @@ -29,7 +29,11 @@ object Node extends NodeInfo { "master" -> JsBoolean(roles.master), "coordinating" -> JsBoolean(roles.coordinating), "ingest" -> JsBoolean(roles.ingest), - "data" -> JsBoolean(roles.data) + "data" -> JsBoolean(roles.data), + "roles" -> JsArray(info \ "roles" match { + case JsDefined(JsArray(roles)) => roles + case _ => Seq.empty + }) ) } diff --git a/app/models/overview/ClosedIndex.scala b/app/models/overview/ClosedIndex.scala index 00dc6702..78463549 100644 --- a/app/models/overview/ClosedIndex.scala +++ b/app/models/overview/ClosedIndex.scala @@ -1,14 +1,17 @@ package models.overview -import play.api.libs.json.{JsBoolean, JsString, Json} +import play.api.libs.json.{JsBoolean, JsNumber, JsString, JsValue, Json} object ClosedIndex { - def apply(name: String) = + def apply(name: String, aliases: JsValue, numShards: JsNumber, numReplicas: JsNumber) = Json.obj( "name" -> JsString(name), "closed" -> JsBoolean(true), - "special" -> JsBoolean(name.startsWith(".")) + "special" -> JsBoolean(name.startsWith(".")), + "aliases" -> aliases, + "num_shards" -> numShards, + "num_replicas" -> numReplicas ) } diff --git a/app/models/overview/ClusterOverview.scala b/app/models/overview/ClusterOverview.scala index 95d77cd1..8c746743 100644 --- a/app/models/overview/ClusterOverview.scala +++ b/app/models/overview/ClusterOverview.scala @@ -59,9 +59,13 @@ object ClusterOverview { Index(index, indexStats, shards, indexAliases, indexBlock) }.toSeq + val metadata = (clusterState \ "metadata" \ "indices").as[JsObject] val closedIndices = blocks.value.collect { // ES < 7.X does not return routing_table for closed indices case (name, block) if !routingTable.contains(name) && (block \ "4").isDefined => - ClosedIndex(name) + val indexAliases = (metadata \ name \ "aliases").as[JsArray] + val numShards = JsNumber((metadata \ name \ "settings" \ "index" \ "number_of_shards").as[JsString].value.toInt) + val numReplicas = JsNumber((metadata \ name \ "settings" \ "index" \ "number_of_replicas").as[JsString].value.toInt) + ClosedIndex(name, indexAliases, numShards, numReplicas) } indices ++ closedIndices diff --git a/app/models/overview/Index.scala b/app/models/overview/Index.scala index 7eeeef30..924d66a0 100644 --- a/app/models/overview/Index.scala +++ b/app/models/overview/Index.scala @@ -17,6 +17,8 @@ object Index { "size_in_bytes" -> (stats \ "primaries" \ "store" \ "size_in_bytes").asOpt[JsNumber].getOrElse(JsNumber(0)), "total_size_in_bytes" -> (stats \ "total" \ "store" \ "size_in_bytes").asOpt[JsNumber].getOrElse(JsNumber(0)), "aliases" -> JsArray(aliases.as[JsObject].keys.map(JsString(_)).toSeq), // 1.4 < does not return aliases obj + "is_write_true_aliases" -> JsArray(aliases.as[JsObject].fieldSet.filter( + _._2.as[JsObject].fieldSet.contains("is_write_index", JsBoolean(true))).map(_._1).map(JsString(_)).toSeq), "num_shards" -> JsNumber((routingTable \ "shards").as[JsObject].keys.map(_.toInt).max + 1), "num_replicas" -> JsNumber((routingTable \ "shards" \ "0").as[JsArray].value.size - 1), "shards" -> JsObject(shardMap) diff --git a/app/models/overview/Node.scala b/app/models/overview/Node.scala index c051f7f8..d33015be 100644 --- a/app/models/overview/Node.scala +++ b/app/models/overview/Node.scala @@ -29,6 +29,10 @@ object Node extends NodeInfo { "data" -> JsBoolean(nodeRoles.data), "coordinating" -> JsBoolean(nodeRoles.coordinating), "ingest" -> JsBoolean(nodeRoles.ingest), + "roles" -> JsArray(info \ "roles" match { + case JsDefined(JsArray(roles)) => roles + case _ => Seq.empty + }), "heap" -> Json.obj( "used" -> (stats \ "jvm" \ "mem" \ "heap_used_in_bytes").as[JsNumber], "committed" -> (stats \ "jvm" \ "mem" \ "heap_committed_in_bytes").as[JsNumber], @@ -59,8 +63,10 @@ object Node extends NodeInfo { } def cpuPercent(nodeStats: JsValue): JsNumber = { - val cpu = (nodeStats \ "os" \ "cpu" \ "percent").asOpt[Int].getOrElse(// 5.X - (nodeStats \ "os" \ "cpu_percent").asOpt[Int].getOrElse(0) // FIXME 2.X + val cpu = (nodeStats \ "os" \ "cpu" \ "percent").asOpt[Int].filter(i => i > 0).getOrElse(// 5.X + (nodeStats \ "process" \ "cpu" \ "percent").asOpt[Int].filter(i => i > 0).getOrElse( // 7.X + (nodeStats \ "os" \ "cpu_percent").asOpt[Int].getOrElse(0) // FIXME 2.X + ) ) JsNumber(BigDecimal(cpu)) } diff --git a/app/services/overview/OverviewDataService.scala b/app/services/overview/OverviewDataService.scala index c0d0c988..7d55c001 100644 --- a/app/services/overview/OverviewDataService.scala +++ b/app/services/overview/OverviewDataService.scala @@ -14,7 +14,7 @@ class OverviewDataService @Inject()(client: ElasticClient) { def overview(target: ElasticServer): Future[JsValue] = { val apis = Seq( - "_cluster/state/master_node,routing_table,blocks", + "_cluster/state/master_node,routing_table,blocks,metadata", "_nodes/stats/jvm,fs,os,process?human=true", "_stats/docs,store?ignore_unavailable=true", "_cluster/settings", diff --git a/public/js/app.js b/public/js/app.js index 6e859d32..a29f4c55 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -1113,7 +1113,7 @@ angular.module('cerebro').controller('OverviewController', ['$scope', '$http', $scope.special_indices = 0; $scope.shardAllocation = true; - $scope.indices_filter = new IndexFilter('', false, false, true, true, 0); + $scope.indices_filter = new IndexFilter('', [], false, false, true, true, 0); $scope.nodes_filter = new NodeFilter('', true, false, false, false, 0); $scope.getPageSize = function() { @@ -1192,6 +1192,14 @@ angular.module('cerebro').controller('OverviewController', ['$scope', '$http', $scope.$watch('nodes_filter', function() { if ($scope.data) { $scope.setNodes($scope.data.nodes); + if ($scope.nodes_filter.name) { + $scope.indices_filter.nodes = $scope.nodes.map(function (node) { + return node.id; + }); + } else { + $scope.indices_filter.nodes = []; + } + $scope.setIndices($scope.data.indices); } }, true); @@ -2265,8 +2273,9 @@ function GroupedSettings(settings) { this.groups = Object.values(groups); } -function IndexFilter(name, closed, special, healthy, asc, timestamp) { +function IndexFilter(name, nodes, closed, special, healthy, asc, timestamp) { this.name = name; + this.nodes = nodes; this.closed = closed; this.special = special; this.healthy = healthy; @@ -2294,6 +2303,7 @@ function IndexFilter(name, closed, special, healthy, asc, timestamp) { // eslint-disable-next-line no-unused-vars return new IndexFilter( this.name, + this.nodes, this.closed, this.special, this.healthy, @@ -2306,6 +2316,7 @@ function IndexFilter(name, closed, special, healthy, asc, timestamp) { return ( other !== null && this.name === other.name && + this.nodes === other.nodes && this.closed === other.closed && this.special === other.special && this.healthy === other.healthy && @@ -2360,6 +2371,12 @@ function IndexFilter(name, closed, special, healthy, asc, timestamp) { } } } + if (matches && this.nodes.length > 0) { + var shardNodes = Object.keys(index.shards); + matches = this.nodes.filter(function(node) { + return shardNodes.indexOf(node) != -1; + }).length > 0; + } return matches; }; } @@ -2402,11 +2419,53 @@ function NodeFilter(name, data, master, ingest, coordinating, timestamp) { }; this.matches = function(node) { - if (this.isBlank()) { - return true; - } else { - return this.matchesName(node.name) && this.matchesType(node); + var matches = true; + if (!this.matchesType(node)) { + matches = false; + } + if (matches && this.name) { + try { + var regExp = new RegExp(this.name.trim(), 'i'); + matches = regExp.test(node.name); + if (!matches) { + var attrs = Object.values(node.attributes); + for (var idx = 0; idx < attrs.length; idx++) { + if ((matches = regExp.test(attrs[idx]))) { + break; + } + } + } + if (!matches) { + for (idx = 0; idx < node.roles.length; idx++) { + if ((matches = regExp.test(node.roles[idx]))) { + break; + } + } + } + } catch (err) { // if not valid regexp, still try normal matching + matches = node.name.indexOf(this.name.toLowerCase()) != -1; + if (!matches) { + var _attrs = Object.values(node.attributes); + for (var _idx = 0; _idx < _attrs.length; _idx++) { + var attr = _attrs[_idx].toLowerCase(); + matches = true; + if ((matches = (attr.indexOf(this.name.toLowerCase()) != -1))) { + break; + } + } + } + if (!matches) { + for (_idx = 0; _idx < node.roles.length; _idx++) { + var role = node.roles[_idx].toLowerCase(); + matches = true; + if ((matches = (role.indexOf(this.name.toLowerCase()) != -1))) { + break; + } + } + } + } } + return matches; }; this.matchesType = function(node) { @@ -2417,14 +2476,6 @@ function NodeFilter(name, data, master, ingest, coordinating, timestamp) { node.coordinating && this.coordinating ); }; - - this.matchesName = function(name) { - if (this.name) { - return name.toLowerCase().indexOf(this.name.toLowerCase()) != -1; - } else { - return true; - } - }; } // eslint-disable-next-line no-unused-vars diff --git a/public/nodes/index.html b/public/nodes/index.html index e157d682..00791cfe 100644 --- a/public/nodes/index.html +++ b/public/nodes/index.html @@ -2,7 +2,7 @@
+ placeholder="filter nodes and indices by nodes">
@@ -86,6 +86,9 @@ {{value}} + + {{value}} +
diff --git a/public/overview.html b/public/overview.html index 81148df3..cd43d3ab 100644 --- a/public/overview.html +++ b/public/overview.html @@ -28,7 +28,7 @@
+ placeholder="filter nodes and indices by nodes">
@@ -63,18 +63,18 @@ -
- - -
+
+ + +
-
- {{index.aliases[0]}} - (+{{index.aliases.length - 1}}) +
+ + {{index.aliases[0]}} + {{index.aliases[0]}} + + + {{index.aliases[0]}}(+{{index.aliases.length - 1}}) + {{index.aliases[0]}}(+{{index.aliases.length - 1}}) + +
+
+ {{alias}} + {{alias}}
@@ -169,6 +179,7 @@ size: {{index.size_in_bytes | bytes}} + shards: {{index.num_shards}} * {{index.num_replicas + 1}}
index closed
@@ -235,9 +246,12 @@ {{value}} + + {{value}} +
-
+
JVM: {{node.jvm_version}} ES: {{node.es_version}}
diff --git a/src/app/components/overview/controller.js b/src/app/components/overview/controller.js index 68a3f294..cfc253d5 100644 --- a/src/app/components/overview/controller.js +++ b/src/app/components/overview/controller.js @@ -14,7 +14,7 @@ angular.module('cerebro').controller('OverviewController', ['$scope', '$http', $scope.special_indices = 0; $scope.shardAllocation = true; - $scope.indices_filter = new IndexFilter('', false, false, true, true, 0); + $scope.indices_filter = new IndexFilter('', [], false, false, true, true, 0); $scope.nodes_filter = new NodeFilter('', true, false, false, false, 0); $scope.getPageSize = function() { @@ -93,6 +93,14 @@ angular.module('cerebro').controller('OverviewController', ['$scope', '$http', $scope.$watch('nodes_filter', function() { if ($scope.data) { $scope.setNodes($scope.data.nodes); + if ($scope.nodes_filter.name) { + $scope.indices_filter.nodes = $scope.nodes.map(function (node) { + return node.id; + }); + } else { + $scope.indices_filter.nodes = []; + } + $scope.setIndices($scope.data.indices); } }, true); diff --git a/src/app/shared/node_filter.js b/src/app/shared/node_filter.js index e997c8c5..e7249dcb 100644 --- a/src/app/shared/node_filter.js +++ b/src/app/shared/node_filter.js @@ -36,11 +36,53 @@ function NodeFilter(name, data, master, ingest, coordinating, timestamp) { }; this.matches = function(node) { - if (this.isBlank()) { - return true; - } else { - return this.matchesName(node.name) && this.matchesType(node); + var matches = true; + if (!this.matchesType(node)) { + matches = false; } + if (matches && this.name) { + try { + var regExp = new RegExp(this.name.trim(), 'i'); + matches = regExp.test(node.name); + if (!matches) { + var attrs = Object.values(node.attributes); + for (var idx = 0; idx < attrs.length; idx++) { + if ((matches = regExp.test(attrs[idx]))) { + break; + } + } + } + if (!matches) { + for (idx = 0; idx < node.roles.length; idx++) { + if ((matches = regExp.test(node.roles[idx]))) { + break; + } + } + } + } catch (err) { // if not valid regexp, still try normal matching + matches = node.name.indexOf(this.name.toLowerCase()) != -1; + if (!matches) { + var _attrs = Object.values(node.attributes); + for (var _idx = 0; _idx < _attrs.length; _idx++) { + var attr = _attrs[_idx].toLowerCase(); + matches = true; + if ((matches = (attr.indexOf(this.name.toLowerCase()) != -1))) { + break; + } + } + } + if (!matches) { + for (_idx = 0; _idx < node.roles.length; _idx++) { + var role = node.roles[_idx].toLowerCase(); + matches = true; + if ((matches = (role.indexOf(this.name.toLowerCase()) != -1))) { + break; + } + } + } + } + } + return matches; }; this.matchesType = function(node) { @@ -51,12 +93,4 @@ function NodeFilter(name, data, master, ingest, coordinating, timestamp) { node.coordinating && this.coordinating ); }; - - this.matchesName = function(name) { - if (this.name) { - return name.toLowerCase().indexOf(this.name.toLowerCase()) != -1; - } else { - return true; - } - }; } diff --git a/test/models/overview/ClusterInitializingShards.scala b/test/models/overview/ClusterInitializingShards.scala index 6e817b79..8a6a0007 100644 --- a/test/models/overview/ClusterInitializingShards.scala +++ b/test/models/overview/ClusterInitializingShards.scala @@ -12,6 +12,19 @@ object ClusterInitializingShards { | "cluster_name" : "elasticsearch", | "master_node" : "cPsT9o5FQ3WRnvqSTXHiVQ", | "blocks" : { }, + | "metadata" : { + | "indices" : { + | "hello" : { + | "settings" : { + | "index" : { + | "number_of_shards" : "1", + | "number_of_replicas" : "1" + | } + | }, + | "aliases" : [ ] + | } + | } + | }, | "routing_table" : { | "indices" : { | "hello" : { diff --git a/test/models/overview/ClusterRelocatingShards.scala b/test/models/overview/ClusterRelocatingShards.scala index 8a0725b4..71724aeb 100644 --- a/test/models/overview/ClusterRelocatingShards.scala +++ b/test/models/overview/ClusterRelocatingShards.scala @@ -10,6 +10,19 @@ object ClusterRelocatingShards extends ClusterStub { | "cluster_name" : "elasticsearch", | "master_node" : "cPsT9o5FQ3WRnvqSTXHiVQ", | "blocks" : { }, + | "metadata" : { + | "indices" : { + | "hello" : { + | "settings" : { + | "index" : { + | "number_of_shards" : "1", + | "number_of_replicas" : "1" + | } + | }, + | "aliases" : [ ] + | } + | } + | }, | "routing_table" : { | "indices" : { | "hello" : { diff --git a/test/models/overview/ClusterWithData.scala b/test/models/overview/ClusterWithData.scala index 50880ce8..69f4ffa5 100644 --- a/test/models/overview/ClusterWithData.scala +++ b/test/models/overview/ClusterWithData.scala @@ -20,6 +20,28 @@ trait ClusterWithData extends ClusterStub { | } | } | }, + | "metadata" : { + | "indices" : { + | "foo" : { + | "settings" : { + | "index" : { + | "number_of_shards" : "1", + | "number_of_replicas" : "1" + | } + | }, + | "aliases" : [ ] + | }, + | "bar" : { + | "settings" : { + | "index" : { + | "number_of_shards" : "1", + | "number_of_replicas" : "1" + | } + | }, + | "aliases" : [ ] + | } + | } + | }, | "routing_table": { | "indices": { | "bar": { diff --git a/test/models/overview/ClusterWithoutData.scala b/test/models/overview/ClusterWithoutData.scala index f4756168..d1321705 100644 --- a/test/models/overview/ClusterWithoutData.scala +++ b/test/models/overview/ClusterWithoutData.scala @@ -16,6 +16,11 @@ object ClusterWithoutData extends ClusterStub { | "indices":{ | | } + | }, + | "metadata":{ + | "indices":{ + | + | } | } |} """.stripMargin