1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00

Merge pull request #3511 from Steve-Mcl/diagnostics

Diagnostics
This commit is contained in:
Nick O'Leary 2022-04-27 22:30:24 +01:00 committed by GitHub
commit 5de078dc61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 666 additions and 47 deletions

View File

@ -165,6 +165,7 @@ module.exports = function(grunt) {
"packages/node_modules/@node-red/editor-client/src/js/ui/common/autoComplete.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/actions.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/diff.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/keyboard.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js",

View File

@ -0,0 +1,23 @@
let runtimeAPI;
let settings;
const apiUtil = require("../util");
module.exports = {
init: function(_settings, _runtimeAPI) {
settings = _settings;
runtimeAPI = _runtimeAPI;
},
getReport: function(req, res) {
const diagnosticsOpts = settings.diagnostics || {};
const opts = {
user: req.user,
scope: diagnosticsOpts.level || "basic"
}
if(diagnosticsOpts.enabled === false || diagnosticsOpts.enabled === "false") {
apiUtil.rejectHandler(req, res, {message: "diagnostics are disabled", status: 403, code: "diagnostics.disabled" })
} else {
runtimeAPI.diagnostics.get(opts)
.then(function(result) { res.json(result); })
.catch(err => apiUtil.rejectHandler(req, res, err))
}
}
}

View File

@ -23,6 +23,7 @@ var context = require("./context");
var auth = require("../auth");
var info = require("./settings");
var plugins = require("./plugins");
var diagnostics = require("./diagnostics");
var apiUtil = require("../util");
@ -34,6 +35,7 @@ module.exports = {
context.init(runtimeAPI);
info.init(settings,runtimeAPI);
plugins.init(runtimeAPI);
diagnostics.init(settings, runtimeAPI);
var needsPermission = auth.needsPermission;
@ -95,6 +97,8 @@ module.exports = {
adminApp.get("/plugins", needsPermission("plugins.read"), plugins.getAll, apiUtil.errorHandler);
adminApp.get("/plugins/messages", needsPermission("plugins.read"), plugins.getCatalogs, apiUtil.errorHandler);
adminApp.get("/diagnostics", needsPermission("diagnostics.read"), diagnostics.getReport, apiUtil.errorHandler);
return adminApp;
}
}

View File

@ -940,6 +940,8 @@
"format": "format JSON",
"rawMode": "Edit JSON",
"uiMode": "Visual editor",
"rawMode-readonly": "JSON",
"uiMode-readonly": "Visual",
"insertAbove": "Insert above",
"insertBelow": "Insert below",
"addItem": "Add item",
@ -1154,6 +1156,9 @@
"start": "Start",
"next": "Next"
},
"diagnostics": {
"title": "System Info"
},
"languages" : {
"de": "German",
"en-US": "English",

View File

@ -940,6 +940,8 @@
"format": "JSONフォーマット",
"rawMode": "JSONを編集",
"uiMode": "ビジュアルエディタ",
"rawMode-readonly": "JSON",
"uiMode-readonly": "ビジュアル",
"insertAbove": "上に挿入",
"insertBelow": "下に挿入",
"addItem": "要素を追加",
@ -1298,22 +1300,23 @@
"zoom-in": "ズームイン",
"zoom-out": "ズームアウト",
"zoom-reset": "ズームリセット",
"toggle-navigator": "ナビゲータ表示切替"
"toggle-navigator": "ナビゲータ表示切替",
"show-system-info": "システムインフォメーション"
},
"validator": {
"errors": {
"invalid-json": "JSONデータが不正: __error__",
"invalid-json-prop": "__prop__: JSONデータが不正: __error__",
"invalid-prop": "プロパティ式が不正",
"invalid-prop-prop": "__prop__: プロパティ式が不正",
"invalid-num": "数値が不正",
"invalid-num-prop": "__prop__: 数値が不正",
"invalid-regexp": "入力パターンが不正",
"invalid-regex-prop": "__prop__: 入力パターンが不正",
"missing-required-prop": "__prop__: プロパティが未設定",
"invalid-config": "__prop__: 設定ノードが不正",
"missing-config": "__prop__: 設定ノードが存在しません",
"validation-error": "__prop__: チェックエラー: __node__, __id__: __error__"
}
"invalid-json": "JSONデータが不正: __error__",
"invalid-json-prop": "__prop__: JSONデータが不正: __error__",
"invalid-prop": "プロパティ式が不正",
"invalid-prop-prop": "__prop__: プロパティ式が不正",
"invalid-num": "数値が不正",
"invalid-num-prop": "__prop__: 数値が不正",
"invalid-regexp": "入力パターンが不正",
"invalid-regex-prop": "__prop__: 入力パターンが不正",
"missing-required-prop": "__prop__: プロパティが未設定",
"invalid-config": "__prop__: 設定ノードが不正",
"missing-config": "__prop__: 設定ノードが存在しません",
"validation-error": "__prop__: チェックエラー: __node__, __id__: __error__"
}
}
}

