mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Cloudwatch: Dynamic labels frontend migration (#48579)
* migrate metric queries * restructure migrations * self review * cleanup tests * ensure alias is not changed * apply pr feedback
This commit is contained in:
@@ -40,17 +40,17 @@ import kbn from 'app/core/utils/kbn';
|
|||||||
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
|
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||||
import { isConstant, isMulti } from 'app/features/variables/guard';
|
import { isConstant, isMulti } from 'app/features/variables/guard';
|
||||||
import { alignCurrentWithMulti } from 'app/features/variables/shared/multiOptions';
|
import { alignCurrentWithMulti } from 'app/features/variables/shared/multiOptions';
|
||||||
import {
|
|
||||||
migrateCloudWatchQuery,
|
|
||||||
migrateMultipleStatsAnnotationQuery,
|
|
||||||
migrateMultipleStatsMetricsQuery,
|
|
||||||
} from 'app/plugins/datasource/cloudwatch/migrations';
|
|
||||||
import { CloudWatchMetricsQuery, LegacyAnnotationQuery } from 'app/plugins/datasource/cloudwatch/types';
|
import { CloudWatchMetricsQuery, LegacyAnnotationQuery } from 'app/plugins/datasource/cloudwatch/types';
|
||||||
import { plugin as gaugePanelPlugin } from 'app/plugins/panel/gauge/module';
|
import { plugin as gaugePanelPlugin } from 'app/plugins/panel/gauge/module';
|
||||||
import { plugin as statPanelPlugin } from 'app/plugins/panel/stat/module';
|
import { plugin as statPanelPlugin } from 'app/plugins/panel/stat/module';
|
||||||
|
|
||||||
import { labelsToFieldsTransformer } from '../../../../../packages/grafana-data/src/transformations/transformers/labelsToFields';
|
import { labelsToFieldsTransformer } from '../../../../../packages/grafana-data/src/transformations/transformers/labelsToFields';
|
||||||
import { mergeTransformer } from '../../../../../packages/grafana-data/src/transformations/transformers/merge';
|
import { mergeTransformer } from '../../../../../packages/grafana-data/src/transformations/transformers/merge';
|
||||||
|
import {
|
||||||
|
migrateCloudWatchQuery,
|
||||||
|
migrateMultipleStatsAnnotationQuery,
|
||||||
|
migrateMultipleStatsMetricsQuery,
|
||||||
|
} from '../../../plugins/datasource/cloudwatch/migrations/dashboardMigrations';
|
||||||
import { VariableHide } from '../../variables/types';
|
import { VariableHide } from '../../variables/types';
|
||||||
|
|
||||||
import { DashboardModel } from './DashboardModel';
|
import { DashboardModel } from './DashboardModel';
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { InlineField } from '@grafana/ui';
|
|||||||
import { Dimensions } from '..';
|
import { Dimensions } from '..';
|
||||||
import { CloudWatchDatasource } from '../../datasource';
|
import { CloudWatchDatasource } from '../../datasource';
|
||||||
import { useDimensionKeys, useMetrics, useNamespaces, useRegions } from '../../hooks';
|
import { useDimensionKeys, useMetrics, useNamespaces, useRegions } from '../../hooks';
|
||||||
import { migrateVariableQuery } from '../../migrations';
|
import { migrateVariableQuery } from '../../migrations/variableQueryMigrations';
|
||||||
import { CloudWatchJsonData, CloudWatchQuery, VariableQuery, VariableQueryType } from '../../types';
|
import { CloudWatchJsonData, CloudWatchQuery, VariableQuery, VariableQueryType } from '../../types';
|
||||||
|
|
||||||
import { MultiFilter } from './MultiFilter';
|
import { MultiFilter } from './MultiFilter';
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import deepEqual from 'fast-deep-equal';
|
import deepEqual from 'fast-deep-equal';
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { migrateMetricQuery } from '../migrations/metricQueryMigrations';
|
||||||
import { CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from '../types';
|
import { CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from '../types';
|
||||||
|
|
||||||
export const DEFAULT_QUERY: Omit<CloudWatchMetricsQuery, 'refId'> = {
|
export const DEFAULT_QUERY: Omit<CloudWatchMetricsQuery, 'refId'> = {
|
||||||
@@ -21,10 +22,11 @@ export const DEFAULT_QUERY: Omit<CloudWatchMetricsQuery, 'refId'> = {
|
|||||||
|
|
||||||
const prepareQuery = (query: CloudWatchMetricsQuery) => {
|
const prepareQuery = (query: CloudWatchMetricsQuery) => {
|
||||||
const withDefaults = { ...DEFAULT_QUERY, ...query };
|
const withDefaults = { ...DEFAULT_QUERY, ...query };
|
||||||
|
const migratedQuery = migrateMetricQuery(withDefaults);
|
||||||
|
|
||||||
// If we didn't make any changes to the object, then return the original object to keep the
|
// If we didn't make any changes to the object, then return the original object to keep the
|
||||||
// identity the same, and not trigger any other useEffects or anything.
|
// identity the same, and not trigger any other useEffects or anything.
|
||||||
return deepEqual(withDefaults, query) ? query : withDefaults;
|
return deepEqual(migratedQuery, query) ? query : migratedQuery;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { isCloudWatchAnnotationQuery, isCloudWatchLogsQuery, isCloudWatchMetrics
|
|||||||
import { CloudWatchLanguageProvider } from './language_provider';
|
import { CloudWatchLanguageProvider } from './language_provider';
|
||||||
import memoizedDebounce from './memoizedDebounce';
|
import memoizedDebounce from './memoizedDebounce';
|
||||||
import { MetricMathCompletionItemProvider } from './metric-math/completion/CompletionItemProvider';
|
import { MetricMathCompletionItemProvider } from './metric-math/completion/CompletionItemProvider';
|
||||||
|
import { migrateMetricQuery } from './migrations/metricQueryMigrations';
|
||||||
import {
|
import {
|
||||||
CloudWatchAnnotationQuery,
|
CloudWatchAnnotationQuery,
|
||||||
CloudWatchJsonData,
|
CloudWatchJsonData,
|
||||||
@@ -266,31 +267,39 @@ export class CloudWatchDatasource
|
|||||||
throw new Error('invalid metric editor mode');
|
throw new Error('invalid metric editor mode');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
replaceMetricQueryVars(
|
||||||
|
query: CloudWatchMetricsQuery,
|
||||||
|
options: DataQueryRequest<CloudWatchQuery>
|
||||||
|
): CloudWatchMetricsQuery {
|
||||||
|
query.region = this.templateSrv.replace(this.getActualRegion(query.region), options.scopedVars);
|
||||||
|
query.namespace = this.replace(query.namespace, options.scopedVars, true, 'namespace');
|
||||||
|
query.metricName = this.replace(query.metricName, options.scopedVars, true, 'metric name');
|
||||||
|
query.dimensions = this.convertDimensionFormat(query.dimensions ?? {}, options.scopedVars);
|
||||||
|
query.statistic = this.templateSrv.replace(query.statistic, options.scopedVars);
|
||||||
|
query.period = String(this.getPeriod(query, options)); // use string format for period in graph query, and alerting
|
||||||
|
query.id = this.templateSrv.replace(query.id, options.scopedVars);
|
||||||
|
query.expression = this.templateSrv.replace(query.expression, options.scopedVars);
|
||||||
|
query.sqlExpression = this.templateSrv.replace(query.sqlExpression, options.scopedVars, 'raw');
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
handleMetricQueries = (
|
handleMetricQueries = (
|
||||||
metricQueries: CloudWatchMetricsQuery[],
|
metricQueries: CloudWatchMetricsQuery[],
|
||||||
options: DataQueryRequest<CloudWatchQuery>
|
options: DataQueryRequest<CloudWatchQuery>
|
||||||
): Observable<DataQueryResponse> => {
|
): Observable<DataQueryResponse> => {
|
||||||
const validMetricsQueries = metricQueries
|
const validMetricsQueries = metricQueries.filter(this.filterQuery).map((q: CloudWatchMetricsQuery): MetricQuery => {
|
||||||
.filter(this.filterQuery)
|
const migratedQuery = migrateMetricQuery(q);
|
||||||
.map((item: CloudWatchMetricsQuery): MetricQuery => {
|
const migratedAndIterpolatedQuery = this.replaceMetricQueryVars(migratedQuery, options);
|
||||||
item.region = this.templateSrv.replace(this.getActualRegion(item.region), options.scopedVars);
|
|
||||||
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.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);
|
|
||||||
item.sqlExpression = this.templateSrv.replace(item.sqlExpression, options.scopedVars, 'raw');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
intervalMs: options.intervalMs,
|
intervalMs: options.intervalMs,
|
||||||
maxDataPoints: options.maxDataPoints,
|
maxDataPoints: options.maxDataPoints,
|
||||||
...item,
|
...migratedAndIterpolatedQuery,
|
||||||
type: 'timeSeriesQuery',
|
type: 'timeSeriesQuery',
|
||||||
datasource: this.getRef(),
|
datasource: this.getRef(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// No valid targets, return the empty result to save a round trip.
|
// No valid targets, return the empty result to save a round trip.
|
||||||
if (isEmpty(validMetricsQueries)) {
|
if (isEmpty(validMetricsQueries)) {
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
import { AnnotationQuery, DataQuery } from '@grafana/data';
|
import { AnnotationQuery, DataQuery } from '@grafana/data';
|
||||||
|
|
||||||
|
import { CloudWatchMetricsQuery, LegacyAnnotationQuery, MetricEditorMode, MetricQueryType } from '../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
migrateCloudWatchQuery,
|
migrateCloudWatchQuery,
|
||||||
migrateMultipleStatsAnnotationQuery,
|
migrateMultipleStatsAnnotationQuery,
|
||||||
migrateMultipleStatsMetricsQuery,
|
migrateMultipleStatsMetricsQuery,
|
||||||
migrateVariableQuery,
|
} from './dashboardMigrations';
|
||||||
} from './migrations';
|
|
||||||
import {
|
|
||||||
CloudWatchMetricsQuery,
|
|
||||||
LegacyAnnotationQuery,
|
|
||||||
MetricEditorMode,
|
|
||||||
MetricQueryType,
|
|
||||||
VariableQueryType,
|
|
||||||
OldVariableQuery,
|
|
||||||
} from './types';
|
|
||||||
|
|
||||||
describe('migration', () => {
|
describe('dashboardMigrations', () => {
|
||||||
describe('migrateMultipleStatsMetricsQuery', () => {
|
describe('migrateMultipleStatsMetricsQuery', () => {
|
||||||
const queryToMigrate = {
|
const queryToMigrate = {
|
||||||
statistics: ['Average', 'Sum', 'Maximum'],
|
statistics: ['Average', 'Sum', 'Maximum'],
|
||||||
@@ -183,94 +176,4 @@ describe('migration', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('migrateVariableQuery', () => {
|
|
||||||
describe('when metrics query is used', () => {
|
|
||||||
describe('and region param is left out', () => {
|
|
||||||
it('should leave an empty region', () => {
|
|
||||||
const query = migrateVariableQuery('metrics(testNamespace)');
|
|
||||||
expect(query.queryType).toBe(VariableQueryType.Metrics);
|
|
||||||
expect(query.namespace).toBe('testNamespace');
|
|
||||||
expect(query.region).toBe('');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('and region param is defined by user', () => {
|
|
||||||
it('should use the user defined region', () => {
|
|
||||||
const query = migrateVariableQuery('metrics(testNamespace2, custom-region)');
|
|
||||||
expect(query.queryType).toBe(VariableQueryType.Metrics);
|
|
||||||
expect(query.namespace).toBe('testNamespace2');
|
|
||||||
expect(query.region).toBe('custom-region');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('when dimension_values query is used', () => {
|
|
||||||
describe('and filter param is left out', () => {
|
|
||||||
it('should leave an empty filter', () => {
|
|
||||||
const query = migrateVariableQuery('dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)');
|
|
||||||
expect(query.queryType).toBe(VariableQueryType.DimensionValues);
|
|
||||||
expect(query.region).toBe('us-east-1');
|
|
||||||
expect(query.namespace).toBe('AWS/RDS');
|
|
||||||
expect(query.metricName).toBe('CPUUtilization');
|
|
||||||
expect(query.dimensionKey).toBe('DBInstanceIdentifier');
|
|
||||||
expect(query.dimensionFilters).toStrictEqual({});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('and filter param is defined by user', () => {
|
|
||||||
it('should use the user defined filter', () => {
|
|
||||||
const query = migrateVariableQuery(
|
|
||||||
'dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier,{"InstanceId":"$instance_id"})'
|
|
||||||
);
|
|
||||||
expect(query.queryType).toBe(VariableQueryType.DimensionValues);
|
|
||||||
expect(query.region).toBe('us-east-1');
|
|
||||||
expect(query.namespace).toBe('AWS/RDS');
|
|
||||||
expect(query.metricName).toBe('CPUUtilization');
|
|
||||||
expect(query.dimensionKey).toBe('DBInstanceIdentifier');
|
|
||||||
expect(query.dimensionFilters).toStrictEqual({ InstanceId: '$instance_id' });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('when resource_arns query is used', () => {
|
|
||||||
it('should parse the query', () => {
|
|
||||||
const query = migrateVariableQuery(
|
|
||||||
'resource_arns(eu-west-1,elasticloadbalancing:loadbalancer,{"elasticbeanstalk:environment-name":["myApp-dev","myApp-prod"]})'
|
|
||||||
);
|
|
||||||
expect(query.queryType).toBe(VariableQueryType.ResourceArns);
|
|
||||||
expect(query.region).toBe('eu-west-1');
|
|
||||||
expect(query.resourceType).toBe('elasticloadbalancing:loadbalancer');
|
|
||||||
expect(query.tags).toStrictEqual({ 'elasticbeanstalk:environment-name': ['myApp-dev', 'myApp-prod'] });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('when ec2_instance_attribute query is used', () => {
|
|
||||||
it('should parse the query', () => {
|
|
||||||
const query = migrateVariableQuery('ec2_instance_attribute(us-east-1,rds:db,{"environment":["$environment"]})');
|
|
||||||
expect(query.queryType).toBe(VariableQueryType.EC2InstanceAttributes);
|
|
||||||
expect(query.region).toBe('us-east-1');
|
|
||||||
expect(query.attributeName).toBe('rds:db');
|
|
||||||
expect(query.ec2Filters).toStrictEqual({ environment: ['$environment'] });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('when OldVariableQuery is used', () => {
|
|
||||||
it('should parse the query', () => {
|
|
||||||
const oldQuery: OldVariableQuery = {
|
|
||||||
queryType: VariableQueryType.EC2InstanceAttributes,
|
|
||||||
namespace: '',
|
|
||||||
region: 'us-east-1',
|
|
||||||
metricName: '',
|
|
||||||
dimensionKey: '',
|
|
||||||
ec2Filters: '{"environment":["$environment"]}',
|
|
||||||
instanceID: '',
|
|
||||||
attributeName: 'rds:db',
|
|
||||||
resourceType: 'elasticloadbalancing:loadbalancer',
|
|
||||||
tags: '{"elasticbeanstalk:environment-name":["myApp-dev","myApp-prod"]}',
|
|
||||||
refId: '',
|
|
||||||
};
|
|
||||||
const query = migrateVariableQuery(oldQuery);
|
|
||||||
expect(query.region).toBe('us-east-1');
|
|
||||||
expect(query.attributeName).toBe('rds:db');
|
|
||||||
expect(query.ec2Filters).toStrictEqual({ environment: ['$environment'] });
|
|
||||||
expect(query.resourceType).toBe('elasticloadbalancing:loadbalancer');
|
|
||||||
expect(query.tags).toStrictEqual({ 'elasticbeanstalk:environment-name': ['myApp-dev', 'myApp-prod'] });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// Functions in this file are called from the app/features/dashboard/state/DashboardMigrator.
|
||||||
|
// Migrations applied by the DashboardMigrator are performed before the plugin is loaded.
|
||||||
|
// DashboardMigrator migrations are tied to a certain minimum version of a dashboard which means they will only be ran once.
|
||||||
|
|
||||||
|
import { DataQuery, AnnotationQuery } from '@grafana/data';
|
||||||
|
import { getNextRefIdChar } from 'app/core/utils/query';
|
||||||
|
|
||||||
|
import { CloudWatchMetricsQuery, LegacyAnnotationQuery, MetricQueryType, MetricEditorMode } from '../types';
|
||||||
|
|
||||||
|
// E.g query.statistics = ['Max', 'Min'] will be migrated to two queries - query1.statistic = 'Max' and query2.statistic = 'Min'
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrates an annotation query that use more than one statistic into multiple queries
|
||||||
|
// E.g query.statistics = ['Max', 'Min'] will be migrated to two queries - query1.statistic = 'Max' and query2.statistic = 'Min'
|
||||||
|
export function migrateMultipleStatsAnnotationQuery(
|
||||||
|
annotationQuery: AnnotationQuery<LegacyAnnotationQuery>
|
||||||
|
): Array<AnnotationQuery<DataQuery>> {
|
||||||
|
const newAnnotations: Array<AnnotationQuery<LegacyAnnotationQuery>> = [];
|
||||||
|
|
||||||
|
if (annotationQuery && 'statistics' in annotationQuery && 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function migrateCloudWatchQuery(query: CloudWatchMetricsQuery) {
|
||||||
|
if (!query.hasOwnProperty('metricQueryType')) {
|
||||||
|
query.metricQueryType = MetricQueryType.Search;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!query.hasOwnProperty('metricEditorMode')) {
|
||||||
|
if (query.metricQueryType === MetricQueryType.Query) {
|
||||||
|
query.metricEditorMode = MetricEditorMode.Code;
|
||||||
|
} else {
|
||||||
|
query.metricEditorMode = query.expression ? MetricEditorMode.Code : MetricEditorMode.Builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
|
import { CloudWatchMetricsQuery } from '../types';
|
||||||
|
|
||||||
|
import { migrateAliasPatterns } from './metricQueryMigrations';
|
||||||
|
|
||||||
|
describe('metricQueryMigrations', () => {
|
||||||
|
interface TestScenario {
|
||||||
|
description?: string;
|
||||||
|
alias: string;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('migrateAliasPatterns', () => {
|
||||||
|
const baseQuery: CloudWatchMetricsQuery = {
|
||||||
|
statistic: 'Average',
|
||||||
|
refId: 'A',
|
||||||
|
id: '',
|
||||||
|
region: 'us-east-2',
|
||||||
|
namespace: 'AWS/EC2',
|
||||||
|
period: '300',
|
||||||
|
alias: '',
|
||||||
|
metricName: 'CPUUtilization',
|
||||||
|
dimensions: {},
|
||||||
|
matchExact: false,
|
||||||
|
expression: '',
|
||||||
|
};
|
||||||
|
describe('when feature is enabled', () => {
|
||||||
|
describe('and label was not previously migrated', () => {
|
||||||
|
const cases: TestScenario[] = [
|
||||||
|
{ description: 'Metric name', alias: '{{metric}}', label: "${PROP('MetricName')}" },
|
||||||
|
{ description: 'Trim pattern', alias: '{{ metric }}', label: "${PROP('MetricName')}" },
|
||||||
|
{ description: 'Namespace', alias: '{{namespace}}', label: "${PROP('Namespace')}" },
|
||||||
|
{ description: 'Period', alias: '{{period}}', label: "${PROP('Period')}" },
|
||||||
|
{ description: 'Region', alias: '{{region}}', label: "${PROP('Region')}" },
|
||||||
|
{ description: 'Statistic', alias: '{{stat}}', label: "${PROP('Stat')}" },
|
||||||
|
{ description: 'Label', alias: '{{label}}', label: '${LABEL}' },
|
||||||
|
{
|
||||||
|
description: 'Non-existing alias pattern',
|
||||||
|
alias: '{{anything_else}}',
|
||||||
|
label: "${PROP('Dim.anything_else')}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'Combined patterns',
|
||||||
|
alias: 'some {{combination}} of {{label}} and {{metric}}',
|
||||||
|
label: "some ${PROP('Dim.combination')} of ${LABEL} and ${PROP('MetricName')}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'Combined patterns not trimmed',
|
||||||
|
alias: 'some {{combination }}{{ label}} and {{metric}}',
|
||||||
|
label: "some ${PROP('Dim.combination')}${LABEL} and ${PROP('MetricName')}",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
test.each(cases)('given old alias %p, it should be migrated to label: %p', ({ alias, label }) => {
|
||||||
|
config.featureToggles.cloudWatchDynamicLabels = true;
|
||||||
|
const testQuery = { ...baseQuery, alias };
|
||||||
|
const result = migrateAliasPatterns(testQuery);
|
||||||
|
expect(result.label).toBe(label);
|
||||||
|
expect(result.alias).toBe(alias);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and label was previously migrated', () => {
|
||||||
|
const cases: TestScenario[] = [
|
||||||
|
{
|
||||||
|
alias: '',
|
||||||
|
label: "${PROP('MetricName')}",
|
||||||
|
},
|
||||||
|
{ alias: '{{metric}}', label: "${PROP('Period')}" },
|
||||||
|
{ alias: '{{namespace}}', label: '' },
|
||||||
|
];
|
||||||
|
test.each(cases)('it should not be migrated once again', ({ alias, label }) => {
|
||||||
|
config.featureToggles.cloudWatchDynamicLabels = true;
|
||||||
|
const testQuery = { ...baseQuery, alias, label };
|
||||||
|
const result = migrateAliasPatterns(testQuery);
|
||||||
|
expect(result.label).toBe(label);
|
||||||
|
expect(result.alias).toBe(alias);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when feature is disabled', () => {
|
||||||
|
const cases: TestScenario[] = [
|
||||||
|
{ alias: '{{period}}', label: "${PROP('MetricName')}" },
|
||||||
|
{ alias: '{{metric}}', label: '' },
|
||||||
|
{ alias: '', label: "${PROP('MetricName')}" },
|
||||||
|
{ alias: '', label: undefined },
|
||||||
|
];
|
||||||
|
test.each(cases)('should not migrate alias', ({ alias, label }) => {
|
||||||
|
config.featureToggles.cloudWatchDynamicLabels = false;
|
||||||
|
const testQuery = { ...baseQuery, alias: `${alias}` };
|
||||||
|
if (label !== undefined) {
|
||||||
|
testQuery.label = label;
|
||||||
|
}
|
||||||
|
const result = migrateAliasPatterns(testQuery);
|
||||||
|
expect(result.label).toBe(label);
|
||||||
|
expect(result.alias).toBe(alias);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
|
import { CloudWatchMetricsQuery } from '../types';
|
||||||
|
|
||||||
|
// Call this function to migrate queries from within the plugin.
|
||||||
|
export function migrateMetricQuery(query: CloudWatchMetricsQuery): CloudWatchMetricsQuery {
|
||||||
|
//add metric query migrations here
|
||||||
|
const migratedQuery = migrateAliasPatterns(query);
|
||||||
|
return migratedQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
const aliasPatterns: Record<string, string> = {
|
||||||
|
metric: `PROP('MetricName')`,
|
||||||
|
namespace: `PROP('Namespace')`,
|
||||||
|
period: `PROP('Period')`,
|
||||||
|
region: `PROP('Region')`,
|
||||||
|
stat: `PROP('Stat')`,
|
||||||
|
label: `LABEL`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function migrateAliasPatterns(query: CloudWatchMetricsQuery): CloudWatchMetricsQuery {
|
||||||
|
if (config.featureToggles.cloudWatchDynamicLabels && !query.hasOwnProperty('label')) {
|
||||||
|
const regex = /{{\s*(.+?)\s*}}/g;
|
||||||
|
query.label =
|
||||||
|
query.alias?.replace(regex, (_, value) => {
|
||||||
|
if (aliasPatterns.hasOwnProperty(value)) {
|
||||||
|
return `\${${aliasPatterns[value]}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `\${PROP('Dim.${value}')}`;
|
||||||
|
}) ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { VariableQueryType, OldVariableQuery } from '../types';
|
||||||
|
|
||||||
|
import { migrateVariableQuery } from './variableQueryMigrations';
|
||||||
|
|
||||||
|
describe('variableQueryMigrations', () => {
|
||||||
|
describe('migrateVariableQuery', () => {
|
||||||
|
describe('when metrics query is used', () => {
|
||||||
|
describe('and region param is left out', () => {
|
||||||
|
it('should leave an empty region', () => {
|
||||||
|
const query = migrateVariableQuery('metrics(testNamespace)');
|
||||||
|
expect(query.queryType).toBe(VariableQueryType.Metrics);
|
||||||
|
expect(query.namespace).toBe('testNamespace');
|
||||||
|
expect(query.region).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and region param is defined by user', () => {
|
||||||
|
it('should use the user defined region', () => {
|
||||||
|
const query = migrateVariableQuery('metrics(testNamespace2, custom-region)');
|
||||||
|
expect(query.queryType).toBe(VariableQueryType.Metrics);
|
||||||
|
expect(query.namespace).toBe('testNamespace2');
|
||||||
|
expect(query.region).toBe('custom-region');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when dimension_values query is used', () => {
|
||||||
|
describe('and filter param is left out', () => {
|
||||||
|
it('should leave an empty filter', () => {
|
||||||
|
const query = migrateVariableQuery('dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)');
|
||||||
|
expect(query.queryType).toBe(VariableQueryType.DimensionValues);
|
||||||
|
expect(query.region).toBe('us-east-1');
|
||||||
|
expect(query.namespace).toBe('AWS/RDS');
|
||||||
|
expect(query.metricName).toBe('CPUUtilization');
|
||||||
|
expect(query.dimensionKey).toBe('DBInstanceIdentifier');
|
||||||
|
expect(query.dimensionFilters).toStrictEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('and filter param is defined by user', () => {
|
||||||
|
it('should use the user defined filter', () => {
|
||||||
|
const query = migrateVariableQuery(
|
||||||
|
'dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier,{"InstanceId":"$instance_id"})'
|
||||||
|
);
|
||||||
|
expect(query.queryType).toBe(VariableQueryType.DimensionValues);
|
||||||
|
expect(query.region).toBe('us-east-1');
|
||||||
|
expect(query.namespace).toBe('AWS/RDS');
|
||||||
|
expect(query.metricName).toBe('CPUUtilization');
|
||||||
|
expect(query.dimensionKey).toBe('DBInstanceIdentifier');
|
||||||
|
expect(query.dimensionFilters).toStrictEqual({ InstanceId: '$instance_id' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when resource_arns query is used', () => {
|
||||||
|
it('should parse the query', () => {
|
||||||
|
const query = migrateVariableQuery(
|
||||||
|
'resource_arns(eu-west-1,elasticloadbalancing:loadbalancer,{"elasticbeanstalk:environment-name":["myApp-dev","myApp-prod"]})'
|
||||||
|
);
|
||||||
|
expect(query.queryType).toBe(VariableQueryType.ResourceArns);
|
||||||
|
expect(query.region).toBe('eu-west-1');
|
||||||
|
expect(query.resourceType).toBe('elasticloadbalancing:loadbalancer');
|
||||||
|
expect(query.tags).toStrictEqual({ 'elasticbeanstalk:environment-name': ['myApp-dev', 'myApp-prod'] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when ec2_instance_attribute query is used', () => {
|
||||||
|
it('should parse the query', () => {
|
||||||
|
const query = migrateVariableQuery('ec2_instance_attribute(us-east-1,rds:db,{"environment":["$environment"]})');
|
||||||
|
expect(query.queryType).toBe(VariableQueryType.EC2InstanceAttributes);
|
||||||
|
expect(query.region).toBe('us-east-1');
|
||||||
|
expect(query.attributeName).toBe('rds:db');
|
||||||
|
expect(query.ec2Filters).toStrictEqual({ environment: ['$environment'] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when OldVariableQuery is used', () => {
|
||||||
|
it('should parse the query', () => {
|
||||||
|
const oldQuery: OldVariableQuery = {
|
||||||
|
queryType: VariableQueryType.EC2InstanceAttributes,
|
||||||
|
namespace: '',
|
||||||
|
region: 'us-east-1',
|
||||||
|
metricName: '',
|
||||||
|
dimensionKey: '',
|
||||||
|
ec2Filters: '{"environment":["$environment"]}',
|
||||||
|
instanceID: '',
|
||||||
|
attributeName: 'rds:db',
|
||||||
|
resourceType: 'elasticloadbalancing:loadbalancer',
|
||||||
|
tags: '{"elasticbeanstalk:environment-name":["myApp-dev","myApp-prod"]}',
|
||||||
|
refId: '',
|
||||||
|
};
|
||||||
|
const query = migrateVariableQuery(oldQuery);
|
||||||
|
expect(query.region).toBe('us-east-1');
|
||||||
|
expect(query.attributeName).toBe('rds:db');
|
||||||
|
expect(query.ec2Filters).toStrictEqual({ environment: ['$environment'] });
|
||||||
|
expect(query.resourceType).toBe('elasticloadbalancing:loadbalancer');
|
||||||
|
expect(query.tags).toStrictEqual({ 'elasticbeanstalk:environment-name': ['myApp-dev', 'myApp-prod'] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,77 +1,6 @@
|
|||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
|
|
||||||
import { AnnotationQuery, DataQuery } from '@grafana/data';
|
import { VariableQuery, VariableQueryType, OldVariableQuery } from '../types';
|
||||||
import { getNextRefIdChar } from 'app/core/utils/query';
|
|
||||||
|
|
||||||
import {
|
|
||||||
CloudWatchMetricsQuery,
|
|
||||||
LegacyAnnotationQuery,
|
|
||||||
MetricEditorMode,
|
|
||||||
MetricQueryType,
|
|
||||||
VariableQuery,
|
|
||||||
VariableQueryType,
|
|
||||||
OldVariableQuery,
|
|
||||||
} from './types';
|
|
||||||
|
|
||||||
// Migrates a metric query that use more than one statistic into multiple queries
|
|
||||||
// E.g query.statistics = ['Max', 'Min'] will be migrated to two queries - query1.statistic = 'Max' and query2.statistic = 'Min'
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrates an annotation query that use more than one statistic into multiple queries
|
|
||||||
// E.g query.statistics = ['Max', 'Min'] will be migrated to two queries - query1.statistic = 'Max' and query2.statistic = 'Min'
|
|
||||||
export function migrateMultipleStatsAnnotationQuery(
|
|
||||||
annotationQuery: AnnotationQuery<LegacyAnnotationQuery>
|
|
||||||
): Array<AnnotationQuery<DataQuery>> {
|
|
||||||
const newAnnotations: Array<AnnotationQuery<LegacyAnnotationQuery>> = [];
|
|
||||||
|
|
||||||
if (annotationQuery && 'statistics' in annotationQuery && 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function migrateCloudWatchQuery(query: CloudWatchMetricsQuery) {
|
|
||||||
if (!query.hasOwnProperty('metricQueryType')) {
|
|
||||||
query.metricQueryType = MetricQueryType.Search;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!query.hasOwnProperty('metricEditorMode')) {
|
|
||||||
if (query.metricQueryType === MetricQueryType.Query) {
|
|
||||||
query.metricEditorMode = MetricEditorMode.Code;
|
|
||||||
} else {
|
|
||||||
query.metricEditorMode = query.expression ? MetricEditorMode.Code : MetricEditorMode.Builder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isVariableQuery(rawQuery: string | VariableQuery | OldVariableQuery): rawQuery is VariableQuery {
|
function isVariableQuery(rawQuery: string | VariableQuery | OldVariableQuery): rawQuery is VariableQuery {
|
||||||
return typeof rawQuery !== 'string' && typeof rawQuery.ec2Filters !== 'string' && typeof rawQuery.tags !== 'string';
|
return typeof rawQuery !== 'string' && typeof rawQuery.ec2Filters !== 'string' && typeof rawQuery.tags !== 'string';
|
||||||
@@ -46,7 +46,9 @@ export interface CloudWatchMetricsQuery extends MetricStat, DataQuery {
|
|||||||
|
|
||||||
//common props
|
//common props
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
alias?: string;
|
alias?: string;
|
||||||
|
label?: string;
|
||||||
|
|
||||||
// Math expression query
|
// Math expression query
|
||||||
expression?: string;
|
expression?: string;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { CustomVariableSupport, DataQueryRequest, DataQueryResponse } from '@gra
|
|||||||
|
|
||||||
import { VariableQueryEditor } from './components/VariableQueryEditor/VariableQueryEditor';
|
import { VariableQueryEditor } from './components/VariableQueryEditor/VariableQueryEditor';
|
||||||
import { CloudWatchDatasource } from './datasource';
|
import { CloudWatchDatasource } from './datasource';
|
||||||
import { migrateVariableQuery } from './migrations';
|
import { migrateVariableQuery } from './migrations/variableQueryMigrations';
|
||||||
import { VariableQuery, VariableQueryType } from './types';
|
import { VariableQuery, VariableQueryType } from './types';
|
||||||
|
|
||||||
export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchDatasource, VariableQuery> {
|
export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchDatasource, VariableQuery> {
|
||||||
|
|||||||
Reference in New Issue
Block a user