github.com/wfusion/gofusion@v1.1.14/common/infra/asynq/asynqmon/ui/src/views/MetricsView.tsx (about) 1 import React from "react"; 2 import { connect, ConnectedProps } from "react-redux"; 3 import { useHistory } from "react-router-dom"; 4 import queryString from "query-string"; 5 import { makeStyles } from "@material-ui/core/styles"; 6 import Container from "@material-ui/core/Container"; 7 import Grid from "@material-ui/core/Grid"; 8 import Typography from "@material-ui/core/Typography"; 9 import WarningIcon from "@material-ui/icons/Warning"; 10 import InfoIcon from "@material-ui/icons/Info"; 11 import prettyBytes from "pretty-bytes"; 12 import { getMetricsAsync } from "../actions/metricsActions"; 13 import { listQueuesAsync } from "../actions/queuesActions"; 14 import { AppState } from "../store"; 15 import QueueMetricsChart from "../components/QueueMetricsChart"; 16 import Tooltip from "../components/Tooltip"; 17 import { currentUnixtime } from "../utils"; 18 import MetricsFetchControls from "../components/MetricsFetchControls"; 19 import { useQuery } from "../hooks"; 20 import { PrometheusMetricsResponse } from "../api"; 21 22 const useStyles = makeStyles((theme) => ({ 23 container: { 24 marginTop: 30, 25 paddingTop: theme.spacing(4), 26 paddingBottom: theme.spacing(4), 27 }, 28 controlsContainer: { 29 display: "flex", 30 justifyContent: "flex-end", 31 position: "fixed", 32 background: theme.palette.background.paper, 33 zIndex: theme.zIndex.appBar, 34 right: 0, 35 top: 64, // app-bar height 36 width: "100%", 37 padding: theme.spacing(2), 38 }, 39 chartInfo: { 40 display: "flex", 41 alignItems: "center", 42 marginBottom: theme.spacing(1), 43 }, 44 infoIcon: { 45 marginLeft: theme.spacing(1), 46 color: theme.palette.grey[500], 47 cursor: "pointer", 48 }, 49 errorMessage: { 50 marginLeft: "auto", 51 display: "flex", 52 alignItems: "center", 53 }, 54 warningIcon: { 55 color: "#ff6700", 56 marginRight: 6, 57 }, 58 })); 59 60 function mapStateToProps(state: AppState) { 61 return { 62 loading: state.metrics.loading, 63 error: state.metrics.error, 64 data: state.metrics.data, 65 pollInterval: state.settings.pollInterval, 66 queues: state.queues.data.map((q) => q.name), 67 }; 68 } 69 70 const connector = connect(mapStateToProps, { 71 getMetricsAsync, 72 listQueuesAsync, 73 }); 74 type Props = ConnectedProps<typeof connector>; 75 76 const ENDTIME_URL_PARAM_KEY = "end"; 77 const DURATION_URL_PARAM_KEY = "duration"; 78 79 function MetricsView(props: Props) { 80 const classes = useStyles(); 81 const history = useHistory(); 82 const query = useQuery(); 83 84 const endTimeStr = query.get(ENDTIME_URL_PARAM_KEY); 85 const endTime = endTimeStr ? parseFloat(endTimeStr) : currentUnixtime(); // default to now 86 87 const durationStr = query.get(DURATION_URL_PARAM_KEY); 88 const duration = durationStr ? parseFloat(durationStr) : 60 * 60; // default to 1h 89 90 const { pollInterval, getMetricsAsync, listQueuesAsync, data } = props; 91 92 const [endTimeSec, setEndTimeSec] = React.useState(endTime); 93 const [durationSec, setDurationSec] = React.useState(duration); 94 const [selectedQueues, setSelectedQueues] = React.useState<string[]>([]); 95 96 const handleEndTimeChange = (endTime: number, isEndTimeFixed: boolean) => { 97 const urlQuery = isEndTimeFixed 98 ? { 99 [ENDTIME_URL_PARAM_KEY]: endTime, 100 [DURATION_URL_PARAM_KEY]: durationSec, 101 } 102 : { 103 [DURATION_URL_PARAM_KEY]: durationSec, 104 }; 105 history.push({ 106 ...history.location, 107 search: queryString.stringify(urlQuery), 108 }); 109 setEndTimeSec(endTime); 110 }; 111 112 const handleDurationChange = (duration: number, isEndTimeFixed: boolean) => { 113 const urlQuery = isEndTimeFixed 114 ? { 115 [ENDTIME_URL_PARAM_KEY]: endTimeSec, 116 [DURATION_URL_PARAM_KEY]: duration, 117 } 118 : { 119 [DURATION_URL_PARAM_KEY]: duration, 120 }; 121 history.push({ 122 ...history.location, 123 search: queryString.stringify(urlQuery), 124 }); 125 setDurationSec(duration); 126 }; 127 128 const handleAddQueue = (qname: string) => { 129 if (selectedQueues.includes(qname)) { 130 return; 131 } 132 setSelectedQueues(selectedQueues.concat(qname)); 133 }; 134 135 const handleRemoveQueue = (qname: string) => { 136 if (selectedQueues.length === 1) { 137 return; // ensure that selected queues doesn't go down to zero once user selected 138 } 139 if (selectedQueues.length === 0) { 140 // when user first select filter (remove once of the queues), 141 // we need to lazily initialize the selectedQueues with the rest (all queues but the selected one). 142 setSelectedQueues(props.queues.filter((q) => q !== qname)); 143 return; 144 } 145 setSelectedQueues(selectedQueues.filter((q) => q !== qname)); 146 }; 147 148 React.useEffect(() => { 149 listQueuesAsync(); 150 }, [listQueuesAsync]); 151 152 React.useEffect(() => { 153 getMetricsAsync(endTimeSec, durationSec, selectedQueues); 154 }, [pollInterval, getMetricsAsync, durationSec, endTimeSec, selectedQueues]); 155 156 return ( 157 <Container maxWidth="lg" className={classes.container}> 158 <div className={classes.controlsContainer}> 159 <MetricsFetchControls 160 endTimeSec={endTimeSec} 161 onEndTimeChange={handleEndTimeChange} 162 durationSec={durationSec} 163 onDurationChange={handleDurationChange} 164 queues={props.queues} 165 selectedQueues={ 166 // If none are selected (e.g. initial state), no filters should apply. 167 selectedQueues.length === 0 ? props.queues : selectedQueues 168 } 169 addQueue={handleAddQueue} 170 removeQueue={handleRemoveQueue} 171 /> 172 </div> 173 <Grid container spacing={3}> 174 {data?.tasks_processed_per_second && ( 175 <Grid item xs={12}> 176 <ChartRow 177 title="Tasks Processed" 178 description="Number of tasks processed (both succeeded and failed) per second." 179 metrics={data.tasks_processed_per_second} 180 endTime={endTimeSec} 181 startTime={endTimeSec - durationSec} 182 /> 183 </Grid> 184 )} 185 {data?.tasks_failed_per_second && ( 186 <Grid item xs={12}> 187 <ChartRow 188 title="Tasks Failed" 189 description="Number of tasks failed per second." 190 metrics={data.tasks_failed_per_second} 191 endTime={endTimeSec} 192 startTime={endTimeSec - durationSec} 193 /> 194 </Grid> 195 )} 196 {data?.error_rate && ( 197 <Grid item xs={12}> 198 <ChartRow 199 title="Error Rate" 200 description="Rate of task failures" 201 metrics={data.error_rate} 202 endTime={endTimeSec} 203 startTime={endTimeSec - durationSec} 204 /> 205 </Grid> 206 )} 207 {data?.queue_size && ( 208 <Grid item xs={12}> 209 <ChartRow 210 title="Queue Size" 211 description="Total number of tasks in a given queue." 212 metrics={data.queue_size} 213 endTime={endTimeSec} 214 startTime={endTimeSec - durationSec} 215 /> 216 </Grid> 217 )} 218 {data?.queue_latency_seconds && ( 219 <Grid item xs={12}> 220 <ChartRow 221 title="Queue Latency" 222 description="Latency of queue, measured by the oldest pending task in the queue." 223 metrics={data.queue_latency_seconds} 224 endTime={endTimeSec} 225 startTime={endTimeSec - durationSec} 226 yAxisTickFormatter={(val: number) => val + "s"} 227 /> 228 </Grid> 229 )} 230 {data?.queue_size && ( 231 <Grid item xs={12}> 232 <ChartRow 233 title="Queue Memory Usage (approx)" 234 description="Memory usage by queue. Approximate value by sampling a few tasks in a queue." 235 metrics={data.queue_memory_usage_approx_bytes} 236 endTime={endTimeSec} 237 startTime={endTimeSec - durationSec} 238 yAxisTickFormatter={(val: number) => { 239 try { 240 return prettyBytes(val); 241 } catch (error) { 242 return val + "B"; 243 } 244 }} 245 /> 246 </Grid> 247 )} 248 {data?.pending_tasks_by_queue && ( 249 <Grid item xs={12}> 250 <ChartRow 251 title="Pending Tasks" 252 description="Number of pending tasks in a given queue." 253 metrics={data.pending_tasks_by_queue} 254 endTime={endTimeSec} 255 startTime={endTimeSec - durationSec} 256 /> 257 </Grid> 258 )} 259 {data?.retry_tasks_by_queue && ( 260 <Grid item xs={12}> 261 <ChartRow 262 title="Retry Tasks" 263 description="Number of retry tasks in a given queue." 264 metrics={data.retry_tasks_by_queue} 265 endTime={endTimeSec} 266 startTime={endTimeSec - durationSec} 267 /> 268 </Grid> 269 )} 270 {data?.archived_tasks_by_queue && ( 271 <Grid item xs={12}> 272 <ChartRow 273 title="Archived Tasks" 274 description="Number of archived tasks in a given queue." 275 metrics={data.archived_tasks_by_queue} 276 endTime={endTimeSec} 277 startTime={endTimeSec - durationSec} 278 /> 279 </Grid> 280 )} 281 </Grid> 282 </Container> 283 ); 284 } 285 286 export default connector(MetricsView); 287 288 /******** Helper components ********/ 289 290 interface ChartRowProps { 291 title: string; 292 description: string; 293 metrics: PrometheusMetricsResponse; 294 endTime: number; 295 startTime: number; 296 yAxisTickFormatter?: (val: number) => string; 297 } 298 299 function ChartRow(props: ChartRowProps) { 300 const classes = useStyles(); 301 return ( 302 <> 303 <div className={classes.chartInfo}> 304 <Typography color="textPrimary">{props.title}</Typography> 305 <Tooltip title={<div>{props.description}</div>}> 306 <InfoIcon fontSize="small" className={classes.infoIcon} /> 307 </Tooltip> 308 {props.metrics.status === "error" && ( 309 <div className={classes.errorMessage}> 310 <WarningIcon fontSize="small" className={classes.warningIcon} /> 311 <Typography color="textSecondary"> 312 Failed to get metrics data: {props.metrics.error} 313 </Typography> 314 </div> 315 )} 316 </div> 317 <QueueMetricsChart 318 data={ 319 props.metrics.status === "error" 320 ? [] 321 : props.metrics.data?.result || [] 322 } 323 endTime={props.endTime} 324 startTime={props.startTime} 325 yAxisTickFormatter={props.yAxisTickFormatter} 326 /> 327 </> 328 ); 329 }