mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Cloudwatch: Migrate queries that use multiple stats to one query per stat (#36925)
* migrate queries that use multiple stats - squash commits * fix typo
This commit is contained in:
@@ -1452,6 +1452,115 @@ describe('DashboardModel', () => {
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('migrating legacy CloudWatch queries', () => {
|
||||
let model: any;
|
||||
let panelTargets: any;
|
||||
|
||||
beforeEach(() => {
|
||||
model = new DashboardModel({
|
||||
annotations: {
|
||||
list: [
|
||||
{
|
||||
actionPrefix: '',
|
||||
alarmNamePrefix: '',
|
||||
alias: '',
|
||||
dimensions: {
|
||||
InstanceId: 'i-123',
|
||||
},
|
||||
enable: true,
|
||||
expression: '',
|
||||
iconColor: 'red',
|
||||
id: '',
|
||||
matchExact: true,
|
||||
metricName: 'CPUUtilization',
|
||||
name: 'test',
|
||||
namespace: 'AWS/EC2',
|
||||
period: '',
|
||||
prefixMatching: false,
|
||||
region: 'us-east-2',
|
||||
statistics: ['Minimum', 'Sum'],
|
||||
},
|
||||
],
|
||||
},
|
||||
panels: [
|
||||
{
|
||||
gridPos: {
|
||||
h: 8,
|
||||
w: 12,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
id: 4,
|
||||
options: {
|
||||
legend: {
|
||||
calcs: [],
|
||||
displayMode: 'list',
|
||||
placement: 'bottom',
|
||||
},
|
||||
tooltipOptions: {
|
||||
mode: 'single',
|
||||
},
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
alias: '',
|
||||
dimensions: {
|
||||
InstanceId: 'i-123',
|
||||
},
|
||||
expression: '',
|
||||
id: '',
|
||||
matchExact: true,
|
||||
metricName: 'CPUUtilization',
|
||||
namespace: 'AWS/EC2',
|
||||
period: '',
|
||||
refId: 'A',
|
||||
region: 'default',
|
||||
statistics: ['Average', 'Minimum', 'p12.21'],
|
||||
},
|
||||
{
|
||||
alias: '',
|
||||
dimensions: {
|
||||
InstanceId: 'i-123',
|
||||
},
|
||||
expression: '',
|
||||
hide: false,
|
||||
id: '',
|
||||
matchExact: true,
|
||||
metricName: 'CPUUtilization',
|
||||
namespace: 'AWS/EC2',
|
||||
period: '',
|
||||
refId: 'B',
|
||||
region: 'us-east-2',
|
||||
statistics: ['Sum'],
|
||||
},
|
||||
],
|
||||
title: 'Panel Title',
|
||||
type: 'timeseries',
|
||||
},
|
||||
],
|
||||
});
|
||||
panelTargets = model.panels[0].targets;
|
||||
});
|
||||
|
||||
it('multiple stats query should have been split into three', () => {
|
||||
expect(panelTargets.length).toBe(4);
|
||||
});
|
||||
|
||||
it('new stats query should get the right statistic', () => {
|
||||
expect(panelTargets[0].statistic).toBe('Average');
|
||||
expect(panelTargets[1].statistic).toBe('Sum');
|
||||
expect(panelTargets[2].statistic).toBe('Minimum');
|
||||
expect(panelTargets[3].statistic).toBe('p12.21');
|
||||
});
|
||||
|
||||
it('new stats queries should be put in the end of the array', () => {
|
||||
expect(panelTargets[0].refId).toBe('A');
|
||||
expect(panelTargets[1].refId).toBe('B');
|
||||
expect(panelTargets[2].refId).toBe('C');
|
||||
expect(panelTargets[3].refId).toBe('D');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createRow(options: any, panelDescriptions: any[]) {
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
ValueMapping,
|
||||
getActiveThreshold,
|
||||
DataTransformerConfig,
|
||||
AnnotationQuery,
|
||||
DataQuery,
|
||||
} from '@grafana/data';
|
||||
// Constants
|
||||
import {
|
||||
@@ -39,6 +41,11 @@ import { plugin as gaugePanelPlugin } from 'app/plugins/panel/gauge/module';
|
||||
import { getStandardFieldConfigs, getStandardOptionEditors } from '@grafana/ui';
|
||||
import { labelsToFieldsTransformer } from '../../../../../packages/grafana-data/src/transformations/transformers/labelsToFields';
|
||||
import { mergeTransformer } from '../../../../../packages/grafana-data/src/transformations/transformers/merge';
|
||||
import {
|
||||
migrateMultipleStatsMetricsQuery,
|
||||
migrateMultipleStatsAnnotationQuery,
|
||||
} from 'app/plugins/datasource/cloudwatch/migrations';
|
||||
import { CloudWatchMetricsQuery, CloudWatchAnnotationQuery } from 'app/plugins/datasource/cloudwatch/types';
|
||||
|
||||
standardEditorsRegistry.setInit(getStandardOptionEditors);
|
||||
standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs);
|
||||
@@ -695,6 +702,31 @@ export class DashboardMigrator {
|
||||
}
|
||||
}
|
||||
|
||||
// Migrates metric queries and/or annotation queries that use more than one statistic.
|
||||
// E.g query.statistics = ['Max', 'Min'] will be migrated to two queries - query1.statistic = 'Max' and query2.statistic = 'Min'
|
||||
// New queries, that were created during migration, are put at the end of the array.
|
||||
migrateCloudWatchQueries() {
|
||||
for (const panel of this.dashboard.panels) {
|
||||
for (const target of panel.targets) {
|
||||
if (isLegacyCloudWatchQuery(target)) {
|
||||
const newQueries = migrateMultipleStatsMetricsQuery(target, [...panel.targets]);
|
||||
for (const newQuery of newQueries) {
|
||||
panel.targets.push(newQuery);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const annotation of this.dashboard.annotations.list) {
|
||||
if (isLegacyCloudWatchAnnotationQuery(annotation)) {
|
||||
const newAnnotationQueries = migrateMultipleStatsAnnotationQuery(annotation);
|
||||
for (const newAnnotationQuery of newAnnotationQueries) {
|
||||
this.dashboard.annotations.list.push(newAnnotationQuery);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
upgradeToGridLayout(old: any) {
|
||||
let yPos = 0;
|
||||
const widthFactor = GRID_COLUMN_COUNT / 12;
|
||||
@@ -1010,6 +1042,25 @@ function upgradeValueMappingsForPanel(panel: PanelModel) {
|
||||
return panel;
|
||||
}
|
||||
|
||||
function isLegacyCloudWatchQuery(target: DataQuery): target is CloudWatchMetricsQuery {
|
||||
return (
|
||||
target.hasOwnProperty('dimensions') &&
|
||||
target.hasOwnProperty('namespace') &&
|
||||
target.hasOwnProperty('region') &&
|
||||
target.hasOwnProperty('statistics')
|
||||
);
|
||||
}
|
||||
|
||||
function isLegacyCloudWatchAnnotationQuery(target: AnnotationQuery<DataQuery>): target is CloudWatchAnnotationQuery {
|
||||
return (
|
||||
target.hasOwnProperty('dimensions') &&
|
||||
target.hasOwnProperty('namespace') &&
|
||||
target.hasOwnProperty('region') &&
|
||||
target.hasOwnProperty('prefixMatching') &&
|
||||
target.hasOwnProperty('statistics')
|
||||
);
|
||||
}
|
||||
|
||||
function upgradeValueMappings(oldMappings: any, thresholds?: ThresholdsConfig): ValueMapping[] | undefined {
|
||||
if (!oldMappings) {
|
||||
return undefined;
|
||||
|
||||
@@ -1016,6 +1016,7 @@ export class DashboardModel {
|
||||
private updateSchema(old: any) {
|
||||
const migrator = new DashboardMigrator(this);
|
||||
migrator.updateSchema(old);
|
||||
migrator.migrateCloudWatchQueries();
|
||||
}
|
||||
|
||||
resetOriginalTime() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defaultsDeep } from 'lodash';
|
||||
import { AnnotationQuery } from './types';
|
||||
import { CloudWatchAnnotationQuery } from './types';
|
||||
|
||||
export class CloudWatchAnnotationsQueryCtrl {
|
||||
static templateUrl = 'partials/annotations.editor.html';
|
||||
@@ -17,7 +17,7 @@ export class CloudWatchAnnotationsQueryCtrl {
|
||||
region: 'default',
|
||||
id: '',
|
||||
alias: '',
|
||||
statistics: ['Average'],
|
||||
statistic: 'Average',
|
||||
matchExact: true,
|
||||
prefixMatching: false,
|
||||
actionPrefix: '',
|
||||
@@ -27,7 +27,7 @@ export class CloudWatchAnnotationsQueryCtrl {
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
|
||||
onChange(query: AnnotationQuery) {
|
||||
onChange(query: CloudWatchAnnotationQuery) {
|
||||
Object.assign(this.annotation, query);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ import React, { ChangeEvent } from 'react';
|
||||
import { LegacyForms } from '@grafana/ui';
|
||||
const { Switch } = LegacyForms;
|
||||
import { PanelData } from '@grafana/data';
|
||||
import { AnnotationQuery } from '../types';
|
||||
import { CloudWatchAnnotationQuery } from '../types';
|
||||
import { CloudWatchDatasource } from '../datasource';
|
||||
import { QueryField, PanelQueryEditor } from './';
|
||||
|
||||
export type Props = {
|
||||
query: AnnotationQuery;
|
||||
query: CloudWatchAnnotationQuery;
|
||||
datasource: CloudWatchDatasource;
|
||||
onChange: (value: AnnotationQuery) => void;
|
||||
onChange: (value: CloudWatchAnnotationQuery) => void;
|
||||
data?: PanelData;
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ export function AnnotationQueryEditor(props: React.PropsWithChildren<Props>) {
|
||||
<>
|
||||
<PanelQueryEditor
|
||||
{...props}
|
||||
onChange={(editorQuery: AnnotationQuery) => onChange({ ...query, ...editorQuery })}
|
||||
onChange={(editorQuery: CloudWatchAnnotationQuery) => onChange({ ...query, ...editorQuery })}
|
||||
onRunQuery={() => {}}
|
||||
history={[]}
|
||||
></PanelQueryEditor>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,9 @@
|
||||
import React, { PureComponent, ChangeEvent } from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { ExploreQueryFieldProps } from '@grafana/data';
|
||||
import { ExploreQueryFieldProps, PanelData } from '@grafana/data';
|
||||
import { LegacyForms, ValidationEvents, EventsWithValidation, Icon } from '@grafana/ui';
|
||||
const { Input, Switch } = LegacyForms;
|
||||
import { CloudWatchQuery, CloudWatchMetricsQuery, CloudWatchJsonData } from '../types';
|
||||
import { CloudWatchQuery, CloudWatchMetricsQuery, CloudWatchJsonData, ExecutedQueryPreview } from '../types';
|
||||
import { CloudWatchDatasource } from '../datasource';
|
||||
import { QueryField, Alias, MetricsQueryFieldsEditor } from './';
|
||||
|
||||
@@ -31,7 +30,7 @@ export const normalizeQuery = ({
|
||||
region,
|
||||
id,
|
||||
alias,
|
||||
statistics,
|
||||
statistic,
|
||||
period,
|
||||
...rest
|
||||
}: CloudWatchMetricsQuery): CloudWatchMetricsQuery => {
|
||||
@@ -43,7 +42,7 @@ export const normalizeQuery = ({
|
||||
region: region || 'default',
|
||||
id: id || '',
|
||||
alias: alias || '',
|
||||
statistics: isEmpty(statistics) ? ['Average'] : statistics,
|
||||
statistic: statistic ?? 'Average',
|
||||
period: period || '',
|
||||
...rest,
|
||||
};
|
||||
@@ -65,55 +64,65 @@ export class MetricsQueryEditor extends PureComponent<Props, State> {
|
||||
onRunQuery();
|
||||
}
|
||||
|
||||
getExecutedQueryPreview(data?: PanelData): ExecutedQueryPreview {
|
||||
if (!(data?.series.length && data?.series[0].meta?.custom)) {
|
||||
return {
|
||||
executedQuery: '',
|
||||
period: '',
|
||||
id: '',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
executedQuery: data?.series[0].meta.executedQueryString ?? '',
|
||||
period: data.series[0].meta.custom['period'],
|
||||
id: data.series[0].meta.custom['id'],
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, onRunQuery } = this.props;
|
||||
const metricsQuery = this.props.query as CloudWatchMetricsQuery;
|
||||
const { showMeta } = this.state;
|
||||
const query = normalizeQuery(metricsQuery);
|
||||
const executedQueries =
|
||||
data && data.series.length && data.series[0].meta && data.state === 'Done'
|
||||
? data.series[0].meta.executedQueryString
|
||||
: null;
|
||||
|
||||
const executedQueryPreview = this.getExecutedQueryPreview(data);
|
||||
return (
|
||||
<>
|
||||
<MetricsQueryFieldsEditor {...{ ...this.props, query }}></MetricsQueryFieldsEditor>
|
||||
{query.statistics.length <= 1 && (
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<QueryField
|
||||
label="Id"
|
||||
tooltip="Id can include numbers, letters, and underscore, and must start with a lowercase letter."
|
||||
>
|
||||
<Input
|
||||
className="gf-form-input width-8"
|
||||
onBlur={onRunQuery}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
||||
this.onChange({ ...metricsQuery, id: event.target.value })
|
||||
}
|
||||
validationEvents={idValidationEvents}
|
||||
value={query.id}
|
||||
/>
|
||||
</QueryField>
|
||||
</div>
|
||||
<div className="gf-form gf-form--grow">
|
||||
<QueryField
|
||||
className="gf-form--grow"
|
||||
label="Expression"
|
||||
tooltip="Optionally you can add an expression here. Please note that if a math expression that is referencing other queries is being used, it will not be possible to create an alert rule based on this query"
|
||||
>
|
||||
<Input
|
||||
className="gf-form-input"
|
||||
onBlur={onRunQuery}
|
||||
value={query.expression || ''}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
||||
this.onChange({ ...metricsQuery, expression: event.target.value })
|
||||
}
|
||||
/>
|
||||
</QueryField>
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<QueryField
|
||||
label="Id"
|
||||
tooltip="Id can include numbers, letters, and underscore, and must start with a lowercase letter."
|
||||
>
|
||||
<Input
|
||||
className="gf-form-input width-8"
|
||||
onBlur={onRunQuery}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
||||
this.onChange({ ...metricsQuery, id: event.target.value })
|
||||
}
|
||||
validationEvents={idValidationEvents}
|
||||
value={query.id}
|
||||
/>
|
||||
</QueryField>
|
||||
</div>
|
||||
)}
|
||||
<div className="gf-form gf-form--grow">
|
||||
<QueryField
|
||||
className="gf-form--grow"
|
||||
label="Expression"
|
||||
tooltip="Optionally you can add an expression here. Please note that if a math expression that is referencing other queries is being used, it will not be possible to create an alert rule based on this query"
|
||||
>
|
||||
<Input
|
||||
className="gf-form-input"
|
||||
onBlur={onRunQuery}
|
||||
value={query.expression || ''}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
||||
this.onChange({ ...metricsQuery, expression: event.target.value })
|
||||
}
|
||||
/>
|
||||
</QueryField>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<QueryField label="Period" tooltip="Minimum interval between points in seconds">
|
||||
@@ -153,21 +162,20 @@ export class MetricsQueryEditor extends PureComponent<Props, State> {
|
||||
<label className="gf-form-label">
|
||||
<a
|
||||
onClick={() =>
|
||||
executedQueries &&
|
||||
executedQueryPreview &&
|
||||
this.setState({
|
||||
showMeta: !showMeta,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Icon name={showMeta && executedQueries ? 'angle-down' : 'angle-right'} />{' '}
|
||||
{showMeta && executedQueries ? 'Hide' : 'Show'} Query Preview
|
||||
<Icon name={showMeta ? 'angle-down' : 'angle-right'} /> {showMeta ? 'Hide' : 'Show'} Query Preview
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
<div className="gf-form gf-form--grow">
|
||||
<div className="gf-form-label gf-form-label--grow" />
|
||||
</div>
|
||||
{showMeta && executedQueries && (
|
||||
{showMeta && (
|
||||
<table className="filter-table form-inline">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -178,13 +186,11 @@ export class MetricsQueryEditor extends PureComponent<Props, State> {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{JSON.parse(executedQueries).map(({ ID, Expression, Period }: any) => (
|
||||
<tr key={ID}>
|
||||
<td>{ID}</td>
|
||||
<td>{Expression}</td>
|
||||
<td>{Period}</td>
|
||||
</tr>
|
||||
))}
|
||||
<tr>
|
||||
<td>{executedQueryPreview.id}</td>
|
||||
<td>{executedQueryPreview.executedQuery}</td>
|
||||
<td>{executedQueryPreview.period}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SelectableValue } from '@grafana/data';
|
||||
import { Segment, SegmentAsync } from '@grafana/ui';
|
||||
import { CloudWatchMetricsQuery, SelectableStrings } from '../types';
|
||||
import { CloudWatchDatasource } from '../datasource';
|
||||
import { Dimensions, QueryInlineField, Stats } from '.';
|
||||
import { Dimensions, QueryInlineField } from '.';
|
||||
|
||||
export type Props = {
|
||||
query: CloudWatchMetricsQuery;
|
||||
@@ -120,12 +120,25 @@ export function MetricsQueryFieldsEditor({
|
||||
/>
|
||||
</QueryInlineField>
|
||||
|
||||
<QueryInlineField label="Stats">
|
||||
<Stats
|
||||
stats={datasource.standardStatistics.map(toOption)}
|
||||
values={metricsQuery.statistics}
|
||||
onChange={(statistics) => onQueryChange({ ...metricsQuery, statistics })}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
<QueryInlineField label="Statistic">
|
||||
<Segment
|
||||
allowCustomValue
|
||||
value={query.statistic}
|
||||
options={[
|
||||
...datasource.standardStatistics.filter((s) => s !== query.statistic).map(toOption),
|
||||
variableOptionGroup,
|
||||
]}
|
||||
onChange={({ value: statistic }) => {
|
||||
if (
|
||||
!datasource.standardStatistics.includes(statistic) &&
|
||||
!/^p\d{2}(?:\.\d{1,2})?$/.test(statistic) &&
|
||||
!statistic.startsWith('$')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
onQueryChange({ ...metricsQuery, statistic });
|
||||
}}
|
||||
/>
|
||||
</QueryInlineField>
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { Stats } from './Stats';
|
||||
|
||||
const toOption = (value: any) => ({ label: value, value });
|
||||
|
||||
describe('Stats', () => {
|
||||
it('should render component', () => {
|
||||
render(
|
||||
<Stats
|
||||
data-testid="stats"
|
||||
values={['Average', 'Minimum']}
|
||||
variableOptionGroup={{ label: 'templateVar', value: 'templateVar' }}
|
||||
onChange={() => {}}
|
||||
stats={['Average', 'Maximum', 'Minimum', 'Sum', 'SampleCount'].map(toOption)}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Average')).toBeInTheDocument();
|
||||
expect(screen.getByText('Minimum')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { SelectableStrings } from '../types';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Segment, Icon } from '@grafana/ui';
|
||||
|
||||
export interface Props {
|
||||
values: string[];
|
||||
onChange: (values: string[]) => void;
|
||||
variableOptionGroup: SelectableValue<string>;
|
||||
stats: SelectableStrings;
|
||||
}
|
||||
|
||||
const removeText = '-- remove stat --';
|
||||
const removeOption: SelectableValue<string> = { label: removeText, value: removeText };
|
||||
|
||||
export const Stats: FunctionComponent<Props> = ({ stats, values, onChange, variableOptionGroup }) => (
|
||||
<>
|
||||
{values &&
|
||||
values.map((value, index) => (
|
||||
<Segment
|
||||
allowCustomValue
|
||||
key={value + index}
|
||||
value={value}
|
||||
options={[removeOption, ...stats, variableOptionGroup]}
|
||||
onChange={({ value }) =>
|
||||
onChange(
|
||||
value === removeText
|
||||
? values.filter((_, i) => i !== index)
|
||||
: values.map((v, i) => (i === index ? value! : v))
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<Segment
|
||||
Component={
|
||||
<a className="gf-form-label query-part">
|
||||
<Icon name="plus" />
|
||||
</a>
|
||||
}
|
||||
allowCustomValue
|
||||
onChange={({ value }) => onChange([...values, value!])}
|
||||
options={[...stats.filter(({ value }) => !values.includes(value!)), variableOptionGroup]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -1,3 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`QueryEditor should render component 1`] = `null`;
|
||||
@@ -1,4 +1,3 @@
|
||||
export { Stats } from './Stats';
|
||||
export { Dimensions } from './Dimensions';
|
||||
export { QueryInlineField, QueryField } from './Forms';
|
||||
export { Alias } from './Alias';
|
||||
|
||||
@@ -263,8 +263,7 @@ export class CloudWatchDatasource extends DataSourceWithBackend<CloudWatchQuery,
|
||||
const validMetricsQueries = metricQueries
|
||||
.filter(
|
||||
(item) =>
|
||||
(!!item.region && !!item.namespace && !!item.metricName && !isEmpty(item.statistics)) ||
|
||||
item.expression?.length > 0
|
||||
(!!item.region && !!item.namespace && !!item.metricName && !!item.statistic) || item.expression?.length > 0
|
||||
)
|
||||
.map(
|
||||
(item: CloudWatchMetricsQuery): MetricQuery => {
|
||||
@@ -272,25 +271,11 @@ export class CloudWatchDatasource extends DataSourceWithBackend<CloudWatchQuery,
|
||||
item.namespace = this.replace(item.namespace, options.scopedVars, true, 'namespace');
|
||||
item.metricName = this.replace(item.metricName, options.scopedVars, true, 'metric name');
|
||||
item.dimensions = this.convertDimensionFormat(item.dimensions, options.scopedVars);
|
||||
item.statistics = item.statistics.map((stat) => this.replace(stat, options.scopedVars, true, 'statistics'));
|
||||
item.statistic = this.templateSrv.replace(item.statistic, options.scopedVars);
|
||||
item.period = String(this.getPeriod(item, options)); // use string format for period in graph query, and alerting
|
||||
item.id = this.templateSrv.replace(item.id, options.scopedVars);
|
||||
item.expression = this.templateSrv.replace(item.expression, options.scopedVars);
|
||||
|
||||
// valid ExtendedStatistics is like p90.00, check the pattern
|
||||
const hasInvalidStatistics = item.statistics.some((s) => {
|
||||
if (s.indexOf('p') === 0) {
|
||||
const matches = /^p\d{2}(?:\.\d{1,2})?$/.exec(s);
|
||||
return !matches || matches[0] !== s;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (hasInvalidStatistics) {
|
||||
throw { message: 'Invalid extended statistics' };
|
||||
}
|
||||
|
||||
return {
|
||||
intervalMs: options.intervalMs,
|
||||
maxDataPoints: options.maxDataPoints,
|
||||
@@ -558,22 +543,32 @@ export class CloudWatchDatasource extends DataSourceWithBackend<CloudWatchQuery,
|
||||
};
|
||||
}),
|
||||
catchError((err) => {
|
||||
if (/^Throttling:.*/.test(err.data.message)) {
|
||||
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.queries).reduce(
|
||||
(res: string[], { refId, region }) =>
|
||||
(refId && !failedRedIds.includes(refId)) || res.includes(region) ? res : [...res, region],
|
||||
[]
|
||||
) as string[];
|
||||
|
||||
regionsAffected.forEach((region) => this.debouncedAlert(this.datasourceName, this.getActualRegion(region)));
|
||||
}
|
||||
|
||||
if (err.data && err.data.message === 'Metric request error' && err.data.error) {
|
||||
err.data.message = err.data.error;
|
||||
}
|
||||
|
||||
return throwError(err);
|
||||
return throwError(() => err);
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -827,7 +822,7 @@ export class CloudWatchDatasource extends DataSourceWithBackend<CloudWatchQuery,
|
||||
|
||||
annotationQuery(options: any) {
|
||||
const annotation = options.annotation;
|
||||
const statistics = annotation.statistics.map((s: any) => this.templateSrv.replace(s));
|
||||
const statistic = this.templateSrv.replace(annotation.statistic);
|
||||
const defaultPeriod = annotation.prefixMatching ? '' : '300';
|
||||
let period = annotation.period || defaultPeriod;
|
||||
period = parseInt(period, 10);
|
||||
@@ -837,7 +832,7 @@ export class CloudWatchDatasource extends DataSourceWithBackend<CloudWatchQuery,
|
||||
namespace: this.templateSrv.replace(annotation.namespace),
|
||||
metricName: this.templateSrv.replace(annotation.metricName),
|
||||
dimensions: this.convertDimensionFormat(annotation.dimensions, {}),
|
||||
statistics: statistics,
|
||||
statistic: statistic,
|
||||
period: period,
|
||||
actionPrefix: annotation.actionPrefix || '',
|
||||
alarmNamePrefix: annotation.alarmNamePrefix || '',
|
||||
|
||||
118
public/app/plugins/datasource/cloudwatch/migration.test.ts
Normal file
118
public/app/plugins/datasource/cloudwatch/migration.test.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { DataQuery } from '@grafana/data';
|
||||
import { migrateMultipleStatsAnnotationQuery, migrateMultipleStatsMetricsQuery } from './migrations';
|
||||
import { CloudWatchAnnotationQuery, CloudWatchMetricsQuery } from './types';
|
||||
|
||||
describe('migration', () => {
|
||||
describe('migrateMultipleStatsMetricsQuery', () => {
|
||||
const queryToMigrate = {
|
||||
statistics: ['Average', 'Sum', 'Maximum'],
|
||||
refId: 'A',
|
||||
};
|
||||
const panelQueries: DataQuery[] = [
|
||||
{ ...queryToMigrate },
|
||||
{
|
||||
refId: 'B',
|
||||
},
|
||||
];
|
||||
const newQueries = migrateMultipleStatsMetricsQuery(queryToMigrate as CloudWatchMetricsQuery, panelQueries);
|
||||
const newMetricQueries = newQueries as CloudWatchMetricsQuery[];
|
||||
|
||||
it('should create one new query for each stat', () => {
|
||||
expect(newQueries.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should assign new queries the right stats', () => {
|
||||
expect(newMetricQueries[0].statistic).toBe('Sum');
|
||||
expect(newMetricQueries[1].statistic).toBe('Maximum');
|
||||
});
|
||||
|
||||
it('should assign new queries the right ref id', () => {
|
||||
expect(newQueries[0].refId).toBe('C');
|
||||
expect(newQueries[1].refId).toBe('D');
|
||||
});
|
||||
|
||||
it('should not have statistics prop anymore', () => {
|
||||
expect(queryToMigrate).not.toHaveProperty('statistics');
|
||||
expect(newQueries[0]).not.toHaveProperty('statistics');
|
||||
expect(newQueries[1]).not.toHaveProperty('statistics');
|
||||
});
|
||||
});
|
||||
|
||||
describe('migrateMultipleStatsMetricsQuery with only one stat', () => {
|
||||
const queryToMigrate = {
|
||||
statistics: ['Average'],
|
||||
refId: 'A',
|
||||
} as CloudWatchMetricsQuery;
|
||||
const panelQueries: DataQuery[] = [
|
||||
{ ...queryToMigrate },
|
||||
{
|
||||
refId: 'B',
|
||||
},
|
||||
];
|
||||
const newQueries = migrateMultipleStatsMetricsQuery(queryToMigrate as CloudWatchMetricsQuery, panelQueries);
|
||||
|
||||
it('should not create any new queries', () => {
|
||||
expect(newQueries.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should have the right stats', () => {
|
||||
expect(queryToMigrate.statistic).toBe('Average');
|
||||
});
|
||||
|
||||
it('should not have statistics prop anymore', () => {
|
||||
expect(queryToMigrate).not.toHaveProperty('statistics');
|
||||
});
|
||||
});
|
||||
|
||||
describe('migrateMultipleStatsAnnotationQuery', () => {
|
||||
const annotationToMigrate = {
|
||||
statistics: ['p23.23', 'SampleCount'],
|
||||
name: 'Test annotation',
|
||||
};
|
||||
|
||||
const newAnnotations = migrateMultipleStatsAnnotationQuery(annotationToMigrate as CloudWatchAnnotationQuery);
|
||||
const newCloudWatchAnnotations = newAnnotations as CloudWatchAnnotationQuery[];
|
||||
|
||||
it('should create one new annotation for each stat', () => {
|
||||
expect(newAnnotations.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should assign new queries the right stats', () => {
|
||||
expect(newCloudWatchAnnotations[0].statistic).toBe('SampleCount');
|
||||
});
|
||||
|
||||
it('should assign new queries the right ref id', () => {
|
||||
expect(newAnnotations[0].name).toBe('Test annotation - SampleCount');
|
||||
});
|
||||
|
||||
it('should not have statistics prop anymore', () => {
|
||||
expect(newCloudWatchAnnotations[0]).not.toHaveProperty('statistics');
|
||||
});
|
||||
|
||||
it('should migrate original query correctly', () => {
|
||||
expect(annotationToMigrate).not.toHaveProperty('statistics');
|
||||
expect(annotationToMigrate.name).toBe('Test annotation - p23.23');
|
||||
});
|
||||
|
||||
describe('migrateMultipleStatsAnnotationQuery with only with stat', () => {
|
||||
const annotationToMigrate = {
|
||||
statistics: ['p23.23'],
|
||||
name: 'Test annotation',
|
||||
} as CloudWatchAnnotationQuery;
|
||||
const newAnnotations = migrateMultipleStatsAnnotationQuery(annotationToMigrate as CloudWatchAnnotationQuery);
|
||||
|
||||
it('should not create new annotations', () => {
|
||||
expect(newAnnotations.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should not change the name', () => {
|
||||
expect(annotationToMigrate.name).toBe('Test annotation');
|
||||
});
|
||||
|
||||
it('should use statistics prop and remove statistics prop', () => {
|
||||
expect(annotationToMigrate.statistic).toEqual('p23.23');
|
||||
expect(annotationToMigrate).not.toHaveProperty('statistics');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
44
public/app/plugins/datasource/cloudwatch/migrations.ts
Normal file
44
public/app/plugins/datasource/cloudwatch/migrations.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { AnnotationQuery, DataQuery } from '@grafana/data';
|
||||
import { getNextRefIdChar } from 'app/core/utils/query';
|
||||
import { CloudWatchAnnotationQuery, CloudWatchMetricsQuery } from './types';
|
||||
|
||||
export function migrateMultipleStatsMetricsQuery(
|
||||
query: CloudWatchMetricsQuery,
|
||||
panelQueries: DataQuery[]
|
||||
): DataQuery[] {
|
||||
const newQueries = [];
|
||||
if (query?.statistics && query?.statistics.length) {
|
||||
query.statistic = query.statistics[0];
|
||||
for (const stat of query.statistics.splice(1)) {
|
||||
newQueries.push({ ...query, statistic: stat });
|
||||
}
|
||||
}
|
||||
for (const newTarget of newQueries) {
|
||||
newTarget.refId = getNextRefIdChar(panelQueries);
|
||||
delete newTarget.statistics;
|
||||
panelQueries.push(newTarget);
|
||||
}
|
||||
delete query.statistics;
|
||||
|
||||
return newQueries;
|
||||
}
|
||||
|
||||
export function migrateMultipleStatsAnnotationQuery(
|
||||
annotationQuery: CloudWatchAnnotationQuery
|
||||
): Array<AnnotationQuery<DataQuery>> {
|
||||
const newAnnotations: CloudWatchAnnotationQuery[] = [];
|
||||
if (annotationQuery?.statistics && annotationQuery?.statistics.length) {
|
||||
for (const stat of annotationQuery.statistics.splice(1)) {
|
||||
const { statistics, name, ...newAnnotation } = annotationQuery;
|
||||
newAnnotations.push({ ...newAnnotation, statistic: stat, name: `${name} - ${stat}` });
|
||||
}
|
||||
annotationQuery.statistic = annotationQuery.statistics[0];
|
||||
// Only change the name of the original if new annotations have been created
|
||||
if (newAnnotations.length !== 0) {
|
||||
annotationQuery.name = `${annotationQuery.name} - ${annotationQuery.statistic}`;
|
||||
}
|
||||
delete annotationQuery.statistics;
|
||||
}
|
||||
|
||||
return newAnnotations as Array<AnnotationQuery<DataQuery>>;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import './query_parameter_ctrl';
|
||||
import { DataSourcePlugin } from '@grafana/data';
|
||||
import { ConfigEditor } from './components/ConfigEditor';
|
||||
import { CloudWatchDatasource } from './datasource';
|
||||
|
||||
@@ -1,241 +0,0 @@
|
||||
import angular from 'angular';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { each, flatten, isEmpty, map, reduce } from 'lodash';
|
||||
import { TemplateSrv } from '@grafana/runtime';
|
||||
|
||||
export class CloudWatchQueryParameterCtrl {
|
||||
/** @ngInject */
|
||||
constructor($scope: any, templateSrv: TemplateSrv, uiSegmentSrv: any) {
|
||||
$scope.init = () => {
|
||||
const target = $scope.target;
|
||||
target.namespace = target.namespace || '';
|
||||
target.metricName = target.metricName || '';
|
||||
target.statistics = target.statistics || ['Average'];
|
||||
target.dimensions = target.dimensions || {};
|
||||
target.period = target.period || '';
|
||||
target.region = target.region || 'default';
|
||||
target.id = target.id || '';
|
||||
target.expression = target.expression || '';
|
||||
|
||||
$scope.regionSegment = uiSegmentSrv.getSegmentForValue($scope.target.region, 'select region');
|
||||
$scope.namespaceSegment = uiSegmentSrv.getSegmentForValue($scope.target.namespace, 'select namespace');
|
||||
$scope.metricSegment = uiSegmentSrv.getSegmentForValue($scope.target.metricName, 'select metric');
|
||||
|
||||
$scope.dimSegments = reduce(
|
||||
$scope.target.dimensions,
|
||||
(memo, value, key) => {
|
||||
memo.push(uiSegmentSrv.newKey(key));
|
||||
memo.push(uiSegmentSrv.newOperator('='));
|
||||
memo.push(uiSegmentSrv.newKeyValue(value));
|
||||
return memo;
|
||||
},
|
||||
[] as any
|
||||
);
|
||||
|
||||
$scope.statSegments = map($scope.target.statistics, (stat) => {
|
||||
return uiSegmentSrv.getSegmentForValue(stat);
|
||||
});
|
||||
|
||||
$scope.ensurePlusButton($scope.statSegments);
|
||||
$scope.ensurePlusButton($scope.dimSegments);
|
||||
$scope.removeDimSegment = uiSegmentSrv.newSegment({
|
||||
fake: true,
|
||||
value: '-- remove dimension --',
|
||||
});
|
||||
$scope.removeStatSegment = uiSegmentSrv.newSegment({
|
||||
fake: true,
|
||||
value: '-- remove stat --',
|
||||
});
|
||||
|
||||
if (isEmpty($scope.target.region)) {
|
||||
$scope.target.region = 'default';
|
||||
}
|
||||
|
||||
if (!$scope.onChange) {
|
||||
$scope.onChange = () => {};
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getStatSegments = () => {
|
||||
return Promise.resolve(
|
||||
flatten([
|
||||
angular.copy($scope.removeStatSegment),
|
||||
map($scope.datasource.standardStatistics, (s) => {
|
||||
return uiSegmentSrv.getSegmentForValue(s);
|
||||
}),
|
||||
uiSegmentSrv.getSegmentForValue('pNN.NN'),
|
||||
])
|
||||
);
|
||||
};
|
||||
|
||||
$scope.statSegmentChanged = (segment: any, index: number) => {
|
||||
if (segment.value === $scope.removeStatSegment.value) {
|
||||
$scope.statSegments.splice(index, 1);
|
||||
} else {
|
||||
segment.type = 'value';
|
||||
}
|
||||
|
||||
$scope.target.statistics = reduce(
|
||||
$scope.statSegments,
|
||||
(memo, seg) => {
|
||||
if (!seg.fake) {
|
||||
memo.push(seg.value);
|
||||
}
|
||||
return memo;
|
||||
},
|
||||
[] as any
|
||||
);
|
||||
|
||||
$scope.ensurePlusButton($scope.statSegments);
|
||||
$scope.onChange();
|
||||
};
|
||||
|
||||
$scope.ensurePlusButton = (segments: any) => {
|
||||
const count = segments.length;
|
||||
const lastSegment = segments[Math.max(count - 1, 0)];
|
||||
|
||||
if (!lastSegment || lastSegment.type !== 'plus-button') {
|
||||
segments.push(uiSegmentSrv.newPlusButton());
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getDimSegments = (segment: any, $index: number) => {
|
||||
if (segment.type === 'operator') {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const target = $scope.target;
|
||||
let query = Promise.resolve([] as any[]);
|
||||
|
||||
if (segment.type === 'key' || segment.type === 'plus-button') {
|
||||
query = $scope.datasource.getDimensionKeys($scope.target.namespace, $scope.target.region);
|
||||
} else if (segment.type === 'value') {
|
||||
const dimensionKey = $scope.dimSegments[$index - 2].value;
|
||||
delete target.dimensions[dimensionKey];
|
||||
query = $scope.datasource.getDimensionValues(
|
||||
target.region,
|
||||
target.namespace,
|
||||
target.metricName,
|
||||
dimensionKey,
|
||||
target.dimensions
|
||||
);
|
||||
}
|
||||
|
||||
return query.then($scope.transformToSegments(true)).then((results) => {
|
||||
if (segment.type === 'key') {
|
||||
results.splice(0, 0, angular.copy($scope.removeDimSegment));
|
||||
}
|
||||
return results;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.dimSegmentChanged = (segment: any, index: number) => {
|
||||
$scope.dimSegments[index] = segment;
|
||||
|
||||
if (segment.value === $scope.removeDimSegment.value) {
|
||||
$scope.dimSegments.splice(index, 3);
|
||||
} else if (segment.type === 'plus-button') {
|
||||
$scope.dimSegments.push(uiSegmentSrv.newOperator('='));
|
||||
$scope.dimSegments.push(uiSegmentSrv.newFake('select dimension value', 'value', 'query-segment-value'));
|
||||
segment.type = 'key';
|
||||
segment.cssClass = 'query-segment-key';
|
||||
}
|
||||
|
||||
$scope.syncDimSegmentsWithModel();
|
||||
$scope.ensurePlusButton($scope.dimSegments);
|
||||
$scope.onChange();
|
||||
};
|
||||
|
||||
$scope.syncDimSegmentsWithModel = () => {
|
||||
const dims: any = {};
|
||||
const length = $scope.dimSegments.length;
|
||||
|
||||
for (let i = 0; i < length - 2; i += 3) {
|
||||
const keySegment = $scope.dimSegments[i];
|
||||
const valueSegment = $scope.dimSegments[i + 2];
|
||||
if (!valueSegment.fake) {
|
||||
dims[keySegment.value] = valueSegment.value;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.target.dimensions = dims;
|
||||
};
|
||||
|
||||
$scope.getRegions = () => {
|
||||
return $scope.datasource
|
||||
.metricFindQuery('regions()')
|
||||
.then((results: any) => {
|
||||
results.unshift({ text: 'default' });
|
||||
return results;
|
||||
})
|
||||
.then($scope.transformToSegments(true));
|
||||
};
|
||||
|
||||
$scope.getNamespaces = () => {
|
||||
return $scope.datasource.metricFindQuery('namespaces()').then($scope.transformToSegments(true));
|
||||
};
|
||||
|
||||
$scope.getMetrics = () => {
|
||||
return $scope.datasource
|
||||
.metricFindQuery('metrics(' + $scope.target.namespace + ',' + $scope.target.region + ')')
|
||||
.then($scope.transformToSegments(true));
|
||||
};
|
||||
|
||||
$scope.regionChanged = () => {
|
||||
$scope.target.region = $scope.regionSegment.value;
|
||||
$scope.onChange();
|
||||
};
|
||||
|
||||
$scope.namespaceChanged = () => {
|
||||
$scope.target.namespace = $scope.namespaceSegment.value;
|
||||
$scope.onChange();
|
||||
};
|
||||
|
||||
$scope.metricChanged = () => {
|
||||
$scope.target.metricName = $scope.metricSegment.value;
|
||||
$scope.onChange();
|
||||
};
|
||||
|
||||
$scope.transformToSegments = (addTemplateVars: any) => {
|
||||
return (results: any) => {
|
||||
const segments = map(results, (segment) => {
|
||||
return uiSegmentSrv.newSegment({
|
||||
value: segment.text,
|
||||
expandable: segment.expandable,
|
||||
});
|
||||
});
|
||||
|
||||
if (addTemplateVars) {
|
||||
each(templateSrv.getVariables(), (variable) => {
|
||||
segments.unshift(
|
||||
uiSegmentSrv.newSegment({
|
||||
type: 'template',
|
||||
value: '$' + variable.name,
|
||||
expandable: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return segments;
|
||||
};
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
}
|
||||
}
|
||||
|
||||
export function cloudWatchQueryParameter() {
|
||||
return {
|
||||
templateUrl: 'public/app/plugins/datasource/cloudwatch/partials/query.parameter.html',
|
||||
controller: CloudWatchQueryParameterCtrl,
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
target: '=',
|
||||
datasource: '=',
|
||||
onChange: '&',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('cloudwatchQueryParameter', cloudWatchQueryParameter);
|
||||
@@ -2,7 +2,6 @@ import { interval, lastValueFrom, of, throwError } from 'rxjs';
|
||||
import {
|
||||
DataFrame,
|
||||
DataQueryErrorType,
|
||||
DataQueryRequest,
|
||||
DataQueryResponse,
|
||||
DataSourceInstanceSettings,
|
||||
dateMath,
|
||||
@@ -17,7 +16,6 @@ import {
|
||||
CloudWatchLogsQuery,
|
||||
CloudWatchLogsQueryStatus,
|
||||
CloudWatchMetricsQuery,
|
||||
CloudWatchQuery,
|
||||
LogAction,
|
||||
} from '../types';
|
||||
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
|
||||
@@ -378,7 +376,7 @@ describe('CloudWatchDatasource', () => {
|
||||
dimensions: {
|
||||
InstanceId: 'i-12345678',
|
||||
},
|
||||
statistics: ['Average'],
|
||||
statistic: 'Average',
|
||||
period: '300',
|
||||
},
|
||||
],
|
||||
@@ -419,7 +417,7 @@ describe('CloudWatchDatasource', () => {
|
||||
namespace: query.targets[0].namespace,
|
||||
metricName: query.targets[0].metricName,
|
||||
dimensions: { InstanceId: ['i-12345678'] },
|
||||
statistics: query.targets[0].statistics,
|
||||
statistic: query.targets[0].statistic,
|
||||
period: query.targets[0].period,
|
||||
}),
|
||||
])
|
||||
@@ -457,7 +455,7 @@ describe('CloudWatchDatasource', () => {
|
||||
dimensions: {
|
||||
InstanceId: 'i-12345678',
|
||||
},
|
||||
statistics: ['Average'],
|
||||
statistic: 'Average',
|
||||
period: '[[period]]',
|
||||
},
|
||||
],
|
||||
@@ -470,30 +468,6 @@ describe('CloudWatchDatasource', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each(['pNN.NN', 'p9', 'p99.', 'p99.999'])('should cancel query for invalid extended statistics (%s)', (stat) => {
|
||||
const { ds } = getTestContext({ response });
|
||||
const query: DataQueryRequest<CloudWatchQuery> = ({
|
||||
range: defaultTimeRange,
|
||||
rangeRaw: { from: 1483228800, to: 1483232400 },
|
||||
targets: [
|
||||
{
|
||||
type: 'Metrics',
|
||||
refId: 'A',
|
||||
region: 'us-east-1',
|
||||
namespace: 'AWS/EC2',
|
||||
metricName: 'CPUUtilization',
|
||||
dimensions: {
|
||||
InstanceId: 'i-12345678',
|
||||
},
|
||||
statistics: [stat],
|
||||
period: '60s',
|
||||
},
|
||||
],
|
||||
} as unknown) as DataQueryRequest<CloudWatchQuery>;
|
||||
|
||||
expect(ds.query.bind(ds, query)).toThrow(/Invalid extended statistics/);
|
||||
});
|
||||
|
||||
it('should return series list', async () => {
|
||||
const { ds } = getTestContext({ response });
|
||||
|
||||
@@ -512,7 +486,7 @@ describe('CloudWatchDatasource', () => {
|
||||
dimensions: {
|
||||
InstanceId: 'i-12345678',
|
||||
},
|
||||
statistics: ['Average'],
|
||||
statistic: 'Average',
|
||||
period: '300',
|
||||
expression: '',
|
||||
};
|
||||
@@ -645,7 +619,7 @@ describe('CloudWatchDatasource', () => {
|
||||
dimensions: {
|
||||
InstanceId: 'i-12345678',
|
||||
},
|
||||
statistics: ['Average'],
|
||||
statistic: 'Average',
|
||||
period: '300s',
|
||||
},
|
||||
],
|
||||
@@ -704,7 +678,7 @@ describe('CloudWatchDatasource', () => {
|
||||
[`$${variableName}`]: `$${variableName}`,
|
||||
},
|
||||
matchExact: false,
|
||||
statistics: [],
|
||||
statistic: '',
|
||||
};
|
||||
|
||||
ds.interpolateVariablesInQueries([logQuery], {});
|
||||
@@ -715,7 +689,7 @@ describe('CloudWatchDatasource', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing CloudWatch query for extended statistics', () => {
|
||||
describe('When performing CloudWatch query for extended statistic', () => {
|
||||
const query: any = {
|
||||
range: defaultTimeRange,
|
||||
rangeRaw: { from: 1483228800, to: 1483232400 },
|
||||
@@ -730,7 +704,7 @@ describe('CloudWatchDatasource', () => {
|
||||
LoadBalancer: 'lb',
|
||||
TargetGroup: 'tg',
|
||||
},
|
||||
statistics: ['p90.00'],
|
||||
statistic: 'p90.00',
|
||||
period: '300s',
|
||||
},
|
||||
],
|
||||
@@ -856,7 +830,7 @@ describe('CloudWatchDatasource', () => {
|
||||
dimensions: {
|
||||
dim2: '[[var2]]',
|
||||
},
|
||||
statistics: ['Average'],
|
||||
statistic: 'Average',
|
||||
period: '300s',
|
||||
},
|
||||
],
|
||||
@@ -884,7 +858,7 @@ describe('CloudWatchDatasource', () => {
|
||||
dim2: '[[var2]]',
|
||||
dim3: '[[var3]]',
|
||||
},
|
||||
statistics: ['Average'],
|
||||
statistic: 'Average',
|
||||
period: '300s',
|
||||
},
|
||||
],
|
||||
@@ -918,7 +892,7 @@ describe('CloudWatchDatasource', () => {
|
||||
dim3: '[[var3]]',
|
||||
dim4: '[[var4]]',
|
||||
},
|
||||
statistics: ['Average'],
|
||||
statistic: 'Average',
|
||||
period: '300s',
|
||||
},
|
||||
],
|
||||
@@ -948,7 +922,7 @@ describe('CloudWatchDatasource', () => {
|
||||
dim2: '[[var2]]',
|
||||
dim3: '[[var3]]',
|
||||
},
|
||||
statistics: ['Average'],
|
||||
statistic: 'Average',
|
||||
period: '300',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -11,7 +11,11 @@ export interface CloudWatchMetricsQuery extends DataQuery {
|
||||
|
||||
metricName: string;
|
||||
dimensions: { [key: string]: string | string[] };
|
||||
statistics: string[];
|
||||
statistic: string;
|
||||
/**
|
||||
* @deprecated use statistic
|
||||
*/
|
||||
statistics?: string[];
|
||||
period: string;
|
||||
alias: string;
|
||||
matchExact: boolean;
|
||||
@@ -49,7 +53,10 @@ export type CloudWatchQuery = CloudWatchMetricsQuery | CloudWatchLogsQuery;
|
||||
export const isCloudWatchLogsQuery = (cloudwatchQuery: CloudWatchQuery): cloudwatchQuery is CloudWatchLogsQuery =>
|
||||
(cloudwatchQuery as CloudWatchLogsQuery).queryMode === 'Logs';
|
||||
|
||||
export interface AnnotationQuery extends CloudWatchMetricsQuery {
|
||||
export interface CloudWatchAnnotationQuery extends CloudWatchMetricsQuery {
|
||||
enable: boolean;
|
||||
name: string;
|
||||
iconColor: string;
|
||||
prefixMatching: boolean;
|
||||
actionPrefix: string;
|
||||
alarmNamePrefix: string;
|
||||
@@ -320,17 +327,8 @@ export interface MetricQuery {
|
||||
// IntervalMs int64
|
||||
// }
|
||||
|
||||
export interface CloudWatchMetricsAnnotation {
|
||||
namespace: string;
|
||||
metricName: string;
|
||||
expression: string;
|
||||
dimensions: {};
|
||||
region: string;
|
||||
export interface ExecutedQueryPreview {
|
||||
id: string;
|
||||
alias: string;
|
||||
statistics: string[];
|
||||
matchExact: true;
|
||||
prefixMatching: false;
|
||||
actionPrefix: string;
|
||||
alarmNamePrefix: string;
|
||||
executedQuery: string;
|
||||
period: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user