Merge pull request #90 from SigNoz/service-map

Service map view
This commit is contained in:
Ankit Nayan 2021-05-10 00:02:07 +05:30 committed by GitHub
commit d07e277220
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 21986 additions and 81 deletions

21590
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.6.2",
"@auth0/auth0-react": "^1.2.0", "@auth0/auth0-react": "^1.2.0",
"@babel/core": "7.12.3", "@babel/core": "7.12.3",
"@material-ui/core": "^4.0.0", "@material-ui/core": "^4.0.0",
@ -88,6 +89,7 @@
"react-css-theme-switcher": "^0.1.6", "react-css-theme-switcher": "^0.1.6",
"react-dev-utils": "^11.0.0", "react-dev-utils": "^11.0.0",
"react-dom": "17.0.0", "react-dom": "17.0.0",
"react-force-graph": "^1.41.0",
"react-graph-vis": "^1.0.5", "react-graph-vis": "^1.0.5",
"react-modal": "^3.12.1", "react-modal": "^3.12.1",
"react-redux": "^7.2.2", "react-redux": "^7.2.2",

View File

@ -0,0 +1,63 @@
import React, { useState } from "react";
import { servicesItem } from "Src/store/actions";
import { InfoCircleOutlined } from "@ant-design/icons";
import { Select } from "antd";
import styled from "styled-components";
const { Option } = Select;
const Container = styled.div`
margin-top: 12px;
display: flex;
.info {
display:flex;
font-family: Roboto;
margin-left: auto;
margin-right: 12px;
color: #4F4F4F;
font-size: 14px;
.anticon-info-circle {
margin-top: 22px;
margin-right: 18px;
}
}
`;
interface SelectServiceProps {
services: servicesItem[];
zoomToService: (arg0: string) => void;
}
const SelectService = (props: SelectServiceProps) => {
const [selectedVal, setSelectedVal] = useState<string>();
const { services, zoomToService } = props;
const handleSelect = (value: string) => {
setSelectedVal(value);
zoomToService(value);
};
return (
<Container>
<Select
style={{ width: 270, marginBottom: "56px" }}
placeholder="Select a service"
onChange={handleSelect}
value={selectedVal}
>
{services.map(({ serviceName }) => (
<Option key={serviceName} value={serviceName}>
{serviceName}
</Option>
))}
</Select>
<div className='info'>
<InfoCircleOutlined />
<div>
<div>-> Size of circles is proportial to the number of requests served by each node </div>
<div>-> Click on node name to reposition the node</div>
</div>
</div>
</Container>
);
};
export default SelectService;

View File

@ -1,71 +0,0 @@
import React from "react";
// import {useState} from "react";
import Graph from "react-graph-vis";
// import { graphEvents } from "react-graph-vis";
//PNOTE - types of react-graph-vis defined in typings folder.
//How is it imported directly?
// type definition for service graph - https://github.com/crubier/react-graph-vis/issues/80
// Set shapes - https://visjs.github.io/vis-network/docs/network/nodes.html#
// https://github.com/crubier/react-graph-vis/issues/93
const graph = {
nodes: [
{
id: 1,
label: "Catalogue",
shape: "box",
color: "green",
border: "black",
size: 100,
},
{ id: 2, label: "Users", shape: "box", color: "#FFFF00" },
{ id: 3, label: "Payment App", shape: "box", color: "#FB7E81" },
{ id: 4, label: "My Sql", shape: "box", size: 10, color: "#7BE141" },
{ id: 5, label: "Redis-db", shape: "box", color: "#6E6EFD" },
],
edges: [
{ from: 1, to: 2, color: { color: "red" }, size: { size: 20 } },
{ from: 2, to: 3, color: { color: "red" } },
{ from: 1, to: 3, color: { color: "red" } },
{ from: 3, to: 4, color: { color: "red" } },
{ from: 3, to: 5, color: { color: "red" } },
],
};
const options = {
layout: {
hierarchical: true,
},
edges: {
color: "#000000",
},
height: "500px",
};
// const events = {
// select: function(event:any) { //PNOTE - TO DO - Get rid of any type
// var { nodes, edges } = event;
// }
// };
const ServiceGraph = () => {
// const [network, setNetwork] = useState(null);
return (
<React.Fragment>
<div> Updated Service Graph module coming soon..</div>
<Graph
graph={graph}
options={options}
// events={events}
// getNetwork={network => {
// // if you want access to vis.js network api you can set the state in a parent component using this property
// }}
/>
</React.Fragment>
);
};
export default ServiceGraph;

