Павло Щербуха

Logo

Персональна освітня сорінка

Розробка на Node.js, VUE.js, Python, IBM Integration Bus (App Connect Ent) , ORACLE PL/SQL
8 September 2023

Node-RED How to create custom node

by Pavlo Shcherbukha

Про що цей блог

Працюючи з Node-Red мені знадобилося зробити логування в JSON спеціальної структури. Переглянув я існуючі node - вони мене не влаштували. Вирішив зробити свою. До цього я раніше їх не робив, але, як виявилося, все не так складно. За 2-3 дні уже більш-менш запрацювало
Приклад можна знайти в моєму github repo В цьому блозі я ділюся доствідом, як створювати custom node. Фактично, мені прийшлося створити дві Node:

Єдине, що я не зробив, так це не освоїв unit тестів для нової node. Але, сподіваюся я це зроблю в недалекому майбутньому.

По факту, я зробив собі custom node, що логує мені роботу flows в JSON структуру, використовуючи winston логер. Найближчим до моєї ідеє є /node-red-contrib-flogger.

Розібравшись з цим підходом, я розумію, що в мене не буде великих проблем написати якийсь власний конектор до бази даних чи черги чи якогось іншого сервера. Підходи я відпрацював на цьому простому прикладі.

2. Формалізація задачі та показ результатів, що вийшли

Мені потрібно розробити custon Node, яка буде пропускати повідомлення “через себе” та логувати в файл або в консоль записи у вигляді json, а саме:

Конфігурація Node розділяється на 2 розділи:

До глобальних налаштувань відносяться такі налаштування:

До локальних налаштувань відносяться:

Структура лога повинна бути приблизно така:

[
    {
        "hostname": "localhost",
        "label": "InReq",
        "level": "info",
        "message": {
            "flow_context": {},
            "global_context": {},
            "payload": {
                "id": "450"
            }
        },
        "timestamp": "2023-09-08T19:58:35.874874+03:00"
    },
    {
        "hostname": "localhost",
        "label": "GET-USER",
        "level": "info",
        "message": {
            "flow_context": {},
            "global_context": {},
            "payload": {
                "fullname": "Petro Petrenko",
                "id": "150",
                "phone": "222-33-44"
            }
        },
        "timestamp": "2023-09-08T19:59:02.278278+03:00"
    }
]

На pic-01 показано простенький flow з Noda-ми логування.

pic-01

А на pic-02 показана параметризація вузлів логування. Як видно з малюнка, запараметризовано запис логів у файли, але помилки пишуться в окремий файл, а інші повідомлення в інший файл.

pic-02

В самому правому віконці показана конфігурація глобального конфігуратора, коли логування виконується в консоль.

3. Покрокова інструкція, як створювати nodes

Перед тим, як програмувати бізнес логіку, потрібно, на мій погляд, визначитися з параметрами конфігурації та логікою UI, що дозволяє ввести ці конфігураційні параметри. Починати потрібно з цього. Ну а далі показую покроково як я це робив.

3.1. Створення порожньої бібліотеки

Створимо вузол wnode. Перше, що робимо, це npm init та вводимо параметри, що запитуються pic-03. У вас створиться порожній package.json Тут головне, вказати найменування пакету, ключ name.

pic-03

Далі, реєструємо node-red вузол wnode та .js файл, що зберігає бізнес логіку

  "node-red": {
    "nodes": {
      "wnode": "wnode.js"
    }
  },

В результаті отримаємо такий от package.json

{
  "name": "wnode",
  "version": "1.0.0",
  "description": "wnode-тестова custom node (demo)",
  "main": "wnode.js",
  "scripts": {
    "test": "test"
  },
  "node-red": {
    "nodes": {
      "wnode": "wnode.js"
    }
  },
  "author": "pasha",
  "license": "ISC"
}

Після цього в цьому ж каталозі встановлюємо потрібні залежності стандартною командою npm install. Нижче показано, як я додаю бібліотеку winston

