mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 06:19:03 +08:00
commit
d07e277220
21590
frontend/package-lock.json
generated
Normal file
21590
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -12,6 +12,7 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.6.2",
|
||||
"@auth0/auth0-react": "^1.2.0",
|
||||
"@babel/core": "7.12.3",
|
||||
"@material-ui/core": "^4.0.0",
|
||||
@ -88,6 +89,7 @@
|
||||
"react-css-theme-switcher": "^0.1.6",
|
||||
"react-dev-utils": "^11.0.0",
|
||||
"react-dom": "17.0.0",
|
||||
"react-force-graph": "^1.41.0",
|
||||
"react-graph-vis": "^1.0.5",
|
||||
"react-modal": "^3.12.1",
|
||||
"react-redux": "^7.2.2",
|
||||
|
63
frontend/src/modules/Servicemap/SelectService.tsx
Normal file
63
frontend/src/modules/Servicemap/SelectService.tsx
Normal 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;
|
@ -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;
|
@ -1,14 +1,158 @@
|
||||
import React from "react";
|
||||
import ServiceGraph from "./ServiceGraph";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
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 (
|
||||
<div>
|
||||
{" "}
|
||||
Service Map module coming soon...
|
||||
{/* <ServiceGraph /> */}
|
||||
<Container>
|
||||
<SelectService
|
||||
services={serviceMap.services}
|
||||
zoomToService={zoomToService}
|
||||
/>
|
||||
<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);
|
||||
|
1
frontend/src/modules/Servicemap/index.ts
Normal file
1
frontend/src/modules/Servicemap/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from "./ServiceMap";
|
75
frontend/src/modules/Servicemap/utils.ts
Normal file
75
frontend/src/modules/Servicemap/utils.ts
Normal 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,
|
||||
};
|
||||
};
|
@ -1,5 +1,6 @@
|
||||
export * from "./types";
|
||||
export * from "./traceFilters";
|
||||
export * from "./serviceMap";
|
||||
export * from "./traces";
|
||||
export * from "./metrics";
|
||||
export * from "./usage";
|
||||
|
68
frontend/src/store/actions/serviceMap.ts
Normal file
68
frontend/src/store/actions/serviceMap.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
};
|
@ -10,6 +10,7 @@ import {
|
||||
getFilteredTraceMetricsAction,
|
||||
getDbOverViewMetricsAction,
|
||||
} from "./metrics";
|
||||
import { serviceMapItemAction, servicesAction } from "./serviceMap";
|
||||
import { getUsageDataAction } from "./usage";
|
||||
import { updateTimeIntervalAction } from "./global";
|
||||
|
||||
@ -28,6 +29,8 @@ export enum ActionTypes {
|
||||
getUsageData = "GET_USAGE_DATE",
|
||||
updateTimeInterval = "UPDATE_TIME_INTERVAL",
|
||||
getFilteredTraceMetrics = "GET_FILTERED_TRACE_METRICS",
|
||||
getServiceMapItems = "GET_SERVICE_MAP_ITEMS",
|
||||
getServices = "GET_SERVICES",
|
||||
}
|
||||
|
||||
export type Action =
|
||||
@ -44,4 +47,6 @@ export type Action =
|
||||
| getExternalMetricsAction
|
||||
| externalErrCodeMetricsActions
|
||||
| getDbOverViewMetricsAction
|
||||
| servicesAction
|
||||
| serviceMapItemAction
|
||||
| externalMetricsAvgDurationAction;
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
usageDataItem,
|
||||
GlobalTime,
|
||||
externalErrCodeMetricsItem,
|
||||
serviceMapStore,
|
||||
customMetricsItem,
|
||||
TraceFilters,
|
||||
} from "../actions";
|
||||
@ -27,7 +28,7 @@ import {
|
||||
import { traceFiltersReducer, inputsReducer } from "./traceFilters";
|
||||
import { traceItemReducer, tracesReducer } from "./traces";
|
||||
import { usageDataReducer } from "./usage";
|
||||
|
||||
import { ServiceMapReducer } from "./serviceMap";
|
||||
export interface StoreState {
|
||||
traceFilters: TraceFilters;
|
||||
inputTag: string;
|
||||
@ -43,6 +44,7 @@ export interface StoreState {
|
||||
usageDate: usageDataItem[];
|
||||
globalTime: GlobalTime;
|
||||
filteredTraceMetrics: customMetricsItem[];
|
||||
serviceMap: serviceMapStore;
|
||||
}
|
||||
|
||||
const reducers = combineReducers<StoreState>({
|
||||
@ -60,6 +62,7 @@ const reducers = combineReducers<StoreState>({
|
||||
usageDate: usageDataReducer,
|
||||
globalTime: updateGlobalTimeReducer,
|
||||
filteredTraceMetrics: filteredTraceMetricsReducer,
|
||||
serviceMap: ServiceMapReducer,
|
||||
});
|
||||
|
||||
export default reducers;
|
||||
|
24
frontend/src/store/reducers/serviceMap.ts
Normal file
24
frontend/src/store/reducers/serviceMap.ts
Normal 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;
|
||||
}
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user