├── .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 | [](https://travis-ci.org/bahaaldine/topology)
2 |
3 | # Topology
4 |
5 | 
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 | };
--------------------------------------------------------------------------------