├── .eslintrc ├── .gitignore ├── .travis.yml ├── .travis └── github_deploy_key.enc ├── README.md ├── example.gif ├── index.js ├── package.json ├── public ├── app.js ├── common │ ├── Topology.js │ └── chart_utils.js ├── hack.js ├── topology.png └── views │ ├── index-controls-tmpl.html │ ├── index.html │ ├── index.js │ └── index.less └── server ├── __tests__ └── index.js ├── lib ├── init_topology_client_config.js └── publish_elasticsearch_client.js └── routes ├── api.js ├── get_cluster_health.js ├── get_data_heat_map.js └── helpers.js /.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | extends: "@elastic/kibana" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log* 2 | node_modules 3 | /build/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6.9.0 4 | 5 | sudo: required 6 | dist: trusty 7 | 8 | script: 9 | - node_modules/.bin/plugin-helpers build 10 | 11 | before_install: 12 | - openssl aes-256-cbc -K $encrypted_5f819ac0d0f8_key -iv $encrypted_5f819ac0d0f8_iv -in .travis/github_deploy_key.enc -out github_deploy_key -d 13 | 14 | deploy: 15 | provider: releases 16 | api_key: 17 | secure: tGKykjJbFRv4hZClpsqBKkTv8VazV8DrWTKbJyqIN4LFMYrUjfkrb6DpO8t0sO+UHHRMMIJWz/WQ3QCOTIQJG2bGiyUjYMJ2sUSOXcLZmgXDPOVbKiGO6YUVsd7CFksBn5dF28jsRxIIK1fvnu4rr1b9zhXo8tXoTWlkRMcrqGoNRYKjDO4o8WeHr2m2V1FtUpZTMIYgjvAbqDD7ePyXkGCLoCyPiBdA6y+iDK079XfKhrHfP/mzvW1F9bQsGH+nbqn0DXZ6d47BC4uFzKIM6C13L077XcBdPwt5qD+mHO+oT+w9ffyvlPO+wR9s+JWor+yl4vuj1d25vMpCaSll9J9dMFkx1CqVMbre5mcGyWkYeesFFphgwapAL8tqHysDOmxaGuY6oGxKEs8SJ8uN518f0sduya9RJTAqpXou39Fse15kYDqOZEDVz4KwlDow80RS8H451DxoVYrU0lBKzc84GV+kw6DCl87okOi/FOo8LeHTxkRB8epL4A1DROrxR7FmwO56jXDJV8oLkdex8Blb3xyt9ubT6wJWWCDOYhP4JPmd66VUAJu7oelmqu7aDh3n8acjvyovlLTd6VQlzrFtkMbAzTJL1qUkWbPXsl/rpIuklzM924exDic0vfs1pGjgPEX1gQkLO15uhm0WGTPCpuWIRpC8vNe24jwWzrI= 18 | file: "$TRAVIS_BUILD_DIR/build/topology-5.1.1.zip" 19 | skip_cleanup: true 20 | on: 21 | repo: bahaaldine/topology 22 | tags: true 23 | 24 | after_success: 25 | - |2- 26 | 27 | $(npm bin)/set-up-ssh --key "$encrypted_5f819ac0d0f8_key" \ 28 | --iv "$encrypted_5f819ac0d0f8_iv" \ 29 | --path-encrypted-key ".travis/github_deploy_key.enc" 30 | -------------------------------------------------------------------------------- /.travis/github_deploy_key.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahaaldine/topology/298fe764c8abb4a6d657c43b7b334871bcf6b34e/.travis/github_deploy_key.enc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/bahaaldine/topology.svg?branch=master)](https://travis-ci.org/bahaaldine/topology) 2 | 3 | # Topology 4 | 5 | ![alt Topology](https://github.com/bahaaldine/topology/blob/master/example.gif?raw=true) 6 | 7 | ## Installation 8 | 9 | If you don't have X-Pack security installed, you can jump to the plugin installation directly [here](https://github.com/bahaaldine/topology/blob/master/README.md#installing-plugin) 10 | 11 | ### Create Topology user & role 12 | 13 | Topology uses the `_cat` API therefore needs monitor priviledge at cluster level. 14 | With the Native Realm (X-Pack Security API) create the following role: 15 | 16 | ```json 17 | PUT /_xpack/security/role/topology_role 18 | { 19 | "cluster": [ 20 | "monitor" 21 | ], 22 | "indices": [ 23 | { 24 | "names": [ 25 | "*" 26 | ], 27 | "privileges": [ 28 | "monitor" 29 | ] 30 | } 31 | ] 32 | } 33 | ``` 34 | And then create a user mapped to the role: 35 | 36 | ```json 37 | POST /_xpack/security/user/topology 38 | { 39 | "password" : "topology", 40 | "roles" : [ "topology_role" ], 41 | "full_name" : "Topo logy", 42 | "email" : "topology@elastic.co", 43 | "enabled": true 44 | } 45 | ``` 46 | Please note that you can choose whatever username and password you want. 47 | 48 | ### Add Topology configuration to Kibana.yml 49 | 50 | With the user and role created, add the following topology settings to the kibana.yml file: 51 | 52 | *Minimal configuration* 53 | ```yaml 54 | topology: 55 | elasticsearch: 56 | username: topology 57 | password: topology 58 | ``` 59 | 60 | *Full-version* 61 | ```yaml 62 | topology: 63 | elasticsearch: 64 | url: url_to_the_elasticsearch_cluser 65 | ssl: 66 | cert: path_to_the_cert_file 67 | key: path_to_the_key_file 68 | ca: path_to_the_ca_file 69 | verify: boolean, whether or not the certificate should be verified 70 | username: topology 71 | password: topology 72 | ``` 73 | 74 | ### Installing plugin 75 | 76 | Topology does not support Kibana version lower than 5.x. The topology version you will use, should be the same than the Kibana version, you just need to adapt the following command: 77 | 78 | ```sh 79 | #Kibana >= 5.x 80 | 81 | ./bin/kibana-plugin install https://github.com/bahaaldine/topology/releases/download/major.minor.patch/topology-major.minor.patch.zip 82 | 83 | ``` 84 | 85 | ## Supported Kibana versions 86 | 87 | This plugin is supported by: 88 | 89 | * Kibana 5 90 | 91 | ## Features: 92 | 93 | * Explore indices / shards / segments topology 94 | * Filter indices 95 | -------------------------------------------------------------------------------- /example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahaaldine/topology/298fe764c8abb4a6d657c43b7b334871bcf6b34e/example.gif -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird'; 2 | import topologyRoutes from './server/routes/api'; 3 | import publishElasticsearchClient from './server/lib/publish_elasticsearch_client'; 4 | 5 | export default function (kibana) { 6 | return new kibana.Plugin({ 7 | require: ['elasticsearch'], 8 | 9 | uiExports: { 10 | 11 | app: { 12 | title: 'topology', 13 | description: 'a cluster Topology explorer', 14 | main: 'plugins/topology/app', 15 | icon: 'plugins/topology/topology.png', 16 | }, 17 | 18 | hacks: [ 19 | 'plugins/topology/hack' 20 | ] 21 | 22 | }, 23 | 24 | config(Joi) { 25 | return Joi.object({ 26 | enabled: Joi.boolean().default(true), 27 | elasticsearch: { 28 | username: Joi.string(), 29 | password: Joi.string(), 30 | url: Joi.string(), 31 | ssl: { 32 | cert: Joi.string(), 33 | key: Joi.string(), 34 | ca: Joi.array().items(Joi.string()), 35 | verify: Joi.boolean() 36 | } 37 | } 38 | }).default(); 39 | }, 40 | 41 | init(server, options) { 42 | 43 | var plugin = this; 44 | topologyRoutes(server); 45 | 46 | plugin.status.yellow('Waiting for Topology'); 47 | server.plugins.elasticsearch.status.on('green', function () { 48 | Promise.try(publishElasticsearchClient(server)) 49 | .then(function (arg) { 50 | plugin.status.green('Ready.'); 51 | }) 52 | .catch(console.log.bind(console)); 53 | }); 54 | 55 | } 56 | 57 | }); 58 | }; 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "topology", 3 | "version": "5.1.1", 4 | "description": "a cluster Topology explorer", 5 | "main": "index.js", 6 | "kibana": { 7 | "version": "5.1.1" 8 | }, 9 | "scripts": { 10 | "lint": "eslint", 11 | "start": "plugin-helpers start", 12 | "test:server": "plugin-helpers test:server", 13 | "test:browser": "plugin-helpers test:browser", 14 | "build": "plugin-helpers build", 15 | "postinstall": "plugin-helpers postinstall" 16 | }, 17 | "devDependencies": { 18 | "@alrra/travis-scripts": "^3.0.1", 19 | "@elastic/eslint-config-kibana": "0.0.2", 20 | "@elastic/plugin-helpers": "latest", 21 | "babel-eslint": "4.1.8", 22 | "bluebird": "^3.0.6", 23 | "chai": "^3.5.0", 24 | "code": "^2.1.0", 25 | "eslint": "1.10.3", 26 | "eslint-plugin-mocha": "1.1.0" 27 | }, 28 | "dependencies": { 29 | "angular": "^1.4.7", 30 | "angular-animate": "^1.4.7", 31 | "angular-aria": "^1.4.7", 32 | "angular-material": "^1.1.1", 33 | "boom": "^4.2.0", 34 | "echarts": "^3.3.1", 35 | "elasticsearch": "^12.1.0", 36 | "mdi": "^1.7.22" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /public/app.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import chrome from 'ui/chrome'; 3 | import uiModules from 'ui/modules'; 4 | import uiRoutes from 'ui/routes'; 5 | 6 | import 'ui/autoload/styles'; 7 | import 'plugins/topology/../node_modules/angular-material/angular-material.min.js'; 8 | import 'plugins/topology/../node_modules/angular-material/angular-material.min.css'; 9 | import 'angular-aria'; 10 | import 'angular-animate'; 11 | 12 | 13 | // core dependendies 14 | import './common/Topology.js'; 15 | 16 | // templates & css 17 | import template from './views/index.html'; 18 | import './views/index.less'; 19 | import './views/index.js'; 20 | 21 | // UI Routes 22 | uiRoutes.enable(); 23 | uiRoutes 24 | .when('/', { 25 | template, 26 | controller: 'indexController' 27 | }); 28 | -------------------------------------------------------------------------------- /public/common/Topology.js: -------------------------------------------------------------------------------- 1 | import uiModules from 'ui/modules'; 2 | import { initChart, getDataHeatMap } from './chart_utils'; 3 | 4 | 5 | uiModules 6 | .get('app/topology', []) 7 | .factory('Topology', ['$http', 'chrome', function ($http, chrome) { 8 | class Topology { 9 | 10 | constructor(container, scope) { 11 | $http.get(chrome.addBasePath('/topology/cluster_health')) 12 | .then( (health) => { 13 | this.description = health.data; 14 | }.bind(this)); 15 | this.scope = scope; 16 | this.chart = initChart(container); 17 | } 18 | 19 | getChart() { 20 | return this.chart; 21 | } 22 | } 23 | return Topology; 24 | 25 | }]) 26 | .factory('DataHeatMap', ['Topology', '$http', 'chrome' 27 | ,function (Topology, $http, chrome) { 28 | class DataHeatMap extends Topology { 29 | 30 | constructor(container, scope) { 31 | super(container, scope); 32 | getDataHeatMap(scope, $http, chrome).then( (option) => this.chart.setOption( option ) ); 33 | console.info(this.chart) 34 | } 35 | 36 | setIndexPattern(indexPattern) { 37 | getDataHeatMap(this.scope, $http, chrome, indexPattern).then( (option) => this.chart.setOption( option ) ); 38 | } 39 | } 40 | return DataHeatMap; 41 | 42 | }]); -------------------------------------------------------------------------------- /public/common/chart_utils.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import echarts from 'plugins/topology/../node_modules/echarts/dist/echarts.min.js'; 3 | 4 | function getIndicesColors( indices ) { 5 | return _.map( indices, ( index ) => index.color ); 6 | } 7 | 8 | function getShardsColors( indices ) { 9 | return _.map( indices, ( index ) => _.map( index.children , ( shard, key ) => shard.color ) ); 10 | } 11 | 12 | function initChart(container) { 13 | return echarts.init(container); 14 | } 15 | 16 | function getDataHeatMap($scope, $http, chrome, indexPattern) { 17 | const formatUtil = echarts.format; 18 | let url = chrome.addBasePath('/topology/data_heat_map'); 19 | if ( typeof indexPattern != "undefined" ) { 20 | url += '?indexPattern='+indexPattern; 21 | } 22 | return $http.get(url) 23 | .then( response => { 24 | return { 25 | tooltip: { 26 | formatter: function (info) { 27 | const value = info.value; 28 | const docsCount = info.data['docs.count']; 29 | const docsDeleted = info.data['docs.deleted']; 30 | const treePathInfo = info.treePathInfo; 31 | const treePath = []; 32 | 33 | for (var i = 1; i < treePathInfo.length; i++) { 34 | treePath.push(treePathInfo[i].name); 35 | } 36 | 37 | var scaleValue = function(value) { 38 | if ( value >= 1024 ) { 39 | return formatUtil.addCommas( value / 1000 ) + ' GB' 40 | } else if ( value >= 1024*1024 ) { 41 | return formatUtil.addCommas( value / (1024*1024) ) + ' TB'; 42 | } 43 | 44 | return formatUtil.addCommas(value) + ' MB'; 45 | } 46 | 47 | let docsStats = ['
Docs count: ' + formatUtil.addCommas(docsCount) + '
', 48 | '
Docs deleted: ' + formatUtil.addCommas(docsDeleted) + '
']; 49 | if ( typeof info.data.segments != 'undefined' ) { 50 | docsStats = ['
Docs count: ' + formatUtil.addCommas(info.data['docs']) + '
'] 51 | $scope.$apply(function () { 52 | $scope.path = formatUtil.encodeHTML(treePath.join('/')); 53 | }); 54 | } 55 | 56 | return [ 57 | '
' + formatUtil.encodeHTML(treePath.join('/')) + '
', 58 | '
Disk Usage: ' + scaleValue(value)+ '
' 59 | ].concat(docsStats).join(''); 60 | } 61 | }, 62 | series: [{ 63 | name: 'path:', 64 | type: 'treemap', 65 | data: response.data.treemap, 66 | visibleMin: null, 67 | leafDepth: 1, 68 | nodeClick: 'link', 69 | breadcrumb: { 70 | left: 'center', 71 | top: 'top', 72 | itemStyle: { 73 | normal: { 74 | color: '#607D8B', 75 | borderWidth: 1, 76 | borderColor: '#90A4AE', 77 | textStyle: { 78 | fontSize: 14 79 | } 80 | } 81 | } 82 | }, 83 | levels: [ 84 | { 85 | color: getIndicesColors(response.data.treemap), 86 | itemStyle: { 87 | normal: { 88 | borderColor: '#2f99c1', 89 | borderWidth: 15, 90 | gapWidth: 15 91 | } 92 | } 93 | }, 94 | { 95 | color: getShardsColors(response.data.treemap), 96 | 97 | itemStyle: { 98 | normal: { 99 | borderColor: '#424242', 100 | borderWidth: 10, 101 | gapWidth: 10 102 | } 103 | } 104 | }, 105 | { 106 | 107 | itemStyle: { 108 | normal: { 109 | borderColor: '#212121', 110 | borderWidth: 10, 111 | gapWidth: 10 112 | } 113 | } 114 | } 115 | ] 116 | }] 117 | } 118 | }); 119 | } 120 | 121 | export { 122 | initChart, 123 | getDataHeatMap 124 | }; -------------------------------------------------------------------------------- /public/hack.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | $(document.body).on('keypress', function (event) { 4 | if (event.which === 58) { 5 | } 6 | }); 7 | -------------------------------------------------------------------------------- /public/topology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahaaldine/topology/298fe764c8abb4a6d657c43b7b334871bcf6b34e/public/topology.png -------------------------------------------------------------------------------- /public/views/index-controls-tmpl.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

{{selectedIndex}}

4 |
5 |
6 | 7 | 8 |
9 | 10 | 11 |
{{ item.name }}
12 |
13 |
14 |
15 |
16 |
17 |
-------------------------------------------------------------------------------- /public/views/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{topology.description.cluster}} /