View File

@ -1,14 +1,158 @@
import React from "react"; import React, { useEffect, useRef, useState } from "react";
import ServiceGraph from "./ServiceGraph"; import { connect } from "react-redux";
import { RouteComponentProps } from "react-router-dom";
import {
GlobalTime,
serviceMapStore,
getServiceMapItems,
getDetailedServiceMapItems,
} from "Src/store/actions";
import { Spin } from "antd";
import styled from "styled-components";
import { StoreState } from "../../store/reducers";
import { getGraphData } from "./utils";
import SelectService from "./SelectService";
import { ForceGraph2D } from "react-force-graph";
const ServiceMap = () => { const Container = styled.div`
.force-graph-container .graph-tooltip {
background: black;
padding: 1px;
.keyval {
display: flex;
.key {
margin-right: 4px;
}
.val {
margin-left: auto;
}
}
}
`;
interface ServiceMapProps extends RouteComponentProps<any> {
serviceMap: serviceMapStore;
globalTime: GlobalTime;
getServiceMapItems: Function;
getDetailedServiceMapItems: Function;
}
interface graphNode {
id: string;
group: number;
}
interface graphLink {
source: string;
target: string;
value: number;
}
export interface graphDataType {
nodes: graphNode[];
links: graphLink[];
}
const ServiceMap = (props: ServiceMapProps) => {
const fgRef = useRef();
const {
getDetailedServiceMapItems,
getServiceMapItems,
globalTime,
serviceMap,
} = props;
useEffect(() => {
getServiceMapItems(globalTime);
getDetailedServiceMapItems(globalTime);
}, []);
useEffect(() => {
fgRef.current && fgRef.current.d3Force("charge").strength(-400);
});
if (!serviceMap.items.length || !serviceMap.services.length) {
return <Spin />;
}
const zoomToService = (value: string) => {
fgRef && fgRef.current.zoomToFit(700, 380, (e) => e.id === value);
};
const { nodes, links } = getGraphData(serviceMap);
const graphData = { nodes, links };
return ( return (
<div> <Container>
{" "} <SelectService
Service Map module coming soon... services={serviceMap.services}
{/* <ServiceGraph /> */} zoomToService={zoomToService}
</div> />
<ForceGraph2D
ref={fgRef}
cooldownTicks={100}
onEngineStop={() => {
fgRef.current.zoomToFit(100, 120);
}}
graphData={graphData}
nodeLabel="id"
linkAutoColorBy={(d) => d.target}
linkDirectionalParticles="value"
linkDirectionalParticleSpeed={(d) => d.value}
nodeCanvasObject={(node, ctx, globalScale) => {
const label = node.id;
const fontSize = node.fontSize;
ctx.font = `${fontSize}px Roboto`;
const width = node.width;
ctx.fillStyle = node.color;
ctx.beginPath();
ctx.arc(node.x, node.y, width, 0, 2 * Math.PI, false);
ctx.fill();
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "#333333";
ctx.fillText(label, node.x, node.y);
}}
onNodeClick={(node) => {
const tooltip = document.querySelector(".graph-tooltip");
if (tooltip && node) {
tooltip.innerHTML = `<div style="color:#333333;padding:12px;background: white;border-radius: 2px;">
<div style="font-weight:bold; margin-bottom:16px;">${node.id}</div>
<div class="keyval">
<div class="key">P99 latency:</div>
<div class="val">${node.p99 / 1000000}ms</div>
</div>
<div class="keyval">
<div class="key">Request:</div>
<div class="val">${node.callRate}/sec</div>
</div>
<div class="keyval">
<div class="key">Error Rate:</div>
<div class="val">${node.errorRate}%</div>
</div>
</div>`;
}
}}
nodePointerAreaPaint={(node, color, ctx) => {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(node.x, node.y, 5, 0, 2 * Math.PI, false);
ctx.fill();
}}
/>
</Container>
); );
}; };
export default ServiceMap; const mapStateToProps = (
state: StoreState,
): {
serviceMap: serviceMapStore;
globalTime: GlobalTime;
} => {
return {
serviceMap: state.serviceMap,
globalTime: state.globalTime,
};
};
export default connect(mapStateToProps, {
getServiceMapItems: getServiceMapItems,
getDetailedServiceMapItems: getDetailedServiceMapItems,
})(ServiceMap);

