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 +4 -4
- data/CHANGELOG.md +6 -0
- data/lib/consul/async/version.rb +1 -1
- data/samples/consul-ui/README.md +11 -2
- data/samples/consul-ui/common/header.html.erb +4 -1
- data/samples/consul-ui/consul-timeline-ui.html.erb +60 -0
- data/samples/consul-ui/js/service.js +2 -1
- data/samples/consul-ui/js/timeline.js +334 -0
- data/samples/consul-ui/timeline.json.erb +188 -0
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6d652b0259f34bd70784ebdfbfe18e042bb48bdd03af5f5d920a71b94bb1281d
|
4
|
+
data.tar.gz: 00116db5cf7b2f11516abb912aab335c2608ec01fdf9ba527f11deb9bc861755
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c53f3a2704b5b52b3febb4c4844c36dae660b163d1de8298acde2f88c0cb9b9e65bc202d35c160d0747b3624bfa1355c0e014ef58e2d31673a5b40d17b0d8847
|
7
|
+
data.tar.gz: bc16ad7b30485b403c19d01251c87ffa66d17b937b09affdf2c74470455283d5121b95909524ec703d29239efe97b626f7e0c70bb34090a04bab69ad224a176c
|
data/CHANGELOG.md
CHANGED
data/lib/consul/async/version.rb
CHANGED
data/samples/consul-ui/README.md
CHANGED
@@ -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
|
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 Status Change</th>
|
38
|
+
<th colspan="2" scope="col">Service 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
|
-
|
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.
|
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-
|
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.
|
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
|