npm install winston

Далі створюємо 2 файли:

3.1.1. Порожній node.js файл

Далі показано порожній .js файл в якому існує дві node`s: WNode - в якій буде сконцентрована бізнеслогіка та WConfigNode в якій буде сконцентрована логіка конфігурації.

    module.exports = function(RED) {
        function WNode(config) {
            var winston = require('./winston');
            
            RED.nodes.createNode(this,config);
            var node = this;
            this.filename = config.filename;
            node.on('input', function(msg) {
                msg.payload = msg.payload
                node.send(msg);
            });
        }

        RED.nodes.registerType("w-node",WNode);

        function WConfigNode(n) {
            RED.nodes.createNode(this,n)
        }

        RED.nodes.registerType("w-config", WConfigNode)

    }

Тут треба звернути увагу на такі особливості:

3.1.2. Підготовка html файлу конфігураційної Node

Параметр confg наповнюється через html файл, wnode.html. В цьому файлі є два обов’язкових розділи:

<script type="text/javascript">
	RED.nodes.registerType('w-config',{

		category: 'config',

		defaults: {
            configname: {value:"", required:true},
            logto: {value:"",required:true},            
			logname: {value:"",required:false},
			logdir: {value:"",required:false},
		},

		label: function() {
			return this.configname
		},

		oneditprepare: function() {
            $("#node-config-input-logto").change(function() {
      			if ($(this).val() === "file") {
                    $("#node-config-fileprop").show()
      			} else {
                    $("#node-config-fileprop").hide()  
                }

		    })
		},

		oneditsave: function() {
		}
	})
</script>

<script type="text/x-red" data-template-name="w-config">
    <div class="form-row">
        <label for="node-config-input-configname"><i class="fa fa-tag"></i>Config name</label>
        <input type="text" id="node-config-input-configname" placeholder="Config name">
    </div>
    <div class="form-row">
		<label for="node-config-input-logto"><i class="fa fa-clock-o"></i> Log to</label>
		<select type="text" id="node-config-input-logto" placeholder="Log to">
			<option value="file">to File</option>
			<option value="console">to console</option>
		</select>
	</div>
    <div id="node-config-fileprop">
        <div id="node-config-input-logname-f" class="form-row">
            <label for="node-config-input-logname"><i class="fa fa-tag"></i> Log file name</label>
            <input type="text" id="node-config-input-logname" placeholder="Log file name">
        </div>
        <div id="node-config-input-logdir-f" class="form-row">
            <label for="node-config-input-logdir"><i class="fa fa-tag"></i> Log dir</label>
            <input type="text" id="node-config-input-logdir" placeholder="Log dir">
        </div>
    </div>    

</script>

На прикладі вище, показана конфігураційна NODE. Основна відмінність від звичайної, це наявність рядка:

 	category: 'config',

та посиланню на конфігураційні поля передує префікс node-config-input-. Далі, в роздід defaults описуються конфігураційні поля. В роздіді oneditprepare (по суті це реація на event on_edit_prepare) можна управляти видимістю заданих на реєстрацію полів. Зверніть увагу на div


    <div id="node-config-fileprop">
        <div id="node-config-input-logname-f" class="form-row">
            <label for="node-config-input-logname"><i class="fa fa-tag"></i> Log file name</label>
            <input type="text" id="node-config-input-logname" placeholder="Log file name">
        </div>
        <div id="node-config-input-logdir-f" class="form-row">
            <label for="node-config-input-logdir"><i class="fa fa-tag"></i> Log dir</label>
            <input type="text" id="node-config-input-logdir" placeholder="Log dir">
        </div>
    </div>    

та умови при яких яких відображається чи не вібражається даний div в залежності від вибраного значення в полі “node-config-input-logto” === defaults.logto

		oneditprepare: function() {
            $("#node-config-input-logto").change(function() {
      			if ($(this).val() === "file") {
                    $("#node-config-fileprop").show()
      			} else {
                    $("#node-config-fileprop").hide()  
                }

		    })
		},

Крім того, слід зауважити, що комбінація defaults.configname та динамічне присвоєння найменування config node label: function() {return this.configname} створюють такий ефект, що можна зберегти кілька конфігурацій і, потім, розробляючи flow можна між ними переключатися.

		defaults: {
            configname: {value:"", required:true},
		},

		label: function() {
			return this.configname
		},


3.1.3. Підготовка html файлу основної Node

Розділ для основної node показано далі. Тут, як бачимо, все дуже схоже до попереднього розділу,

<script type="text/javascript">
    RED.nodes.registerType('w-node',{
        category: 'output',
        color: '#FFAAAA',
        icon: "file.png",
        defaults: {
            name: {value:""},
            loglabel: {value:"",required:true},
            loglevel: {value:"INFO", required: true},
            configname: {type:"w-config"},
        },
        inputs:1,
        outputs:1,
        label: function() {
            return this.name||"w-node";
        }
    });
</script>

<script type="text/html" data-template-name="w-node">
    <div class="form-row">
        <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
        <input type="text" id="node-input-name" placeholder="Name">
    </div>
    <div class="form-row">
        <label for="node-input-loglabel"><i class="fa fa-tag"></i>Log Label</label>
        <input type="text" id="node-input-loglabel" placeholder="LogLabel">
    </div>
	<div class="form-row">
		<label for="node-input-loglevel"><i class="fa fa-exclamation"></i> Level</label>
		<select type="text" id="node-input-loglevel" placeholder="Log Level">
			<option value="ERROR">ERROR</option>
			<option value="WARN">WARN</option>
			<option value="INFO">INFO</option>
			<option value="DEBUG">DEBUG</option>
			<option value="TRACE">TRACE</option>
		</select>
	</div>
    <div class="form-row">
		<label for="node-input-configname"><i class="fa fa-cogs"></i> Log Config</label>
		<input type="text" id="node-input-configname" placeholder="Log Configuration">
	</div>

   
</script>

але є деякі відмінності:

Коли всі ці файли підготовані можна спробувати підключити пакет до Node-RED

3.2 Підключеня пакету до node-red

Для цого потрібно повернутися в каталог де інстальовано Node RED та виконати команду npm install але з посиланням на каталог вашої node ,а не на npm репозиторій:

npm i /wnode

В результаті ваш пакет буде включено в основний package.json тільки з ознакою “file:”: ** “wnode”: “file:wnode”,**

{
  "name": "node-red-test01",
  "version": "1.0.0",
  "description": "testing nodre red development and deployment on openshift",
  "main": "index.js",
  "scripts": {
    "dev": "node  node_modules/node-red/red.js --settings ./dev/settings.js --userDir .  --port 1880 --verbose user-registration.json",
    "test": "node  node_modules/node-red/red.js --settings ./test/settings.js --userDir .  --port 8080 --verbose user-registration.json",
    "devloader": "node  node_modules/node-red/red.js --settings ./dev/settings.js --userDir .  --port 1880 --verbose loader.json",
    "testloader": "node  node_modules/node-red/red.js --settings ./test/settings.js --userDir .  --port 8080 --verbose loader.json",
    "start": "node  node_modules/node-red/red.js --settings ./prod/settings.js --userDir . --verbose --port 8080 $FLOW_NAME"
  },
  "author": "psh",
  "license": "ISC",
  "dependencies": {
    "node-red": "^3.0.2",
    "wnode": "file:wnode",
    "wshlog": "file:wshlog"
  }
}


Тепер запукаємо наш тестовий flow і дивимося що буде


npm run dev

Flow доступний за URL http://localhost:1880

Ну а на pic-04 показно простенький приклад, що з того всього вийшло

pic-04

Детально з прикладом можна ознайомитися за лінком: node-red-custom-node.git

tags: