React+Redoor IPC мониторинг

от автора

В одном из наших проектов, мы использовали IPC (inter-process communication) на сокетах. Довольно большой проект, торгового бота, где были множество модулей которые взаимодействовали друг с другом.  По мере роста сложности стал вопрос о мониторинге, что происходит в микросервисах.  Мы решили создать свое приложение для отслеживания, потока данных на всего двух библиотеках react и redoor. Я хотел бы поделиться с вами нашим подходом.

Микросервисы обмениваются между собой JSON объектами, с двумя полями: имя и данные. Имя — это идентификатор какому сервису предназначается объект и поле данные — полезная нагрузка. Пример:

{ name:'ticket_delete', data:{id:1} }

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

И так начнем. Для примера сделаем простейшее приложение и веб сервер. Приложение будет состоять из трех модулей. На картинке они обозначены пунктирными линиями. Таймер, статистика и кнопки управления статистикой.

Создадим простой Web Socket сервер.

/** src/ws_server/echo_server.js */  const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8888 });  function sendToAll( data) {   let str = JSON.stringify(data);   wss.clients.forEach(function each(client) {     client.send(str);   }); }  // Отправляем данные каждую секунду setInterval(e=>{   let d = new Date();   let H = d.getHours();   let m = ('0'+d.getMinutes()).substr(-2);   let s = ('0'+d.getSeconds()).substr(-2);   let time_str = `${H}:${m}:${s}`;   sendToAll({name:'timer', data:{time_str}}); },1000);

Сервер каждую секунду формирует строку с датой и отправляет всем подключившимся клиентам. Открываем консоль и запускаем сервер:

node src/ws_server/echo_server.js

Теперь перейдем к проекту приложения. Для сборки и отладки будем использовать rollup конфигурация ниже.

rollup.config.js
import serve from 'rollup-plugin-serve'; import babel from '@rollup/plugin-babel'; import { nodeResolve } from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import hmr from 'rollup-plugin-hot' import postcss from 'rollup-plugin-postcss'; import autoprefixer from 'autoprefixer' import replace from '@rollup/plugin-replace';  const browsers = [  "last 2 years",  "> 0.1%",  "not dead"]  let is_production = process.env.BUILD === 'production';  const replace_cfg = {   'process.env.NODE_ENV': JSON.stringify( is_production ? 'production' : 'development' ),   preventAssignment:false, }  const babel_cfg = {     babelrc: false,     presets: [       [         "@babel/preset-env",         {           targets: {             browsers: browsers           },         }       ],       "@babel/preset-react"     ],     exclude: 'node_modules/**',     plugins: [       "@babel/plugin-proposal-class-properties",       ["@babel/plugin-transform-runtime", {          "regenerator": true       }],       [ "transform-react-jsx" ]     ],     babelHelpers: 'runtime' }   const cfg = {   input: [     'src/main.js',   ],   output: {     dir:'dist',     format: 'iife',     sourcemap: true,     exports: 'named',   },   inlineDynamicImports: true,   plugins: [     replace(replace_cfg),     babel(babel_cfg),     postcss({       plugins: [         autoprefixer({           overrideBrowserslist: browsers         }),       ]     }),     commonjs({         sourceMap: true,     }),     nodeResolve({         browser: true,         jsnext: true,         module: false,     }),     serve({       open: false,       host: 'localhost',       port: 3000,     }),   ], } ;   export default cfg; 

Точка входа нашего проекта main.js создадим его.

/** src/main.js */ import React, { createElement, Component, createContext } from 'react'; import ReactDOM from 'react-dom'; import {Connect, Provider} from './store' import Timer from './Timer/Timer'  const Main = () => (   <Provider>     <h1>ws stats</h1>     <Timer/>   </Provider> ); const root = document.body.appendChild(document.createElement("DIV")); ReactDOM.render(<Main />, root);

Теперь создадим стор для нашего проекта

/** src/store.js */ import React, { createElement, Component, createContext } from 'react'; import createStoreFactory from 'redoor'; import * as actionsWS from './actionsWS' import * as actionsTimer from './Timer/actionsTimer'  const createStore = createStoreFactory({Component, createContext, createElement}); const { Provider, Connect } = createStore(   [     actionsWS,     // websocket actions     actionsTimer,  // Timer actions   ] ); export { Provider, Connect };

Прежде чем создавать модуль таймера нам надо получать данные от сервера. Создадим акшнес файл для работы с сокетом.

/** src/actionsWS.js */ export const  __module_name = 'actionsWS' let __emit; // получаем функцию emit от redoor export const bindStateMethods = (getState, setState, emit) => {   __emit = emit }; // подключаемся к серверу let wss = new WebSocket('ws://localhost:8888') // получаем все сообщения от сервера и отправляем их в поток redoor wss.onmessage = (msg) => {   let d = JSON.parse(msg.data);   __emit(d.name, d.data); } 

Здесь надо остановиться поподробнее. Наши сервисы отправляют данные в виде объекта с полями: имя и данные. В библиотеке redoor можно так же создавать потоки событий в которые мы просто передаем данные и имя. Выглядит это примерно так:

    +------+     | emit | --- events --+--------------+----- ... ------+-------------> +------+              |              |                |                       v              v                v                  +----------+   +----------+     +----------+                  | actions1 |   | actions2 | ... | actionsN |                  +----------+   +----------+     +----------+

Таким образом каждый модуль имеет возможность «слушать» свои события и по надобности и чужие тоже.

Теперь создадим собственно сам модуль таймера. В папке Timer создадим два файла Timer.js и actionsTimer.js

/** src/Timer/Timer.js */  import React from 'react'; import {Connect} from '../store' import s from './Timer.module.css'  const Timer = ({timer_str}) => <div className={s.root}>   {timer_str} </div>  export default Connect(Timer);

Здесь все просто, таймер берет из глобального стейта timer_str который обновляется в actionsTimer.js. Функция Connect подключает модуль к redoor.

/** src/Timer/actionsTimer.js */ export const  __module_name = 'actionsTimer' let __setState;  // получаем метод для обновления стейта export const bindStateMethods = (getState, setState) => {   __setState = setState; };  // инициализируем переменную таймера export const initState = {   timer_str:'' }  // "слушаем" поток событий нам нужен "timer" export const listen = (name,data) =>{   name === 'timer' && updateTimer(data); } // обновляем стейт  function updateTimer(data) {   __setState({timer_str:data.time_str}) }

В акшес файле, мы «слушаем» событие timer таймера (функция listen) и как только оно будет получено обновляем стейт и выводим строку с данными.

Подробнее о функциях redoor:

__module_name — зарезервированная переменная нужна просто для отладки она сообщает в какой модуль входят акшенсы.

bindStateMethods — функция для получения setState, поскольку данные приходят асинхронно нам надо получить в локальных переменных функцию обновления стейта.

initState — функция или объект инициализации данных модуля в нашем случае это timer_str

listen— функция в которую приходят все события сгенерированные redoor.

Готово. Запускаем компиляцию и открываем браузер по адресу http://localhost:3000

npx rollup -c rollup.config.js --watch

Должны появиться часики с временем. Перейдём к более сложному. По аналогии с таймером добавим еще модуль статистики. Для начала добавим новый генератор данных в echo_server.js

/** src/ws_server/echo_server.js */  ... let g_interval = 1; // Данные статистики setInterval(e=>{   let stats_array = [];   for(let i=0;i<30;i++) {     stats_array.push((Math.random()*(i*g_interval))|0);   }   let data  = {     stats_array   }   sendToAll({name:'stats', data}); },500);  ... 

И добавим модуль в проект. Для этого создадим папку Stats в которой создадим Stats.js и actionsStats.js

/** src/Stats/Stats.js */ import React from 'react'; import {Connect} from '../store' import s from './Stats.module.css'  const Bar = ({h})=><div className={s.bar} style={{height:`${h}`px}}>   {h} </div>  const Stats = ({stats_array})=><div className={s.root}>   <div className={s.bars}>     {stats_array.map((it,v)=><Bar key={v} h={it} />)}   </div> </div>  export default Connect(Stats);
/** src/Stats/actionsStats.js */ export const  __module_name = 'actionsStats' let __setState = null;  export const bindStateMethods = (getState, setState, emit) => {   __setState = setState; }  export const initState = {   stats_array:[], }  export const listen = (name,data) =>{   name === 'stats' && updateStats(data); }  function updateStats(data) {   __setState({     stats_array:data.stats_array,   }) }

и подключаем новый модуль к стору

/** src/store.js */ ... import * as actionsStats from './Stats/actionsStats'  const { Provider, Connect } = createStore(   [     actionsWS,     actionsTimer,     actionsStats //<-- модуль Stats   ] ); ...

В итоге мы должны получить это:

Как видите модуль Stats принципиально не отличается от модуля Timer, только отображение не строки, а массива данных. Что если мы хотим не только получать данные, но и отправлять их на сервер? Добавим управление статистикой.

В нашем примере переменная g_interval это угловой коэффициент наклона нормировки случайной величины. Попробуем ей управлять с нашего приложения.

Добавим пару кнопок к графику статистики. Плюс будет увеличвать значение interval минус уменьшать.

/** src/Stats/Stats.js */ ... import Buttons from './Buttons' // импортируем модуль ... const Stats = ({cxRun, stats_array})=><div className={s.root}>   <div className={s.bars}>     {stats_array.map((it,v)=><Bar key={v} h={it} />)}   </div>   <Buttons/> {/*Модуль кнопочки*/} </div> ...

И сам модуль с кнопочками

/** src/Stats/Buttons.js */ import React from 'react'; import {Connect} from '../store' import s from './Stats.module.css'  const DATA_INTERVAL_PLUS = {   name:'change_interval',   interval:1 } const DATA_INTERVAL_MINUS = {   name:'change_interval',   interval:-1 }  const Buttons = ({cxEmit, interval})=><div className={s.root}>   <div className={s.btns}>       <button onClick={e=>cxEmit('ws_send',DATA_INTERVAL_PLUS)}>         plus       </button>        <div className={s.len}>interval:{interval}</div>        <button onClick={e=>cxEmit('ws_send',DATA_INTERVAL_MINUS)}>         minus       </button>   </div> </div>  export default Connect(Buttons);

Получаем панель с кнопочками:

И модифицируем actionsWS.js

/** src/actionsWS.js */ ...  let wss = new WebSocket('ws://localhost:8888')  wss.onmessage = (msg) => {   let d = JSON.parse(msg.data);   __emit(d.name, d.data); }  // "слушаем" событие отправить данные на сервер export const listen = (name,data) => {   name === 'ws_send' && sendMsg(data); } // отправляем данные function sendMsg(msg) {   wss.send(JSON.stringify(msg)) }

Здесь мы в модуле Buttons.js воспользовались встроенной функции (cxEmit) создания события в библиотеке redoor. Событие ws_send «слушает» модуль actionsWS.js. Полезная нагрузка data — это два объекта: DATA_INTERVAL_PLUS и DATA_INTERVAL_MINUS. Таким образам если нажать кнопку плюс на сервер будет отправлен объект { name:'change_interval', interval:1 }

На сервере добавляем

/** src/ws_server/echo_server.js */ ...  wss.on('connection', function onConnect(ws) {   // "слушаем" приложение на событие "change_interval"   // от модуля Buttons.js   ws.on('message', function incoming(data) {     let d = JSON.parse(data);     d.name === 'change_interval' && change_interval(d);   }); });  let g_interval = 1; // меняем интервал function change_interval(data) {   g_interval += data.interval;   // создаем событие, что интервал изменен   sendToAll({name:'interval_changed', data:{interval:g_interval}}); }  ... 

И последний штрих необходимо отразить изменение интервала в модуле Buttons.js. Для этого в actionsStats.js начнём слушать событие «interval_changed» и обновлять переменную interval

/** src/Stats/actionsStats.js */ ...  export const initState = {   stats_array:[],   interval:1 // добавляем переменную интервал }  export const listen = (name,data) =>{   name === 'stats' && updateStats(data);      // "слушаем" событие обновления интервала   name === 'interval_changed' && updateInterval(data); } // обнавляем интервал function updateInterval(data) {   __setState({     interval:data.interval,   }) }  function updateStats(data) {   __setState({     stats_array:data.stats_array,   }) } 

Итак, мы получили три независимых модуля, где каждый модуль следит только за своим событием и отображает только его. Что довольно удобно когда еще не ясна до конца структура и протоколы на этапе прототипирования. Надо только добавить, что поскольку все события имеют сквозную структуру то надо четко придерживаться шаблона создания события мы для себя выбрали такую: (MODULEN AME)_(FUNCTION NAME)_(VAR NAME).

Надеюсь было полезно. Исходные коды проекта, как обычно, на гитхабе.

ссылка на оригинал статьи https://habr.com/ru/post/557040/