elasticsearch-paramedic-rack 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.travis.yml +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +38 -0
- data/Rakefile +24 -0
- data/elasticsearch-paramedic-rack.gemspec +24 -0
- data/lib/elasticsearch-paramedic-rack.rb +10 -0
- data/lib/elasticsearch-paramedic-rack/middelware.rb +23 -0
- data/lib/elasticsearch-paramedic-rack/version.rb +7 -0
- data/public/elasticsearch-paramedic/audio/alert-green.mp3 +0 -0
- data/public/elasticsearch-paramedic/audio/alert-red.mp3 +0 -0
- data/public/elasticsearch-paramedic/audio/alert-yellow.mp3 +0 -0
- data/public/elasticsearch-paramedic/css/app.css +312 -0
- data/public/elasticsearch-paramedic/css/libs/cubism.css +57 -0
- data/public/elasticsearch-paramedic/css/libs/style.css +499 -0
- data/public/elasticsearch-paramedic/img/apple-touch-icon.png +0 -0
- data/public/elasticsearch-paramedic/img/glyphicons-halflings-white.png +0 -0
- data/public/elasticsearch-paramedic/img/glyphicons-halflings.png +0 -0
- data/public/elasticsearch-paramedic/img/spinner.gif +0 -0
- data/public/elasticsearch-paramedic/index.html +196 -0
- data/public/elasticsearch-paramedic/js/app.js +474 -0
- data/public/elasticsearch-paramedic/js/cubism.js +109 -0
- data/public/elasticsearch-paramedic/js/libs/colorbrewer.min.js +1 -0
- data/public/elasticsearch-paramedic/js/libs/cubism.elasticsearch.js +273 -0
- data/public/elasticsearch-paramedic/js/libs/cubism.v1.js +986 -0
- data/public/elasticsearch-paramedic/js/libs/cubism.v1.min.js +1 -0
- data/public/elasticsearch-paramedic/js/libs/d3.v2.min.js +4 -0
- data/public/elasticsearch-paramedic/js/libs/ember-0.9.8.js +20150 -0
- data/public/elasticsearch-paramedic/js/libs/ember-0.9.8.min.js +14 -0
- data/public/elasticsearch-paramedic/js/libs/jquery-1.7.2.min.js +4 -0
- data/test/middelware_test.rb +63 -0
- data/test/test_helper.rb +12 -0
- metadata +134 -0
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,196 @@
|
|
1
|
+
<!doctype html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Paramedic</title>
|
5
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
6
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
7
|
+
<link rel="apple-touch-icon" href="img/apple-touch-icon.png">
|
8
|
+
<link rel="stylesheet" href="css/libs/style.css">
|
9
|
+
<link rel="stylesheet" href="css/libs/cubism.css">
|
10
|
+
<link rel="stylesheet" href="css/app.css">
|
11
|
+
<!--[if lt IE 9]><script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
|
12
|
+
</head>
|
13
|
+
<body>
|
14
|
+
<header class="clearfix">
|
15
|
+
<script type="text/x-handlebars">
|
16
|
+
{{#with App.cluster}}
|
17
|
+
|
18
|
+
<section {{bindAttr class=":cluster_name status"}}>
|
19
|
+
<span class="label">Cluster Name</span>
|
20
|
+
<h1>{{cluster_name}}</h1>
|
21
|
+
|
22
|
+
<p>
|
23
|
+
<span class="label">Status</span>
|
24
|
+
<span {{bindAttr class=":status status"}}>{{status}}</span>
|
25
|
+
<span class="label">Nodes</span>
|
26
|
+
{{number_of_nodes}}
|
27
|
+
<span class="label">Docs</span>
|
28
|
+
{{#bind docs_count}}{{number_with_delimiter docs_count}}{{/bind}}
|
29
|
+
</p>
|
30
|
+
</section>
|
31
|
+
|
32
|
+
<section class="shards">
|
33
|
+
<p><span class="label">Shards</span></p>
|
34
|
+
<p><span class="label darker">Primary</span>
|
35
|
+
{{active_primary_shards}}</p>
|
36
|
+
<p><span class="label darker">Relocating</span>
|
37
|
+
{{relocating_shards}}</p>
|
38
|
+
</section>
|
39
|
+
|
40
|
+
<section class="shards">
|
41
|
+
<p><span class="label"> </span></p>
|
42
|
+
<p><span class="label darker">Initializing</span>
|
43
|
+
{{initializing_shards}}</p>
|
44
|
+
<p><span class="label darker">Unassigned</span>
|
45
|
+
{{unassigned_shards}}</p>
|
46
|
+
</section>
|
47
|
+
|
48
|
+
{{#with App }}
|
49
|
+
<section {{bindAttr class=":endpoint refresh_allowed:polling-active refreshing:polling-in-progress"}}>
|
50
|
+
{{/with}}
|
51
|
+
<p title="Change ElasticSearch URL">
|
52
|
+
<span class="label">URL</span>
|
53
|
+
{{view Ember.TextField valueBinding="App.elasticsearch_url" id="elasticsearch_url"}}
|
54
|
+
</p>
|
55
|
+
|
56
|
+
<p class="refresh">
|
57
|
+
<span class="icon-refresh"></span>
|
58
|
+
<span class="refresh-label">Refresh every</span>
|
59
|
+
<span class="refresh-controls">
|
60
|
+
{{view Ember.Select
|
61
|
+
contentBinding="App.refresh_intervals"
|
62
|
+
selectionBinding="App.refresh_interval"
|
63
|
+
optionLabelPath="content.label"
|
64
|
+
optionValuePath="content.value"}}
|
65
|
+
<button {{action "toggle" target="App.toggleRefreshAllowedButton"}}>{{App.toggleRefreshAllowedButton.text}}</button>
|
66
|
+
</span>
|
67
|
+
</p>
|
68
|
+
<p class="alerts">
|
69
|
+
{{view Ember.Checkbox id="sound-enabled" checkedBinding="App.sounds_enabled"}}
|
70
|
+
<label for="sound-enabled" class="dimmed">Sounds?</label>
|
71
|
+
</p>
|
72
|
+
</section>
|
73
|
+
|
74
|
+
|
75
|
+
{{/with}}
|
76
|
+
</script>
|
77
|
+
</header>
|
78
|
+
|
79
|
+
<section id="cubism">
|
80
|
+
<script type="text/x-handlebars">
|
81
|
+
<h2 class="label clear">
|
82
|
+
Stats
|
83
|
+
<small><a {{action "toggle" target="App.toggleChart"}}>{{App.toggleChart.text}}</a></small>
|
84
|
+
</h2>
|
85
|
+
</script>
|
86
|
+
<div id="chart"></div>
|
87
|
+
</section>
|
88
|
+
|
89
|
+
<h2 class="label">Nodes</h2>
|
90
|
+
<script type="text/x-handlebars">
|
91
|
+
<div id="nodes" class="clearfix">
|
92
|
+
{{#each App.nodes}}
|
93
|
+
<div {{bindAttr class=":node master"}}>
|
94
|
+
<h3><span {{bindAttr class="master:icon-star"}}></span> {{name}}</h3>
|
95
|
+
<div class="meta">
|
96
|
+
<p><span class="label">ID: </span>{{id}}</p>
|
97
|
+
<p><span class="label">IP: </span>{{http_address}}</p>
|
98
|
+
<p><span class="label">Host: </span>{{hostname}}</p>
|
99
|
+
<p><span class="label">Load: </span>{{load}}</p>
|
100
|
+
<p><span class="label">Size: </span>{{disk}}</p>
|
101
|
+
<p><span class="label">Docs: </span>{{#bind docs}}{{number_with_delimiter docs}}{{/bind}}</p>
|
102
|
+
<p><span class="label">Heap: </span>{{jvm_heap_used}}
|
103
|
+
<small class="dimmed" title="Heap max">/{{jvm_heap_max}}</small></p>
|
104
|
+
</div>
|
105
|
+
</div>
|
106
|
+
{{/each}}
|
107
|
+
</div>
|
108
|
+
</script>
|
109
|
+
<br>
|
110
|
+
|
111
|
+
<h2 class="label">Indices</h2>
|
112
|
+
<script type="text/x-handlebars">
|
113
|
+
<div id="indices">
|
114
|
+
{{#each index in App.indices.sorted}}
|
115
|
+
{{#with index}}
|
116
|
+
<div {{bindAttr class=":index :clearfix state show_detail:expanded"}}>
|
117
|
+
<div class="basic-info clearfix">
|
118
|
+
<h3><a {{bindAttr href="url"}} title="Browse Index">{{name}}</a></h3>
|
119
|
+
|
120
|
+
<div class="buttons">
|
121
|
+
{{#unless closed}}
|
122
|
+
<button {{action "showDetail" target="App.indices"}}>
|
123
|
+
{{#if show_detail}}Hide details{{else}}Show details{{/if}}
|
124
|
+
</button>
|
125
|
+
{{/unless}}
|
126
|
+
</div>
|
127
|
+
|
128
|
+
<div class="shards">
|
129
|
+
{{#each shards}}
|
130
|
+
<div {{bindAttr class=":shard primary state recovery.stage" title="state"}}>
|
131
|
+
{{name}}
|
132
|
+
</div>
|
133
|
+
{{/each}}
|
134
|
+
</div>
|
135
|
+
|
136
|
+
<div class="meta">
|
137
|
+
<p>
|
138
|
+
{{settings.number_of_shards}} shards /
|
139
|
+
{{settings.number_of_replicas}} replicas /
|
140
|
+
{{#bind docs}}{{number_with_delimiter docs}}{{/bind}} docs /
|
141
|
+
{{size}} /
|
142
|
+
{{indexing.index_time}} indexing /
|
143
|
+
{{search.query_time}} querying /
|
144
|
+
{{state}}
|
145
|
+
</p>
|
146
|
+
</div>
|
147
|
+
</div>
|
148
|
+
|
149
|
+
<!-- Shard Allocation -->
|
150
|
+
{{#if show_detail}}
|
151
|
+
<div class="extra-info shards clearfix">
|
152
|
+
{{#unless show_detail_loaded}}
|
153
|
+
<div class="loading">Waiting for data...</div>
|
154
|
+
{{/unless}}
|
155
|
+
{{#each nodes}}
|
156
|
+
<div class="node">
|
157
|
+
<h3>{{name}} <small>{{hostname}}</small></h3>
|
158
|
+
<div class="clearfix">
|
159
|
+
{{#each shards}}
|
160
|
+
<div {{bindAttr class=":shard primary state recovery.stage" title="state"}}>
|
161
|
+
<h3>{{name}}</h3>
|
162
|
+
<div class="meta">
|
163
|
+
<p>{{state}}/{{recovery.stage}}</p>
|
164
|
+
<p>
|
165
|
+
{{recovery.time}}
|
166
|
+
{{recovery.size}}
|
167
|
+
</p>
|
168
|
+
</div>
|
169
|
+
</div>
|
170
|
+
{{/each}}
|
171
|
+
</div>
|
172
|
+
</div>
|
173
|
+
{{/each}}
|
174
|
+
</div>
|
175
|
+
{{/if}}
|
176
|
+
</div>
|
177
|
+
{{/with}}
|
178
|
+
{{/each}}
|
179
|
+
</ul>
|
180
|
+
</script>
|
181
|
+
|
182
|
+
<audio id="alert-green" src="audio/alert-green.mp3"></audio>
|
183
|
+
<audio id="alert-yellow" src="audio/alert-yellow.mp3"></audio>
|
184
|
+
<audio id="alert-red" src="audio/alert-red.mp3"></audio>
|
185
|
+
|
186
|
+
<script src="js/libs/jquery-1.7.2.min.js"></script>
|
187
|
+
<!-- <script src="js/libs/ember-0.9.8.min.js"></script> -->
|
188
|
+
<script src="js/libs/ember-0.9.8.js"></script>
|
189
|
+
<script src="js/libs/colorbrewer.min.js"></script>
|
190
|
+
<script src="js/libs/d3.v2.min.js"></script>
|
191
|
+
<script src="js/libs/cubism.v1.js"></script>
|
192
|
+
<script src="js/libs/cubism.elasticsearch.js"></script>
|
193
|
+
<script src="js/app.js"></script>
|
194
|
+
<script src="js/cubism.js"></script>
|
195
|
+
</body>
|
196
|
+
</html>
|
@@ -0,0 +1,474 @@
|
|
1
|
+
function l(m) { Ember.Logger.log(m); }
|
2
|
+
|
3
|
+
var App = Em.Application.create({
|
4
|
+
name: "Paramedic",
|
5
|
+
|
6
|
+
ready: function() {
|
7
|
+
l(App.name + ' (re)loaded.')
|
8
|
+
App.__initialize_page()
|
9
|
+
App.__perform_refresh()
|
10
|
+
App.__initialize_cubism()
|
11
|
+
return this._super()
|
12
|
+
},
|
13
|
+
|
14
|
+
elasticsearch_url: function() {
|
15
|
+
var location = window.location
|
16
|
+
return (/_plugin/.test(location.href.toString())) ? location.protocol + "//" + location.host : "http://localhost:9200"
|
17
|
+
}(),
|
18
|
+
|
19
|
+
refresh_intervals : Ember.ArrayController.create({
|
20
|
+
content: [
|
21
|
+
{label: '1 sec', value: 1000},
|
22
|
+
{label: '5 sec', value: 5000},
|
23
|
+
{label: '15 sec', value: 15*1000},
|
24
|
+
{label: '1 min', value: 60*1000},
|
25
|
+
{label: '5 min', value: 5*60*1000},
|
26
|
+
{label: '15 min', value: 15*60*1000}
|
27
|
+
]
|
28
|
+
}),
|
29
|
+
|
30
|
+
refresh_allowed: true,
|
31
|
+
sounds_enabled: false,
|
32
|
+
|
33
|
+
__perform_refresh: function() {
|
34
|
+
App.cluster.__perform_refresh()
|
35
|
+
App.nodes.__perform_refresh()
|
36
|
+
App.indices.__perform_refresh()
|
37
|
+
},
|
38
|
+
|
39
|
+
__initialize_cubism: function() {
|
40
|
+
App.Cubism.setup()
|
41
|
+
},
|
42
|
+
|
43
|
+
__initialize_page: function() {
|
44
|
+
$("link[rel=apple-touch-icon]").attr("href", App.apple_touch_icon_b64)
|
45
|
+
}
|
46
|
+
});
|
47
|
+
|
48
|
+
App.refresh_interval = App.refresh_intervals.toArray()[1]
|
49
|
+
|
50
|
+
// ===== Models ===================================================================================
|
51
|
+
|
52
|
+
App.Cluster = Ember.Object.extend({
|
53
|
+
});
|
54
|
+
|
55
|
+
App.Node = Ember.Object.extend({
|
56
|
+
});
|
57
|
+
|
58
|
+
App.Index = Ember.Object.extend({
|
59
|
+
url: function() {
|
60
|
+
return App.elasticsearch_url + '/' + this.name + '/_search?pretty'
|
61
|
+
}.property("name").cacheable(),
|
62
|
+
|
63
|
+
closed: function() {
|
64
|
+
return (this.state && this.state == 'close')
|
65
|
+
}.property("state").cacheable()
|
66
|
+
});
|
67
|
+
|
68
|
+
App.Index.Shard = Ember.Object.extend({
|
69
|
+
});
|
70
|
+
|
71
|
+
// ===== Controllers ==============================================================================
|
72
|
+
|
73
|
+
App.cluster = Ember.Object.create({
|
74
|
+
content: App.Cluster.create({}),
|
75
|
+
|
76
|
+
refresh: function() {
|
77
|
+
clearTimeout(App.cluster.poller)
|
78
|
+
setTimeout(function() { App.set("refreshing", false) }, 1000)
|
79
|
+
App.cluster.poller = setTimeout( function() { App.cluster.__perform_refresh() }, App.refresh_interval.value )
|
80
|
+
},
|
81
|
+
|
82
|
+
__perform_refresh: function() {
|
83
|
+
if (!App.refresh_allowed) { return }
|
84
|
+
var self = this;
|
85
|
+
|
86
|
+
var __load_cluster_info = function(data) {
|
87
|
+
App.cluster.setProperties(data)
|
88
|
+
App.cluster.refresh();
|
89
|
+
}
|
90
|
+
|
91
|
+
App.set("refreshing", true)
|
92
|
+
$.getJSON(App.elasticsearch_url+"/_cluster/health", __load_cluster_info);
|
93
|
+
}
|
94
|
+
});
|
95
|
+
|
96
|
+
App.nodes = Ember.ArrayController.create({
|
97
|
+
content: [],
|
98
|
+
|
99
|
+
contains: function(item) {
|
100
|
+
return (Ember.typeOf(item) == 'string') ? this.mapProperty('id').contains(item) : this._super();
|
101
|
+
},
|
102
|
+
|
103
|
+
refresh: function() {
|
104
|
+
clearTimeout(App.nodes.poller)
|
105
|
+
setTimeout(function() { App.set("refreshing", false) }, 1000)
|
106
|
+
App.nodes.poller = setTimeout( function() { App.nodes.__perform_refresh() }, App.refresh_interval.value )
|
107
|
+
},
|
108
|
+
|
109
|
+
__perform_refresh: function() {
|
110
|
+
if (!App.refresh_allowed) { return }
|
111
|
+
var self = this;
|
112
|
+
|
113
|
+
var __load_nodes_info = function(data) {
|
114
|
+
for (var node_id in data.nodes) {
|
115
|
+
if ( !self.contains(node_id) ) self.addObject(App.Node.create({ id: node_id }))
|
116
|
+
var node = self.findProperty("id", node_id)
|
117
|
+
.set("name", data.nodes[node_id]['name'])
|
118
|
+
.set("hostname", data.nodes[node_id]['hostname'])
|
119
|
+
.set("http_address", data.nodes[node_id]['http_address'])
|
120
|
+
.set("jvm_heap_max", data.nodes[node_id]['jvm']['mem']['heap_max'])
|
121
|
+
.set("start_time", data.nodes[node_id]['jvm']['start_time'])
|
122
|
+
}
|
123
|
+
|
124
|
+
// Remove missing nodes from the collection
|
125
|
+
// TODO: Use model instance identity, contains(), etc
|
126
|
+
//
|
127
|
+
self.forEach(function(item) {
|
128
|
+
var loc = self.content.length || 0
|
129
|
+
while(--loc >= 0) {
|
130
|
+
var curObject = self.content.objectAt(loc)
|
131
|
+
if ( item && !Ember.keys(data.nodes).contains(item.id) && curObject.id === item.id) {
|
132
|
+
self.content.removeAt(loc)
|
133
|
+
}
|
134
|
+
}
|
135
|
+
})
|
136
|
+
|
137
|
+
App.nodes.refresh();
|
138
|
+
};
|
139
|
+
|
140
|
+
var __load_nodes_stats = function(data) {
|
141
|
+
for (var node_id in data.nodes) {
|
142
|
+
var node = self.findProperty("id", node_id)
|
143
|
+
if (node) {
|
144
|
+
node
|
145
|
+
.set("disk", data.nodes[node_id]['indices']['store']['size'])
|
146
|
+
.set("docs", data.nodes[node_id]['indices']['docs']['count'])
|
147
|
+
.set("load", data.nodes[node_id]['os']['load_average'][0].toFixed(3))
|
148
|
+
.set("cpu", data.nodes[node_id]['process']['cpu']['percent'])
|
149
|
+
.set("jvm_heap_used", data.nodes[node_id]['jvm']['mem']['heap_used'])
|
150
|
+
}
|
151
|
+
}
|
152
|
+
};
|
153
|
+
|
154
|
+
App.set("refreshing", true)
|
155
|
+
$.getJSON(App.elasticsearch_url+"/_cluster/nodes?jvm", __load_nodes_info);
|
156
|
+
$.getJSON(App.elasticsearch_url+"/_cluster/nodes/stats?indices&os&process&jvm", __load_nodes_stats);
|
157
|
+
}
|
158
|
+
});
|
159
|
+
|
160
|
+
App.indices = Ember.ArrayController.create({
|
161
|
+
content: [],
|
162
|
+
|
163
|
+
contains: function(item) {
|
164
|
+
return (Ember.typeOf(item) == 'string') ? this.mapProperty('name').contains(item) : this._super();
|
165
|
+
},
|
166
|
+
|
167
|
+
refresh: function() {
|
168
|
+
clearTimeout(App.indices.poller)
|
169
|
+
setTimeout(function() { App.set("refreshing", false) }, 1000)
|
170
|
+
App.indices.poller = setTimeout( function() { App.indices.__perform_refresh() }, App.refresh_interval.value )
|
171
|
+
},
|
172
|
+
|
173
|
+
sorted: function() {
|
174
|
+
return this.get("content")
|
175
|
+
.toArray()
|
176
|
+
.sort(function(a,b) { if (a.name < b.name) return -1; if (a.name > b.name) return 1; return 0; })
|
177
|
+
}.property("content.@each").cacheable(),
|
178
|
+
|
179
|
+
showDetail: function(event) {
|
180
|
+
// l(event.context.name)
|
181
|
+
// l(this)
|
182
|
+
event.context.toggleProperty("show_detail")
|
183
|
+
},
|
184
|
+
|
185
|
+
__perform_refresh: function() {
|
186
|
+
if (!App.refresh_allowed) { return }
|
187
|
+
var self = this;
|
188
|
+
|
189
|
+
var __load_cluster_state = function(data) {
|
190
|
+
for (var index_name in data.metadata.indices) {
|
191
|
+
// Mark master node
|
192
|
+
//
|
193
|
+
var master_node = App.nodes.content.findProperty("id", data.master_node)
|
194
|
+
if (master_node) master_node.set("master", true)
|
195
|
+
|
196
|
+
// Create or find an index
|
197
|
+
//
|
198
|
+
if ( !self.contains(index_name) ) self.addObject(App.Index.create({ name: index_name }))
|
199
|
+
var index = self.findProperty("name", index_name)
|
200
|
+
|
201
|
+
// Update index properties
|
202
|
+
//
|
203
|
+
index
|
204
|
+
.set("state", data.metadata.indices[index_name]['state'])
|
205
|
+
|
206
|
+
.set("settings", Ember.Object.create({
|
207
|
+
number_of_replicas: data.metadata.indices[index_name]['settings']['index.number_of_replicas'],
|
208
|
+
number_of_shards: data.metadata.indices[index_name]['settings']['index.number_of_shards']
|
209
|
+
}))
|
210
|
+
|
211
|
+
.set("aliases", data.metadata.indices[index_name]['aliases'])
|
212
|
+
|
213
|
+
// Shards
|
214
|
+
//
|
215
|
+
var shards = [],
|
216
|
+
primaries = [],
|
217
|
+
replicas = [],
|
218
|
+
unassigned = []
|
219
|
+
|
220
|
+
index
|
221
|
+
.set("shards", function() {
|
222
|
+
if (data.routing_table.indices[index_name]) {
|
223
|
+
|
224
|
+
for (var shard_name in data.routing_table.indices[index_name]['shards']) {
|
225
|
+
|
226
|
+
data.routing_table.indices[index_name]['shards'][shard_name].forEach(function(s) {
|
227
|
+
|
228
|
+
var shard = App.Index.Shard.create({name: shard_name})
|
229
|
+
shard.set("state", s.state)
|
230
|
+
.set("primary", s.primary)
|
231
|
+
.set("index", s.index)
|
232
|
+
.set("node_id", s.node)
|
233
|
+
.set("relocating_node_id", s.relocating_node)
|
234
|
+
|
235
|
+
if (s.primary) primaries .addObject(shard)
|
236
|
+
if (!s.primary && s.node) replicas .addObject(shard)
|
237
|
+
if (!s.primary && !s.node) unassigned.addObject(shard)
|
238
|
+
});
|
239
|
+
|
240
|
+
}
|
241
|
+
}
|
242
|
+
|
243
|
+
// Sort unassingned shards to series [0 .. n, 0 .. n]
|
244
|
+
// [0, 0, 1, 1, 2, 2] becomes: [0, 1, 2, 0, 1, 2]
|
245
|
+
//
|
246
|
+
var unassigned_sorted = []
|
247
|
+
unassigned_sorted.length = unassigned.length
|
248
|
+
|
249
|
+
var num_shards = primaries.length,
|
250
|
+
num_replicas = unassigned.length/num_shards;
|
251
|
+
|
252
|
+
for (var i = 0; i < num_shards; i++) {
|
253
|
+
// Create slices: [0, 0]; [1, 1]; [2, 2]
|
254
|
+
unassigned.slice(i*num_replicas, i*num_replicas+num_replicas).forEach(function(item,index) {
|
255
|
+
// Position for first slices: 0, 3
|
256
|
+
// Position for second slices: 1, 4
|
257
|
+
// Position for third slices: 2, 5
|
258
|
+
var position = i + num_shards * index
|
259
|
+
unassigned_sorted[position] = item
|
260
|
+
})
|
261
|
+
};
|
262
|
+
|
263
|
+
unassigned_sorted = unassigned_sorted.filter(function(i){return i != null})
|
264
|
+
return shards.concat(primaries, replicas, unassigned_sorted)
|
265
|
+
}())
|
266
|
+
|
267
|
+
if (index.show_detail) {
|
268
|
+
index.set("nodes", function() {
|
269
|
+
var nodes = []
|
270
|
+
if (data.routing_table.indices[index_name]) {
|
271
|
+
for (var shard_name in data.routing_table.indices[index_name]['shards']) {
|
272
|
+
|
273
|
+
data.routing_table.indices[index_name]['shards'][shard_name].forEach(function(shard_data) {
|
274
|
+
if (shard_data.node) {
|
275
|
+
|
276
|
+
// Find the node
|
277
|
+
// var node = App.nodes.content.findProperty("id", shard_data.node)
|
278
|
+
var node = nodes.findProperty("id", shard_data.node)
|
279
|
+
if (!node) {
|
280
|
+
var node = App.Node.create( App.nodes.content.findProperty("id", shard_data.node) )
|
281
|
+
nodes.addObject(node)
|
282
|
+
}
|
283
|
+
|
284
|
+
// Initialize node.shards
|
285
|
+
if (node && !node.shards) node.set("shards", [])
|
286
|
+
|
287
|
+
// Find shard in index.shards
|
288
|
+
var shard = index.shards.find(function(item) {
|
289
|
+
return item.name == shard_data.shard && item.node_id == shard_data.node && item.index == shard_data.index
|
290
|
+
})
|
291
|
+
|
292
|
+
// Remove shard from node.shards
|
293
|
+
node.shards.forEach(function(item, index) {
|
294
|
+
if (item.name == shard_data.shard && item.node_id == shard_data.node && item.index == shard_data.index) {
|
295
|
+
node.shards.removeAt(index)
|
296
|
+
}
|
297
|
+
})
|
298
|
+
|
299
|
+
// Add (possibly updated) shard back into collection
|
300
|
+
if (shard) { node.shards.addObject(shard) }
|
301
|
+
|
302
|
+
node.set("shards", node.shards.sort(function(a,b) { return a.name > b.name; }))
|
303
|
+
}
|
304
|
+
});
|
305
|
+
};
|
306
|
+
}
|
307
|
+
index.set("show_detail_loaded", true)
|
308
|
+
return nodes
|
309
|
+
}())
|
310
|
+
}
|
311
|
+
|
312
|
+
// Remove deleted indices from the collection
|
313
|
+
// TODO: Use model instance identity for this
|
314
|
+
//
|
315
|
+
self.forEach(function(item) {
|
316
|
+
// console.log(item.name)
|
317
|
+
var loc = self.content.length || 0
|
318
|
+
while(--loc >= 0) {
|
319
|
+
var curObject = self.content.objectAt(loc)
|
320
|
+
if ( item && !Ember.keys(data.metadata.indices).contains(item.name) && curObject.name === item.name) {
|
321
|
+
self.content.removeAt(loc)
|
322
|
+
}
|
323
|
+
}
|
324
|
+
})
|
325
|
+
|
326
|
+
}
|
327
|
+
};
|
328
|
+
|
329
|
+
var __load_indices_stats = function(data) {
|
330
|
+
App.cluster.set("docs_count",
|
331
|
+
data._all.primaries.docs ? data._all.primaries.docs.count : 0)
|
332
|
+
|
333
|
+
for (var index_name in data._all.indices) {
|
334
|
+
var index = self.findProperty("name", index_name)
|
335
|
+
if (!index) continue
|
336
|
+
|
337
|
+
index
|
338
|
+
.set("size", data._all.indices[index_name]['primaries']['store']['size'])
|
339
|
+
.set("size_in_bytes", data._all.indices[index_name]['primaries']['store']['size_in_bytes'])
|
340
|
+
.set("docs", data._all.indices[index_name]['primaries']['docs']['count'])
|
341
|
+
.set("indexing", data._all.indices[index_name]['primaries']['indexing'])
|
342
|
+
.set("search", data._all.indices[index_name]['primaries']['search'])
|
343
|
+
.set("get", data._all.indices[index_name]['primaries']['get'])
|
344
|
+
}
|
345
|
+
};
|
346
|
+
|
347
|
+
var __load_indices_status = function(data) {
|
348
|
+
for (var index_name in data.indices) {
|
349
|
+
var index = self.findProperty("name", index_name)
|
350
|
+
if (!index) continue
|
351
|
+
if (!index.show_detail) continue
|
352
|
+
|
353
|
+
for (var shard_name in data.indices[index_name]['shards']) {
|
354
|
+
// var shard = index.shards.findProperty("name", shard_name)
|
355
|
+
|
356
|
+
data.indices[index_name]['shards'][shard_name].forEach(function(shard_data) {
|
357
|
+
var shard = index.shards.find(function(item) {
|
358
|
+
return item.name == shard_name && item.node_id == shard_data['routing']['node']
|
359
|
+
})
|
360
|
+
// if (!shard) continue
|
361
|
+
if (shard) {
|
362
|
+
|
363
|
+
// l(shard_data)
|
364
|
+
shard
|
365
|
+
.set("size", shard_data.index.size)
|
366
|
+
// .set("docs", shard_data.docs.num_docs)
|
367
|
+
shard
|
368
|
+
.set("recovery", function() {
|
369
|
+
var recovery_type = shard_data['peer_recovery'] ? 'peer_recovery' : 'gateway_recovery'
|
370
|
+
|
371
|
+
return {
|
372
|
+
stage: shard_data[recovery_type].stage,
|
373
|
+
time: shard_data[recovery_type].time,
|
374
|
+
progress: shard_data[recovery_type].index.progress,
|
375
|
+
size: shard_data[recovery_type].index.size,
|
376
|
+
reused_size: shard_data[recovery_type].index.reused_size
|
377
|
+
}
|
378
|
+
}())
|
379
|
+
}
|
380
|
+
});
|
381
|
+
}
|
382
|
+
}
|
383
|
+
};
|
384
|
+
|
385
|
+
App.set("refreshing", true)
|
386
|
+
$.getJSON(App.elasticsearch_url+"/_cluster/state", __load_cluster_state);
|
387
|
+
$.getJSON(App.elasticsearch_url+"/_stats", __load_indices_stats);
|
388
|
+
$.getJSON(App.elasticsearch_url+"/_status?recovery=true", __load_indices_status);
|
389
|
+
|
390
|
+
// Schedule next run
|
391
|
+
//
|
392
|
+
App.indices.refresh();
|
393
|
+
}
|
394
|
+
});
|
395
|
+
|
396
|
+
// ===== Views ==================================================================================
|
397
|
+
|
398
|
+
App.toggleRefreshAllowedButton = Ember.View.create({
|
399
|
+
text: 'Stop',
|
400
|
+
|
401
|
+
toggle: function(event) {
|
402
|
+
this.set("text", ( App.refresh_allowed == true ) ? 'Start' : 'Stop')
|
403
|
+
App.toggleProperty("refresh_allowed")
|
404
|
+
}
|
405
|
+
});
|
406
|
+
|
407
|
+
App.toggleChart = Ember.View.create({
|
408
|
+
text: 'Hide',
|
409
|
+
|
410
|
+
toggle: function(event) {
|
411
|
+
var chart = $("#chart"),
|
412
|
+
visible = chart.is(":visible")
|
413
|
+
|
414
|
+
this.set("text", visible ? 'Show' : 'Hide')
|
415
|
+
visible ? chart.hide('fast') : chart.show('fast')
|
416
|
+
}
|
417
|
+
});
|
418
|
+
|
419
|
+
// ===== Observers ==============================================================================
|
420
|
+
|
421
|
+
App.addObserver('elasticsearch_url', function(event) {
|
422
|
+
// TODO: Use the `blur` event, so we're not trying to load partial URLs
|
423
|
+
Ember.Logger.log("ElasticSearch URL changed to " + this.get("elasticsearch_url"))
|
424
|
+
App.cluster.set("content", App.Cluster.create({}))
|
425
|
+
App.nodes.set("content", [])
|
426
|
+
App.indices.set("content", [])
|
427
|
+
App.ready()
|
428
|
+
App.Cubism.reset()
|
429
|
+
});
|
430
|
+
|
431
|
+
App.addObserver('refresh_interval', function() {
|
432
|
+
Ember.Logger.log("Refresh interval changed to " + App.refresh_interval.label)
|
433
|
+
App.ready()
|
434
|
+
});
|
435
|
+
|
436
|
+
App.addObserver('refresh_allowed', function() {
|
437
|
+
App.refresh_allowed ? App.Cubism.start() : App.Cubism.stop()
|
438
|
+
App.__perform_refresh()
|
439
|
+
});
|
440
|
+
|
441
|
+
App.nodes.addObserver('@each.name', function() {
|
442
|
+
// Wait until we have node names...
|
443
|
+
if ( !App.nodes.everyProperty("name") ) return;
|
444
|
+
|
445
|
+
Ember.Logger.log("Nodes changed to: " + App.nodes.mapProperty("name").join("; "))
|
446
|
+
App.Cubism.reset()
|
447
|
+
});
|
448
|
+
|
449
|
+
App.cluster.addObserver('cluster_name', function() {
|
450
|
+
$('title').text('Paramedic | ' + this.get('cluster_name'))
|
451
|
+
});
|
452
|
+
|
453
|
+
App.cluster.addObserver('status', function() {
|
454
|
+
if (App.get("sounds_enabled")) {
|
455
|
+
// FIXME: When running as a plugin, audio won't play again when `var a = $('#alert-'+this.get("status"))[0]`
|
456
|
+
var a = new Audio('audio/alert-'+this.get("status")+'.mp3')
|
457
|
+
a.volume=0.7
|
458
|
+
a.play()
|
459
|
+
}
|
460
|
+
});
|
461
|
+
|
462
|
+
// ===== Helpers ================================================================================
|
463
|
+
|
464
|
+
Handlebars.registerHelper('number_with_delimiter', function(property) {
|
465
|
+
var delimiter = ' '
|
466
|
+
, value = (isNaN(this)) ? Ember.getPath(this, property) : this.toString();
|
467
|
+
// console.log(this, property, value)
|
468
|
+
// Credit: http://stackoverflow.com/a/2254896/95696
|
469
|
+
return value ? value.toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1"+delimiter) : value
|
470
|
+
});
|
471
|
+
|
472
|
+
// ===== Varia ==================================================================================
|
473
|
+
|
474
|
+
App.apple_touch_icon_b64 = "data:image/png;base64,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";
|