View File

@ -0,0 +1 @@
export { default } from "./ServiceMap";

View File

@ -0,0 +1,75 @@
import { uniqBy, uniq, maxBy, cloneDeep, find } from "lodash";
import { serviceMapStore } from "Src/store/actions";
import { graphDataType } from "./ServiceMap";
const MIN_WIDTH = 12;
const MAX_WIDTH = 20;
const DEFAULT_FONT_SIZE = 6;
export const getDimensions = (num, highest) => {
const percentage = (num / highest) * 100;
const width = (percentage * (MAX_WIDTH - MIN_WIDTH)) / 100 + MIN_WIDTH;
const fontSize = DEFAULT_FONT_SIZE;
return {
fontSize,
width,
};
};
export const getGraphData = (serviceMap: serviceMapStore): graphDataType => {
const { items, services } = serviceMap;
const highestCallCount = maxBy(items, (e) => e.callCount).callCount;
const highestCallRate = maxBy(services, (e) => e.callRate).callRate;
const divNum = Number(
String(1).padEnd(highestCallCount.toString().length, "0"),
);
const links = cloneDeep(items).map((node) => {
const { parent, child, callCount } = node;
return {
source: parent,
target: child,
value: (100 - callCount / divNum) * 0.01,
};
});
const uniqParent = uniqBy(cloneDeep(items), "parent").map((e) => e.parent);
const uniqChild = uniqBy(cloneDeep(items), "child").map((e) => e.child);
const uniqNodes = uniq([...uniqParent, ...uniqChild]);
const nodes = uniqNodes.map((node, i) => {
const service = find(services, (service) => service.serviceName === node);
let color = "#88CEA5";
if (!service) {
return {
id: node,
group: i + 1,
fontSize: DEFAULT_FONT_SIZE,
width: MIN_WIDTH,
color,
nodeVal: MIN_WIDTH,
callRate: 0,
errorRate: 0,
p99: 0,
};
}
if (service.errorRate > 0) {
color = "#F98989";
} else if (service.fourXXRate > 0) {
color = "#F9DA7B";
}
const { fontSize, width } = getDimensions(service.callRate, highestCallRate);
return {
id: node,
group: i + 1,
fontSize,
width,
color,
nodeVal: width,
callRate: service.callRate.toFixed(2),
errorRate: service.errorRate,
p99: service.p99,
};
});
return {
nodes,
links,
};
};

View File

@ -1,5 +1,6 @@
export * from "./types"; export * from "./types";
export * from "./traceFilters"; export * from "./traceFilters";
export * from "./serviceMap";
export * from "./traces"; export * from "./traces";
export * from "./metrics"; export * from "./metrics";
export * from "./usage"; export * from "./usage";

View File

@ -0,0 +1,68 @@
import { Dispatch } from "redux";
import api, { apiV1 } from "../../api";
import { GlobalTime } from "./global";
import { ActionTypes } from "./types";
export interface serviceMapStore {
items: servicesMapItem[];
services: servicesItem[];
}
export interface servicesItem {
serviceName: string;
p99: number;
avgDuration: number;
numCalls: number;
callRate: number;
numErrors: number;
errorRate: number;
num4XX: number;
fourXXRate: number;
}
export interface servicesMapItem {
parent: string;
child: string;
callCount: number;
}
export interface serviceMapItemAction {
type: ActionTypes.getServiceMapItems;
payload: servicesMapItem[];
}
export interface servicesAction {
type: ActionTypes.getServices;
payload: servicesItem[];
}
export const getServiceMapItems = (globalTime: GlobalTime) => {
return async (dispatch: Dispatch) => {
let request_string =
"/serviceMapDependencies?start=" +
globalTime.minTime +
"&end=" +
globalTime.maxTime;
const response = await api.get<servicesMapItem[]>(apiV1 + request_string);
dispatch<serviceMapItemAction>({
type: ActionTypes.getServiceMapItems,
payload: response.data,
});
};
};
export const getDetailedServiceMapItems = (globalTime: GlobalTime) => {
return async (dispatch: Dispatch) => {
let request_string =
"/services?start=" + globalTime.minTime + "&end=" + globalTime.maxTime;
const response = await api.get<servicesItem[]>(apiV1 + request_string);
dispatch<servicesAction>({
type: ActionTypes.getServices,
payload: response.data,
});
};
};

