Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev metrics: visualisation of queries in metrics #3891

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,19 @@
import * as React from 'react';
import {
EmptyState,
EmptyStateBody,
EmptyStateIcon,
EmptyStateVariant,
} from '@patternfly/react-core';
import { ChartLineIcon } from '@patternfly/react-icons';

const EmptyStateQuery: React.FC = () => (
<EmptyState variant={EmptyStateVariant.full}>
<EmptyStateIcon size="sm" icon={ChartLineIcon} />
<EmptyStateBody>
Select a query or enter your own to view metrics for this project
</EmptyStateBody>
</EmptyState>
);

export default EmptyStateQuery;
@@ -0,0 +1,11 @@
.odc-metrics-chart {
border: 1px solid var(--pf-global--BorderColor--100);
padding: var(--pf-global--spacer--sm);
.query-browser__wrapper {
border: none;
}

tr > :first-child {
width: 40px;
}
}
@@ -0,0 +1,64 @@
import * as _ from 'lodash';
import * as React from 'react';
import { connect } from 'react-redux';
import { RootState } from '@console/internal/redux';
import { queryBrowserDeleteAllQueries } from '@console/internal/actions/ui';
import { getActiveNamespace } from '@console/internal/reducers/ui';
import {
QueryBrowser,
QueryObj,
QueryBrowserProps,
} from '@console/internal/components/monitoring/query-browser';
import { QueryTable } from '@console/internal/components/monitoring/metrics';
import EmptyStateQuery from './EmptyStateQuery';
import './MetricsChart.scss';

type StateProps = {
queries: QueryObj[];
namespace: string;
};
type MetricsChartProps = {
deleteAll?: () => never;
queries: QueryObj[];
namespace: string;
};
const DEFAULT_TIME_SPAN = 30 * 60 * 1000;

