CloudWatch: remove core imports from CloudWatchMetricsQueryRunner (#80926)

* CloudWatch: remove core imports from CloudWatchMetricsQueryRunner

* use getAppEvents to publish error

* use default wait time

* put test back in original position

* fix throttling error message link
This commit is contained in:
Kevin Yu
2024-02-06 11:24:14 -08:00
committed by GitHub
parent 3b852bb582
commit 6373557c78
3 changed files with 104 additions and 31 deletions

View File

@@ -20,7 +20,7 @@ export const ThrottlingErrorMessage = ({ region }: Props) => (
target="_blank"
rel="noreferrer"
className="text-link"
href="https://grafana.com/docs/grafana/latest/datasources/cloudwatch/#service-quotas"
href="https://grafana.com/docs/grafana/latest/datasources/cloudwatch/#manage-service-quotas"
>
documentation
</a>

View File

@@ -3,7 +3,6 @@ import { of } from 'rxjs';
import { CustomVariableModel, getFrameDisplayName, VariableHide } from '@grafana/data';
import { dateTime } from '@grafana/data/src/datetime/moment_wrapper';
import { toDataQueryResponse } from '@grafana/runtime';
import * as redux from 'app/store/store';
import {
namespaceVariable,
@@ -19,6 +18,13 @@ import { setupMockedMetricsQueryRunner } from '../__mocks__/MetricsQueryRunner';
import { validMetricSearchBuilderQuery, validMetricSearchCodeQuery } from '../__mocks__/queries';
import { MetricQueryType, MetricEditorMode, CloudWatchMetricsQuery } from '../types';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getAppEvents: () => ({
publish: jest.fn(),
}),
}));
describe('CloudWatchMetricsQueryRunner', () => {
describe('performTimeSeriesQuery', () => {
it('should return the same length of data as result', async () => {
@@ -92,6 +98,68 @@ describe('CloudWatchMetricsQueryRunner', () => {
});
});
it('should enrich the error message for throttling errors', async () => {
const partialQuery: CloudWatchMetricsQuery = {
metricQueryType: MetricQueryType.Search,
metricEditorMode: MetricEditorMode.Builder,
queryMode: 'Metrics',
namespace: 'AWS/EC2',
metricName: 'CPUUtilization',
dimensions: {
InstanceId: 'i-12345678',
},
statistic: 'Average',
period: '300',
expression: '',
id: '',
region: '',
refId: '',
};
const queries: CloudWatchMetricsQuery[] = [
{ ...partialQuery, refId: 'A', region: 'us-east-1' },
{ ...partialQuery, refId: 'B', region: 'us-east-2' },
];
const dataWithThrottlingError = {
data: {
message: 'Throttling: exception',
results: {
A: {
frames: [],
series: [],
tables: [],
error: 'Throttling: exception',
refId: 'A',
meta: {},
},
B: {
frames: [],
series: [],
tables: [],
error: 'Throttling: exception',
refId: 'B',
meta: {},
},
},
},
};
const expectedUsEast1Message =
'Please visit the AWS Service Quotas console at https://us-east-1.console.aws.amazon.com/servicequotas/home?region=us-east-1#!/services/monitoring/quotas/L-5E141212 to request a quota increase or see our documentation at https://grafana.com/docs/grafana/latest/datasources/cloudwatch/#manage-service-quotas to learn more. Throttling: exception';
const expectedUsEast2Message =
'Please visit the AWS Service Quotas console at https://us-east-2.console.aws.amazon.com/servicequotas/home?region=us-east-2#!/services/monitoring/quotas/L-5E141212 to request a quota increase or see our documentation at https://grafana.com/docs/grafana/latest/datasources/cloudwatch/#manage-service-quotas to learn more. Throttling: exception';
const { runner, request, queryMock } = setupMockedMetricsQueryRunner({
response: toDataQueryResponse(dataWithThrottlingError),
});
await expect(runner.handleMetricQueries(queries, request, queryMock)).toEmitValuesWith((received) => {
expect(received[0].errors).toHaveLength(2);
expect(received[0]?.errors?.[0].message).toEqual(expectedUsEast1Message);
expect(received[0]?.errors?.[1].message).toEqual(expectedUsEast2Message);
});
});
describe('When performing CloudWatch metrics query', () => {
const queries: CloudWatchMetricsQuery[] = [
{
@@ -275,13 +343,6 @@ describe('CloudWatchMetricsQueryRunner', () => {
},
};
beforeEach(() => {
redux.setStore({
...redux.store,
dispatch: jest.fn(),
});
});
it('should display one alert error message per region+datasource combination', async () => {
const { runner, request, queryMock } = setupMockedMetricsQueryRunner({
response: toDataQueryResponse(dataWithThrottlingError),

View File

@@ -3,6 +3,7 @@ import React from 'react';
import { catchError, map, Observable, of } from 'rxjs';
import {
AppEvents,
DataFrame,
DataQueryError,
DataQueryRequest,
@@ -13,11 +14,7 @@ import {
rangeUtil,
ScopedVars,
} from '@grafana/data';
import { TemplateSrv } from '@grafana/runtime';
import { notifyApp } from 'app/core/actions';
import { createErrorNotification } from 'app/core/copy/appNotification';
import { store } from 'app/store/store';
import { AppNotificationTimeout } from 'app/types';
import { TemplateSrv, getAppEvents } from '@grafana/runtime';
import { ThrottlingErrorMessage } from '../components/Errors/ThrottlingErrorMessage';
import memoizedDebounce from '../memoizedDebounce';
@@ -27,24 +24,23 @@ import { filterMetricsQuery } from '../utils/utils';
import { CloudWatchRequest } from './CloudWatchRequest';
const getThrottlingErrorMessage = (region: string, message: string) =>
`Please visit the AWS Service Quotas console at https://${region}.console.aws.amazon.com/servicequotas/home?region=${region}#!/services/monitoring/quotas/L-5E141212 to request a quota increase or see our documentation at https://grafana.com/docs/grafana/latest/datasources/cloudwatch/#manage-service-quotas to learn more. ${message}`;
const displayAlert = (datasourceName: string, region: string) =>
store.dispatch(
notifyApp(
createErrorNotification(
`CloudWatch request limit reached in ${region} for data source ${datasourceName}`,
'',
undefined,
React.createElement(ThrottlingErrorMessage, { region }, null)
)
)
);
getAppEvents().publish({
type: AppEvents.alertError.name,
payload: [
`CloudWatch request limit reached in ${region} for data source ${datasourceName}`,
'',
undefined,
React.createElement(ThrottlingErrorMessage, { region }, null),
],
});
// This class handles execution of CloudWatch metrics query data queries
export class CloudWatchMetricsQueryRunner extends CloudWatchRequest {
debouncedThrottlingAlert: (datasourceName: string, region: string) => void = memoizedDebounce(
displayAlert,
AppNotificationTimeout.Error
);
debouncedThrottlingAlert: (datasourceName: string, region: string) => void = memoizedDebounce(displayAlert);
constructor(instanceSettings: DataSourceInstanceSettings<CloudWatchJsonData>, templateSrv: TemplateSrv) {
super(instanceSettings, templateSrv);
@@ -123,13 +119,13 @@ export class CloudWatchMetricsQueryRunner extends CloudWatchRequest {
});
if (res.errors?.length) {
this.alertOnErrors(res.errors, request);
this.alertOnThrottlingErrors(res.errors, request);
}
return {
data: dataframes,
// DataSourceWithBackend will not throw an error, instead it will return "errors" field along with the response
errors: res.errors,
errors: this.enrichThrottlingErrorMessages(request, res.errors),
};
}),
catchError((err: unknown) => {
@@ -142,7 +138,23 @@ export class CloudWatchMetricsQueryRunner extends CloudWatchRequest {
);
}
alertOnErrors(errors: DataQueryError[], request: DataQueryRequest<CloudWatchQuery>) {
enrichThrottlingErrorMessages(request: DataQueryRequest<CloudWatchQuery>, errors?: DataQueryError[]) {
if (!errors || errors.length === 0) {
return errors;
}
const result: DataQueryError[] = [];
errors.forEach((error) => {
if (error.message && (/^Throttling:.*/.test(error.message) || /^Rate exceeded.*/.test(error.message))) {
const region = this.getActualRegion(request.targets.find((target) => target.refId === error.refId)?.region);
result.push({ ...error, message: getThrottlingErrorMessage(region, error.message) });
} else {
result.push(error);
}
});
return result;
}
alertOnThrottlingErrors(errors: DataQueryError[], request: DataQueryRequest<CloudWatchQuery>) {
const hasThrottlingError = errors.some(
(err) => err.message && (/^Throttling:.*/.test(err.message) || /^Rate exceeded.*/.test(err.message))
);