sig-monitor: Dump stack atomically
[src/app-framework-binder.git] / test / monitoring / monitor.js
1 /*
2  * Copyright (C) 2017 "IoT.bzh"
3  * Author: José Bollo <jose.bollo@iot.bzh>
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *   http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17
18 var afb;
19 var ws;
20
21 var t_api;
22 var t_verb;
23 var t_logmsg;
24 var t_traceevent;
25 var t_verbosity;
26 var t_trace;
27 var t_separator;
28
29 var apis = {};
30 var events = [];
31 var inhibit = false;
32 var msgs = false;
33 var autoscroll = false;
34
35 var root_node;
36 var connected_node;
37 var trace_events_node;
38 var logmsgs_node;
39 var apis_node;
40 var all_node;
41
42 /* flags */
43 var show_perms = false;
44 var show_monitor_events = false;
45
46 _.templateSettings = { interpolate: /\{\{(.+?)\}\}/g };
47
48 function untrace_all() {
49         do_call("monitor/trace", {drop: true});
50 }
51
52 function disconnect(status) {
53         class_toggle(root_node, { on: "off" }, "off");
54         connected_node.innerHTML = "Connection Closed";
55         connected_node.className = status;
56         if (ws) {
57                 untrace_all();
58                 ws.onclose = ws.onabort = null;
59                 ws.close();
60         }
61         ws = null;
62         if (afb)
63                 at("param-token").value = afb.context.token;
64         afb = null;
65 }
66
67 function on_disconnect() {
68         disconnect("ok");
69 }
70
71 function connect() {
72         ws && ws.close();
73         afb = new AFB({
74                 host: at("param-host").value + ":" + at("param-port").value,
75                 token: at("param-token").value
76         });
77         ws = new afb.ws(onopen, onabort);
78 }
79
80 function on_connect(evt) {
81         connect();
82 }
83
84 function init() {
85         /* prepare the DOM templates */
86         t_api = at("t-api").content.firstElementChild;
87         t_verb = at("t-verb").content.firstElementChild;
88         t_logmsg = at("t-logmsg").content.firstElementChild;
89         t_traceevent = at("t-traceevent").content.firstElementChild;
90         t_verbosity = at("t-verbosity").content.firstElementChild;
91         t_trace = at("t-trace").content.firstElementChild;
92         t_separator = at("t-separator").content.firstElementChild;
93
94         root_node = at("root");
95         connected_node = at("connected");
96         trace_events_node = at("trace-events");
97         logmsgs_node = at("logmsgs");
98         apis_node = at("apis");
99         all_node = at("all");
100
101         plug(t_api, ".verbosity", t_verbosity);
102         plug(t_api, ".trace", t_trace);
103         plug(all_node, ".trace", t_trace);
104         plug(all_node, ".verbosity", t_verbosity);
105         plug(at("common"), ".verbosity", t_verbosity);
106         for_all_nodes(root_node, ".opclo", function(n){n.onclick = on_toggle_opclo});
107         for_all_nodes(root_node, ".opclo ~ :not(.closedoff)", function(n){n.onclick = on_toggle_opclo});
108         for_all_nodes(root_node, ".verbosity select", function(n){n.onchange = set_verbosity});
109         for_all_nodes(root_node, ".trace-item input", function(n){n.onchange = on_trace_change});
110         at("disconnect").onclick = on_disconnect;
111         at("connect").onclick = on_connect;
112         at("droptracevts").onclick = drop_all_trace_events;
113         at("dropmsgs").onclick = drop_all_logmsgs;
114         at("stopmsgs").onclick = toggle_logmsgs;
115         start_logmsgs(false);
116         trace_events_node.onclick = on_toggle_traceevent;
117         at("autoscroll").onclick = toggle_autoscroll;
118         start_autoscroll(true);
119         at("addsep").onclick = add_separator;
120         at("experts").onclick = toggle_experts;
121
122         at("param-host").value = document.location.hostname;
123         at("param-port").value = document.location.port;
124         var args = new URLSearchParams(document.location.search.substring(1));
125         at("param-token").value = args.get("x-afb-token") || args.get("token") || "hello";
126
127         document.onbeforeunload = on_disconnect;
128
129         connect();
130 }
131
132 function for_all_nodes(root, sel, fun) {
133         (root ? root : document).querySelectorAll(sel).forEach(fun);
134 }
135
136 function get(sel,x) {
137         if (!x)
138                 x = document;
139         var r = x.querySelector(sel);
140         return r;
141 }
142 function at(id) { return document.getElementById(id); }
143
144 function plug(target, sel, node) {
145         var x = get(sel, target);
146         var n = target.ownerDocument.importNode(node, true);
147         x.parentNode.insertBefore(n, x);
148         x.parentNode.removeChild(x);
149 }
150
151 function onopen() {
152         class_toggle(root_node, { off: "on" }, "on");
153         connected_node.innerHTML = "Connected " + ws.url;
154         connected_node.className = "ok";
155         ws.onevent("*", gotevent);
156         ws.onclose = onabort;
157         untrace_all();
158         for_all_nodes(all_node, ".trace-box", update_trace_box);
159         do_call("monitor/get", {apis:true,verbosity:true}, on_got_apis, on_error_apis);
160 }
161
162 function onabort() {
163         disconnect("error");
164 }
165
166 function start_autoscroll(val) {
167         at("autoscroll").textContent = (autoscroll = val) ? "Stop scroll" : "Start scroll";
168 }
169
170 function toggle_autoscroll() {
171         start_autoscroll(!autoscroll);
172 }
173
174 function add_separator() {
175         var x = document.importNode(t_separator, true);
176         trace_events_node.append(x);
177         if (autoscroll)
178                 x.scrollIntoView();
179         if (msgs) {
180                 x = document.importNode(t_separator, true);
181                 logmsgs_node.append(x);
182                 if (autoscroll)
183                         x.scrollIntoView();
184         }
185 }
186
187 function start_logmsgs(val) {
188         at("stopmsgs").textContent = (msgs = val) ? "Stop logs" : "Get logs";
189 }
190
191 function toggle_logmsgs() {
192         start_logmsgs(!msgs);
193 }
194
195 function drop_all_logmsgs() {
196         logmsgs_node.innerHTML = "";
197 }
198
199 function drop_all_trace_events() {
200         trace_events_node.innerHTML = "";
201 }
202
203 function add_logmsg(tag, content, add) {
204         if (!msgs) return;
205         var x = document.importNode(t_logmsg, true);
206         get(".tag", x).textContent = tag;
207         get(".content", x).textContent = content;
208         get(".close", x).onclick = function(evt){x.remove();};
209         if (add)
210                 x.className = x.className + " " + add;
211         logmsgs_node.append(x);
212         if (autoscroll)
213                 x.scrollIntoView();
214 }
215
216 function add_error(tag, obj) {
217         add_logmsg(tag, JSON.stringify(obj, null, 1), "error");
218 }
219
220 function on_error_apis(obj) {
221         add_error("can't get apis", obj);
222 }
223
224 function do_call(api_verb, request, onsuccess, onerror) {
225         var call = api_verb + "(" + JSON.stringify(request, null, 1) + ")";
226         add_logmsg("send request", call, "call");
227         ws.call(api_verb, request).then(
228                 function(obj){
229                         add_logmsg("receive success", call + " -> " + JSON.stringify(obj, null, 1), "retok");
230                         if (onsuccess)
231                                 onsuccess(obj);
232                 },
233                 function(obj){
234                         add_logmsg("receive error", call + " -> ", JSON.stringify(obj, null, 1), "reterr");
235                         if (onerror)
236                                 onerror(obj);
237                 });
238 }
239
240 /* show all verbosities */
241 function on_got_verbosities(obj) {
242         inhibit = true;
243         _.each(obj.response.verbosity, function(verbosity, api_name){
244                 if (api_name == "monitor") return;
245                 var node = api_name ? apis[api_name].node : at("common");
246                 if (node)
247                         get(".verbosity option[value='"+verbosity+"']", node).selected = true;
248         });
249         inhibit = false;
250 }
251
252 function set_verbosity(evt) {
253         if (inhibit) return;
254         inhibit = true;
255         var obj = evt.target;
256         var req = {verbosity:{}};
257         var name = obj.API ? obj.API.name : obj === get(".select", all_node) ? "*" : "";
258         if (name != "*") {
259                 req.verbosity[name] = obj.value;
260         } else {
261                 req.verbosity = obj.value;
262         }
263         inhibit = false;
264         do_call("monitor/set", req);
265         do_call("monitor/get", {verbosity:true}, on_got_verbosities);
266 }
267
268 /* show all apis */
269 function on_got_apis(obj) {
270         inhibit = true;
271         var saved_apis = apis;
272         apis = {};
273         apis_node.innerHTML = "";
274         _.each(obj.response.apis, function(api_desc, api_name){
275                 if (api_name == "monitor") return;
276                 var api = saved_apis[api_name];
277                 if (!api) {
278                         /* create the node */
279                         api = {
280                                 node: document.importNode(t_api, true),
281                                 verbs: {},
282                                 name: api_name
283                         };
284                         api.node.API = api;
285                         api.node.dataset.api = api_name;
286                         api.vnode = get(".verbs", api.node);
287                         get(".name", api.node).textContent = api_name;
288                         var s = get(".verbosity select", api.node);
289                         s.API = api;
290                         s.onchange = set_verbosity;
291                         for_all_nodes(api.node, ".opclo", function(n){n.onclick = on_toggle_opclo});
292                         for_all_nodes(api.node, ".opclo ~ :not(.closedoff)", function(n){n.onclick = on_toggle_opclo});
293                         for_all_nodes(api.node, ".trace-item input", function(n){n.onchange = on_trace_change});
294                 } else {
295                         /* reactivate the expected traces */
296                         for_all_nodes(api.node, ".trace-box", update_trace_box);
297                 }
298                 apis[api_name] = api;
299                 if (api_desc == null) {
300                         get(".desc", api.node).textContent = "?? unrecoverable ??";
301                 } else {
302                         get(".desc", api.node).textContent = api_desc.info.description || "";
303                         _.each(api_desc.paths, function(verb_desc, path_name){
304                                 var verb_name = path_name.substring(1);
305                                 var verb = api.verbs[verb_name];
306                                 if (!verb) {
307                                         verb = {
308                                                 node: document.importNode(t_verb, true),
309                                                 name: verb_name,
310                                                 api: api
311                                         };
312                                         verb.node.VERB = verb;
313                                         verb.node.dataset.verb = verb_name;
314                                         api.verbs[verb_name] = verb;
315                                         get(".name", verb.node).textContent = verb_name;
316                                         var g = verb_desc.get ||{};
317                                         var r = g["responses"] || {};
318                                         var t = r["200"] || {};
319                                         var d = t.description || "";
320                                         get(".desc", verb.node).textContent = d;
321                                         if (show_perms) {
322                                                 var p = g["x-permissions"] || "";
323                                                 get(".perm", verb.node).textContent = p ? JSON.stringify(p, null, 1) : "";
324                                         }
325                                         api.vnode.append(verb.node);
326                                 }
327                         });
328                 }
329                 apis_node.append(api.node);
330         });
331         inhibit = false;
332         on_got_verbosities(obj);
333 }
334
335 function on_toggle_opclo(evt) {
336         toggle_opened_closed(evt.target.parentElement);
337 }
338
339 function toggle_experts(evt) {
340         toggle_opened_closed(evt.target);
341 }
342
343 function update_trace_box(node) {
344         set_trace_box(node, false);
345 }
346
347 function set_trace_box(node, clear) {
348         var api = node;
349         while (api && !api.dataset.api)
350                 api = api.parentElement;
351         var tag = api.dataset.api + "/" + node.dataset.trace;
352         var value = false;
353         for_all_nodes(node, "input", function(n){ if (n.checked) value = n.value; });
354         if (clear)
355                 do_call("monitor/trace", {drop: {tag: tag}});
356         if (value != "no") {
357                 var spec = {tag: tag, name: "trace"};
358                 spec[node.dataset.trace] = value;
359                 if (api.dataset.api != "*")
360                         spec.api = api.dataset.api;
361                 do_call("monitor/trace", {add: spec});
362         }
363 }
364
365 function on_trace_change(evt) {
366         var obj = evt.target;
367         var box = obj.parentElement;
368         while (box && !box.dataset.trace)
369                 box = box.parentElement;
370         for_all_nodes(box, "input", function(n){n.checked = false;});
371         obj.checked = true;
372         set_trace_box(box, true);
373 }
374
375 function makecontent(node, deep, val) {
376         if (--deep > 0) {
377                 if (_.isObject(val)) {
378                         node.append(makeobj(val, deep));
379                         return;
380                 }
381                 if (_.isArray(val)) {
382                         node.append(makearr(val, deep));
383                         return;
384                 }
385         }
386         node.innerHTML = '<pre>' + obj2html(val) + '</pre>';
387 }
388
389 function makearritem(tbl, deep, val) {
390         var tr = document.createElement("tr");
391         var td = document.createElement("td");
392         tr.append(td);
393         tbl.append(tr);
394         makecontent(td, deep, val);
395 }
396
397 function makearr(arr, deep) {
398         var node = document.createElement("table");
399         node.className = "array";
400         _.each(arr, function(v) { makearritem(node, deep, v);});
401         return node;
402 }
403
404 function makeobjitem(tbl, deep, key, val) {
405         var tr = document.createElement("tr");
406         var td1 = document.createElement("td");
407         var td2 = document.createElement("td");
408         tr.className = key;
409         tr.append(td1);
410         td1.textContent = key;
411         tr.append(td2);
412         tbl.append(tr);
413         makecontent(td2, deep, val);
414 }
415
416 function makeobj(obj, deep, ekey, eobj) {
417         var node = document.createElement("table");
418         node.className = "object";
419         _.each(_.keys(obj).sort(), function(k) { makeobjitem(node, deep, k, obj[k]);});
420         if (ekey)
421                 makeobjitem(node, deep, ekey, eobj);
422         return node;
423 }
424
425 function gotevent(obj) {
426         if (obj.event != "monitor/trace")
427                 add_logmsg("unexpected event!", JSON.stringify(obj, null, 1), "event");
428         else {
429                 add_logmsg("trace event", JSON.stringify(obj, null, 1), "trace");
430                 gottraceevent(obj);
431         }
432 }
433
434 function gottraceevent(obj) {
435         var data = obj.data;
436         var type = data.type;
437         var desc = data[type];
438         if (!show_monitor_events) {
439                 if (type == "event" ? desc.name.startsWith("monitor/") : desc.api == "monitor")
440                         return;
441         }
442         var x = document.importNode(t_traceevent, true);
443         x.dataset.event = obj;
444         get(".close", x).onclick = function(evt){x.remove();};
445         x.className = x.className + " " + type;
446         get(".time", x).textContent = data.time;
447         get(".tag", x).textContent = ({
448                 request: function(r) { return r.api + "/" + r.verb + "  [" + r.index + "] " + r.action; },
449                 service: function(r) { return r.api + "@" + r.action; },
450                 daemon: function(r) { return r.api + ":" + r.action; },
451                 event: function(r) { return r.name + "!" + r.action; },
452                 global: function(r) { return "$" + r.action; },
453                 })[type](desc);
454         var tab = makeobj(desc, 4);
455         if ("data" in data)
456                 makeobjitem(tab, 2, "data", data.data);
457         get(".content", x).append(tab);
458         trace_events_node.append(x);
459         if (autoscroll)
460                 x.scrollIntoView();
461 }
462
463 function class_toggle(node, assoc, defval) {
464         var matched = false;
465         var cs = node.className.split(" ").map(
466                 function(x){
467                         if (!matched && (x in assoc)) {
468                                 matched = true;
469                                 return assoc[x];
470                         }
471                         return x == defval ? "" : x;
472                 }).join(" ");
473         if (!matched && defval)
474                 cs = cs + " " + defval;
475         node.className = cs;
476 }
477
478 function toggle_opened_closed(node, defval) {
479         class_toggle(node, { closed: "opened", opened: "closed" }, defval);
480 }
481
482 function on_toggle_traceevent(evt) {
483         if (getSelection() != "") return;
484         var node = evt.target;
485         while(node && node.parentElement != trace_events_node)
486                 node = node.parentElement;
487         node && toggle_opened_closed(node);
488 }
489
490 function obj2html(json) {
491         json = JSON.stringify(json, undefined, 2);
492         json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
493         return json.replace(
494                 /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
495                 function (match) {
496                         var cls = 'number';
497                         if (/^"/.test(match)) {
498                                 if (/:$/.test(match)) {
499                                         cls = 'key';
500                                 } else {
501                                         cls = 'string';
502                                         match = match.replace(/\\n/g, "\\n<br>");
503                                 }
504                         } else if (/true|false/.test(match)) {
505                                 cls = 'boolean';
506                         } else if (/null/.test(match)) {
507                                 cls = 'null';
508                         }
509                         return '<span class="json ' + cls + '">' + match + '</span>';
510                 });
511 }
512