From ace2c7c4d0628ee825b13861373f0956e4033009 Mon Sep 17 00:00:00 2001 From: dutexion Date: Sat, 28 Sep 2024 14:50:11 +0900 Subject: [PATCH] =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=A7=B5=20?= =?UTF-8?q?=EC=99=84=20=EB=B2=84=EC=A0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/servicemap.tsx | 329 ++++++++++++++++++---------------- 1 file changed, 174 insertions(+), 155 deletions(-) diff --git a/src/components/servicemap.tsx b/src/components/servicemap.tsx index b42f30b..dcda201 100644 --- a/src/components/servicemap.tsx +++ b/src/components/servicemap.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useMemo } from 'react'; import { data } from './data'; interface Node { @@ -20,16 +20,47 @@ interface Edge { latency_avg_ms: number; } -interface NodePosition { +interface Metrics { + timestamp: string; + nodes: Node[]; + edges: Edge[]; +} + +interface JsonData { + data: { + metrics: Metrics[]; + }; +} + +interface ServiceNodeProps { x: number; y: number; + node: Node; + onNodeClick: (node: Node) => void; + onNodeHover: (node: Node | null) => void; + maxCalls: number; + isConnected: boolean; + isHovered: boolean; } -const ServiceNode: React.FC<{ - node: Node; - position: NodePosition; - onClick: (node: Node) => void; -}> = ({ node, position, onClick }) => { +interface ServiceLinkProps { + source: { x: number; y: number }; + target: { x: number; y: number }; + edge: Edge; + maxCalls: number; + isConnected: boolean; +} + +const ServiceNode: React.FC = ({ + x, + y, + node, + onNodeClick, + onNodeHover, + maxCalls, + isConnected, + isHovered, +}) => { const getColor = (type: string) => { switch (type) { case 'SERVICE': @@ -41,60 +72,57 @@ const ServiceNode: React.FC<{ } }; + const radius = 20 + (node.calls / maxCalls) * 30; + const opacity = isConnected ? 1 : 0.3; + return ( - onClick(node)}> - - - {/* {node.node_id.split('-').slice(0, -1).join('-')} */} + onNodeClick(node)} + onMouseEnter={() => onNodeHover(node)} + onMouseLeave={() => onNodeHover(null)} + style={{ cursor: 'pointer' }} + > + + {node.node_id} + {isHovered && } ); }; -const ServiceLink: React.FC<{ - source: NodePosition; - target: NodePosition; - edge: Edge; -}> = ({ source, target, edge }) => { - const dx = target.x - source.x; - const dy = target.y - source.y; - const angle = Math.atan2(dy, dx); - - const sourceRadius = 30; - const targetRadius = 30; - - const startX = source.x + sourceRadius * Math.cos(angle); - const startY = source.y + sourceRadius * Math.sin(angle); - const endX = target.x - targetRadius * Math.cos(angle); - const endY = target.y - targetRadius * Math.sin(angle); - - // Calculate control points for a curved line - const midX = (startX + endX) / 2; - const midY = (startY + endY) / 2; - const curveFactor = 0.2; - const controlX = midX - curveFactor * (endY - startY); - const controlY = midY + curveFactor * (endX - startX); +const ServiceLink: React.FC = ({ source, target, edge, maxCalls, isConnected }) => { + const offset = 54; + const arrowOffset = -0; + + const startX = source.x; + const startY = source.y; + const endX = target.x - ((target.x - source.x) * offset) / Math.hypot(target.x - source.x, target.y - source.y); + const endY = target.y - ((target.y - source.y) * offset) / Math.hypot(target.x - source.x, target.y - source.y); + + const strokeWidth = 1 + (edge.calls / maxCalls) * 5; + const opacity = isConnected ? 0.8 : 0.1; return ( - + + + + + - - {edge.calls} calls + + {edge.calls} calls, {edge.latency_avg_ms.toFixed(2)}ms ); @@ -102,146 +130,137 @@ const ServiceLink: React.FC<{ const ServiceMap: React.FC = () => { const [selectedNode, setSelectedNode] = useState(null); - const [nodePositions, setNodePositions] = useState>({}); + const [hoveredNode, setHoveredNode] = useState(null); - const metrics = data.data.metrics[0]; + const jsonData: JsonData = data; + + const metrics = jsonData.data.metrics[0]; const nodes = metrics.nodes; const edges = metrics.edges; - useEffect(() => { - const calculateNodePositions = () => { - const positions: Record = {}; - const svgWidth = 1200; - const svgHeight = 800; - const horizontalPadding = 100; - const verticalPadding = 50; - - // Calculate in-degree and out-degree for each node - const inDegree: Record = {}; - const outDegree: Record = {}; - edges.forEach((edge) => { - inDegree[edge.target] = (inDegree[edge.target] || 0) + 1; - outDegree[edge.source] = (outDegree[edge.source] || 0) + 1; - }); - - // Sort nodes based on the difference between out-degree and in-degree - const sortedNodes = [...nodes].sort((a, b) => { - const aDiff = (outDegree[a.node_id] || 0) - (inDegree[a.node_id] || 0); - const bDiff = (outDegree[b.node_id] || 0) - (inDegree[b.node_id] || 0); - return bDiff - aDiff; - }); - - // Assign nodes to layers - const layers: string[][] = []; - sortedNodes.forEach((node) => { - let layerIndex = 0; - while (true) { - if (!layers[layerIndex]) { - layers[layerIndex] = []; - } - if (layers[layerIndex].length < 5) { - // Limit 5 nodes per layer - layers[layerIndex].push(node.node_id); - break; - } - layerIndex++; - } - }); - - // Barycentric method to reduce edge crossings - const optimizeLayers = (layers: string[][]) => { - for (let i = 1; i < layers.length; i++) { - const currentLayer = layers[i]; - const prevLayer = layers[i - 1]; - - const nodeOrder: Record = {}; - prevLayer.forEach((node, index) => { - nodeOrder[node] = index; - }); - - const barycenters = currentLayer.map((node) => { - const connectedNodes = edges - .filter((e) => e.source === node || e.target === node) - .map((e) => (e.source === node ? e.target : e.source)) - .filter((n) => prevLayer.includes(n)); - if (connectedNodes.length === 0) return 0; - return connectedNodes.reduce((sum, n) => sum + nodeOrder[n], 0) / connectedNodes.length; - }); - - currentLayer.sort((a, b) => { - const indexA = currentLayer.indexOf(a); - const indexB = currentLayer.indexOf(b); - return barycenters[indexA] - barycenters[indexB]; - }); - } - }; - - // Apply barycentric method multiple times - for (let i = 0; i < 3; i++) { - optimizeLayers(layers); + const calculateReferences = () => { + const references = nodes.reduce( + (acc, node) => { + acc[node.node_id] = { incoming: 0, outgoing: 0, uniqueTargets: new Set(), connectedNodes: new Set() }; + return acc; + }, + {} as Record< + string, + { incoming: number; outgoing: number; uniqueTargets: Set; connectedNodes: Set } + >, + ); + + const uniqueEdgesMap = new Map(); + + edges.forEach((edge) => { + const key = `${edge.source}-${edge.target}`; + if (uniqueEdgesMap.has(key)) { + const existingEdge = uniqueEdgesMap.get(key)!; + existingEdge.calls += edge.calls; + } else { + uniqueEdgesMap.set(key, { ...edge }); } + }); + + uniqueEdgesMap.forEach((edge) => { + references[edge.target].incoming += edge.calls; + references[edge.source].outgoing += edge.calls; + references[edge.source].uniqueTargets.add(edge.target); + references[edge.source].connectedNodes.add(edge.target); + references[edge.target].connectedNodes.add(edge.source); + }); + + return { references, uniqueEdges: Array.from(uniqueEdgesMap.values()) }; + }; + + const { references, uniqueEdges } = calculateReferences(); - // Calculate positions based on optimized layers - layers.forEach((layer, layerIndex) => { - const layerX = horizontalPadding + (svgWidth - 2 * horizontalPadding) * (layerIndex / (layers.length - 1)); - layer.forEach((nodeId, nodeIndex) => { - const layerY = verticalPadding + (svgHeight - 2 * verticalPadding) * (nodeIndex / (layer.length - 1)); - positions[nodeId] = { x: layerX, y: layerY }; - }); - }); + const nodePositions = useMemo(() => { + const width = 1200; + const height = 800; + const padding = 100; + const positions: Record = {}; - setNodePositions(positions); - }; + // Sort nodes by the number of unique targets (descending order) + const sortedNodes = [...nodes].sort( + (a, b) => references[b.node_id].uniqueTargets.size - references[a.node_id].uniqueTargets.size, + ); - calculateNodePositions(); - }, [nodes, edges]); + sortedNodes.forEach((node, index) => { + const x = padding + ((width - 2 * padding) * index) / (nodes.length - 1); + const y = height / 2 + (Math.random() - 0.5) * (height / 2 - padding); + + positions[node.node_id] = { x, y }; + }); + + return positions; + }, []); // Empty dependency array ensures this calculation happens only once const handleNodeClick = (node: Node) => { setSelectedNode(node); }; + const handleNodeHover = (node: Node | null) => { + setHoveredNode(node); + }; + + const maxCalls = Math.max(...nodes.map((node) => node.calls)); + const maxEdgeCalls = Math.max(...uniqueEdges.map((edge) => edge.calls)); + + const isConnected = (nodeId: string) => { + if (!hoveredNode) return true; + return hoveredNode.node_id === nodeId || references[hoveredNode.node_id].connectedNodes.has(nodeId); + }; + return ( -
-

Optimized Hierarchical Service Map

- - - - - - - {edges.map((edge, index) => ( +
+

Datadog APM Service Map

+ + {uniqueEdges.map((edge, index) => ( ))} {nodes.map((node) => ( ))} {selectedNode && ( -
-

{selectedNode.node_id}

+
+

{selectedNode.node_id}

Type: {selectedNode.type}

Calls: {selectedNode.calls}

Successes: {selectedNode.successes}

Failures: {selectedNode.failures}

Avg Latency: {selectedNode.latency_avg_ms.toFixed(2)}ms

+

Referenced by: {references[selectedNode.node_id].incoming}

+

References: {references[selectedNode.node_id].outgoing}

+

Unique Targets: {references[selectedNode.node_id].uniqueTargets.size}

+

Unique Edge Calls:

+
    + {uniqueEdges + .filter((edge) => edge.source === selectedNode.node_id || edge.target === selectedNode.node_id) + .map((edge, index) => ( +
  • + {edge.source} → {edge.target}: {edge.calls} calls +
  • + ))} +
)}