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