LoginSignup
0
0

【React】React+Pythonでマイクロサービスの依存関係を可視化してみた

Last updated at Posted at 2023-11-26

やりたいこと

私の会社ではマイクロサービスアーキテクチャを採用している。
そこでサービス同士の複雑な依存関係を可視化してみたい。
※企業秘密の観点からサービス名はハッシュ値に置換してある。
service.gif

要件

  • 閲覧したいサービスの依存関係を矢印の向きとノードの色、線の種類(点線は切れても致命的ではない関係性)で表現したい。
  • サービスを検索できるようにしたい。
  • ノードがクリックされたらそのノードのサービスにまつわる依存関係を表示させたい。

システム

スクリーンショット 2023-11-26 21.34.48.png
現在表示されたものをキープしたまま追加でクリックされたノードにまつわる関係性を描写するために非同期処理が必要。よってフロントアプリとしてReactを用いる。正直Pythonなくても実現可能なのだが、私がReactが初めてのため、React側が欲しい形を可能な限りPyhton側で処理して渡したいのでバックエンドアプリとしてPythonを用いた。

Python

フレームワークとしてflaskを用いた。
データの取得元が社内のAPIサービスというのもあり、秘匿的な観点からPython部分は割愛する。(Qiita用にAPIレスポンス受信後のコードを編集するのが面倒😅)

ちなみにPython側からReact側に返すJSONはこんな感じ
例:サービスID314に関係する依存関係を取得したい場合

request
http://pythonアプリのエンドポイント/react/314
response
{
"edge_array": [
[
"314",
"358",
1
],
[
"314",
"926",
1
],
[
"314",
"935",
0
],
[
"314",
"1022",
0
],
[
"314",
"2435",
1
],
[
"63",
"314",
1
],
[
"144",
"314",
1
],
[
"222",
"314",
1
],
[
"345",
"314",
0
],
[
"351",
"314",
0
],
[
"352",
"314",
0
],
[
"533",
"314",
1
],
[
"536",
"314",
1
],
[
"551",
"314",
1
],
[
"748",
"314",
0
],
[
"802",
"314",
0
],
[
"807",
"314",
0
],
[
"808",
"314",
0
],
[
"1279",
"314",
0
],
[
"1488",
"314",
0
],
[
"1607",
"314",
0
],
[
"1614",
"314",
0
],
[
"1745",
"314",
0
],
[
"1851",
"314",
0
],
[
"2074",
"314",
1
],
[
"2353",
"314",
0
],
[
"2499",
"314",
0
],
[
"2580",
"314",
0
],
[
"2856",
"314",
0
],
[
"2932",
"314",
0
],
[
"2934",
"314",
0
]
],
"node_array": [
[
"314",
"314",
1
],
[
"358",
"358",
2
],
[
"926",
"926",
2
],
[
"935",
"935",
2
],
[
"1022",
"1022",
2
],
[
"2435",
"2435",
2
],
[
"63",
"63",
3
],
[
"144",
"144",
3
],
[
"222",
"222",
3
],
[
"345",
"345",
3
],
[
"351",
"351",
3
],
[
"352",
"352",
3
],
[
"533",
"533",
3
],
[
"536",
"536",
3
],
[
"551",
"551",
3
],
[
"748",
"748",
3
],
[
"802",
"802",
3
],
[
"807",
"807",
3
],
[
"808",
"808",
3
],
[
"1279",
"1279",
3
],
[
"1488",
"1488",
3
],
[
"1607",
"1607",
3
],
[
"1614",
"1614",
3
],
[
"1745",
"1745",
3
],
[
"1851",
"1851",
3
],
[
"2074",
"2074",
3
],
[
"2353",
"2353",
3
],
[
"2499",
"2499",
3
],
[
"2580",
"2580",
3
],
[
"2856",
"2856",
3
],
[
"2932",
"2932",
3
],
[
"2934",
"2934",
3
]
]
}

二つのオブジェクト"node_array"と"edge_array"を返している。

"node_array"はノードのリストである

"node_array": [["ノード", "ラベル", グループ], ...]

ラベルの箇所をこのQiita上ではノードと同じ番号記載にしてある。
本来はここに実際のサービス名がくるので各ノードはサービス名で表示される。
グループを指定すると同じグループ同士のノードの色を同じにすることができる。クリックされたサービスを橙色、クリックされたサービスが依存しているサービスを青色、クリックされたサービスに依存しているサービスを水色で表現した。

