consul-templaterb 1.8.1 → 1.8.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4575bd9573abe22918149095549c1942aba92f447bc57c9bf466e3f4e692955f
4
- data.tar.gz: e0f1daefa7fd071b16becceeb955acbf465a8c63f09a787161ea26786342be5e
3
+ metadata.gz: 6d652b0259f34bd70784ebdfbfe18e042bb48bdd03af5f5d920a71b94bb1281d
4
+ data.tar.gz: 00116db5cf7b2f11516abb912aab335c2608ec01fdf9ba527f11deb9bc861755
5
5
  SHA512:
6
- metadata.gz: d06c13276838fc70fd31beeac8875b89f9b80b9efde59614be9b6b0f272e8fba819ef40424ff3b6f0a300f1f17eb719f16b66dd697b86193e3a3c7b92ea79a11
7
- data.tar.gz: 4664f4a357a97d5f3047a328aa4907b08d4dab11d570a961cde0e51d0ac4453649ddb97d76cda113509e93ed43f99c96f69b86d50872d1ab38e2285a811e0f60
6
+ metadata.gz: c53f3a2704b5b52b3febb4c4844c36dae660b163d1de8298acde2f88c0cb9b9e65bc202d35c160d0747b3624bfa1355c0e014ef58e2d31673a5b40d17b0d8847
7
+ data.tar.gz: bc16ad7b30485b403c19d01251c87ffa66d17b937b09affdf2c74470455283d5121b95909524ec703d29239efe97b626f7e0c70bb34090a04bab69ad224a176c
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## (UNRELEASED)
4
4
 
5
+ ## 1.8.2 (December 18, 2018)
6
+
7
+ NEW FEATURES:
8
+
9
+ * Added Consul timeline that displays all the changes on services in Consul UI.
10
+
5
11
  ## 1.8.1 (December 12, 2018)
6
12
 
7
13
  BUGFIX:
@@ -1,5 +1,5 @@
1
1
  module Consul
2
2
  module Async
3
- VERSION = '1.8.1'.freeze
3
+ VERSION = '1.8.2'.freeze
4
4
  end
5
5
  end
@@ -4,6 +4,14 @@ A simple HTML5 app that displays all services within Consul with AJAX requests.
4
4
  It supports fitering based on tags and does not rely on Consul to display pages,
5
5
  meaning that it can be scaled horizontally without any troubles with Consul.
6
6
 
7
+ ## Features
8
+
9
+ * List all services details, with very fast lookups.
10
+ * `consul-timeline-ui.html` Let you see all changes applied to your services with history
11
+ * List all keys
12
+ * List all nodes
13
+ * List datacenters
14
+
7
15
  ## Is it prod ready?
8
16
 
9
17
  This application is used for several months within Criteo to replace Consul native interface and
@@ -44,8 +52,8 @@ instead. Example:
44
52
 
45
53
  ```shell
46
54
  consul-templaterb -c http://localhost:8500 \
47
- --template samples/consul-ui/consul-services-ui.html:samples/consul-ui/index.html \
48
- samples/consul-ui/consul_template.json.erb
55
+ --template samples/consul-ui/consul-services-ui.html.erb:samples/consul-ui/index.html \
56
+ samples/consul-ui/*.erb
49
57
  ```
50
58
 
51
59
  Will generate index.html and consul_template.json in your directory, so you might serve it directly.
@@ -58,3 +66,4 @@ This app supports the following environment variables:
58
66
  * INSTANCE_MUST_TAG: Second level of filtering (optional, default to SERVICES_TAG_FILTER)
59
67
  * INSTANCE_EXCLUDE_TAG: Exclude instances having the given tag (default: canary)
60
68
  * EXCLUDE_SERVICES: comma-separated services to exclude (default: consul-agent-http,mesos-slave,mesos-agent-watcher)
69
+ * CONSUL_TIMELINE_BUFFER: number of entries to keep in the timeline. 1000 by default.
@@ -3,7 +3,7 @@
3
3
  # CONSUL_TOOLS_SUFFIX: suffix for the address of consul tools
