2 # ============LICENSE_START===============================================
3 # Copyright (C) 2020 Nordix Foundation. All rights reserved.
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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 # ============LICENSE_END=================================================
20 // Sim mon server - query the agent and the simulators for counters and other data
21 // Presents a web page on localhost:9999/mon
23 var LOCALHOST="http://127.0.0.1:"
24 var MRSTUB_PORT="3905"
28 var PRODSTUB_PORT="8092"
30 var http = require('http');
32 var express = require('express');
33 const { POINT_CONVERSION_HYBRID } = require('constants')
40 app.get("/",function(req, res){
44 //Get parameter value from other server
45 function getSimCtr(url, index, cb) {
49 http.get(url, (resp) => {
50 // A chunk of data has been recieved.
51 resp.on('data', (chunk) => {
55 // The whole response has been received.
56 resp.on('end', () => {
57 var code=resp.statusCode
58 if (code > 199 && code < 300) {
61 cb("not found", index);
65 }).on("error", (err) => {
66 console.log("Error: " + err.message);
67 cb("no response", index);
70 cb("no response", index);
75 //Format a comma separated list of data to a html-safe string with fixed fieldsizes
76 function formatDataRow(commaList) {
78 var tmp=commaList.split(',');
79 for(var i=0;i<tmp.length;i++) {
81 var len = fieldSize-data.length;
86 str=str+data+" ";
91 //Format a comma separated list of ids to a html-safe string with fixed fieldsizes
92 function formatIdRow(commaList) {
94 var tmp=commaList.split(',');
95 for(var i=0;i<tmp.length;i++) {
96 tmp[i] = tmp[i].trim();
97 var data="<"+tmp[i]+">";
98 var len = fieldSize+4-data.length;
100 data = data+" ";
103 str=str+data+" ";
108 //Format a list of ids to a html-safe string in compact format
109 function formatIdRowCompact(commaList) {
110 if (commaList == undefined) {
114 var tmp=commaList.split(',');
115 for(var i=0;i<tmp.length;i++) {
116 tmp[i] = tmp[i].trim();
117 var data="<"+tmp[i]+">";
118 str=str+data+" ";
123 //Pad a string upto a certain size using a pad string
124 function padding(val, fieldSize, pad) {
126 for(var i=s.length;i<fieldSize;i++) {
132 //Function to check if the previous call has returned, if so return true, if not return false
133 //For preventing multiple calls to slow containers.
134 function checkFunctionFlag(flag) {
135 if (flagstore.hasOwnProperty(flag)) {
136 if (flagstore[flag] == 0) {
139 } else if (flagstore[flag] > 10) {
140 //Reset flag after ten attempts
141 console.log("Force release flag "+flag)
145 //Previous call not returned
146 console.log("Flag not available "+flag)
147 flagstore[flag]=flagstore[flag]+1
155 //Clear flag for parameter
156 function clearFlag(flag) {
160 //Status variables, for parameters values fetched from other simulators
161 var mr1="", mr2="", mr3="", mr4="", mr5="", mr6="";
163 //Status variables for agent
170 //Status variables for callback receiver
176 //Container names and ports of the ric simulator
180 //Status variables for each ric simulator
188 //Status variables, for parameters values fetched from ecs
189 var ecs1="", ecs2="", ecs3="", ecs4="", ecs_types="-", ecs_producers="-";
190 var ecs_producer_arr=new Array(0)
191 var ecs_producer_type_arr=new Array(0)
192 var ecs_producer_jobs_arr=new Array(0)
193 var ecs_producer_status_arr=new Array(0)
195 //Status variables, for parameters values fetched from prodstub
196 var ps2="", ps3="", ps4="", ps_types="-", ps_producers="-";
197 var ps_producer_type_arr=new Array(0)
198 var ps_producer_jobs_arr=new Array(0)
199 var ps_producer_delivery_arr=new Array(0)
204 //Counts the number of get request for the html page
207 var refreshCount_pol=-1
209 var refreshCount_ecs=-1
211 var refreshCount_cr=-1
213 var ricbasename="ricsim"
215 function fetchAllMetrics_pol() {
217 console.log("Fetching policy metrics " + refreshCount_pol)
219 if (refreshCount_pol < 0) {
220 refreshCount_pol = -1
223 refreshCount_pol = refreshCount_pol - 1
228 //Extract the port numbers from the running simulators, for every 3 calls
229 const { exec } = require('child_process');
230 exec('docker ps --filter "name='+ricbasename+'" --format "{{.Names}} {{.Ports}}" | sed s/0.0.0.0:// | cut -d \'>\' -f1 | sed \'s/[[-]]*$//\'', (err, stdout, stderr) => {
233 simulators=`${stdout}`.replace(/(\r\n|\n|\r)/gm," ");
234 simulators=simulators.trim();
235 var sims=simulators.split(" ")
238 for(i=0;i<sims.length;i=i+2) {
239 simnames[i/2]=sims[i]
240 simports[i/2]=sims[i+1]
246 //Get metric values from the simulators
247 for(var index=0;index<simnames.length;index++) {
249 if (checkFunctionFlag("simvar1_"+index)) {
250 getSimCtr(LOCALHOST+simports[index]+"/counter/num_instances", index, function(data, index) {
251 simvar1[index] = data;
252 clearFlag("simvar1_"+index)
255 if (checkFunctionFlag("simvar2_"+index)) {
256 getSimCtr(LOCALHOST+simports[index]+"/counter/num_types", index, function(data,index) {
257 simvar2[index] = data;
258 clearFlag("simvar2_"+index)
261 if (checkFunctionFlag("simvar3_"+index)) {
262 getSimCtr(LOCALHOST+simports[index]+"/policytypes", index, function(data,index) {
263 data=data.replace(/\[/g,'');
264 data=data.replace(/\]/g,'');
265 data=data.replace(/ /g,'');
266 data=data.replace(/\"/g,'');
267 simvar3[index] = data;
268 clearFlag("simvar3_"+index)
271 if (checkFunctionFlag("simvar4_"+index)) {
272 getSimCtr(LOCALHOST+simports[index]+"/counter/interface", index, function(data,index) {
273 simvar4[index] = data;
274 clearFlag("simvar4_"+index)
277 if (checkFunctionFlag("simvar5_"+index)) {
278 getSimCtr(LOCALHOST+simports[index]+"/counter/remote_hosts", index, function(data,index) {
279 simvar5[index] = data;
280 clearFlag("simvar5_"+index)
283 if (checkFunctionFlag("simvar6_"+index)) {
284 getSimCtr(LOCALHOST+simports[index]+"/counter/datadelivery", index, function(data,index) {
285 simvar6[index] = data;
286 clearFlag("simvar6_"+index)
291 //MR - get metrics values from the MR stub
292 if (checkFunctionFlag("mr1")) {
293 getSimCtr(LOCALHOST+MRSTUB_PORT+"/counter/requests_submitted", 0, function(data, index) {
298 if (checkFunctionFlag("mr2")) {
299 getSimCtr(LOCALHOST+MRSTUB_PORT+"/counter/requests_fetched", 0, function(data, index) {
304 if (checkFunctionFlag("mr3")) {
305 getSimCtr(LOCALHOST+MRSTUB_PORT+"/counter/current_requests", 0, function(data, index) {
310 if (checkFunctionFlag("mr4")) {
311 getSimCtr(LOCALHOST+MRSTUB_PORT+"/counter/responses_submitted", 0, function(data, index) {
316 if (checkFunctionFlag("mr5")) {
317 getSimCtr(LOCALHOST+MRSTUB_PORT+"/counter/responses_fetched", 0, function(data, index) {
322 if (checkFunctionFlag("mr6")) {
323 getSimCtr(LOCALHOST+MRSTUB_PORT+"/counter/current_responses", 0, function(data, index) {
329 //CR - get metrics values from the callbackreceiver
330 if (checkFunctionFlag("cr1")) {
331 getSimCtr(LOCALHOST+CR_PORT+"/counter/received_callbacks", 0, function(data, index) {
336 if (checkFunctionFlag("cr2")) {
337 getSimCtr(LOCALHOST+CR_PORT+"/counter/fetched_callbacks", 0, function(data, index) {
342 if (checkFunctionFlag("cr3")) {
343 getSimCtr(LOCALHOST+CR_PORT+"/counter/current_messages", 0, function(data, index) {
348 //Agent - more get metrics from the agent
349 if (checkFunctionFlag("ag1")) {
350 getSimCtr(LOCALHOST+AGENT_PORT+"/status", 0, function(data, index) {
355 if (checkFunctionFlag("ag2")) {
356 getSimCtr(LOCALHOST+AGENT_PORT+"/services", 0, function(data, index) {
359 var jd=JSON.parse(data);
361 if (ag2.length > 1) {
364 ag2=ag2+(jd[key]["serviceName"]).trim()
373 if (checkFunctionFlag("ag3")) {
374 getSimCtr(LOCALHOST+AGENT_PORT+"/policy_types", 0, function(data, index) {
377 var jd=JSON.parse(data);
379 if (ag3.length > 0) {
382 ag3=ag3+jd[key].trim()
392 if (checkFunctionFlag("ag4")) {
393 getSimCtr(LOCALHOST+AGENT_PORT+"/policy_ids", 0, function(data, index) {
395 var jd=JSON.parse(data);
405 if (checkFunctionFlag("ag5")) {
406 getSimCtr(LOCALHOST+AGENT_PORT+"/rics", 0, function(data, index) {
408 var jd=JSON.parse(data);
418 fetchAllMetrics_pol();
423 function fetchAllMetrics_ecs() {
425 console.log("Fetching enrichment metrics - timer:" + refreshCount_ecs)
427 if (refreshCount_ecs < 0) {
428 refreshCount_ecs = -1
431 refreshCount_ecs = refreshCount_ecs - 1
435 if (checkFunctionFlag("ecs_stat")) {
436 getSimCtr(LOCALHOST+ECS_PORT+"/status", 0, function(data, index) {
442 var jd=JSON.parse(data);
444 ecs2=""+jd["no_of_producers"]
445 ecs3=""+jd["no_of_types"]
446 ecs4=""+jd["no_of_jobs"]
449 ecs1="error response"
450 ecs2="error response"
451 ecs3="error response"
452 ecs4="error response"
456 getSimCtr(LOCALHOST+ECS_PORT+"/ei-producer/v1/eitypes", 0, function(data, index) {
459 var jd=JSON.parse(data);
460 for(var i=0;i<jd.length;i++) {
461 if (ecs_types.length == 1) {
464 ecs_types=""+ecs_types+jd[i]+" "
468 ecs_types="error response"
472 getSimCtr(LOCALHOST+ECS_PORT+"/ei-producer/v1/eiproducers", 0, function(data, index) {
475 var jd=JSON.parse(data);
476 var tmp_ecs_producer_arr=new Array(jd.length)
477 for(var i=0;i<jd.length;i++) {
478 if (ecs_producers.length == 1) {
481 ecs_producers=""+ecs_producers+jd[i]+" "
482 tmp_ecs_producer_arr[i]=jd[i]
484 ecs_producer_arr = tmp_ecs_producer_arr
487 ecs_producers="error response"
488 ecs_producer_arr=new Array(0)
492 ecs_producer_type_arr = JSON.parse(JSON.stringify(ecs_producer_arr))
493 for(var x=0;x<ecs_producer_type_arr.length;x++) {
494 getSimCtr(LOCALHOST+ECS_PORT+"/ei-producer/v1/eiproducers/"+ecs_producer_type_arr[x], x, function(data, x) {
495 var row=""+ecs_producer_type_arr[x]+" : "
497 var jd=JSON.parse(data);
498 var jda=jd["supported_ei_types"]
499 for(var j=0;j<jda.length;j++) {
500 row=""+row+jda[j]["ei_type_identity"]+" "
502 ecs_producer_type_arr[x]=row
505 ecs_producer_type_arr=new Array(0)
510 ecs_producer_jobs_arr = JSON.parse(JSON.stringify(ecs_producer_arr))
511 for(var x=0;x<ecs_producer_jobs_arr.length;x++) {
512 getSimCtr(LOCALHOST+ECS_PORT+"/ei-producer/v1/eiproducers/"+ecs_producer_jobs_arr[x]+"/eijobs", x, function(data, x) {
513 var row=""+ecs_producer_jobs_arr[x]+" : "
515 var jd=JSON.parse(data);
516 for(var j=0;j<jd.length;j++) {
518 row=""+row+jda["ei_job_identity"]+"("+jda["ei_type_identity"]+") "
520 ecs_producer_jobs_arr[x]=row
523 ecs_producer_jobs_arr=new Array(0)
528 ecs_producer_status_arr = JSON.parse(JSON.stringify(ecs_producer_arr))
529 for(var x=0;x<ecs_producer_status_arr.length;x++) {
530 getSimCtr(LOCALHOST+ECS_PORT+"/ei-producer/v1/eiproducers/"+ecs_producer_status_arr[x]+"/status", x, function(data, x) {
531 var row=""+ecs_producer_status_arr[x]+" : "
533 var jd=JSON.parse(data);
534 row=""+row+jd["operational_state"]
535 ecs_producer_status_arr[x]=row
538 ecs_producer_status_arr=new Array(0)
542 clearFlag("ecs_stat")
544 if (checkFunctionFlag("prodstub_stat")) {
545 getSimCtr(LOCALHOST+PRODSTUB_PORT+"/status", x, function(data, x) {
546 var ctr2_map=new Map()
547 var ctr3_map=new Map()
552 ps_producer_type_arr=new Array()
553 ps_producer_jobs_arr=new Array()
554 ps_producer_delivery_arr=new Array()
559 var jp=JSON.parse(data);
560 for(var prod_name in jp) {
561 ctr2_map.set(prod_name, prod_name)
564 var row=""+prod_name+" : "
565 var rowj=""+prod_name+" : "
566 var rowd=""+prod_name+" : "
567 ps_producers += prod_name + " "
571 for(var i=0;i<ta.length;i++) {
572 ctr3_map.set(ta[i], ta[i])
575 } else if (ji == "supervision_response") {
576 } else if (ji == "supervision_counter") {
577 } else if (ji == "types") {
582 var job_data=jj[ji]["json"]
583 if (job_data != undefined) {
584 rowj += "("+job_data["ei_type_identity"]+")"
586 rowd += "("+jj[ji]["delivery_attempts"]+")"
589 ps_producer_type_arr[(ctr2-1)]=row
590 ps_producer_jobs_arr[(ctr2-1)]=rowj
591 ps_producer_delivery_arr[(ctr2-1)]=rowd
595 for(const [key, value] of ctr3_map.entries()) {
596 ps_types += key + " "
602 ps_producers="error response"
603 ps_types="error response"
604 ps_producer_type_arr=new Array()
605 ps_producer_jobs_arr=new Array()
606 ps_producer_delivery_arr=new Array()
612 clearFlag("prodstub_stat")
615 fetchAllMetrics_ecs();
620 function fetchAllMetrics_cr() {
622 console.log("Fetching CR DB - timer:" + refreshCount_ecs)
624 if (refreshCount_cr < 0) {
628 refreshCount_cr = refreshCount_cr - 1
632 if (checkFunctionFlag("cr_stat")) {
633 getSimCtr(LOCALHOST+CR_PORT+"/db", 0, function(data, index) {
636 cr_db=JSON.parse(data);
644 fetchAllMetrics_cr();
649 app.get("/mon3",function(req, res){
651 console.log("Creating CR DB page - timer: " + refreshCount_ecs)
653 if (refreshCount_cr < 0) {
658 var json_str=JSON.stringify(cr_db, null, 1)
659 var htmlStr = "<!DOCTYPE html>" +
662 "<meta http-equiv=\"refresh\" content=\"2\">"+ //2 sec auto refresh
663 "<title>CR DB dump</title>"+
665 "<body style=\"white-space: pre-wrap\">" +
673 app.get("/mon2",function(req, res){
675 console.log("Creating enrichment metrics - timer: " + refreshCount_ecs)
677 if (refreshCount_ecs < 0) {
679 fetchAllMetrics_ecs()
683 var summary=req.query.summary
685 if (summary == undefined) {
686 return res.redirect('/mon2?summary=false');
690 var htmlStr = "<!DOCTYPE html>" +
693 "<meta http-equiv=\"refresh\" content=\"2\">"+ //2 sec auto refresh
694 "<title>Enrichment coordinator service and producer stub</title>"+
697 "<font size=\"-3\" face=\"summary\">"
698 if (summary == "false") {
699 htmlStr=htmlStr+"<p>Set query param '?summary' to true to only show summary statistics</p>"
701 htmlStr=htmlStr+"<p>Set query param '?summary' to false to only show full statistics</p>"
703 htmlStr=htmlStr+"</font>" +
704 "<h3>Enrichment Coordinator Service</h3>" +
705 "<font face=\"monospace\">" +
706 "Status:..........." + formatDataRow(ecs1) + "<br>" +
707 "Producers:........" + formatDataRow(ecs2) + "<br>" +
708 "Types:............" + formatDataRow(ecs3) + "<br>" +
709 "Jobs:............." + formatDataRow(ecs4) + "<br>" +
711 if (summary == "false") {
714 "<font face=\"monospace\">" +
715 "Producer ids:....." + formatDataRow(ecs_producers) + "<br>" +
716 "Type ids:........." + formatDataRow(ecs_types) + "<br>" +
718 for(var i=0;i<ecs_producer_type_arr.length;i++) {
719 var tmp=ecs_producer_type_arr[i]
720 if (tmp != undefined) {
721 var s = "Producer types...." + formatDataRow(ecs_producer_type_arr[i]) + "<br>"
725 htmlStr=htmlStr+"<br>";
726 for(var i=0;i<ecs_producer_jobs_arr.length;i++) {
727 var tmp=ecs_producer_jobs_arr[i]
728 if (tmp != undefined) {
729 var s = "Producer jobs....." + formatDataRow(ecs_producer_jobs_arr[i]) + "<br>"
733 htmlStr=htmlStr+"<br>";
734 for(var i=0;i<ecs_producer_status_arr.length;i++) {
735 var tmp=ecs_producer_status_arr[i]
736 if (tmp != undefined) {
737 var s = "Producer status..." + formatDataRow(ecs_producer_status_arr[i]) + "<br>"
741 htmlStr=htmlStr+"<br>"+"<br>" +
745 "<h3>Producer stub</h3>" +
746 "<font face=\"monospace\">" +
747 "Producers:........" + formatDataRow(ps2) + "<br>" +
748 "Types:............" + formatDataRow(ps3) + "<br>" +
749 "Jobs:............." + formatDataRow(ps4) + "<br>" +
751 if (summary == "false") {
754 "<font face=\"monospace\">" +
755 "Producer ids:....." + formatDataRow(ps_producers) + "<br>" +
756 "Type ids:........." + formatDataRow(ps_types) + "<br>" +
758 for(var i=0;i<ps_producer_type_arr.length;i++) {
759 var tmp=ps_producer_type_arr[i]
760 if (tmp != undefined) {
761 var s = "Producer types...." + formatDataRow(ps_producer_type_arr[i]) + "<br>"
765 htmlStr=htmlStr+"<br>";
766 for(var i=0;i<ps_producer_jobs_arr.length;i++) {
767 var tmp=ps_producer_jobs_arr[i]
768 if (tmp != undefined) {
769 var s = "Producer jobs....." + formatDataRow(ps_producer_jobs_arr[i]) + "<br>"
773 htmlStr=htmlStr+"<br>";
774 for(var i=0;i<ps_producer_delivery_arr.length;i++) {
775 var tmp=ps_producer_delivery_arr[i]
776 if (tmp != undefined) {
777 var s = "Producer delivery." + formatDataRow(ps_producer_delivery_arr[i]) + "<br>"
789 // Monitor for policy management
790 app.get("/mon",function(req, res){
792 console.log("Creating policy metrics page " + refreshCount_pol)
794 if (refreshCount_pol < 0) {
796 fetchAllMetrics_pol()
800 var bn=req.query.basename
802 if (bn == undefined) {
804 return res.redirect('/mon?basename=ricsim');
810 var htmlStr = "<!DOCTYPE html>" +
813 "<meta http-equiv=\"refresh\" content=\"2\">"+ //2 sec auto refresh
814 "<title>Policy Agent and simulator monitor</title>"+
817 "<font size=\"-3\" face=\"monospace\">" +
818 "<p>Change basename in url if other ric sim prefix is used</p>" +
820 "<h3>Policy agent</h3>" +
821 "<font face=\"monospace\">" +
822 "Status:..............................." + formatDataRow(ag1) + "<br>" +
823 "Services:............................." + formatIdRowCompact(ag2) + "<br>" +
824 "Types:................................" + formatIdRowCompact(ag3) + "<br>" +
825 "Number of instances:.................." + formatDataRow(ag4) + "<br>" +
826 "Near-RT RICs:........................." + formatDataRow(ag5) + "<br>" +
828 "<h3>MR Stub interface</h3>" +
829 "<font face=\"monospace\">"+
830 "Submitted requests:............................" + formatDataRow(mr1) + "<br>" +
831 "Fetched requests:.............................." + formatDataRow(mr2) + "<br>" +
832 "Current requests waiting:......................" + formatDataRow(mr3) + "<br>" +
833 "Submitted responses:..........................." + formatDataRow(mr4) + "<br>" +
834 "Fetched responses.............................." + formatDataRow(mr5) + "<br>" +
835 "Current responses waiting......................" + formatDataRow(mr6) + "<br>" +
837 "<h3>Callback receiver</h3>" +
838 "<font face=\"monospace\">" +
839 "Callbacks received:..................." + formatDataRow(cr1) + "<br>" +
840 "Callbacks fetched:...................." + formatDataRow(cr2) + "<br>" +
841 "Number of waiting callback messages:.." + formatDataRow(cr3) + "<br>" +
843 "<h3>Near-RT RIC Simulators</h3>" +
844 "<font face=\"monospace\">"
846 htmlStr=htmlStr+padding("Near-RT RIC Simulator name", 35," ")
847 htmlStr=htmlStr+padding("Types", 10," ")
848 htmlStr=htmlStr+padding("Instances", 12," ")
849 htmlStr=htmlStr+padding("Data delivery", 12," ")+"<br>"
850 htmlStr=htmlStr+padding("",70,"=")+"<br>"
851 for(var simIndex=0;simIndex<simnames.length;simIndex++) {
852 htmlStr=htmlStr+padding(simnames[simIndex]+ " ("+simports[simIndex]+")",35," ");
853 htmlStr=htmlStr+padding(simvar2[simIndex],10," ")
854 htmlStr=htmlStr+padding(simvar1[simIndex],12 ," ")
855 htmlStr=htmlStr+padding(simvar6[simIndex],12," ")
856 htmlStr=htmlStr+"<br>";
859 htmlStr=htmlStr+"<br>";
860 htmlStr=htmlStr+padding("Near-RT RIC Simulator name", 35," ")
861 htmlStr=htmlStr+padding("Version", 20," ")
862 htmlStr=htmlStr+padding("Type-IDs", 10," ")+"<br>"
863 htmlStr=htmlStr+padding("",65,"=")+"<br>"
864 for(simIndex=0;simIndex<simnames.length;simIndex++) {
865 htmlStr=htmlStr+padding(simnames[simIndex]+ " ("+simports[simIndex]+")",35," ");
866 htmlStr=htmlStr+padding(simvar4[simIndex],20," ")
867 htmlStr=htmlStr+padding(formatIdRowCompact(simvar3[simIndex]),10," ")
868 htmlStr=htmlStr+"<br>";
871 htmlStr=htmlStr+"<br>";
872 htmlStr=htmlStr+padding("Near-RT RIC Simulator name", 35," ")
873 htmlStr=htmlStr+padding("Remote hosts", 50," ")+"<br>"
874 htmlStr=htmlStr+padding("",90,"=")+"<br>"
875 for(simIndex=0;simIndex<simnames.length;simIndex++) {
876 htmlStr=htmlStr+padding(simnames[simIndex]+ " ("+simports[simIndex]+")",35," ");
877 htmlStr=htmlStr+padding(simvar5[simIndex],50," ")
878 htmlStr=htmlStr+"<br>";
887 var httpServer = http.createServer(app);
889 httpServer.listen(httpPort);
890 console.log("Simulator monitor listening (http) at "+httpPort);
891 console.log("Open the web page on localhost:9999/mon to view the policy statistics page.")
892 console.log("Open the web page on localhost:9999/mon2 to view the enrichment statistics page.")
893 console.log("Open the web page on localhost:9999/mon3 to view CR DB in json.")