View File

@ -730,6 +730,7 @@ var RED = (function() {
RED.search.init();
RED.actionList.init();
RED.editor.init();
RED.diagnostics.init();
RED.diff.init();

View File

@ -0,0 +1,61 @@
RED.diagnostics = (function () {
function init() {
if (RED.settings.get('diagnostics.ui', true) === false) {
return;
}
RED.actions.add("core:show-system-info", function () { show(); });
}
function show() {
$.ajax({
headers: {
"Accept": "application/json"
},
cache: false,
url: 'diagnostics',
success: function (data) {
var json = JSON.stringify(data || {}, "", 4);
if (json === "{}") {
json = "{\n\n}";
}
RED.editor.editJSON({
title: RED._('diagnostics.title'),
value: json,
requireValid: true,
readOnly: true,
toolbarButtons: [
{
text: RED._('clipboard.export.copy'),
icon: 'fa fa-copy',
click: function () {
RED.clipboard.copyText(json, $(this), RED._('clipboard.copyMessageValue'))
}
},
{
text: RED._('clipboard.download'),
icon: 'fa fa-download',
click: function () {
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(json));
element.setAttribute('download', "system-info.json");
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
},
]
});
},
error: function (jqXHR, textStatus, errorThrown) {
console.log("Unexpected error loading system info:", jqXHR.status, textStatus, errorThrown);
}
});
}
return {
init: init,
};
})();

View File

@ -21,7 +21,9 @@
'<ul id="red-ui-editor-type-json-tabs"></ul>'+
'<div id="red-ui-editor-type-json-tab-raw" class="red-ui-editor-type-json-tab-content hide">'+
'<div class="form-row" style="margin-bottom: 3px; text-align: right;">'+
'<button id="node-input-json-reformat" class="red-ui-button red-ui-button-small"><span data-i18n="jsonEditor.format"></span></button>'+
'<span class="button-group">'+
'<button id="node-input-json-reformat" class="red-ui-button red-ui-button-small"><span data-i18n="jsonEditor.format"></span></button>'+
'<span class="button-group">'+
'</div>'+
'<div class="form-row node-text-editor-row">'+
'<div style="height: 200px;min-height: 150px;" class="node-text-editor" id="node-input-json"></div>'+
@ -34,7 +36,7 @@
var activeTab;
function insertNewItem(parent,index,copyIndex) {
function insertNewItem(parent,index,copyIndex,readOnly) {
var newValue = "";
if (parent.children.length > 0) {
@ -60,26 +62,26 @@
newKey = keyRoot+"-"+(keySuffix++);
}
}
var newItem = handleItem(newKey,newValue,parent.depth+1,parent);
var newItem = handleItem(newKey,newValue,parent.depth+1,parent,readOnly);
parent.treeList.insertChildAt(newItem, index, true);
parent.treeList.expand();
}
function showObjectMenu(button,item) {
function showObjectMenu(button,item,readOnly) {
var elementPos = button.offset();
var options = [];
if (item.parent) {
options.push({id:"red-ui-editor-type-json-menu-insert-above", icon:"fa fa-toggle-up", label:RED._('jsonEditor.insertAbove'),onselect:function(){
var index = item.parent.children.indexOf(item);
insertNewItem(item.parent,index,index);
insertNewItem(item.parent,index,index,readOnly);
}});
options.push({id:"red-ui-editor-type-json-menu-insert-below", icon:"fa fa-toggle-down", label:RED._('jsonEditor.insertBelow'),onselect:function(){
var index = item.parent.children.indexOf(item)+1;
insertNewItem(item.parent,index,index-1);
insertNewItem(item.parent,index,index-1,readOnly);
}});
}
if (item.type === 'array' || item.type === 'object') {
options.push({id:"red-ui-editor-type-json-menu-add-child", icon:"fa fa-plus", label:RED._('jsonEditor.addItem'),onselect:function(){
insertNewItem(item,item.children.length,item.children.length-1);
insertNewItem(item,item.children.length,item.children.length-1,readOnly);
}});
}
if (item.parent) {
@ -121,7 +123,7 @@
newKey = keyRoot+"-"+(keySuffix++);
}
}
var newItem = handleItem(newKey,convertToObject(item),item.parent.depth+1,item.parent);
var newItem = handleItem(newKey,convertToObject(item),item.parent.depth+1,item.parent,readOnly);
var index = item.parent.children.indexOf(item)+1;
item.parent.treeList.insertChildAt(newItem, index, true);
@ -171,24 +173,24 @@
menuOptionMenu.show();
}
function parseObject(obj,depth,parent) {
function parseObject(obj,depth,parent,readOnly) {
var result = [];
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
result.push(handleItem(prop,obj[prop],depth,parent));
result.push(handleItem(prop,obj[prop],depth,parent,readOnly));
}
}
return result;
}
function parseArray(obj,depth,parent) {
function parseArray(obj,depth,parent,readOnly) {
var result = [];
var l = obj.length;
for (var i=0;i<l;i++) {
result.push(handleItem(i,obj[i],depth,parent));
result.push(handleItem(i,obj[i],depth,parent,readOnly));
}
return result;
}
function handleItem(key,val,depth,parent) {
function handleItem(key,val,depth,parent,readOnly) {
var item = {depth:depth, type: typeof val};
var container = $('<span class="red-ui-editor-type-json-editor-label">');
if (key != null) {
@ -204,11 +206,14 @@
if (parent && parent.type === "array") {
keyLabel.addClass("red-ui-editor-type-json-editor-label-array-key")
}
if(readOnly) {
keyLabel.addClass("readonly")
}
keyLabel.on("click", function(evt) {
if (item.parent.type === 'array') {
return;
}
if (readOnly) { return; }
evt.preventDefault();
evt.stopPropagation();
var w = Math.max(150,keyLabel.width());
@ -253,10 +258,10 @@
item.expanded = depth < 2;
item.type = "array";
item.deferBuild = depth >= 2;
item.children = parseArray(val,depth+1,item);
item.children = parseArray(val,depth+1,item,readOnly);
} else if (val !== null && item.type === "object") {
item.expanded = depth < 2;
item.children = parseObject(val,depth+1,item);
item.children = parseObject(val,depth+1,item,readOnly);
item.deferBuild = depth >= 2;
} else {
item.value = val;
@ -287,7 +292,11 @@
//
var orphanedChildren;
var valueLabel = $('<span class="red-ui-editor-type-json-editor-label-value">').addClass(valClass).text(valValue).appendTo(container);
if (readOnly) {
valueLabel.addClass("readonly")
}
valueLabel.on("click", function(evt) {
if (readOnly) { return; }
evt.preventDefault();
evt.stopPropagation();
if (valType === 'str') {
@ -395,17 +404,19 @@
valueLabel.hide();
})
item.gutter = $('<span class="red-ui-editor-type-json-editor-item-gutter"></span>');
if (parent) {//red-ui-editor-type-json-editor-item-handle
$('<span class="red-ui-editor-type-json-editor-item-handle"><i class="fa fa-bars"></span>').appendTo(item.gutter);
} else {
$('<span></span>').appendTo(item.gutter);
if(!readOnly) {
if (parent) {
$('<span class="red-ui-editor-type-json-editor-item-handle"><i class="fa fa-bars"></span>').appendTo(item.gutter);
} else {
$('<span></span>').appendTo(item.gutter);
}
$('<button type="button" class="editor-button editor-button-small"><i class="fa fa-caret-down"></button>').appendTo(item.gutter).on("click", function(evt) {
evt.preventDefault();
evt.stopPropagation();
showObjectMenu($(this), item, readOnly);
});
}
$('<button type="button" class="editor-button editor-button-small"><i class="fa fa-caret-down"></button>').appendTo(item.gutter).on("click", function(evt) {
evt.preventDefault();
evt.stopPropagation();
showObjectMenu($(this), item);
});
item.element = container;
return item;
}
@ -501,7 +512,25 @@
open: function(tray) {
var trayBody = tray.find('.red-ui-tray-body');
var dialogForm = RED.editor.buildEditForm(tray.find('.red-ui-tray-body'),'dialog-form',type,'editor');
var toolbarButtons = options.toolbarButtons || [];
if (toolbarButtons.length) {
toolbarButtons.forEach(function (button) {
var element = $('<button type="button" class="red-ui-button red-ui-button-small"> </button>')
.insertBefore("#node-input-json-reformat")
.on("click", function (evt) {
evt.preventDefault();
if (button.click !== undefined) {
button.click.call(element, evt);
}
});
if (button.id) { element.attr("id", button.id); }
if (button.title) { element.attr("title", button.title); }
if (button.icon) { element.append($("<i></i>").attr("class", button.icon)); }
if (button.label || button.text) {
element.append($("<span></span>").text(" " + (button.label || button.text)));
}
});
}
var container = $("#red-ui-editor-type-json-tab-ui-container").css({"height":"100%"});
var filterDepth = Infinity;
var list = $('<div class="red-ui-debug-msg-payload red-ui-editor-type-json-editor">').appendTo(container).treeList({
@ -531,11 +560,13 @@
})
});
expressionEditor = RED.editor.createEditor({
id: 'node-input-json',
value: "",
mode:"ace/mode/json",
value: value||"",
mode:"ace/mode/json",
readOnly: !!options.readOnly,
stateId: options.stateId,
focus: true
});
@ -576,7 +607,7 @@
var raw = expressionEditor.getValue().trim() ||"{}";
try {
var parsed = JSON.parse(raw);
rootNode = handleItem(null,parsed,0,null);
rootNode = handleItem(null,parsed,0,null,options.readOnly);
rootNode.class = "red-ui-editor-type-json-root-node"
list.treeList('data',[rootNode]);
} catch(err) {
@ -594,12 +625,12 @@
tabs.addTab({
id: 'json-raw',
label: RED._('jsonEditor.rawMode'),
label: options.readOnly ? RED._('jsonEditor.rawMode-readonly') : RED._('jsonEditor.rawMode'),
content: $("#red-ui-editor-type-json-tab-raw")
});
tabs.addTab({
id: 'json-ui',
label: RED._('jsonEditor.uiMode'),
label: options.readOnly ? RED._('jsonEditor.uiMode-readonly') : RED._('jsonEditor.uiMode'),
content: $("#red-ui-editor-type-json-tab-ui")
});
finishedBuild = true;

View File

@ -701,6 +701,10 @@ div.red-ui-button-small.red-ui-color-picker-opacity-slider-handle {
border-color: $list-item-background-hover;
border-style: dashed;
}
&.readonly {
cursor: pointer;
pointer-events: none;
}
}
.red-ui-editor-type-json-editor-item-gutter {
width: 48px;
@ -720,6 +724,10 @@ div.red-ui-button-small.red-ui-color-picker-opacity-slider-handle {
> span, > button {
display: none;
}
&.readonly {
cursor: pointer;
pointer-events: none;
}
}

View File

@ -0,0 +1,202 @@
const os = require('os');
const fs = require('fs');
let runtime;
let isContainerCached;
let isWSLCached;
const isInWsl = () => {
if (isWSLCached === undefined) {
isWSLCached = getIsInWSL();
}
return isWSLCached;
function getIsInWSL() {
if (process.platform !== 'linux') {
return false;
}
try {
if (os.release().toLowerCase().includes('microsoft')) {
if (isInContainer()) {
return false;
}
return true;
}
return fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft') ? !isInContainer() : false;
} catch (_) {
return false;
}
}
};
const isInContainer = () => {
if (isContainerCached === undefined) {
isContainerCached = hasDockerEnv() || hasDockerCGroup();
}
return isContainerCached;
function hasDockerEnv() {
try {
fs.statSync('/.dockerenv');
return true;
} catch {
return false;
}
}
function hasDockerCGroup() {
try {
const s = fs.readFileSync('/proc/self/cgroup', 'utf8');
if (s.includes('docker')) {
return "docker"
} else if (s.includes('kubepod')) {
return "kubepod"
} else if (s.includes('lxc')) {
return "lxc"
}
} catch {
return false;
}
}
}
function buildDiagnosticReport(scope, callback) {
const modules = {};
const nl = runtime.nodes.getNodeList();
for (let i = 0; i < nl.length; i++) {
if (modules[nl[i].module]) {
continue;
}
modules[nl[i].module] = nl[i].version
}
const now = new Date();
const {locale, timeZone} = Intl.DateTimeFormat().resolvedOptions();
const report = {
report: "diagnostics",
scope: scope,
time: {
utc: now.toUTCString(),
local: now.toLocaleString(),
},
intl: {
locale, timeZone
},
nodejs: {
version: process.version,
arch: process.arch,
platform: process.platform,
memoryUsage: process.memoryUsage(),
},
os: {
containerised: isInContainer(),
wsl: isInWsl(),
totalmem: os.totalmem(),
freemem: os.freemem(),
arch: os.arch(),
loadavg: os.loadavg(),
platform: os.platform(),
release: os.release(),
type: os.type(),
uptime: os.uptime(),
version: os.version(),
},
runtime: {
isStarted: runtime.isStarted(),
modules: modules,
version: runtime.settings.version,
settings: {
available: runtime.settings.available(),
apiMaxLength: runtime.settings.apiMaxLength || "UNSET",
//coreNodesDir: runtime.settings.coreNodesDir,
disableEditor: runtime.settings.disableEditor,
contextStorage: listContextModules(),
debugMaxLength: runtime.settings.debugMaxLength || "UNSET",
editorTheme: runtime.settings.editorTheme || "UNSET",
flowFile: runtime.settings.flowFile || "UNSET",
mqttReconnectTime: runtime.settings.mqttReconnectTime || "UNSET",
serialReconnectTime: runtime.settings.serialReconnectTime || "UNSET",
adminAuth: runtime.settings.adminAuth ? "SET" : "UNSET",
httpAdminRoot: runtime.settings.httpAdminRoot || "UNSET",
httpAdminCors: runtime.settings.httpAdminCors ? "SET" : "UNSET",
httpNodeAuth: runtime.settings.httpNodeAuth ? "SET" : "UNSET",
httpNodeRoot: runtime.settings.httpNodeRoot || "UNSET",
httpNodeCors: runtime.settings.httpNodeCors ? "SET" : "UNSET",
httpStatic: runtime.settings.httpStatic ? "SET" : "UNSET",
httpStaticRoot: runtime.settings.httpStaticRoot || "UNSET",
httpStaticCors: runtime.settings.httpStaticCors ? "SET" : "UNSET",
uiHost: runtime.settings.uiHost ? "SET" : "UNSET",
uiPort: runtime.settings.uiPort ? "SET" : "UNSET",
userDir: runtime.settings.userDir ? "SET" : "UNSET",
}
}
}
// if (scope == "admin") {
// const moreSettings = {
// adminAuth_type: (runtime.settings.adminAuth && runtime.settings.adminAuth.type) ? runtime.settings.adminAuth.type : "UNSET",
// httpAdminCors: runtime.settings.httpAdminCors ? runtime.settings.httpAdminCors : "UNSET",
// httpNodeCors: runtime.settings.httpNodeCors ? runtime.settings.httpNodeCors : "UNSET",
// httpStaticCors: runtime.settings.httpStaticCors ? "SET" : "UNSET",
// settingsFile: runtime.settings.settingsFile ? runtime.settings.settingsFile : "UNSET",
// uiHost: runtime.settings.uiHost ? runtime.settings.uiHost : "UNSET",
// uiPort: runtime.settings.uiPort ? runtime.settings.uiPort : "UNSET",
// userDir: runtime.settings.userDir ? runtime.settings.userDir : "UNSET",
// }
// const moreNodejs = {
// execPath: process.execPath,
// pid: process.pid,
// }
// const moreOs = {
// cpus: os.cpus(),
// homedir: os.homedir(),
// hostname: os.hostname(),
// networkInterfaces: os.networkInterfaces(),
// }
// report.runtime.settings = Object.assign({}, report.runtime.settings, moreSettings);
// report.nodejs = Object.assign({}, report.nodejs, moreNodejs);
// report.os = Object.assign({}, report.os, moreOs);
// }
callback(report);
/** gets a sanitised list containing only the module name */
function listContextModules() {
const keys = Object.keys(runtime.settings.contextStorage);
const result = {};
keys.forEach(e => {
result[e] = {
module: String(runtime.settings.contextStorage[e].module)
}
})
return result;
}
}
module.exports = {
init: function (_runtime) {
runtime = _runtime;
},
/**
* Gets the node-red diagnostics report
* @param {{scope: string}} opts - settings
* @return {Promise} the diagnostics information
* @memberof @node-red/diagnostics
*/
get: async function (opts) {
return new Promise(function (resolve, reject) {
opts = opts || {}
try {
runtime.log.audit({ event: "diagnostics.get", scope: opts.scope }, opts.req);
buildDiagnosticReport(opts.scope, (report) => resolve(report));
} catch (error) {
error.status = 500;
reject(error);
}
})
},
}

View File

@ -29,6 +29,7 @@ var api = module.exports = {
api.projects.init(runtime);
api.context.init(runtime);
api.plugins.init(runtime);
api.diagnostics.init(runtime);
},
comms: require("./comms"),
@ -39,6 +40,7 @@ var api = module.exports = {
projects: require("./projects"),
context: require("./context"),
plugins: require("./plugins"),
diagnostics: require("./diagnostics"),
isStarted: async function(opts) {
return runtime.isStarted();

View File

@ -142,6 +142,13 @@ var api = module.exports = {
}
safeSettings.flowEncryptionType = runtime.nodes.getCredentialKeyType();
safeSettings.diagnostics = {
//unless diagnostics.ui and diagnostics.enabled are explicitly false, they will default to true.
enabled: (runtime.settings.diagnostics && runtime.settings.diagnostics.enabled === false) ? false : true,
ui: (runtime.settings.diagnostics && runtime.settings.diagnostics.ui === false) ? false : true
}
runtime.settings.exportNodeSettings(safeSettings);
runtime.plugins.exportPluginSettings(safeSettings);
}

View File

@ -400,6 +400,11 @@ module.exports = {
*/
version: externalAPI.version,
/**
* @memberof @node-red/diagnostics
*/
diagnostics:externalAPI.diagnostics,
storage: storage,
events: events,
hooks: hooks,

View File

@ -229,5 +229,12 @@ module.exports = {
* @see @node-red/editor-api_auth
* @memberof node-red
*/
auth: api.auth
auth: api.auth,
/**
* The editor authentication api.
* @see @node-red/editor-api_auth
* @memberof node-red
*/
get diagnostics() { return api.diagnostics }
};

View File

@ -242,6 +242,7 @@ module.exports = {
/*******************************************************************************
* Runtime Settings
* - lang
* - diagnostics
* - logging
* - contextStorage
* - exportGlobalContextKeys
@ -254,6 +255,19 @@ module.exports = {
*/
// lang: "de",
/** Configure diagnostics options
* - enabled: When `enabled` is `true` (or unset), diagnostics data will
* be available at http://localhost:1880/diagnostics
* - ui: When `ui` is `true` (or unset), the action `show-system-info` will
* be available to logged in users of node-red editor
*/
diagnostics: {
/** enable or disable diagnostics endpoint. Must be set to `false` to disable */
enabled: true,
/** enable or disable diagnostics display in the node-red editor. Must be set to `false` to disable */
ui: true,
},
/** Configure the logging output */
logging: {
/** Only console logging is currently supported */

View File

@ -0,0 +1,119 @@
const should = require("should");
const request = require('supertest');
const express = require('express');
const bodyParser = require("body-parser");
const sinon = require('sinon');
let app;
const NR_TEST_UTILS = require("nr-test-utils");
const diagnostics = NR_TEST_UTILS.require("@node-red/editor-api/lib/admin/diagnostics");
describe("api/editor/diagnostics", function() {
before(function() {
app = express();
app.use(bodyParser.json());
app.get("/diagnostics",diagnostics.getReport);
});
it('returns the diagnostics report when explicitly enabled', function(done) {
const settings = { diagnostics: { ui: true, enabled: true } }
const runtimeAPI = {
diagnostics: {
get: async function (opts) {
return new Promise(function (resolve, reject) {
opts = opts || {}
try {
resolve({ opts: opts, a:1, b:2});
} catch (error) {
error.status = 500;
reject(error);
}
})
}
}
}
diagnostics.init(settings, runtimeAPI);
request(app)
.get("/diagnostics")
.expect(200)
.end(function(err,res) {
if (err || typeof res.error === "object") {
return done(err || res.error);
}
res.should.have.property("statusCode",200);
res.body.should.have.property("a",1);
res.body.should.have.property("b",2);
done();
});
});
it('returns the diagnostics report when not explicitly enabled (implicitly enabled)', function(done) {
const settings = { diagnostics: { enabled: undefined } }
const runtimeAPI = {
diagnostics: {
get: async function (opts) {
return new Promise(function (resolve, reject) {
opts = opts || {}
try {
resolve({ opts: opts, a:3, b:4});
} catch (error) {
error.status = 500;
reject(error);
}
})
}
}
}
diagnostics.init(settings, runtimeAPI);
request(app)
.get("/diagnostics")
.expect(200)
.end(function(err,res) {
if (err || typeof res.error === "object") {
return done(err || res.error);
}
res.should.have.property("statusCode",200);
res.body.should.have.property("a",3);
res.body.should.have.property("b",4);
done();
});
});
it('should error when setting is disabled', function(done) {
const settings = { diagnostics: { ui: true, enabled: false } }
const runtimeAPI = {
diagnostics: {
get: async function (opts) {
return new Promise(function (resolve, reject) {
opts = opts || {}
try {
resolve({ opts: opts});
} catch (error) {
error.status = 500;
reject(error);
}
})
}
}
}
diagnostics.init(settings, runtimeAPI);
request(app)
.get("/diagnostics")
.expect(403)
.end(function(err,res) {
if (!err && typeof res.error !== "object") {
return done(new Error("accessing diagnostics endpoint while disabled should raise error"));
}
res.should.have.property("statusCode",403);
res.body.should.have.property("message","diagnostics are disabled");
res.body.should.have.property("code","diagnostics.disabled");
done();
});
});
});

View File

@ -0,0 +1,126 @@
var should = require("should");
var sinon = require("sinon");
var NR_TEST_UTILS = require("nr-test-utils");
var diagnostics = NR_TEST_UTILS.require("@node-red/runtime/lib/api/diagnostics")
var mockLog = () => ({
log: sinon.stub(),
debug: sinon.stub(),
trace: sinon.stub(),
warn: sinon.stub(),
info: sinon.stub(),
metric: sinon.stub(),
audit: sinon.stub(),
_: function() { return "abc"}
})
describe("runtime-api/diagnostics", function() {
describe("get", function() {
before(function() {
diagnostics.init({
isStarted: () => true,
nodes: {
getNodeList: () => [{module:"node-red", version:"9.9.9"},{module:"node-red-node-inject", version:"8.8.8"}]
},
settings: {
version: "7.7.7",
available: () => true,
//apiMaxLength: xxx, deliberately left blank. Should arrive in report as "UNSET"
debugMaxLength: 1111,
disableEditor: false,
flowFile: "flows.json",
mqttReconnectTime: 321,
serialReconnectTime: 432,
adminAuth: {},//should be sanitised to "SET"
httpAdminRoot: "/admin/root/",
httpAdminCors: {},//should be sanitised to "SET"
httpNodeAuth: {},//should be sanitised to "SET"
httpNodeRoot: "/node/root/",
httpNodeCors: {},//should be sanitised to "SET"
httpStatic: "/var/static/",//should be sanitised to "SET"
httpStaticRoot: "/static/root/",
httpStaticCors: {},//should be sanitised to "SET"
uiHost: "something.secret.com",//should be sanitised to "SET"
uiPort: 1337,//should be sanitised to "SET"
userDir: "/var/super/secret/",//should be sanitised to "SET",
contextStorage: {
default : { module: "memory" },
file: { module: "localfilesystem" },
secured: { module: "secure_store", user: "fred", pass: "super-duper-secret" },
},
editorTheme: {}
},
log: mockLog()
});
})
it("returns basic user settings", function() {
return diagnostics.get({scope:"fake_scope"}).then(result => {
should(result).be.type("object");
//result.xxxxx
Object.keys(result)
const reportPropCount = Object.keys(result).length;
reportPropCount.should.eql(7);//ensure no more than 7 keys are present in the report (avoid leakage of extra info)
result.should.have.property("report","diagnostics");
result.should.have.property("scope","fake_scope");
result.should.have.property("time").type("object");
result.should.have.property("intl").type("object");
result.should.have.property("nodejs").type("object");
result.should.have.property("os").type("object");
result.should.have.property("runtime").type("object");
//result.runtime.xxxxx
const runtimeCount = Object.keys(result.runtime).length;
runtimeCount.should.eql(4);//ensure no more than 4 keys are present in runtime
result.runtime.should.have.property('isStarted',true)
result.runtime.should.have.property('modules').type("object");
result.runtime.should.have.property('settings').type("object");
result.runtime.should.have.property('version','7.7.7');
//result.runtime.modules.xxxxx
const moduleCount = Object.keys(result.runtime.modules).length;
moduleCount.should.eql(2);//ensure no more than the 2 modules specified are present
result.runtime.modules.should.have.property('node-red','9.9.9');
result.runtime.modules.should.have.property('node-red-node-inject','8.8.8');
//result.runtime.settings.xxxxx
const settingsCount = Object.keys(result.runtime.settings).length;
settingsCount.should.eql(21);//ensure no more than the 21 settings listed below are present in the settings object
result.runtime.settings.should.have.property('available',true);
result.runtime.settings.should.have.property('apiMaxLength', "UNSET");//deliberately disabled to ensure UNSET is returned
result.runtime.settings.should.have.property('debugMaxLength', 1111);
result.runtime.settings.should.have.property('disableEditor', false);
result.runtime.settings.should.have.property('editorTheme', {});
result.runtime.settings.should.have.property('flowFile', "flows.json");
result.runtime.settings.should.have.property('mqttReconnectTime', 321);
result.runtime.settings.should.have.property('serialReconnectTime', 432);
result.runtime.settings.should.have.property("adminAuth", "SET"); //should be sanitised to "SET"
result.runtime.settings.should.have.property("httpAdminCors", "SET"); //should be sanitised to "SET"
result.runtime.settings.should.have.property('httpAdminRoot', "/admin/root/");
result.runtime.settings.should.have.property("httpNodeAuth", "SET"); //should be sanitised to "SET"
result.runtime.settings.should.have.property("httpNodeCors", "SET"); //should be sanitised to "SET"
result.runtime.settings.should.have.property('httpNodeRoot', "/node/root/");
result.runtime.settings.should.have.property("httpStatic", "SET"); //should be sanitised to "SET"
result.runtime.settings.should.have.property('httpStaticRoot', "/static/root/");
result.runtime.settings.should.have.property("httpStaticCors", "SET"); //should be sanitised to "SET"
result.runtime.settings.should.have.property("uiHost", "SET"); //should be sanitised to "SET"
result.runtime.settings.should.have.property("uiPort", "SET"); //should be sanitised to "SET"
result.runtime.settings.should.have.property("userDir", "SET"); //should be sanitised to "SET"
result.runtime.settings.should.have.property('contextStorage').type("object");
//result.runtime.settings.contextStorage.xxxxx
const contextCount = Object.keys(result.runtime.settings.contextStorage).length;
contextCount.should.eql(3);//ensure no more than the 3 settings listed below are present in the contextStorage object
result.runtime.settings.contextStorage.should.have.property('default', {module:"memory"});
result.runtime.settings.contextStorage.should.have.property('file', {module:"localfilesystem"});
result.runtime.settings.contextStorage.should.have.property('secured', {module:"secure_store"}); //only module should be present, other fields are dropped for security
})
})
});
});