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  }