diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 8cb33f6e5f5..83628fd7967 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -1,6 +1,6 @@ import angular, { IQService } from 'angular'; import _ from 'lodash'; -import { dateMath, ScopedVars } from '@grafana/data'; +import { dateMath, ScopedVars, toDataFrame, TimeRange } from '@grafana/data'; import kbn from 'app/core/utils/kbn'; import { CloudWatchQuery } from './types'; import { DataSourceApi, DataQueryRequest, DataSourceInstanceSettings } from '@grafana/ui'; @@ -92,7 +92,7 @@ export default class CloudWatchDatasource extends DataSourceApi queries: queries, }; - return this.performTimeSeriesQuery(request); + return this.performTimeSeriesQuery(request, options.range); } getPeriod(target: any, options: any, now?: number) { @@ -141,26 +141,75 @@ export default class CloudWatchDatasource extends DataSourceApi return period; } - performTimeSeriesQuery(request: any) { + buildCloudwatchConsoleUrl( + { region, namespace, metricName, dimensions, statistics, period }: CloudWatchQuery, + start: string, + end: string, + title: string + ) { + const conf = { + view: 'timeSeries', + stacked: false, + title, + start, + end, + region, + metrics: [ + ...statistics.map(stat => [ + namespace, + metricName, + ...Object.entries(dimensions).reduce((acc, [key, value]) => [...acc, key, value], []), + { + stat, + period, + }, + ]), + ], + }; + + return `https://${region}.console.aws.amazon.com/cloudwatch/deeplink.js?region=${region}#metricsV2:graph=${encodeURIComponent( + JSON.stringify(conf) + )}`; + } + + performTimeSeriesQuery(request: any, { from, to }: TimeRange) { return this.awsRequest('/api/tsdb/query', request).then((res: any) => { - const data = []; - - if (res.results) { - for (const query of request.queries) { - const queryRes = res.results[query.refId]; - if (queryRes) { - for (const series of queryRes.series) { - const s = { target: series.name, datapoints: series.points } as any; - if (queryRes.meta.unit) { - s.unit = queryRes.meta.unit; - } - data.push(s); - } - } - } + if (!res.results) { + return { data: [] }; } + const dataFrames = Object.values(request.queries).reduce((acc: any, queryRequest: any) => { + const queryResult = res.results[queryRequest.refId]; + if (!queryResult) { + return acc; + } - return { data: data }; + const link = this.buildCloudwatchConsoleUrl( + queryRequest, + from.toISOString(), + to.toISOString(), + `query${queryRequest.refId}` + ); + + return [ + ...acc, + ...queryResult.series.map(({ name, points, meta }: any) => { + const series = { target: name, datapoints: points }; + const dataFrame = toDataFrame(meta && meta.unit ? { ...series, unit: meta.unit } : series); + for (const field of dataFrame.fields) { + field.config.links = [ + { + url: link, + title: 'View in CloudWatch console', + targetBlank: true, + }, + ]; + } + return dataFrame; + }), + ]; + }, []); + + return { data: dataFrames }; }); } diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts index 9163112d58c..78df1db18f2 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts @@ -15,6 +15,8 @@ describe('CloudWatchDatasource', () => { } as DataSourceInstanceSettings; const templateSrv = new TemplateSrv(); + const start = 1483196400 * 1000; + const defaultTimeRange = { from: new Date(start), to: new Date(start + 3600 * 1000) }; const timeSrv = { time: { from: 'now-1h', to: 'now' }, @@ -39,7 +41,7 @@ describe('CloudWatchDatasource', () => { let requestParams: { queries: CloudWatchQuery[] }; const query = { - range: { from: 'now-1h', to: 'now' }, + range: defaultTimeRange, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ { @@ -110,7 +112,7 @@ describe('CloudWatchDatasource', () => { ]); const query = { - range: { from: 'now-1h', to: 'now' }, + range: defaultTimeRange, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ { @@ -136,7 +138,7 @@ describe('CloudWatchDatasource', () => { it.each(['pNN.NN', 'p9', 'p99.', 'p99.999'])('should cancel query for invalid extended statistics (%s)', stat => { const query = { - range: { from: 'now-1h', to: 'now' }, + range: defaultTimeRange, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ { @@ -157,8 +159,8 @@ describe('CloudWatchDatasource', () => { it('should return series list', done => { ctx.ds.query(query).then((result: any) => { - expect(result.data[0].target).toBe(response.results.A.series[0].name); - expect(result.data[0].datapoints[0][0]).toBe(response.results.A.series[0].points[0][0]); + expect(result.data[0].name).toBe(response.results.A.series[0].name); + expect(result.data[0].fields[0].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]); done(); }); }); @@ -187,7 +189,7 @@ describe('CloudWatchDatasource', () => { it('should query for the datasource region if empty or "default"', done => { const query = { - range: { from: 'now-1h', to: 'now' }, + range: defaultTimeRange, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ { @@ -213,7 +215,7 @@ describe('CloudWatchDatasource', () => { describe('When performing CloudWatch query for extended statistics', () => { const query = { - range: { from: 'now-1h', to: 'now' }, + range: defaultTimeRange, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ { @@ -260,8 +262,8 @@ describe('CloudWatchDatasource', () => { it('should return series list', done => { ctx.ds.query(query).then((result: any) => { - expect(result.data[0].target).toBe(response.results.A.series[0].name); - expect(result.data[0].datapoints[0][0]).toBe(response.results.A.series[0].points[0][0]); + expect(result.data[0].name).toBe(response.results.A.series[0].name); + expect(result.data[0].fields[0].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]); done(); }); }); @@ -316,7 +318,7 @@ describe('CloudWatchDatasource', () => { it('should generate the correct query for single template variable', done => { const query = { - range: { from: 'now-1h', to: 'now' }, + range: defaultTimeRange, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ { @@ -341,7 +343,7 @@ describe('CloudWatchDatasource', () => { it('should generate the correct query for multilple template variables', done => { const query = { - range: { from: 'now-1h', to: 'now' }, + range: defaultTimeRange, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ { @@ -377,7 +379,7 @@ describe('CloudWatchDatasource', () => { it('should generate the correct query for multilple template variables, lack scopedVars', done => { const query = { - range: { from: 'now-1h', to: 'now' }, + range: defaultTimeRange, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ { @@ -412,7 +414,7 @@ describe('CloudWatchDatasource', () => { it('should generate the correct query for multilple template variables with expression', done => { const query: any = { - range: { from: 'now-1h', to: 'now' }, + range: defaultTimeRange, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ {