In one of our projects, we used IPC (inter-process communication) on sockets. Quite a large project, a trading bot, where there were many modules that interacted with each other. As the complexity grew, it became a question of monitoring what was happening in microservices. We decided to create our own application for tracking data flow using only two libraries react and redoor . I would like to share our approach with you.
Microservices exchange JSON objects with each other, with two fields: name and data. The name is the identifier to which service the object is intended and the data field is the payload. Example:
{ name:'ticket_delete', data:{id:1} }
Since the service is quite crude and the protocols changed every week, so monitoring should be as simple and modular as possible. Accordingly, in the application, each module must display the data intended for it, and so by adding, deleting data, we must get a set of independent modules for monitoring processes in microservices.
So, let's begin. For example, let's make a simple application and a web server. The application will consist of three modules. They are indicated by dotted lines in the picture. Timer, statistics and statistics control buttons.
Let's create a simple Web Socket server.
/** 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,
})
}
So, we got three independent modules, where each module monitors only its own event and displays only it. Which is quite convenient when the structure and protocols at the prototyping stage are not yet completely clear. It is only necessary to add that since all events have an end-to-end structure, we must clearly adhere to the template for creating an event, we have chosen the following for ourselves: ( MODULEN AME)_(FUNCTION NAME)_(VAR NAME)
.
Hope it was helpful. The source codes of the project, as usual, are on the github.