4
4
  # CONSUL_TOOLS_PREFIX: prefix for the address of consul tools
5
5
  # CONSUL_TOOLS: comma sperated list of consul tools
6
- tools = (ENV['CONSUL_TOOLS'] || 'services,nodes,keys').split(",")
6
+ tools = (ENV['CONSUL_TOOLS'] || 'services,nodes,keys,timeline').split(",")
7
7
  tools_suffix = ENV['CONSUL_TOOLS_PREFIX'] || '-ui.html'
8
8
  tools_prefix = ENV['CONSUL_TOOLS_SUFFIX'] || 'consul-'
9
9
 
@@ -38,6 +38,9 @@
38
38
  <style id="css-states">
39
39
  .service-tags { display: none; }
40
40
  </style>
41
+ <style id="serviceCol">
42
+ .service-tags { display: none; }
43
+ </style>
41
44
  </head>
42
45
  <body>
43
46
  <nav class="navbar navbar-expand-md navbar-dark bg-secondary">
@@ -0,0 +1,60 @@
1
+ <% datasource = ENV['TIMELINE_DATASOURCE'] || 'timeline.json'
2
+ # Time to wait before reloading configuration again in seconds (0 = never)
3
+ refresh = ENV['REFRESH'] || '3600' %><%= render_file('common/header.html.erb', title: 'Services Timeline') %>
4
+ <div class="main">
5
+ <div class="row mx-0">
6
+ <div id="filter-menu" class="col-2 col-m-3 px-4 pt-4">
7
+ <div class="form-group">
8
+ <div class="input-group">
9
+ <input id="service-filter" type="text" placeholder="filter instances" class="form-control" />
10
+ <div class="input-group-append">
11
+ <span class="input-group-text" id="service-counter"></span>
12
+ </div>
13
+ </div>
14
+ </div>
15
+ <div id="service-wrapper" >
16
+ <ul id="service-list" class="list-group">
17
+ <li onfocus="serviceTimeline.selectService(this)" onclick="serviceTimeline.selectService(this)" value="" class="serviceListItem list-group-item list-group-item-actionn active" id="anyService"><div class="statuses float-right"><span class="lookup badge badge-pill badge-dark">1000</span></div><div class="service-name">All</div></li>
18
+ </ul>
19
+ </div>
20
+ </div>
21
+ <div class="col-10 col-m-9">
22
+ <h2 class="text-center" id="service-title"></h2>
23
+ <div class="row mb-2">
24
+ <div class="input-group float-left col-12">
25
+ <input id="instance-filter" type="text" placeholder="filter nodes by name or tags" class="form-control" />
26
+ </div>
27
+ </div>
28
+ <div id="instances-wrapper">
29
+ <div id="events-list" class="list-group">
30
+ <table id="all-events" class="table table-striped table-hover table-sm">
31
+ <thead class="thead-dark">
32
+ <tr>
33
+ <th scope="col">Time</th>
34
+ <th scope="col" class="serviceCol">Service</th>
35
+ <th scope="col">Instance</th>
36
+ <th scope="col">Check State</th>
37
+ <th scope="col">Service&nbsp;Status&nbsp;Change</th>
38
+ <th colspan="2" scope="col">Service&nbsp;Instances</th>
39
+ </tr>
40
+ </thead>
41
+ <tbody>
42
+ </tbody>
43
+ </table>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ <!-- Optional JavaScript -->
50
+ <!-- JavaScript Dependencies: jQuery, Popper.js, Bootstrap JS, Shards JS -->
51
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
52
+ <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
53
+ <script src="js/utils.js"></script>
54
+ <script src="js/timeline.js"></script>
55
+ <script type="text/javascript">
56
+ serviceTimeline = new ServiceTimeline('<%= datasource %>','<%= refresh %>');
57
+ </script>
58
+ <script src="vendors/highlight/highlight.pack.js"></script>
59
+ </body>
60
+ </html>
@@ -235,7 +235,8 @@ class ConsulService {
235
235
  }
236
236
 
