CloudMonitoring: Improve metric handling for DISTRIBUTION value types (#96412)

* Set crossSeriesReducer value based on metric valueType

* Update label retrieval to retrieve all values

* Update tests

* Update documentation

* Add tooltip

* fix typo

* Docs lint

---------

Co-authored-by: Irene Rodríguez <irene.rodriguez@grafana.com>
This commit is contained in:
Andreas Christou 2024-11-18 12:27:40 +00:00 committed by GitHub
parent 2ff9baa271
commit 78930314c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 81 additions and 7 deletions

View File

@ -66,8 +66,17 @@ The metrics query editor helps you select metrics, group and aggregate by labels
1. _(Optional)_ Use the plus and minus icons in the filter and group-by sections to add and remove filters or group-by clauses.
Google Cloud Monitoring supports several metrics types, such as `GAUGE`, `DELTA,` and `CUMULATIVE`.
Each supports different aggregation options, such as reducers and aligners.
The metrics query editor lists available aggregation methods for a selected metric, and sets a default reducer and aligner when you select a metric.
Each supports different aggregation options, such as reducers and aligners. Additionally, metrics have specific value types that can be either scalar or a distribution.
The metrics query editor lists available aggregation methods for a selected metric, and sets a default aggregation, reducer and aligner when you select a metric.
In the case that the metric value type is a distribution, the aggregation will be set by default to the mean. For scalar value types, there is no aggregation by default.
The various metrics are documented [here](https://cloud.google.com/monitoring/api/metrics_gcp) and further details on the kinds and types of metrics can be found [here](https://cloud.google.com/monitoring/api/v3/kinds-and-types).
{{% admonition type="note" %}}
Distribution metrics are typically best visualized as either a heatmap or histogram. When visualizing in this way, aggregation is not necessary. However, for other visualization types, performance degradation may be observed when attempting to query distribution metrics that are not aggregated due to the number of potential buckets that can be returned. For more information on how to visualize distribution metrics refer to [this page](https://cloud.google.com/monitoring/charts/charting-distribution-metrics).
{{% /admonition %}}
### Apply a filter

View File

@ -22,7 +22,13 @@ export const Aggregation = (props: Props) => {
const selected = useSelectedFromOptions(aggOptions, props);
return (
<EditorField label="Group by function" data-testid="cloud-monitoring-aggregation">
<EditorField
label="Group by function"
data-testid="cloud-monitoring-aggregation"
tooltip={
'Aggregation function used on the metric data. Defaults to none for scalar data and mean for distribution data. Not applying an aggregation to distribution data may lead to performance issues.'
}
>
<Select
width="auto"
onChange={({ value }) => props.onChange(value!)}

View File

@ -9,7 +9,7 @@ import { getTemplateSrv } from '@grafana/runtime';
import { createMockDatasource } from '../__mocks__/cloudMonitoringDatasource';
import { createMockMetricDescriptor } from '../__mocks__/cloudMonitoringMetricDescriptor';
import { createMockTimeSeriesList } from '../__mocks__/cloudMonitoringQuery';
import { PreprocessorType, MetricKind } from '../types/query';
import { PreprocessorType, MetricKind, ValueTypes } from '../types/query';
import { defaultTimeSeriesList } from './MetricQueryEditor';
import { VisualMetricQueryEditor } from './VisualMetricQueryEditor';
@ -98,7 +98,11 @@ describe('VisualMetricQueryEditor', () => {
replace = (target?: string) => target || '';
const onChange = jest.fn();
const query = createMockTimeSeriesList();
const mockMetricDescriptor = createMockMetricDescriptor({ displayName: 'metricName_test', type: 'test_type' });
const mockMetricDescriptor = createMockMetricDescriptor({
displayName: 'metricName_test',
type: 'test_type',
valueType: ValueTypes.DOUBLE,
});
const datasource = createMockDatasource({
getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor(), mockMetricDescriptor]),
filterMetricsByType: jest.fn().mockResolvedValue([createMockMetricDescriptor(), mockMetricDescriptor]),
@ -129,7 +133,56 @@ describe('VisualMetricQueryEditor', () => {
await select(metricName, 'metricName_test', { container: document.body });
});
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({ filters: ['metric.type', '=', mockMetricDescriptor.type] })
expect.objectContaining({
filters: ['metric.type', '=', mockMetricDescriptor.type],
crossSeriesReducer: 'REDUCE_NONE',
})
);
});
it('can select a metric name with DISTRIBUTION valueType', async () => {
replace = (target?: string) => target || '';
const onChange = jest.fn();
const query = createMockTimeSeriesList();
const mockMetricDescriptor = createMockMetricDescriptor({
displayName: 'metricName_test',
type: 'test_type',
valueType: ValueTypes.DISTRIBUTION,
});
const datasource = createMockDatasource({
getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor(), mockMetricDescriptor]),
filterMetricsByType: jest.fn().mockResolvedValue([createMockMetricDescriptor(), mockMetricDescriptor]),
getLabels: jest.fn().mockResolvedValue([]),
});
const range = getDefaultTimeRange();
render(
<VisualMetricQueryEditor
{...defaultProps}
onChange={onChange}
datasource={datasource}
query={query}
range={range}
/>
);
const service = await screen.findByLabelText('Service');
openMenu(service);
await act(async () => {
await select(service, 'Srv', { container: document.body });
});
const metricName = await screen.findByLabelText('Metric name');
openMenu(metricName);
await userEvent.type(metricName, 'test');
await waitFor(() => expect(document.body).toHaveTextContent('metricName_test'));
await act(async () => {
await select(metricName, 'metricName_test', { container: document.body });
});
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
filters: ['metric.type', '=', mockMetricDescriptor.type],
crossSeriesReducer: 'REDUCE_MEAN',
})
);
});

View File

@ -209,6 +209,11 @@ export function Editor({
// On metric name change reset query to defaults except project name and filters
Object.assign(query, {
...defaultTimeSeriesList(datasource),
// If the metric value type is DISTRIBUTION use REDUCE_MEAN in order to avoid
// returning data frames with a large number of frames (as we return a frame per bucket).
// DISTRIBUTION metrics only typically make sense with an aggregation performed against them or
// when filtered to a specific label value.
crossSeriesReducer: valueType === ValueTypes.DISTRIBUTION ? 'REDUCE_MEAN' : 'REDUCE_NONE',
projectName: query.projectName,
filters: query.filters,
});

View File

@ -106,7 +106,8 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
{
projectName: this.templateSrv.replace(projectName),
groupBys: this.interpolateGroupBys(aggregation?.groupBys || [], {}),
crossSeriesReducer: aggregation?.crossSeriesReducer ?? 'REDUCE_NONE',
// Use REDUCE_NONE to retrieve all available labels for the metric
crossSeriesReducer: 'REDUCE_NONE',
view: 'HEADERS',
},
this.templateSrv.replace(metricType)