mirror of https://github.com/grafana/grafana.git
214 lines
7.7 KiB
TypeScript
214 lines
7.7 KiB
TypeScript
import { findLast, isEmpty } from 'lodash';
|
|
import React from 'react';
|
|
import { catchError, map, Observable, of, throwError } from 'rxjs';
|
|
|
|
import {
|
|
DataFrame,
|
|
DataQueryRequest,
|
|
DataQueryResponse,
|
|
DataSourceInstanceSettings,
|
|
dateTimeFormat,
|
|
FieldType,
|
|
rangeUtil,
|
|
ScopedVars,
|
|
TimeRange,
|
|
} from '@grafana/data';
|
|
import { notifyApp } from 'app/core/actions';
|
|
import { createErrorNotification } from 'app/core/copy/appNotification';
|
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
|
import { store } from 'app/store/store';
|
|
import { AppNotificationTimeout } from 'app/types';
|
|
|
|
import { ThrottlingErrorMessage } from '../components/Errors/ThrottlingErrorMessage';
|
|
import memoizedDebounce from '../memoizedDebounce';
|
|
import { migrateMetricQuery } from '../migrations/metricQueryMigrations';
|
|
import { CloudWatchJsonData, CloudWatchMetricsQuery, CloudWatchQuery, DataQueryError } from '../types';
|
|
import { filterMetricsQuery } from '../utils/utils';
|
|
|
|
import { CloudWatchRequest } from './CloudWatchRequest';
|
|
|
|
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)
|
|
)
|
|
)
|
|
);
|
|
// This class handles execution of CloudWatch metrics query data queries
|
|
export class CloudWatchMetricsQueryRunner extends CloudWatchRequest {
|
|
debouncedAlert: (datasourceName: string, region: string) => void = memoizedDebounce(
|
|
displayAlert,
|
|
AppNotificationTimeout.Error
|
|
);
|
|
|
|
constructor(
|
|
instanceSettings: DataSourceInstanceSettings<CloudWatchJsonData>,
|
|
templateSrv: TemplateSrv,
|
|
queryFn: (request: DataQueryRequest<CloudWatchQuery>) => Observable<DataQueryResponse>
|
|
) {
|
|
super(instanceSettings, templateSrv, queryFn);
|
|
}
|
|
|
|
handleMetricQueries = (
|
|
metricQueries: CloudWatchMetricsQuery[],
|
|
options: DataQueryRequest<CloudWatchQuery>
|
|
): Observable<DataQueryResponse> => {
|
|
const timezoneUTCOffset = dateTimeFormat(Date.now(), {
|
|
timeZone: options.timezone,
|
|
format: 'Z',
|
|
}).replace(':', '');
|
|
|
|
const validMetricsQueries = metricQueries.filter(this.filterMetricQuery).map((q) => {
|
|
const migratedQuery = migrateMetricQuery(q);
|
|
const migratedAndIterpolatedQuery = this.replaceMetricQueryVars(migratedQuery, options.scopedVars);
|
|
|
|
return {
|
|
timezoneUTCOffset,
|
|
intervalMs: options.intervalMs,
|
|
maxDataPoints: options.maxDataPoints,
|
|
...migratedAndIterpolatedQuery,
|
|
type: 'timeSeriesQuery',
|
|
datasource: this.ref,
|
|
};
|
|
});
|
|
|
|
// No valid targets, return the empty result to save a round trip.
|
|
if (isEmpty(validMetricsQueries)) {
|
|
return of({ data: [] });
|
|
}
|
|
|
|
const request: DataQueryRequest<CloudWatchQuery> = {
|
|
...options,
|
|
targets: validMetricsQueries,
|
|
};
|
|
|
|
return this.performTimeSeriesQuery(request, options.range);
|
|
};
|
|
|
|
interpolateMetricsQueryVariables(
|
|
query: CloudWatchMetricsQuery,
|
|
scopedVars: ScopedVars
|
|
): Pick<
|
|
CloudWatchMetricsQuery,
|
|
'alias' | 'metricName' | 'namespace' | 'period' | 'dimensions' | 'sqlExpression' | 'expression'
|
|
> {
|
|
return {
|
|
alias: this.replaceVariableAndDisplayWarningIfMulti(query.alias, scopedVars),
|
|
metricName: this.replaceVariableAndDisplayWarningIfMulti(query.metricName, scopedVars),
|
|
namespace: this.replaceVariableAndDisplayWarningIfMulti(query.namespace, scopedVars),
|
|
period: this.replaceVariableAndDisplayWarningIfMulti(query.period, scopedVars),
|
|
expression: this.templateSrv.replace(query.expression, scopedVars),
|
|
sqlExpression: this.replaceVariableAndDisplayWarningIfMulti(query.sqlExpression, scopedVars),
|
|
dimensions: this.convertDimensionFormat(query.dimensions ?? {}, scopedVars),
|
|
};
|
|
}
|
|
|
|
performTimeSeriesQuery(
|
|
request: DataQueryRequest<CloudWatchQuery>,
|
|
{ from, to }: TimeRange
|
|
): Observable<DataQueryResponse> {
|
|
return this.query(request).pipe(
|
|
map((res) => {
|
|
const dataframes: DataFrame[] = res.data;
|
|
if (!dataframes || dataframes.length <= 0) {
|
|
return { data: [] };
|
|
}
|
|
|
|
const lastError = findLast(res.data, (v) => !!v.error);
|
|
|
|
dataframes.forEach((frame) => {
|
|
frame.fields.forEach((field) => {
|
|
if (field.type === FieldType.time) {
|
|
// field.config.interval is populated in order for Grafana to fill in null values at frame intervals
|
|
field.config.interval = frame.meta?.custom?.period * 1000;
|
|
}
|
|
});
|
|
});
|
|
|
|
return {
|
|
data: dataframes,
|
|
error: lastError ? { message: lastError.error } : undefined,
|
|
};
|
|
}),
|
|
catchError((err: DataQueryError<CloudWatchMetricsQuery>) => {
|
|
const isFrameError = err.data?.results;
|
|
|
|
// Error is not frame specific
|
|
if (!isFrameError && err.data && err.data.message === 'Metric request error' && err.data.error) {
|
|
err.message = err.data.error;
|
|
return throwError(() => err);
|
|
}
|
|
|
|
// The error is either for a specific frame or for all the frames
|
|
const results: Array<{ error?: string }> = Object.values(err.data?.results ?? {});
|
|
const firstErrorResult = results.find((r) => r.error);
|
|
if (firstErrorResult) {
|
|
err.message = firstErrorResult.error;
|
|
}
|
|
|
|
if (results.some((r) => r.error && /^Throttling:.*/.test(r.error))) {
|
|
const failedRedIds = Object.keys(err.data?.results ?? {});
|
|
const regionsAffected = Object.values(request.targets).reduce(
|
|
(res: string[], { refId, region }) =>
|
|
(refId && !failedRedIds.includes(refId)) || res.includes(region) ? res : [...res, region],
|
|
[]
|
|
);
|
|
regionsAffected.forEach((region) => {
|
|
const actualRegion = this.getActualRegion(region);
|
|
if (actualRegion) {
|
|
this.debouncedAlert(this.instanceSettings.name, actualRegion);
|
|
}
|
|
});
|
|
}
|
|
|
|
return throwError(() => err);
|
|
})
|
|
);
|
|
}
|
|
|
|
filterMetricQuery(query: CloudWatchMetricsQuery): boolean {
|
|
return filterMetricsQuery(query);
|
|
}
|
|
|
|
replaceMetricQueryVars(query: CloudWatchMetricsQuery, scopedVars: ScopedVars): CloudWatchMetricsQuery {
|
|
query.region = this.templateSrv.replace(this.getActualRegion(query.region), scopedVars);
|
|
query.namespace = this.replaceVariableAndDisplayWarningIfMulti(query.namespace, scopedVars, true, 'namespace');
|
|
query.metricName = this.replaceVariableAndDisplayWarningIfMulti(query.metricName, scopedVars, true, 'metric name');
|
|
query.dimensions = this.convertDimensionFormat(query.dimensions ?? {}, scopedVars);
|
|
query.statistic = this.templateSrv.replace(query.statistic, scopedVars);
|
|
query.period = String(this.getPeriod(query, scopedVars)); // use string format for period in graph query, and alerting
|
|
query.id = this.templateSrv.replace(query.id, scopedVars);
|
|
query.expression = this.templateSrv.replace(query.expression, scopedVars);
|
|
query.sqlExpression = this.templateSrv.replace(query.sqlExpression, scopedVars, 'raw');
|
|
if (query.accountId) {
|
|
query.accountId = this.templateSrv.replace(query.accountId, scopedVars);
|
|
}
|
|
|
|
return query;
|
|
}
|
|
|
|
getPeriod(target: CloudWatchMetricsQuery, scopedVars: ScopedVars) {
|
|
let period = this.templateSrv.replace(target.period, scopedVars);
|
|
if (period && period.toLowerCase() !== 'auto') {
|
|
let p: number;
|
|
if (/^\d+$/.test(period)) {
|
|
p = parseInt(period, 10);
|
|
} else {
|
|
p = rangeUtil.intervalToSeconds(period);
|
|
}
|
|
|
|
if (p < 1) {
|
|
p = 1;
|
|
}
|
|
|
|
return String(p);
|
|
}
|
|
|
|
return period;
|
|
}
|
|
}
|