237
237
  displayService(service) {
238
- $("#service-title").html(service['name']);
238
+ var titleText = service['name'] + ' <a href="consul-timeline-ui.html?service=' + service['name'] + '">timeline</a>';
239
+ $("#service-title").html(titleText);
239
240
  $("#instances-list").html("");
240
241
 
241
242
  var serviceStatus = buildServiceStatus(service);
@@ -0,0 +1,334 @@
1
+ // strict
2
+ class ServiceTimeline {
3
+ constructor(ressourceURL, refresh) {
4
+ this.ressourceURL = ressourceURL;
5
+ this.fetchRessource();
6
+ this.serviceList = $("#service-list");
7
+ this.serviceFilter = $("#service-filter");
8
+ this.serviceFilter.keyup(this.filterService);
9
+ this.serviceInstanceFilter = '';
10
+ this.instanceFilter = $("#instance-filter");
11
+ this.instanceFilter.keyup(this.doFilter);
12
+ this.refresh = parseInt(refresh);
13
+ this.filterStatus = null;
14
+ this.refreshTimeout = null;
15
+ this.serviceFilterCounter = $("#service-counter");
16
+ this.serviceFilterCount = 0;
17
+ this.services = {}
18
+ this.presentServices = {}
19
+ var sT = this;
20
+ }
21
+
22
+ fetchRessource() {
23
+ $.ajax({url: this.ressourceURL, cache: false, dataType: "json", sourceObject: this, success: function(result){
24
+ serviceTimeline.initRessource(result);
25
+ }});
26
+ }
27
+
28
+ initRessource(data) {
29
+ this.data = data;
30
+ this.reloadTimeline(true);
31
+ }
32
+
33
+ createServiceDefItem(label, serviceName, counter) {
34
+ var listItem = document.createElement('li');
35
+ listItem.setAttribute('onfocus','serviceTimeline.selectService(this, true)');
36
+ listItem.setAttribute('onclick','serviceTimeline.selectService(this, true)');
37
+ listItem.setAttribute('value', serviceName);
38
+ var serviceNameItem = document.createElement('div');
39
+ serviceNameItem.setAttribute('class', 'service-name');
40
+ serviceNameItem.appendChild(document.createTextNode(label));
41
+ listItem.appendChild(serviceNameItem);
42
+ var listItemClass = 'serviceListItem list-group-item list-group-item-action';
43
+ listItem.setAttribute('class', listItemClass);
44
+
45
+ var statuses = document.createElement('div');
46
+ statuses.setAttribute('class','statuses float-right');
47
+ statuses.appendChild(this.createBadge(counter, 'dark'));
48
+ listItem.prepend(statuses);
49
+ return listItem;
50
+ }
51
+
52
+ reloadTimeline(firstReload) {
53
+ if (!this.data) {
54
+ console.log("No data to display");
55
+ }
56
+ this.serviceList.html('');
57
+ this.serviceFilterCount = 0;
58
+ var servicesPerName = {};
59
+ var numberOfEvents = this.data.length;
60
+ for (var i = 0 ; i < numberOfEvents; i++) {
61
+ var e = this.data[i];
62
+ var srvName = e.service;
63
+ var arr = servicesPerName[srvName];
64
+ if (arr == null) {
65
+ arr = 0;
66
+ }
67
+ servicesPerName[srvName] = (arr + 1);
68
+ }
69
+ var sorted = Object.keys(servicesPerName).sort();
70
+ var serviceListItems = this.serviceList[0];
71
+ var allServices = this.createServiceDefItem('All', '', numberOfEvents);
72
+ allServices.setAttribute('id', 'anyService');
73
+ serviceListItems.appendChild(allServices);
74
+ this.presentServices = servicesPerName;
75
+ for (var i = 0 ; i < sorted.length; i++) {
76
+ var serviceName = sorted[i];
77
+ var counter = servicesPerName[serviceName];
78
+ var listItem = this.createServiceDefItem(serviceName, serviceName, counter);
79
+ serviceListItems.appendChild(listItem);
80
+ }
81
+ this.displayEvents();
82
+ if (firstReload) {
83
+ var sT = this;
84
+ setTimeout(function(){
85
+ var filterParam = new URL(location.href).searchParams.get('filter');
86
+ if (filterParam) {
87
+ $('#instance-filter')[0].value = filterParam;
88
+ }
89
+ var urlParam = new URL(location.href).searchParams.get('service');
90
+ if (urlParam === null) {
91
+ var servicePrefix = '#service_'
92
+ if (location.hash.startsWith(servicePrefix)) {
93
+ urlParam = location.hash.substr(servicePrefix.length)
94
+ }
95
+ }
96
+ var found = false;
97
+ if (urlParam && urlParam != '.*') {
98
+ var nodes = document.getElementById('service-list').childNodes;
99
+ for (var i = 0; i < nodes.length; i++) {
100
+ if($(nodes[i]).find(".service-name").html() == urlParam) {
101
+ var selectedElement = $(nodes[i])
102
+ found = true;
103
+ sT.selectService(selectedElement, false);
104
+ setTimeout(function(){
105
+ selectedElement.focus();
106
+ }, 150);
107
+ break;
108
+ }
109
+ }
110
+ }
111
+ if (!found) {
112
+ sT.selectService($('#anyService', false));
113
+ }
114
+ }, 150);
115
+ }
116
+ }
117
+
118
+ appChild(rootKind, elem) {
119
+ var root = document.createElement(rootKind)
120
+ root.appendChild(elem);
121
+ return root
122
+ }
123
+
124
+ buildCell(row, elem, clazz, value) {
125
+ var td = document.createElement(elem);
126
+ td.setAttribute('class', clazz);
127
+ td.appendChild(value);
128
+ row.appendChild(td);
129
+ return td;
130
+ }
131
+
132
+ createBadge(status, clazz) {
133
+ if (status == null) {
134
+ status = 'missing';
135
+ clazz = 'dark';
136
+ } else if (status != null && clazz == null) {
137
+ if (status == 'passing') {
138
+ clazz='success';
139
+ } else if (clazz != 'warning') {
140
+ clazz='danger';
141
+ }
142
+ }
143
+ var span = document.createElement('span');
144
+ span.setAttribute('class', 'lookup badge badge-pill badge-' + clazz);
145
+ span.appendChild(document.createTextNode(status));
146
+ return span;
147
+ }
148
+
149
+ doFilter() {
150
+ var filterValue = $('#instance-filter')[0].value;
151
+ this.refreshTimeout = null;
152
+ var matcher;
153
+ try {
154
+ matcher = new RegExp(filterValue);
155
+ } catch (e) {
156
+ var safeReg = filterValue.replace(/[-[\]{}()*+?.,\\^$|]/g, "\\$&")
157
+ console.log("Failed to compile regexp for '" + filterValue + "', using strict lookup due to: " + e);
158
+ matcher = new RegExp(safeReg);
159
+ }
160
+ console.log("Filtering on service", serviceTimeline.serviceInstanceFilter, " with ", matcher, "filterValue:=", filterValue);
161
+ var isCorrectService = function(){ return true; };
162
+ if (serviceTimeline.serviceInstanceFilter == ''){
163
+ var stylesheet = document.getElementById('serviceCol');
164
+ var txt = '';
165
+ if (filterValue) {
166
+ txt+='tr.filtered { display: none; }';
167
+ }
168
+ stylesheet.textContent = txt;
169
+ } else {
170
+ var stylesheet = document.getElementById('serviceCol');
171
+ var txt = '.serviceCol';
172
+ if (filterValue) {
173
+ txt+=',tr.filtered'
174
+ }
175
+ for (var i in this.presentServices) {
176
+ if (i != serviceTimeline.serviceInstanceFilter) {
177
+ txt+=',tr.srv-'+i;
178
+ }
179
+ }
180
+ stylesheet.textContent = txt + ' { display: none; }';
181
+ isCorrectService = function(ui) { return ui.hasClass('srv-' + serviceTimeline.serviceInstanceFilter) };
182
+ }
183
+ if (filterValue) {
184
+ $("#all-events > tbody").children('tr').each(function (){
185
+ var ui = $(this);
186
+ var shouldShow = isCorrectService(ui) && ui.children('.lookup').is(function (){
187
+ var elem = $(this);
188
+ if (elem[0].innerHTML.match(matcher)) {
189
+ return true;
190
+ }
191
+ return false;
192
+ });
193
+ if (shouldShow) {
194
+ ui.removeClass('filtered');
195
+ } else {
196
+ ui.addClass('filtered');
197
+ }
198
+ });
199
+ }
200
+ }
201
+
202
+ selectService(source, updateUrl) {
203
+ $(this.selectedService).removeClass('active');
204
+ var serviceName = $(source).find(".service-name").html()
205
+ this.selectedService = source.closest('li');
206
+ $(this.selectedService).addClass('active');
207
+ if (serviceName == 'All') {
208
+ serviceName = '';
209
+ $("#service-title").html('');
210
+ } else {
211
+ var titleText = '<a href="consul-services-ui.html?service=' + serviceName + '">'+serviceName+'</a>';
212
+ $("#service-title").html(titleText);
213
+ }
214
+ serviceTimeline.serviceInstanceFilter = serviceName;
215
+ if (updateUrl) {
216
+ serviceTimeline.updateURL(serviceName == 'All' ? '' : serviceName);
217
+ }
218
+ this.doFilter();
219
+ }
220
+
221
+ updateURL(link) {
222
+ var newUrl = window.location.protocol + "//" + window.location.host + window.location.pathname;
223
+ if (link) {
224
+ newUrl += '?service=' + link
225
+ }
226
+ window.history.pushState({},"",newUrl);
227
+ }
228
+
229
+ filterService() {
230
+ var filter;
231
+ var serviceVal = serviceTimeline.serviceFilter.val();
232
+ try {
233
+ filter = new RegExp(serviceVal);
234
+ } catch (e) {
235
+ var safeReg = serviceVal.replace(/[-[\]{}()*+?.,\\^$|]/g, "\\$&")
236
+ console.log("Failed to compile regexp for '" + serviceVal + "', using strict lookup due to: " + e);
237
+ filter = new RegExp(safeReg);
238
+ }
239
+ serviceTimeline.serviceFilterCount = 0;
240
+ var showProxiesInList = this.showProxiesInList;
241
+ serviceTimeline.serviceList.children('.serviceListItem').each(function (){
242
+ var ui = $(this);
243
+ if(this.getElementsByClassName('service-name')[0].innerHTML == 'All' || this.getElementsByClassName('service-name')[0].innerHTML.match(filter)) {
244
+ ui.removeClass('d-none');
245
+ ui.addClass('d-block');
246
+ } else {
247
+ ui.removeClass('d-block');
248
+ ui.addClass('d-none');
249
+ }
250
+ });
251
+ }
252
+
253
+ displayEvents() {
254
+ //$("#service-title").html(service['name']);
255
+ var tableBody = $('#all-events > tbody');
256
+ tableBody.html("");
257
+ var tbody = tableBody[0];
258
+ var filter = "";
259
+ for (var i = 0 ; i < this.data.length; i++) {
260
+ var e = this.data[i];
261
+ var row = document.createElement('tr');
262
+ row.setAttribute("class", 'srv-' + e.service);
263
+ this.buildCell(row, 'td', 'ts', this.appChild('time', document.createTextNode(e.ts)));
264
+ this.buildCell(row, 'td', 'lookup serviceName serviceCol', document.createTextNode(e.service));
265
+ var text = e.instance;
266
+ if (e.instance_info && e.instance_info.node) {
267
+ text = e.instance_info.node;
268
+ if (e.instance_info.port > 0) {
269
+ text += ":" + e.instance_info.port
270
+ }
271
+ }
272
+ var instanceCell = this.buildCell(row, 'td', 'lookup instance', document.createTextNode(text));
273
+ instanceCell.setAttribute("title", e.instance);
274
+ {
275
+ var checksCell = document.createElement("div")
276
+ this.buildCell(row, 'td', 'lookup checks', checksCell);
277
+ for (var j = 0; j < e.checks.length; j++) {
278
+ var c = e.checks[j];
279
+ var statusSpan = document.createElement('div');
280
+ statusSpan.setAttribute('class', 'checkTransition');
281
+ statusSpan.appendChild(this.createBadge(c['old']));
282
+ statusSpan.appendChild(document.createTextNode('→'));
283
+ var newBadge = this.createBadge(c['new']);
284
+ newBadge.setAttribute('title', c['output']);
285
+ statusSpan.appendChild(newBadge);
286
+ checksCell.appendChild(statusSpan);
287
+ var checkName = document.createElement('div');
288
+ checkName.setAttribute('class', 'lookup checkName');
289
+ checkName.setAttribute('title', c['id']);
290
+ checkName.appendChild(document.createTextNode(c['name']));
291
+ checksCell.appendChild(checkName);
292
+ }
293
+ }
294
+ {
295
+ var statusSpan = document.createElement('span');
296
+ statusSpan.appendChild(this.createBadge(e['old_state']));
297
+ statusSpan.appendChild(document.createTextNode('→'));
298
+ statusSpan.appendChild(this.createBadge(e['new_state']));
299
+ this.buildCell(row, 'td', 'status', statusSpan);
300
+ }
301
+ {
302
+ var allInstances = document.createElement('span');
303
+ var nSuccess = e['stats']['passing'];
304
+ if (nSuccess > 0) {
305
+ var success = this.createBadge(nSuccess, 'success');
306
+ allInstances.appendChild(success);
307
+ }
308
+ var nWarnings = e['stats']['warning'];
309
+ if (nWarnings > 0) {
310
+ var elem = this.createBadge(nWarnings, 'warning');
311
+ allInstances.appendChild(elem);
312
+ }
313
+ var nCritical = e['stats']['critical'];
314
+ if (nCritical > 0) {
315
+ var elem = this.createBadge(nCritical, 'danger');
316
+ allInstances.appendChild(elem);
317
+ }
318
+ var elem = this.createBadge(nCritical);
319
+ var percent = Math.round(nSuccess * 100 / e['stats']['total']);
320
+ var clazz = 'secondary'
321
+ if (percent > 90) {
322
+ clazz = 'success';
323
+ } else if (percent < 20) {
324
+ clazz = 'danger';
325
+ } else if (percent < 50) {
326
+ clazz = 'warning';
327
+ }
328
+ allInstances.appendChild(this.createBadge(percent + " %", clazz));
329
+ row.appendChild(allInstances);
330
+ }
331
+ tbody.prepend(row)
332
+ }
333
+ }
334
+ }
@@ -0,0 +1,188 @@
1
+ <%
2
+ require 'json'
3
+
4
+ @current_time = Time.now.utc
5
+ cur_state = services.map do |service_name, _tags|
6
+ next if service_name.match?(/^lbl7-/)
7
+ next if service_name.match?(/^wmi-/)
8
+ next if service_name.match?(/collectd-/)
9
+ next if service_name.match?(/-(eu|as|us)$/)
10
+ next if service_name.match?(/-admin$/)
11
+
12
+ snodes = service(service_name)
13
+ cur_stats = {
14
+ 'passing' => 0,
15
+ 'warning' => 0,
16
+ 'critical' => 0,
17
+ 'total' => snodes.count
18
+ }
19
+ snodes.each do |snode|
20
+ case snode.status.downcase
21
+ when 'passing'
22
+ cur_stats['passing'] += 1
23
+ when 'warning'
24
+ cur_stats['warning'] += 1
25
+ else
26
+ cur_stats['critical'] += 1
27
+ end
28
+ end
29
+ instances = snodes
30
+ .sort { |a, b| a['Node']['Node'] <=> b['Node']['Node'] }
31
+ .map do |instance|
32
+ ["#{instance['Node']['Node']}:#{instance['Service']['ID']}",
33
+ {
34
+ 'address' => instance.service_address,
35
+ 'node' => instance['Node']['Node'],
36
+ 'port' => instance['Service']['Port'],
37
+ 'idx' => instance['Service']['ModifyIndex'],
38
+ 'status' => instance.status,
39
+ 'stats' => cur_stats,
40
+ 'checks' => instance['Checks'].map { |check| [check['CheckID'], { 'name' => check['Name'], 'status' => check['Status'], 'output' => check['Output'] }] }.to_h
41
+ }]
42
+ end.to_h
43
+ [service_name, instances]
44
+ end
45
+ .compact
46
+ .to_h
47
+
48
+ old_state = if @previous_state
49
+ @previous_state
50
+ else
51
+ cur_state
52
+ end
53
+
54
+ class RingBuffer < Array
55
+ attr_reader :max_size
56
+
57
+ def initialize(max_size:, enum: nil)
58
+ @max_size = max_size
59
+ enum&.each { |e| self << e }
60
+ end
61
+
62
+ def <<(element)
63
+ if size < @max_size || @max_size.nil?
64
+ super
65
+ else
66
+ shift
67
+ push(element)
68
+ end
69
+ end
70
+
71
+ alias push <<
72
+ end
73
+
74
+ def diff(old, new)
75
+ diff = OpenStruct.new
76
+ diff.appeared = new - old
77
+ diff.disappeared = old - new
78
+ diff.stayed = new & old
79
+ diff
80
+ end
81
+
82
+ @events = RingBuffer.new(max_size: (ENV['CONSUL_TIMELINE_BUFFER'] || 1000).to_i) unless @events
83
+
84
+ def log_event(line)
85
+ puts "#{Time.now.to_i} #{line}" if ENV['DEBUG_TIMELINE']
86
+ end
87
+
88
+ def store_event(service: service_name, instance: nil, old_state: nil, new_state: nil, instance_info: nil, checks: [])
89
+ STDERR.puts "empty instance_info for #{service} ; #{instance} ; #{new_state}" unless instance_info
90
+ @events << { 'service' => service, 'instance' => instance, 'old_state' => old_state, 'new_state' => new_state,
91
+ 'ts' => @current_time, 'instance_info' => instance_info }.tap do |ev|
92
+ ev['checks'] = checks if checks
93
+ ev['stats'] = if instance_info
94
+ instance_info['stats']
95
+ else
96
+ {}
97
+ end
98
+ end
99
+ end
100
+
101
+ def compute_checks(old_state, cur_state, service_name, instance_name)
102
+ old_checks = old_state.dig(service_name, instance_name, 'checks') || {}
103
+ new_checks = cur_state.dig(service_name, instance_name, 'checks') || {}
104
+ check_diff = diff(old_checks.keys, new_checks.keys)
105
+ checks = []
106
+ check_diff.stayed.each do |check_id|
107
+ old_status = old_state.dig(service_name, instance_name, 'checks', check_id, 'status')
108
+ cur_status = cur_state.dig(service_name, instance_name, 'checks', check_id, 'status')
109
+
110
+ next if old_status == cur_status
111
+ check_name = cur_state.dig(service_name, instance_name, 'checks', check_id, 'name')
112
+ check_name = old_state.dig(service_name, instance_name, 'checks', check_id, 'name') unless check_name
113
+ check_name = check_id unless check_name
114
+ checks << { 'id' => check_id,
115
+ 'old' => old_status,
116
+ 'new' => cur_status,
117
+ 'name' => check_name }.tap do |check|
118
+ check['output'] = (cur_state.dig(service_name, instance_name, 'checks', check_id, 'output') || '')[0..512]
119
+ end
120
+ end
121
+ checks
122
+ end
123
+
124
+ service_diff = diff(old_state.keys, cur_state.keys)
125
+
126
+ service_diff.disappeared.each do |service_name|
127
+ old_state[service_name].each do |instance_name, instance_info|
128
+ checks = compute_checks(old_state, cur_state, service_name, instance_name)
129
+ store_event(service: service_name,
130
+ instance: instance_name,
131
+ old_state: old_state[service_name][instance_name]['status'],
132
+ new_state: nil,
133
+ instance_info: instance_info,
134
+ checks: checks)
135
+ end
136
+ end
137
+
138
+ def instances_are_equal(o_state, n_state)
139
+ return true if o_state == n_state
140
+ return false unless o_state.nil? || n_state.nil?
141
+ %w[address node port status].each do |field|
142
+ return false if o_state[field] != n_state[field]
143
+ end
144
+ true
145
+ end
146
+
147
+ (service_diff.stayed + service_diff.appeared).each do |service_name|
148
+ instance_diff = diff((old_state[service_name] || {}).keys, cur_state[service_name].keys)
149
+
150
+ instance_diff.disappeared.each do |instance_name|
151
+ checks = compute_checks(old_state, cur_state, service_name, instance_name)
152
+ store_event(service: service_name,
153
+ old_state: old_state[service_name][instance_name]['status'],
154
+ new_state: nil,
155
+ instance: instance_name,
156
+ instance_info: old_state[service_name][instance_name],
157
+ checks: checks)
158
+ end
159
+ instance_diff.appeared.each do |instance_name|
160
+ checks = compute_checks(old_state, cur_state, service_name, instance_name)
161
+ store_event(service: service_name,
162
+ old_state: nil,
163
+ new_state: cur_state[service_name][instance_name]['status'],
164
+ instance: instance_name,
165
+ instance_info: cur_state[service_name][instance_name],
166
+ checks: checks)
167
+ end
168
+
169
+ instance_diff.stayed.each do |instance_name, _instance_info|
170
+ checks = compute_checks(old_state, cur_state, service_name, instance_name)
171
+ o_state = old_state[service_name][instance_name]['status']
172
+ n_state = cur_state[service_name][instance_name]['status']
173
+ next if instances_are_equal(o_state, n_state) && checks.empty?
174
+ store_event(service: service_name,
175
+ old_state: o_state,
176
+ new_state: n_state,
177
+ instance: instance_name,
178
+ instance_info: cur_state[service_name][instance_name],
179
+ checks: checks)
180
+ end
181
+ end
182
+ # We save the previous state only when we have a complete state once
183
+ if template_info['was_rendered_once']
184
+ warn "First full rendering completed at #{@current_time} !" unless @previous_state
185
+ @previous_state = cur_state
186
+ end
187
+
188
+ %><%= JSON.generate(@events)%>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: consul-templaterb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.1
4
+ version: 1.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - SRE Core Services
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-12-12 00:00:00.000000000 Z
11
+ date: 2018-12-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: em-http-request
@@ -179,6 +179,7 @@ files:
179
179
  - samples/consul-ui/consul-keys-ui.html.erb
180
180
  - samples/consul-ui/consul-nodes-ui.html.erb
181
181
  - samples/consul-ui/consul-services-ui.html.erb
182
+ - samples/consul-ui/consul-timeline-ui.html.erb
182
183
  - samples/consul-ui/consul_keys.json.erb
183
184
  - samples/consul-ui/consul_nodes.json.erb
184
185
  - samples/consul-ui/consul_services.json.erb
@@ -186,7 +187,9 @@ files:
186
187
  - samples/consul-ui/js/keys.js
187
188
  - samples/consul-ui/js/nodes.js
188
189
  - samples/consul-ui/js/service.js
190
+ - samples/consul-ui/js/timeline.js
189
191
  - samples/consul-ui/js/utils.js
192
+ - samples/consul-ui/timeline.json.erb
190
193
  - samples/consul-ui/vendors/highlight/atom-one-dark.css
191
194
  - samples/consul-ui/vendors/highlight/highlight.pack.js
192
195
  - samples/consul_template.html.erb
@@ -228,7 +231,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
228
231
  version: '0'
229
232
  requirements: []
230
233
  rubyforge_project:
231
- rubygems_version: 2.7.8
234
+ rubygems_version: 2.7.7
232
235
  signing_key:
233
236
  specification_version: 4
234
237
  summary: Implementation of Consul template using Ruby and .erb templating language