Skip to content

Commit

Permalink
Bug 1856351: Fix build details page charts
Browse files Browse the repository at this point in the history
- Create new global time related constants
- Update build details page area charts to only from build start to completion
- Minimum queried timepan to one minute
- Show seconds on build charts if timespan is less than 5 minutes
- Force x domain on build charts to match the queried timespan so that chart will not show less than
  one minute on the x-axis.
- Add new xMutator callback arg to graph util `mapLimitsRequests` function to allow for post-processing of
  utilization chart x values
- Add new xMutator and yMutator callback args to graph util `getRangeVectorStats` function to allow
  for post-processing of prometheus response data x and y values
- Remove logic from getRangeVectorStats function that removed second and millisecond data from all x
  values
- Update PrometheusMultilineUtilizationItem to use new xMutator argument to trim seconds and
  milliseconds off of multiline area chart data points.
- Create new global constants for prometheus query default samples and timespan
- Update Area and AreaChart components to use new global prometheus constants and accept domain prop
- Update twentyFourHourTime util function to accept `showSeconds` arg and get rid of
  twentyFourHourTimeWithSeconds function.
- Update usage of twentyFourHourTime/twentyFourHourTimeWithSeconds function where needed
  • Loading branch information
TheRealJon committed Oct 27, 2020
1 parent 9234180 commit f6c127a
Show file tree
Hide file tree
Showing 14 changed files with 149 additions and 102 deletions.
2 changes: 0 additions & 2 deletions frontend/__tests__/components/graphs/utils.spec.ts
Expand Up @@ -41,9 +41,7 @@ describe('getRangeVectorStats()', () => {

const [d1, d2] = data[0];
const date1 = new Date(1000);
date1.setSeconds(0, 0);
const date2 = new Date(2000);
date2.setSeconds(0, 0);
expect(d1.x).toEqual(date1);
expect(d1.y).toEqual(123.4);
expect(d2.x).toEqual(date2);
Expand Down
1 change: 1 addition & 0 deletions frontend/packages/console-shared/src/constants/index.ts
Expand Up @@ -2,3 +2,4 @@ export * from './pod';
export * from './resource';
export * from './common';
export * from './ui';
export * from './time';
5 changes: 5 additions & 0 deletions frontend/packages/console-shared/src/constants/time.ts
@@ -0,0 +1,5 @@
export const ONE_SECOND = 1000;
export const ONE_MINUTE = 60 * ONE_SECOND;
export const ONE_HOUR = 60 * ONE_MINUTE;
export const ONE_DAY = 24 * ONE_HOUR;
export const ONE_WEEK = 7 * ONE_DAY;
Expand Up @@ -21,7 +21,6 @@ import { ByteDataTypes } from '@console/shared/src/graph-helper/data-utils';
import { VMDashboardContext } from '../../vms/vm-dashboard-context';
import { findVMIPod } from '../../../selectors/pod/selectors';
import { getUtilizationQueries, getMultilineUtilizationQueries, VMQueries } from './queries';
import { getPrometheusQueryEndTimestamp } from '@console/internal/components/graphs/helpers';

// TODO: extend humanizeCpuCores() from @console/internal for the flexibility of space
const humanizeCpuCores = (v) => {
Expand All @@ -36,7 +35,7 @@ const adjustDurationForStart = (start: number, createdAt: string): number => {
if (!createdAt) {
return start;
}
const endTimestamp = getPrometheusQueryEndTimestamp();
const endTimestamp = Date.now();
const startTimestamp = endTimestamp - start;
const createdAtTimestamp = Date.parse(createdAt);
const adjustedStart = endTimestamp - createdAtTimestamp;
Expand Down
41 changes: 29 additions & 12 deletions frontend/public/components/build.tsx
Expand Up @@ -5,7 +5,7 @@ import * as classNames from 'classnames';
import { sortable } from '@patternfly/react-table';
import { Alert } from '@patternfly/react-core';

import { Status } from '@console/shared';
import { ONE_HOUR, ONE_MINUTE, Status } from '@console/shared';
import { ByteDataTypes } from '@console/shared/src/graph-helper/data-utils';
import {
K8sResourceKindReference,
Expand Down Expand Up @@ -42,6 +42,7 @@ import { BuildLogs } from './build-logs';
import { ResourceEventStream } from './events';
import { Area, requirePrometheus } from './graphs';
import { BuildModel } from '../models';
import { twentyFourHourTime } from './utils/datetime';

const BuildsReference: K8sResourceKindReference = 'Build';

Expand Down Expand Up @@ -127,40 +128,56 @@ export const BuildNumberLink = ({ build }) => {
};

const BuildGraphs = requirePrometheus(({ build }) => {
const podName = _.get(build, ['metadata', 'annotations', 'openshift.io/build.pod-name']);
const podName = build.metadata.annotations?.['openshift.io/build.pod-name'];
if (!podName) {
return null;
}

const endTime = build.status.completionTimestamp
? new Date(build.status.completionTimestamp).getTime()
: Date.now();
const runTime = build.status.startTimestamp
? endTime - new Date(build.status.startTimestamp).getTime()
: ONE_HOUR;
const timespan = Math.max(runTime, ONE_MINUTE); // Minimum timespan of one minute
const namespace = build.metadata.namespace;

const domain = React.useMemo(() => ({ x: [endTime - timespan, endTime] }), [endTime, timespan]);
const areaProps = React.useMemo(
() => ({
namespace,
endTime,
timespan,
formatDate: (d) => twentyFourHourTime(d, timespan < 5 * ONE_MINUTE),
domain, // force domain to match queried timespan
}),
[domain, endTime, namespace, timespan],
);
return (
<>
<div className="row">
<div className="col-md-12 col-lg-4">
<Area
title="Memory Usage"
humanize={humanizeBinaryBytes}
byteDataType={ByteDataTypes.BinaryBytes}
namespace={namespace}
humanize={humanizeBinaryBytes}
query={`sum(container_memory_working_set_bytes{pod='${podName}',namespace='${namespace}',container=''}) BY (pod, namespace)`}
title="Memory Usage"
{...areaProps}
/>
</div>
<div className="col-md-12 col-lg-4">
<Area
title="CPU Usage"
humanize={humanizeCpuCores}
namespace={namespace}
query={`pod:container_cpu_usage:sum{pod='${podName}',container='',namespace='${namespace}'}`}
title="CPU Usage"
{...areaProps}
/>
</div>
<div className="col-md-12 col-lg-4">
<Area
title="Filesystem"
humanize={humanizeBinaryBytes}
byteDataType={ByteDataTypes.BinaryBytes}
namespace={namespace}
humanize={humanizeBinaryBytes}
query={`pod:container_fs_usage_bytes:sum{pod='${podName}',container='',namespace='${namespace}'}`}
title="Filesystem"
{...areaProps}
/>
</div>
</div>
Expand Down
Expand Up @@ -147,6 +147,16 @@ const networkOutQueriesPopup = [
},
];

// TODO (jon) Fix PrometheusMultilineUtilization so that x-values returned from multiple prometheus
// queries are "synced" on the x-axis (same number of points with the same x-values). In order to do
// so, we have to make sure that the same end time, samples, and duration are used across all
// queries. This is a temporary work around. See https://issues.redhat.com/browse/CONSOLE-2424
const trimSecondsXMutator = (x) => {
const d = new Date(x * 1000);
d.setSeconds(0, 0);
return d;
};

export const PrometheusUtilizationItem = withDashboardResources<PrometheusUtilizationItemProps>(
({
watchPrometheus,
Expand Down Expand Up @@ -307,7 +317,7 @@ export const PrometheusMultilineUtilizationItem = withDashboardResources<
isLoading = true;
return false;
}
stats.push(getRangeVectorStats(response, query.desc)?.[0] || []);
stats.push(getRangeVectorStats(response, query.desc, null, trimSecondsXMutator)?.[0] || []);
});
}

Expand Down
67 changes: 40 additions & 27 deletions frontend/public/components/graphs/area.tsx
Expand Up @@ -9,6 +9,7 @@ import {
ChartVoronoiContainer,
getCustomTheme,
ChartGroup,
ChartAreaProps,
} from '@patternfly/react-charts';
import { global_warning_color_100 as warningColor } from '@patternfly/react-tokens/dist/js/global_warning_color_100';
import { global_danger_color_100 as dangerColor } from '@patternfly/react-tokens/dist/js/global_danger_color_100';
Expand All @@ -19,15 +20,18 @@ import { PrometheusEndpoint } from './helpers';
import { PrometheusGraph, PrometheusGraphLink } from './prometheus-graph';
import { usePrometheusPoll } from './prometheus-poll-hook';
import { areaTheme } from './themes';
import { DataPoint, CursorVoronoiContainer } from './';
import {
DataPoint,
CursorVoronoiContainer,
DEFAULT_PROMETHEUS_SAMPLES,
DEFAULT_PROMETHEUS_TIMESPAN,
} from './';
import { mapLimitsRequests } from './utils';
import { GraphEmpty } from './graph-empty';
import { ChartLegendTooltip } from './tooltip';

const DEFAULT_HEIGHT = 180;
const DEFAULT_SAMPLES = 60;
const DEFAULT_TICK_COUNT = 3;
const DEFAULT_TIMESPAN = 60 * 60 * 1000; // 1 hour

export enum AreaChartStatus {
ERROR = 'ERROR',
Expand Down Expand Up @@ -57,6 +61,7 @@ export const AreaChart: React.FC<AreaChartProps> = ({
chartStyle,
byteDataType = '',
showAllTooltip,
...rest
}) => {
// Note: Victory incorrectly typed ThemeBaseProps.padding as number instead of PaddingProps
// @ts-ignore
Expand All @@ -76,11 +81,20 @@ export const AreaChart: React.FC<AreaChartProps> = ({
[processedData],
);

const tickFormat = React.useCallback((tick) => `${humanize(tick, unit, unit).string}`, [
const xTickFormat = React.useCallback((tick) => formatDate(tick), [formatDate]);
const yTickFormat = React.useCallback((tick) => `${humanize(tick, unit, unit).string}`, [
humanize,
unit,
]);

const domain = React.useMemo<AreaChartProps['domain']>(
() => ({
...(allZero && { y: [0, 1] }),
...(rest.domain ?? {}),
}),
[allZero, rest.domain],
);

const getLabel = React.useCallback(
(prop, includeDate = true) => {
const { x, y } = prop.datum as DataPoint<Date>;
Expand Down Expand Up @@ -131,10 +145,10 @@ export const AreaChart: React.FC<AreaChartProps> = ({
theme={theme}
scale={{ x: 'time', y: 'linear' }}
padding={padding}
{...(allZero && { domain: { y: [0, 1] } })}
domain={domain}
>
{xAxis && <ChartAxis tickCount={tickCount} tickFormat={formatDate} />}
{yAxis && <ChartAxis dependentAxis tickCount={tickCount} tickFormat={tickFormat} />}
{xAxis && <ChartAxis tickCount={tickCount} tickFormat={xTickFormat} />}
{yAxis && <ChartAxis dependentAxis tickCount={tickCount} tickFormat={yTickFormat} />}
<ChartGroup>
{processedData.map((datum, index) => (
<ChartArea
Expand All @@ -155,52 +169,50 @@ export const AreaChart: React.FC<AreaChartProps> = ({
};

export const Area: React.FC<AreaProps> = ({
endTime = Date.now(),
namespace,
query,
limitQuery,
requestedQuery,
samples = DEFAULT_SAMPLES,
samples = DEFAULT_PROMETHEUS_SAMPLES,
timeout,
timespan = DEFAULT_TIMESPAN,
timespan = DEFAULT_PROMETHEUS_TIMESPAN,
...rest
}) => {
const [utilization, , utilizationLoading] = usePrometheusPoll({
const prometheusPollProps = {
endpoint: PrometheusEndpoint.QUERY_RANGE,
endTime,
namespace,
query,
samples,
timeout,
timespan,
};

const [utilization, , utilizationLoading] = usePrometheusPoll({
query,
...prometheusPollProps,
});
const [limit, , limitLoading] = usePrometheusPoll({
endpoint: PrometheusEndpoint.QUERY_RANGE,
namespace,
query: limitQuery,
samples,
timeout,
timespan,
...prometheusPollProps,
});
const [requested, , requestedLoading] = usePrometheusPoll({
endpoint: PrometheusEndpoint.QUERY_RANGE,
namespace,
query: requestedQuery,
samples,
timeout,
timespan,
...prometheusPollProps,
});

const loading = utilizationLoading && limitLoading && requestedLoading;
const { data, chartStyle } = mapLimitsRequests(utilization, limit, requested);
const loading =
utilizationLoading &&
(limitQuery ? limitLoading : true) &&
(requestedQuery ? requestedLoading : true);

return (
<AreaChart data={data} loading={loading} query={query} chartStyle={chartStyle} {...rest} />
<AreaChart chartStyle={chartStyle} data={data} loading={loading} query={query} {...rest} />
);
};

export type AreaChartProps = {
className?: string;
formatDate?: (date: Date) => string;
domain?: ChartAreaProps['domain'];
formatDate?: (date: Date, showSeconds?: boolean) => string;
humanize?: Humanize;
height?: number;
loading?: boolean;
Expand All @@ -219,6 +231,7 @@ export type AreaChartProps = {
};

type AreaProps = AreaChartProps & {
endTime?: number;
namespace?: string;
query: string;
samples?: number;
Expand Down
26 changes: 13 additions & 13 deletions frontend/public/components/graphs/helpers.ts
@@ -1,6 +1,10 @@
import * as _ from 'lodash-es';

import { PROMETHEUS_BASE_PATH, PROMETHEUS_TENANCY_BASE_PATH } from './index';
import {
PROMETHEUS_BASE_PATH,
PROMETHEUS_TENANCY_BASE_PATH,
DEFAULT_PROMETHEUS_SAMPLES,
DEFAULT_PROMETHEUS_TIMESPAN,
} from './index';

export enum PrometheusEndpoint {
LABEL = 'api/v1/label',
Expand All @@ -9,20 +13,16 @@ export enum PrometheusEndpoint {
QUERY_RANGE = 'api/v1/query_range',
}

export const getPrometheusQueryEndTimestamp = () => Date.now();

// Range vector queries require end, start, and step search params
const getRangeVectorSearchParams = (
timespan: number,
endTime: number = getPrometheusQueryEndTimestamp(),
samples: number = 60,
endTime: number = Date.now(),
samples: number = DEFAULT_PROMETHEUS_SAMPLES,
timespan: number = DEFAULT_PROMETHEUS_TIMESPAN,
): URLSearchParams => {
const params = new URLSearchParams();
if (timespan > 0) {
params.append('start', `${(endTime - timespan) / 1000}`);
params.append('end', `${endTime / 1000}`);
params.append('step', `${timespan / samples / 1000}`);
}
params.append('start', `${(endTime - timespan) / 1000}`);
params.append('end', `${endTime / 1000}`);
params.append('step', `${timespan / samples / 1000}`);
return params;
};

Expand All @@ -35,7 +35,7 @@ const getSearchParams = ({
}: PrometheusURLProps): URLSearchParams => {
const searchParams =
endpoint === PrometheusEndpoint.QUERY_RANGE
? getRangeVectorSearchParams(timespan, endTime, samples)
? getRangeVectorSearchParams(endTime, samples, timespan)
: new URLSearchParams();
_.each(params, (value, key) => value && searchParams.append(key, value.toString()));
return searchParams;
Expand Down
4 changes: 3 additions & 1 deletion frontend/public/components/graphs/index.tsx
@@ -1,13 +1,15 @@
import * as React from 'react';
import { createContainer } from '@patternfly/react-charts';
import { AsyncComponent } from '../utils/async';
import { ONE_HOUR } from '@console/shared/src/constants/time';

// Constants
export const PROMETHEUS_BASE_PATH = window.SERVER_FLAGS.prometheusBaseURL;
export const PROMETHEUS_TENANCY_BASE_PATH = window.SERVER_FLAGS.prometheusTenancyBaseURL;
export const ALERT_MANAGER_BASE_PATH = window.SERVER_FLAGS.alertManagerBaseURL;
export const ALERT_MANAGER_TENANCY_BASE_PATH = 'api/alertmanager-tenancy'; // remove it once it get added to SERVER_FLAGS

export const DEFAULT_PROMETHEUS_SAMPLES = 60;
export const DEFAULT_PROMETHEUS_TIMESPAN = ONE_HOUR;
// Components
export * from './require-prometheus';
export { errorStatus, Status } from './status';
Expand Down
11 changes: 4 additions & 7 deletions frontend/public/components/graphs/prometheus-poll-hook.ts
@@ -1,19 +1,16 @@
import { useURLPoll } from '../utils/url-poll-hook';
import { getPrometheusURL, PrometheusEndpoint } from './helpers';
import { PrometheusResponse } from '.';

const DEFAULT_SAMPLES = 60;
const DEFAULT_TIMESPAN = 60 * 60 * 1000; // 1 hour
import { DEFAULT_PROMETHEUS_SAMPLES, DEFAULT_PROMETHEUS_TIMESPAN, PrometheusResponse } from '.';

export const usePrometheusPoll = ({
delay,
endpoint,
endTime = undefined,
endTime,
namespace,
query,
samples = DEFAULT_SAMPLES,
samples = DEFAULT_PROMETHEUS_SAMPLES,
timeout,
timespan = DEFAULT_TIMESPAN,
timespan = DEFAULT_PROMETHEUS_TIMESPAN,
}: PrometheusPollProps) => {
const url = getPrometheusURL({ endpoint, endTime, namespace, query, samples, timeout, timespan });

Expand Down

0 comments on commit f6c127a

Please sign in to comment.