elasticsearch-paramedic-rack 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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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";
         |