export const MetricsChart: React.FC<MetricsChartProps> = ({ deleteAll, queries, namespace }) => {
const [queryStrings, setQueryStrings] = React.useState<QueryBrowserProps['queries']>([]);
const [disabledSeries, setDisabledSeries] = React.useState<QueryBrowserProps['disabledSeries']>(
[],
);
// TO delete all queries patched on componenet unmount
React.useEffect(() => deleteAll, [deleteAll]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this line doesn't actually do anything. If it does have a purpose, maybe add some comments explaining the purpose, because at first glance it appears to be dead code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to delete Queries on component unmount from the store, have added comments

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, right, I forgot that you can return a clean up function from React.useEffect. Cool, thanks for adding comments.

React.useEffect(() => {
setQueryStrings((prev) => {
const next = _.compact(_.map(queries, 'query'));
return _.isEqual(prev, next) ? prev : next;
});
setDisabledSeries((prev) => {
const next = _.compact(_.map(queries, 'disabledSeries'));
return _.isEqual(prev, next) ? prev : next;
});
}, [queries]);
return queryStrings.length === 0 ? (
<EmptyStateQuery />
) : (
<div className="odc-metrics-chart">
<QueryBrowser
defaultTimespan={DEFAULT_TIME_SPAN}
disabledSeries={disabledSeries}
namespace={namespace}
queries={queryStrings}
/>
<QueryTable index={0} namespace={namespace} />
</div>
);
};

const mapStateToProps = (state: RootState): StateProps => ({
queries: state.UI.getIn(['queryBrowser', 'queries']).toJS(),
namespace: getActiveNamespace(state),
});

export default connect(mapStateToProps, { deleteAll: queryBrowserDeleteAllQueries })(MetricsChart);
@@ -0,0 +1,8 @@
.odc-metrics-query-input {
margin-bottom: 20px;
&__menu {
max-height: 50vh;
overflow: auto;
width: 100%;
}
}
@@ -0,0 +1,72 @@
import * as React from 'react';
import { connect } from 'react-redux';
import * as fuzzy from 'fuzzysearch';
import { RootState } from '@console/internal/redux';
import { Dropdown } from '@console/internal/components/utils';
import { queryBrowserRunQueries, queryBrowserPatchQuery } from '@console/internal/actions/ui';
import { getActiveNamespace } from '@console/internal/reducers/ui';
import { QueryObj } from '@console/internal/components/monitoring/query-browser';
import { metricsQuery, getTopMetricsQueries } from '../queries';
import './MetricsQueryInput.scss';

const ADD_NEW_QUERY = '#ADD_NEW_QUERY#';

type StateProps = {
namespace: string;
};

type MetricsQueryInputProps = {
patchQuery: (patch: QueryObj) => void;
runQueries: () => never;
namespace: string;
};

export const MetricsQueryInput: React.FC<MetricsQueryInputProps> = ({
patchQuery,
runQueries,
namespace,
}) => {
const items = metricsQuery;
const autocompleteFilter = (text, item) => fuzzy(text, item);
const defaultActionItem = [
{
actionTitle: `Custom Query`,
actionKey: ADD_NEW_QUERY,
},
];

const [metric, setMetric] = React.useState('');
React.useEffect(() => {
if (metric && metric !== ADD_NEW_QUERY) {
const query = getTopMetricsQueries(namespace)[metric];
patchQuery({ text: query });
runQueries();
}
}, [metric, namespace, patchQuery, runQueries]);
const onChange = (selVal: string) => {
setMetric(selVal);
};

return (
<Dropdown
autocompleteFilter={autocompleteFilter}
items={items || {}}
actionItems={defaultActionItem}
dropDownClassName="odc-metrics-query-input dropdown--full-width"
menuClassName="odc-metrics-query-input__menu dropdown-menu--text-wrap"
onChange={onChange}
title={'Select Query'}
/>
);
};

const mapStateToProps = (state: RootState): StateProps => ({
namespace: getActiveNamespace(state),
});

const mapDispatchToProps = (dispatch) => ({
runQueries: () => dispatch(queryBrowserRunQueries()),
patchQuery: (v: QueryObj) => dispatch(queryBrowserPatchQuery(0, v)),
});

export default connect(mapStateToProps, mapDispatchToProps)(MetricsQueryInput);
@@ -1,12 +1,26 @@
import * as React from 'react';
import { Helmet } from 'react-helmet';
import ConnectedMetricsQueryInput from './MetricsQueryInput';
import ConnectedMetricsChart from './MetricsChart';

const MonitoringMetrics: React.FC = () => {
return (
<>
<Helmet>
<title>Metrics</title>
</Helmet>
<div className="co-m-pane__body">
<div className="row">
<div className="col-xs-12 col-md-6">
<ConnectedMetricsQueryInput />
</div>
</div>
<div className="row">
<div className="col-xs-12">
<ConnectedMetricsChart />
</div>
</div>
</div>
</>
);
};
Expand Down
@@ -0,0 +1,20 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import { MetricsChart } from '../MetricsChart';
import EmptyStateQuery from '../EmptyStateQuery';

describe('Metrics Chart', () => {
let props: React.ComponentProps<typeof MetricsChart>;
beforeEach(() => {
props = {
queries: [],
namespace: 'my-app',
};
});

it('should render EmptyStateQuery if queries are not present', () => {
props.queries = [];
const wrapper = shallow(<MetricsChart {...props} />);
expect(wrapper.find(EmptyStateQuery)).toHaveLength(1);
});
});
32 changes: 32 additions & 0 deletions frontend/packages/dev-console/src/components/monitoring/queries.ts
@@ -0,0 +1,32 @@
import * as _ from 'lodash';

export const metricsQuery = {
PODS_BY_CPU: 'PODS_BY_CPU',
PODS_BY_MEMORY: 'PODS_BY_MEMORY',
PODS_BY_FILESYSTEM: 'PODS_BY_FILESYSTEM',
PODS_BY_NETWORK: 'PODS_BY_NETWORK',
};

const topMetricsQueries = {
[metricsQuery.PODS_BY_CPU]: _.template(
`topk(25, sort_desc(sum(avg_over_time(pod:container_cpu_usage:sum{container="",pod!="",namespace='<%= project %>'}[5m])) BY (pod, namespace)))`,
),
[metricsQuery.PODS_BY_MEMORY]: _.template(
`topk(25, sort_desc(sum(avg_over_time(container_memory_working_set_bytes{container="",pod!="",namespace='<%= project %>'}[5m])) BY (pod, namespace)))`,
),
[metricsQuery.PODS_BY_FILESYSTEM]: _.template(
`topk(25, sort_desc(sum(pod:container_fs_usage_bytes:sum{container="",pod!="",namespace='<%= project %>'}) BY (pod, namespace)))`,
),
[metricsQuery.PODS_BY_NETWORK]: _.template(
`topk(25, sort_desc(sum(rate(container_network_receive_bytes_total{ container="POD", pod!= "", namespace = '<%= project %>'}[5m]) + rate(container_network_transmit_bytes_total{ container="POD", pod!= "", namespace = '<%= project %>'}[5m])) BY (namespace, pod)))`,
),
};

export const getTopMetricsQueries = (project: string) => ({
[metricsQuery.PODS_BY_CPU]: topMetricsQueries[metricsQuery.PODS_BY_CPU]({ project }),
[metricsQuery.PODS_BY_MEMORY]: topMetricsQueries[metricsQuery.PODS_BY_MEMORY]({ project }),
[metricsQuery.PODS_BY_FILESYSTEM]: topMetricsQueries[metricsQuery.PODS_BY_FILESYSTEM]({
project,
}),
[metricsQuery.PODS_BY_NETWORK]: topMetricsQueries[metricsQuery.PODS_BY_NETWORK]({ project }),
});
2 changes: 1 addition & 1 deletion frontend/public/components/monitoring/metrics.tsx
Expand Up @@ -769,7 +769,7 @@ const QueryTable_: React.FC<QueryTableProps> = ({
</>
);
};
const QueryTable = connect(queryTableStateToProps, queryDispatchToProps)(QueryTable_);
export const QueryTable = connect(queryTableStateToProps, queryDispatchToProps)(QueryTable_);

const NamespaceAlert_: React.FC<{ dismiss: () => undefined; isDismissed: boolean }> = ({
dismiss,
Expand Down
2 changes: 1 addition & 1 deletion frontend/public/components/monitoring/query-browser.tsx
Expand Up @@ -680,7 +680,7 @@ type ZoomableGraphProps = {
xDomain?: AxisDomain;
};

type QueryBrowserProps = {
export type QueryBrowserProps = {
defaultSamples?: number;
defaultTimespan?: number;
disabledSeries?: Labels[][];
Expand Down