From 6373557c7882d499bd77acd13a0a0c8b41d48b8e Mon Sep 17 00:00:00 2001 From: Kevin Yu Date: Tue, 6 Feb 2024 11:24:14 -0800 Subject: [PATCH] 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 --- .../Errors/ThrottlingErrorMessage.tsx | 2 +- .../CloudWatchMetricsQueryRunner.test.ts | 77 +++++++++++++++++-- .../CloudWatchMetricsQueryRunner.ts | 56 ++++++++------ 3 files changed, 104 insertions(+), 31 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/components/Errors/ThrottlingErrorMessage.tsx b/public/app/plugins/datasource/cloudwatch/components/Errors/ThrottlingErrorMessage.tsx index 2dcdba51765..1e03ad21c90 100644 --- a/public/app/plugins/datasource/cloudwatch/components/Errors/ThrottlingErrorMessage.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/Errors/ThrottlingErrorMessage.tsx @@ -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 diff --git a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.test.ts b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.test.ts index 2c9ecfa31de..81939eb5f5c 100644 --- a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.test.ts +++ b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.test.ts @@ -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), diff --git a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.ts b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.ts index b0d36396628..44088ce2868 100644 --- a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.ts +++ b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.ts @@ -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, 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) { + enrichThrottlingErrorMessages(request: DataQueryRequest, 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) { const hasThrottlingError = errors.some( (err) => err.message && (/^Throttling:.*/.test(err.message) || /^Rate exceeded.*/.test(err.message)) );