consul-templaterb 1.8.1 → 1.8.2

Sign up to get free protection for your applications and to get access to all the features.
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