4 | 5 | 6 | 7 | 8 |
9 |
10 |
-------------------------------------------------------------------------------- /public/views/index.js: -------------------------------------------------------------------------------- 1 | import uiModules from 'ui/modules'; 2 | import _ from 'lodash'; 3 | 4 | uiModules 5 | .get('app/topology', ['ngMaterial']) 6 | .controller('indexController', [ '$scope', function ($scope) { 7 | 8 | }]) 9 | .directive('indexTopology', [ 'DataHeatMap', '$window', '$timeout', '$mdBottomSheet' 10 | , function (DataHeatMap, $window, $timeout, $mdBottomSheet) { 11 | return { 12 | link: function($scope, $element, attrs) { 13 | 14 | $scope.topology = new DataHeatMap($element[0], $scope); 15 | $scope.indexPattern = '*'; 16 | 17 | const resizeChart = function() { 18 | if($scope.topology.getChart() != null 19 | && typeof $scope.topology.getChart() != 'undefined') { 20 | $scope.topology.getChart().resize({ 21 | width: angular.element('.topology-container')[0].offsetWidth, 22 | height: angular.element('.topology-container')[0].offsetHeight - 100 23 | }); 24 | } 25 | $scope.$digest(); 26 | } 27 | 28 | angular.element($window).bind('resize', resizeChart); 29 | $timeout( function() { resizeChart(); } ); 30 | 31 | $scope.$watch('indexPattern', (indexPattern) => { 32 | $scope.topology.setIndexPattern(indexPattern) 33 | }); 34 | 35 | /*$scope.$watch('path', (path) => { 36 | if ( path.indexOf('/') > 0 ) { 37 | $scope.selectedIndex = path.split('/')[0]; 38 | } 39 | });*/ 40 | 41 | $scope.$watch('topology.chart._model._componentsMap.series[0]._viewRoot.name', (root) => { 42 | if ( typeof root != "undefined" && root.indexOf(':') < 0 ) { 43 | $scope.selectedIndex = root; 44 | } else { 45 | $scope.selectedIndex = null; 46 | } 47 | }); 48 | 49 | $scope.showIndexControls = function() { 50 | $scope.alert = ''; 51 | $mdBottomSheet.show({ 52 | template: require('plugins/topology/views/index-controls-tmpl.html'), 53 | controller: 'IndexControlsCtrl', 54 | locals : { 55 | index : $scope.selectedIndex 56 | } 57 | }).then(function(clickedItem) { 58 | }); 59 | }; 60 | } 61 | } 62 | }]) 63 | .controller('IndexControlsCtrl', function($scope, $mdBottomSheet, index) { 64 | $scope.selectedIndex = index; 65 | $scope.items = [ 66 | { name: 'Stats', icon: 'table' }, 67 | { name: 'Open', icon: 'window-open' }, 68 | { name: 'Close', icon: 'window-closed' }, 69 | { name: 'Clear cache', icon: 'broom' }, 70 | { name: 'Merge', icon: 'call-merge' }, 71 | { name: 'Refresh', icon: 'refresh' }, 72 | { name: 'Flush', icon: 'blur-radial' }, 73 | { name: 'Delete', icon: 'delete' } 74 | ]; 75 | 76 | $scope.listItemClick = function($index) { 77 | var clickedItem = $scope.items[$index]; 78 | $mdBottomSheet.hide(clickedItem); 79 | }; 80 | }); -------------------------------------------------------------------------------- /public/views/index.less: -------------------------------------------------------------------------------- 1 | @import "../../node_modules/mdi/css/materialdesignicons.min.css"; 2 | 3 | @font-face { 4 | font-family: 'Material Icons'; 5 | font-style: normal; 6 | font-weight: 400; 7 | src: url('../../node_modules/mdi/fonts/materialdesignicons-webfont.eot'), 8 | url('../../node_modules/mdi/fonts/materialdesignicons-webfont.woff2') format('woff2'), 9 | url('../../node_modules/mdi/fonts/materialdesignicons-webfont.woff') format('woff'), 10 | url('../../node_modules/mdi/fonts/materialdesignicons-webfont.ttf') format('truetype'); 11 | } 12 | 13 | i.mdi { 14 | margin: 0px 5px 0px 0px; 15 | } 16 | i.left.mdi { 17 | margin: 0px 0px 0px 5px; 18 | } 19 | i.center.mdi { 20 | margin: 0px 0px 0px 0px; 21 | } 22 | .mdi.md-18 { font-size: 18px; } 23 | .mdi.md-24 { font-size: 24px; } 24 | .mdi.md-25 { font-size: 25px; } 25 | .mdi.md-36 { font-size: 36px; } 26 | .mdi.md-48 { font-size: 48px; } 27 | .mdi.white { color: #fff; } 28 | 29 | .application { 30 | background-color: #2f99c1; 31 | } 32 | 33 | .app-wrapper { 34 | overflow: hidden; 35 | } 36 | 37 | .topology-container { 38 | background-color: #2f99c1; 39 | } 40 | 41 | .topology-container > .title { 42 | color: white; 43 | font-weight: 400; 44 | text-align: center; 45 | } 46 | 47 | .topology-container > .title > md-input-container { 48 | margin-top: 20px; 49 | margin-bottom: 10px; 50 | } 51 | 52 | .topology-container > .title > md-input-container > input { 53 | color: white; 54 | border: none; 55 | font-family: "Open Sans", "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; 56 | font-size: 36px; 57 | line-height: 1.3; 58 | height: 46px; 59 | } 60 | 61 | .topology-container > .title > md-input-container > .md-errors-spacer { 62 | display: none; 63 | } 64 | 65 | .topology-container > .title > button { 66 | background: #3caed2; 67 | color: white; 68 | text-transform: capitalize; 69 | } 70 | 71 | .topology-container > div > div > canvas { 72 | background-color: #2f99c1; 73 | } 74 | 75 | .index-heat-map-controls { 76 | background: #3caed2; 77 | color: white; 78 | border: none; 79 | } 80 | 81 | .index-heat-map-controls > div > md-list > md-list-item > div > button > div { 82 | font-weight: 700 !important; 83 | } -------------------------------------------------------------------------------- /server/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | describe('suite', () => { 4 | it('is a test test', () => { 5 | expect(true).to.equal(false); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /server/lib/init_topology_client_config.js: -------------------------------------------------------------------------------- 1 | import { pick, clone } from 'lodash'; 2 | import url from 'url'; 3 | import { readFileSync } from 'fs'; 4 | 5 | const readFile = file => readFileSync(file, 'utf8'); 6 | const configPrefix = 'topology'; 7 | 8 | function getElasticsearchConfig(config, isRemoteKibana) { 9 | /* if Topology requires a specific user to get cluster level information 10 | * then we grab the username and password from the topology configuration 11 | */ 12 | const authConfig = pick(config.get(`${configPrefix}.elasticsearch`), 'username', 'password'); 13 | 14 | let urlConfig = pick(config.get('elasticsearch'), 'url', 'ssl'); 15 | if ( isRemoteKibana ) { 16 | urlConfig = pick(config.get(`${configPrefix}.elasticsearch`), 'url', 'ssl'); 17 | } 18 | 19 | const esConfig = { ...urlConfig, ...authConfig }; 20 | 21 | return { 22 | ...esConfig, 23 | configSource: isRemoteKibana ? 'remote' : 'local', 24 | keepAlive: true 25 | }; 26 | } 27 | 28 | function getSSLconfiguration(config) { 29 | const sslConfig = config.ssl; 30 | 31 | sslConfig.rejectUnauthorized = sslConfig.verify; 32 | if (sslConfig.cert && sslConfig.key) { 33 | sslConfig.cert = readFile(sslConfig.cert); 34 | sslConfig.key = readFile(sslConfig.key); 35 | } 36 | if (sslConfig.ca) { 37 | sslConfig.ca = sslConfig.ca.map(readFile); 38 | } 39 | 40 | return sslConfig; 41 | } 42 | 43 | function getElasticsearchURL(config, withAuthentification) { 44 | const elasticsearchURL = url.parse(config.url); 45 | 46 | if ( withAuthentification ) { 47 | if (config.username && config.password) { 48 | elasticsearchURL.auth = `${config.username}:${config.password}`; 49 | } 50 | } 51 | return elasticsearchURL; 52 | } 53 | 54 | function getConfigObjects(config, isRemoteKibana) { 55 | const elastisearchConfig = getElasticsearchConfig(config, isRemoteKibana); 56 | 57 | return { 58 | options: elastisearchConfig 59 | , noAuthUri: getElasticsearchURL(elastisearchConfig, false) 60 | , authUri: getElasticsearchURL(elastisearchConfig, true) 61 | , ssl: getSSLconfiguration(elastisearchConfig) 62 | }; 63 | } 64 | 65 | export default function initTopologyClientConfiguration(config) { 66 | const isRemoteKibana = Boolean(config.get(`${configPrefix}.elasticsearch.url`)); 67 | const configObjects = getConfigObjects(config, isRemoteKibana); 68 | 69 | if (!isRemoteKibana) { 70 | config.set(`${configPrefix}.elasticsearch`, pick(configObjects.options, 'url', 'username', 'password')); 71 | config.set(`${configPrefix}.elasticsearch.ssl`, pick(configObjects.ssl, 'verify', 'cert', 'key', 'ca')); 72 | } 73 | 74 | delete configObjects.options.ssl; 75 | delete configObjects.ssl.verify; 76 | 77 | return { ...configObjects }; 78 | } 79 | -------------------------------------------------------------------------------- /server/lib/publish_elasticsearch_client.js: -------------------------------------------------------------------------------- 1 | import { once, bindKey } from 'lodash'; 2 | import url from 'url'; 3 | import Promise from 'bluebird'; 4 | import elasticsearch from 'elasticsearch'; 5 | import initTopologyClientConfig from './init_topology_client_config'; 6 | 7 | function createElasticsearchClient(options, uri, ssl) { 8 | return new elasticsearch.Client({ 9 | host: url.format(uri), 10 | ssl: ssl, 11 | plugins: options.plugins, 12 | keepAlive: options.keepAlive, 13 | defer: function () { 14 | return Promise.defer(); 15 | } 16 | }); 17 | } 18 | 19 | function publishClient(server) { 20 | return function() { 21 | const config = server.config(); 22 | let client = server.plugins.elasticsearch.client; 23 | 24 | /* We only need to publish a dedicated Topology client if 25 | * X-Pack security is installed 26 | */ 27 | if ( Boolean(server.plugins.xpack_main) ) { 28 | const { options, authUri, noAuthUri, ssl } = initTopologyClientConfig(config); 29 | client = createElasticsearchClient(options, authUri, ssl); 30 | } 31 | 32 | server.on('close', bindKey(client, 'close')); 33 | server.expose('client', client); 34 | } 35 | } 36 | 37 | const publishElasticsearchClient = once(publishClient); 38 | 39 | export default publishElasticsearchClient; -------------------------------------------------------------------------------- /server/routes/api.js: -------------------------------------------------------------------------------- 1 | import get_cluster_health from './get_cluster_health' 2 | import get_data_heat_map from './get_data_heat_map' 3 | 4 | export default function (server) { 5 | server = get_cluster_health(server); 6 | server = get_data_heat_map(server); 7 | }; 8 | -------------------------------------------------------------------------------- /server/routes/get_cluster_health.js: -------------------------------------------------------------------------------- 1 | import { getClusterHealth } from './helpers'; 2 | import Promise from 'bluebird'; 3 | import Boom from 'boom'; 4 | 5 | export default function (server) { 6 | 7 | server.route({ 8 | path: '/topology/cluster_health', 9 | method: 'GET', 10 | handler: function (req, reply) { 11 | Promise.try(getClusterHealth(server, req)) 12 | .then(function(health) { 13 | reply( health[0] ); 14 | }) 15 | .catch(function(err){ 16 | reply(Boom.badRequest(err.name + ': ' + err.message)); 17 | }); 18 | } 19 | }); 20 | 21 | return server; 22 | } -------------------------------------------------------------------------------- /server/routes/get_data_heat_map.js: -------------------------------------------------------------------------------- 1 | import { getDataHeatMap } from './helpers'; 2 | import Promise from 'bluebird'; 3 | import Boom from 'boom'; 4 | 5 | export default function (server) { 6 | 7 | server.route({ 8 | path: '/topology/data_heat_map', 9 | method: 'GET', 10 | handler: function (req, reply) { 11 | Promise.try(getDataHeatMap(server, req)) 12 | .then(function(topology) { 13 | reply(topology); 14 | }) 15 | .catch(function( err ) { 16 | reply({}); 17 | }); 18 | } 19 | }); 20 | 21 | return server; 22 | } -------------------------------------------------------------------------------- /server/routes/helpers.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import Promise from 'bluebird'; 3 | 4 | const healthColors = { 5 | index: { 6 | green: '#4CAF50', 7 | yellow: '#F9A825', 8 | red: '#F44336' 9 | }, 10 | shard: { 11 | started: '#03A9F4', 12 | unassigned: '#9E9E9E' 13 | } 14 | } 15 | 16 | function catHealth(client) { 17 | return client.cat.health({format: 'json'}); 18 | } 19 | 20 | function catIndices(client, indexPattern) { 21 | return client.cat.indices({format: 'json', index: indexPattern}); 22 | } 23 | 24 | function catShards(client, index) { 25 | return client.cat.shards({format: 'json', index: index}); 26 | } 27 | 28 | function catSegments(client, index) { 29 | return client.cat.segments({format: 'json', index: index}); 30 | } 31 | 32 | function toMB(value) { 33 | if ( value ) { 34 | const parsedValue = parseInt(value.substring(0, value.length - 2)); 35 | switch ( value.slice(-2) ) { 36 | case 'kb': return (parsedValue / 1024).toFixed(2) ; 37 | case 'gb': return (parsedValue * 1024).toFixed(2) ; 38 | case 'tb': return (parsedValue * 1024 * 1024).toFixed(2) ; 39 | default: return parsedValue 40 | } 41 | } else { 42 | return 0; 43 | } 44 | } 45 | 46 | function getDataHeatMap(server, req) { 47 | return function() { 48 | return getIndicesTopology(server, req).then( (indexTopology) => buildTreeMap(indexTopology) ); 49 | } 50 | } 51 | 52 | function buildTreeMap(topology = {}) { 53 | return { 54 | cluster: topology.health, 55 | treemap: _.map(topology.indices, (indexItem, indexName) => { 56 | return { 57 | ...indexItem, 58 | value: toMB(indexItem['store.size']), 59 | name: indexName, 60 | path: indexName, 61 | color: healthColors.index[indexItem.health], 62 | children: _.map(indexItem.shards, (shardItem, shardName) => { 63 | return { 64 | ...shardItem, 65 | value: shardItem.store == null ? 0 : toMB(shardItem.store), 66 | name: shardName, 67 | path: indexName + '/' + shardName, 68 | color: healthColors.shard[shardItem.state.toLowerCase()], 69 | children: _.map(shardItem.segments, (segmentItem, segmentName) => { 70 | return { 71 | ...segmentItem, 72 | value: toMB(segmentItem['size']), 73 | name: segmentName, 74 | path: indexName + '/' + shardName+ '/' + segmentName 75 | } 76 | }) 77 | } 78 | }) 79 | } 80 | }) 81 | } 82 | } 83 | 84 | function getClusterHealth(server, req) { 85 | const client = server.plugins.topology.client; 86 | return function() { 87 | return catHealth(client); 88 | } 89 | } 90 | 91 | function getIndicesTopology(server, req) { 92 | const client = server.plugins.topology.client; 93 | const indexPattern = req.query.indexPattern; 94 | 95 | let topology = { health: {}, indices : {} }; 96 | 97 | return catHealth(client).then( (catHealthResponse) => { 98 | topology.health = catHealthResponse[0]; 99 | 100 | return catIndices(client, indexPattern).then( (catIndicesResponse) => { 101 | // adding each index to the topology 102 | catIndicesResponse.map(index => { 103 | topology.indices[index.index] = { ...index }; 104 | topology.indices[index.index].shards = { }; 105 | }); 106 | 107 | // preparing a comma separated string of index name for shards and segments apix@ 108 | const shardPromises = _.chain(catIndicesResponse) 109 | .map( (item) => item.index ) 110 | .chunk(20) 111 | .map( (indices) => catShards(client, indices.join(',')) ) 112 | .value() 113 | 114 | const segmentPromises = _.chain(catIndicesResponse) 115 | .map( (item) => item.index ) 116 | .chunk(20) 117 | .map( (indices) => catSegments(client, indices.join(',')) ) 118 | .value() 119 | 120 | 121 | return Promise.all(shardPromises).then( (catShardsResponse) => { 122 | function getShardTypeName(shardType) { 123 | return shardType == 'p' ? 'primary' : 'replica'; 124 | } 125 | 126 | // adding each shard to the relative index topology document 127 | [].concat(...catShardsResponse).map(shard => { 128 | topology.indices[shard.index].shards[getShardTypeName(shard.prirep) + '-' + shard.shard] = { ...shard }; 129 | topology.indices[shard.index].shards[getShardTypeName(shard.prirep) + '-' + shard.shard].segments = {}; 130 | }); 131 | 132 | return Promise.all(segmentPromises).then( (catSegmentsResponse) => { 133 | // adding each segment to the relative shards 134 | [].concat(...catSegmentsResponse).map(segment => { 135 | // segments exist only for assigned shards 136 | if ( typeof topology.indices[segment.index].shards[getShardTypeName(segment.prirep) + '-' + segment.shard] != "undefined" ) { 137 | topology.indices[segment.index].shards[getShardTypeName(segment.prirep) + '-' + segment.shard].segments['segment-' + segment.segment] = { ...segment }; 138 | } 139 | }); 140 | 141 | return topology; 142 | }); 143 | }); 144 | }); 145 | }); 146 | } 147 | 148 | export { 149 | getClusterHealth, 150 | getDataHeatMap 151 | }; --------------------------------------------------------------------------------