View File

@ -10,6 +10,7 @@ import {
getFilteredTraceMetricsAction, getFilteredTraceMetricsAction,
getDbOverViewMetricsAction, getDbOverViewMetricsAction,
} from "./metrics"; } from "./metrics";
import { serviceMapItemAction, servicesAction } from "./serviceMap";
import { getUsageDataAction } from "./usage"; import { getUsageDataAction } from "./usage";
import { updateTimeIntervalAction } from "./global"; import { updateTimeIntervalAction } from "./global";
@ -28,6 +29,8 @@ export enum ActionTypes {
getUsageData = "GET_USAGE_DATE", getUsageData = "GET_USAGE_DATE",
updateTimeInterval = "UPDATE_TIME_INTERVAL", updateTimeInterval = "UPDATE_TIME_INTERVAL",
getFilteredTraceMetrics = "GET_FILTERED_TRACE_METRICS", getFilteredTraceMetrics = "GET_FILTERED_TRACE_METRICS",
getServiceMapItems = "GET_SERVICE_MAP_ITEMS",
getServices = "GET_SERVICES",
} }
export type Action = export type Action =
@ -44,4 +47,6 @@ export type Action =
| getExternalMetricsAction | getExternalMetricsAction
| externalErrCodeMetricsActions | externalErrCodeMetricsActions
| getDbOverViewMetricsAction | getDbOverViewMetricsAction
| servicesAction
| serviceMapItemAction
| externalMetricsAvgDurationAction; | externalMetricsAvgDurationAction;

View File

@ -10,6 +10,7 @@ import {
usageDataItem, usageDataItem,
GlobalTime, GlobalTime,
externalErrCodeMetricsItem, externalErrCodeMetricsItem,
serviceMapStore,
customMetricsItem, customMetricsItem,
TraceFilters, TraceFilters,
} from "../actions"; } from "../actions";
@ -27,7 +28,7 @@ import {
import { traceFiltersReducer, inputsReducer } from "./traceFilters"; import { traceFiltersReducer, inputsReducer } from "./traceFilters";
import { traceItemReducer, tracesReducer } from "./traces"; import { traceItemReducer, tracesReducer } from "./traces";
import { usageDataReducer } from "./usage"; import { usageDataReducer } from "./usage";
import { ServiceMapReducer } from "./serviceMap";
export interface StoreState { export interface StoreState {
traceFilters: TraceFilters; traceFilters: TraceFilters;
inputTag: string; inputTag: string;
@ -43,6 +44,7 @@ export interface StoreState {
usageDate: usageDataItem[]; usageDate: usageDataItem[];
globalTime: GlobalTime; globalTime: GlobalTime;
filteredTraceMetrics: customMetricsItem[]; filteredTraceMetrics: customMetricsItem[];
serviceMap: serviceMapStore;
} }
const reducers = combineReducers<StoreState>({ const reducers = combineReducers<StoreState>({
@ -60,6 +62,7 @@ const reducers = combineReducers<StoreState>({
usageDate: usageDataReducer, usageDate: usageDataReducer,
globalTime: updateGlobalTimeReducer, globalTime: updateGlobalTimeReducer,
filteredTraceMetrics: filteredTraceMetricsReducer, filteredTraceMetrics: filteredTraceMetricsReducer,
serviceMap: ServiceMapReducer,
}); });
export default reducers; export default reducers;

View File

@ -0,0 +1,24 @@
import { ActionTypes, Action, serviceMapStore } from "../actions";
export const ServiceMapReducer = (
state: serviceMapStore = {
items: [],
services: [],
},
action: Action,
) => {
switch (action.type) {
case ActionTypes.getServiceMapItems:
return {
...state,
items: action.payload,
};
case ActionTypes.getServices:
return {
...state,
services: action.payload,
};
default:
return state;
}
};