"edge_array"はノードとノードを結ぶ線のためのリストである

"edge_array": [["ノードA","ノードB",1],["ノードB","ノードC",0]

例えばこの場合はノードAからノードBに対して矢印が実践で接続され、ノードBからノードCに対して点線で矢印が接続される。

React

描写にVis.jsを利用。このVis.jsモジュールにノードオブジェクトとエッジオブジェクトを渡して描写している。

index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import GraphVisualization from './GraphVisualization';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
  <React.StrictMode>
    <GraphVisualization />
  </React.StrictMode>
);

reportWebVitals();
GraphVisualization.js
import React, { Component } from 'react';
import { DataSet, Network } from 'vis';
import axios from 'axios';
import Template from './Template';

class GraphVisualization extends Component {
    constructor(props) {
        super(props);
        this.state = 
            nodes: new DataSet([]),
            edges: new DataSet([]),
            clickedUnicornIdList: [],
        };
    }


    // エッジの中身を変更する処理
    updateEdgeContent = (nodeA, nodeB) => {

        const edgesToRemove = this.state.edges.get({
            filter: (edge) => (edge.from === nodeA && edge.to === nodeB) || (edge.from === nodeB && edge.to === nodeA)
        });

        edgesToRemove.forEach((edge) => {
            const edgeIdToRemove = edge.id;
            edge.length = 1500
            this.state.edges.update({ from: edge.from, to: edge.to, length: 1500 });
        });

        this.setState({ edges: this.state.edges });

    }

    // ノードがクリックされた場合の処理
    handleNodeClick = async (event) => {
        if (event.nodes.length > 0) {
            const clickedNodeId = event.nodes[0];
            const clickedNode = this.state.nodes.get(clickedNodeId);

            try {
                const response = await axios.get(`http://Python側のエンドポイント/react/${clickedNode.id}`);
                const nodeArray = response.data.node_array;

                for (const node of nodeArray) {
                    const nodeId = node[0];
                    const nodeName = node[1];
                    const group = node[2];

                    const isNodeIdIncluded = this.state.nodes.get().some(node => node.id === nodeId);

                    if (!isNodeIdIncluded) {
                        if (group == 2) {
                            this.state.nodes.add({ id: nodeId, label: nodeName, color: { background: 'paleturquoise', border: 'paleturquoise', highlight: { background: 'darkkhaki', border: 'darkkhaki' } } }); // 例: 新しいノードを追加
                        } else {
                            this.state.nodes.add({ id: nodeId, label: nodeName, color: { background: 'lightsteelblue', border: 'lightsteelblue', highlight: { background: 'darkkhaki', border: 'darkkhaki' } } }); // 例: 新しいノードを追加
                        }
                    }
                }

                const edgeArray = response.data.edge_array;
                for (const edge of edgeArray) {
                    const from = edge[0];
                    const to = edge[1];
                    const lineType = edge[2];

                    function findEdgeByFromTo(edges, fromNodeId, toNodeId) {
                        return edges.get().find(edge => edge.from === fromNodeId && edge.to === toNodeId);
                    }

                    const foundEdge = findEdgeByFromTo(this.state.edges, from, to);

                    if (!foundEdge) {
                        if (lineType === 1) {
                            this.state.edges.add({ from: from, to: to, arrows: 'to', dashes: true, label: 'No Effect from Disconnection', color: { highlight: 'red' } });
                        } else {
                            this.state.edges.add({ from: from, to: to, arrows: 'to', color: 'bule', color: { highlight: 'red' } });
                        }
                    }
                }

                // 一個前にクリックされたノードを取得
                const last = this.state.clickedUnicornIdList.slice(-1)[0];

                // ノードの中身を変更
                // console.log(last, clickedNode.id)
                this.updateEdgeContent(last, clickedNode.id);

                // ステートを更新して再レンダリング
                this.setState({ nodes: this.state.nodes, edges: this.state.edges });

                // クリックされたノードIDをリストの末尾に追加
                this.state.clickedUnicornIdList.push(clickedNode.id);

            } catch (error) {
                //console.error('Error while making the GET request:', error);
            }
        }
    };

    componentDidMount() {

        this.options = {
            physics: {
                barnesHut: {
                    springLength: 200, // ノード間の距離
                    springConstant: 0.2, // ノード間のスプリングの強度
                    damping: 0.2, // ダンピング係数
                    avoidOverlap: 0.2, // ノードが重ならないように調整
                },
            },

            nodes: {
                //shape: "dot",
                size: 10,
                font: {
                    color: 'black',
                },
                color: "black"
            },
            edges: {
                arrows: 'to',
                smooth: false
            },
        };
        const container = document.getElementById('graph-container');
        this.network = new Network(container, this.state, this.options);
        this.network.on('click', this.handleNodeClick);
    }

    render() {
        return (
            <div>
                <Template />
                <div className="center">
                    <div className="form-group">
                        <select name="unitid" required onChange={(event) => {
                            const word = event.target.value;
                        }}>
                            // 検索欄に表示させたいサービス群
                            <option value="1">サービスA</option>
                            <option value="314">サービスB</option>
                            <option value="3223">サービスC</option>
                            <option value="3633">サービスD</option>
                            <option value="7">サービスE</option>
                            <option value="1510">サービスF</option>
                            <option value="1279">サービスG</option>
                            <option value="1806">サービスH</option>
                        </select>

                        <button onClick={async () => {
                            const word = document.querySelector('[name="unitid"]').value;
                            const res = await fetch(`http://Python側のエンドポイント/react/${word}`, { method: "GET" });
                            const json = await res.json();
                            const nodeArray = json.node_array;
                            const edgeArray = json.edge_array;

                            for (const node of nodeArray) {
                                const nodeId = node[0];
                                const nodeName = node[1];
                                const group = node[2];

                                const isNodeIdIncluded = this.state.nodes.get().some(node => node.id === nodeId);

                                if (!isNodeIdIncluded) {
                                    if (group == 2) {
                                        if (nodeId == word) {
                                            this.state.nodes.add({ id: nodeId, label: nodeName, color: { background: 'darkkhaki', border: 'darkkhaki', highlight: { background: 'gray', border: 'gray' } } });
                                        } else {
                                            this.state.nodes.add({ id: nodeId, label: nodeName, color: { background: 'paleturquoise', border: 'paleturquoise', highlight: { background: 'darkkhaki', border: 'darkkhaki' } } }); // 例: 新しいノードを追加
                                        }
                                    } else {
                                        if (nodeId == word) {
                                            this.state.nodes.add({ id: nodeId, label: nodeName, color: { background: 'darkkhaki', border: 'darkkhaki', highlight: { background: 'gray', border: 'gray' } } });
                                        } else {
                                            this.state.nodes.add({ id: nodeId, label: nodeName, color: { background: 'lightsteelblue', border: 'lightsteelblue', highlight: { background: 'darkkhaki', border: 'darkkhaki' } } }); // 例: 新しいノードを追加
                                        }
                                    }
                                }
                            }

                            for (const edge of edgeArray) {
                                const from = edge[0];
                                const to = edge[1];
                                const lineType = edge[2];

                                function findEdgeByFromTo(edges, fromNodeId, toNodeId) {
                                    return edges.get().find(edge => edge.from === fromNodeId && edge.to === toNodeId);
                                }

                                const foundEdge = findEdgeByFromTo(this.state.edges, from, to);

                                if (!foundEdge) {
                                    if (lineType === 1) {
                                        this.state.edges.add({ from: from, to: to, arrows: 'to', dashes: true, label: 'No Effect from Disconnection', color: { highlight: 'red' } });
                                    } else {
                                        this.state.edges.add({ from: from, to: to, arrows: 'to', color: 'bule', color: { highlight: 'red' } });
                                    }
                                }
                            }
                            this.setState({ nodes: this.state.nodes, edges: this.state.edges });
                        }}>see</button>
                        <button onClick={() => {
                            const emptyNodes = new DataSet([]);
                            const emptyEdges = new DataSet([]);

                            this.setState({
                                nodes: emptyNodes,
                                edges: emptyEdges,
                            }, () => {
                                // setStateの完了後にページを再読み込み
                                window.location.reload();
                            });
                        }}>clear</button>
                    </div>
                </div>
                <div className="center">
                    <div id="graph-container" style={{ width: '2500px', height: '1200px' }} />
                </div>
            </div>
        );
    }
}

export default GraphVisualization;

各種CSS、publicディレクトリの中身は割愛。

初めてのReactだった。

npx create-react-app {ディレクトリ名}

でサクッと作れて

npm start  

でサクッと実行できるのはいいですね。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0