123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628 |
- import { lastValueFrom, of } from 'rxjs';
- import { toArray } from 'rxjs/operators';
- import {
- ArrayVector,
- DataFrame,
- dataFrameToJSON,
- dateTime,
- Field,
- FieldType,
- LogLevel,
- LogRowModel,
- MutableDataFrame,
- } from '@grafana/data';
- import { setDataSourceSrv } from '@grafana/runtime';
- import {
- dimensionVariable,
- expressionVariable,
- labelsVariable,
- limitVariable,
- logGroupNamesVariable,
- metricVariable,
- namespaceVariable,
- setupMockedDataSource,
- regionVariable,
- } from './__mocks__/CloudWatchDataSource';
- import { validLogsQuery, validMetricsQuery } from './__mocks__/queries';
- import { LOGSTREAM_IDENTIFIER_INTERNAL, LOG_IDENTIFIER_INTERNAL } from './datasource';
- import {
- CloudWatchLogsQueryStatus,
- CloudWatchMetricsQuery,
- CloudWatchQuery,
- MetricEditorMode,
- MetricQueryType,
- } from './types';
- describe('datasource', () => {
- describe('query', () => {
- it('should return error if log query and log groups is not specified', async () => {
- const { datasource } = setupMockedDataSource();
- const observable = datasource.query({ targets: [{ queryMode: 'Logs' as 'Logs' }] } as any);
- await expect(observable).toEmitValuesWith((received) => {
- const response = received[0];
- expect(response.error?.message).toBe('Log group is required');
- });
- });
- it('should return empty response if queries are hidden', async () => {
- const { datasource } = setupMockedDataSource();
- const observable = datasource.query({ targets: [{ queryMode: 'Logs' as 'Logs', hide: true }] } as any);
- await expect(observable).toEmitValuesWith((received) => {
- const response = received[0];
- expect(response.data).toEqual([]);
- });
- });
- const testTable: Array<{ query: CloudWatchQuery; valid: boolean }> = [
- { query: { ...validLogsQuery, hide: true }, valid: false },
- { query: { ...validLogsQuery, hide: false }, valid: true },
- { query: { ...validMetricsQuery, hide: true }, valid: false },
- { query: { ...validMetricsQuery, hide: true, id: 'queryA' }, valid: true },
- { query: { ...validMetricsQuery, hide: false }, valid: true },
- ];
- test.each(testTable)('should filter out hidden queries unless id is provided', ({ query, valid }) => {
- const { datasource } = setupMockedDataSource();
- expect(datasource.filterQuery(query)).toEqual(valid);
- });
- it('should interpolate variables in the query', async () => {
- const { datasource, fetchMock } = setupMockedDataSource();
- await lastValueFrom(
- datasource
- .query({
- targets: [
- {
- queryMode: 'Logs',
- region: '$region',
- expression: 'fields $fields',
- logGroupNames: ['/some/$group'],
- },
- ],
- } as any)
- .pipe(toArray())
- );
- expect(fetchMock.mock.calls[0][0].data.queries[0]).toMatchObject({
- queryString: 'fields templatedField',
- logGroupNames: ['/some/templatedGroup'],
- region: 'templatedRegion',
- });
- });
- it('should interpolate multi-value template variable for log group names in the query', async () => {
- const { datasource, fetchMock } = setupMockedDataSource({
- variables: [expressionVariable, logGroupNamesVariable, regionVariable],
- mockGetVariableName: false,
- });
- await lastValueFrom(
- datasource
- .query({
- targets: [
- {
- queryMode: 'Logs',
- region: '$region',
- expression: 'fields $fields',
- logGroupNames: ['$groups'],
- },
- ],
- } as any)
- .pipe(toArray())
- );
- expect(fetchMock.mock.calls[0][0].data.queries[0]).toMatchObject({
- queryString: 'fields templatedField',
- logGroupNames: ['templatedGroup-1', 'templatedGroup-2'],
- region: 'templatedRegion',
- });
- });
- it('should add links to log queries', async () => {
- const { datasource } = setupForLogs();
- const observable = datasource.query({
- targets: [
- {
- queryMode: 'Logs',
- logGroupNames: ['test'],
- refId: 'a',
- },
- ],
- } as any);
- const emits = await lastValueFrom(observable.pipe(toArray()));
- expect(emits).toHaveLength(1);
- expect(emits[0].data[0].fields.find((f: Field) => f.name === '@xrayTraceId').config.links).toMatchObject([
- {
- title: 'Xray',
- url: '',
- internal: {
- query: { query: '${__value.raw}', region: 'us-west-1', queryType: 'getTrace' },
- datasourceUid: 'xray',
- datasourceName: 'Xray',
- },
- },
- ]);
- expect(emits[0].data[0].fields.find((f: Field) => f.name === '@message').config.links).toMatchObject([
- {
- title: 'View in CloudWatch console',
- url: "https://us-west-1.console.aws.amazon.com/cloudwatch/home?region=us-west-1#logs-insights:queryDetail=~(end~'2020-12-31T19*3a00*3a00.000Z~start~'2020-12-31T19*3a00*3a00.000Z~timeType~'ABSOLUTE~tz~'UTC~editorString~'~isLiveTail~false~source~(~'test))",
- },
- ]);
- });
- describe('debouncedCustomAlert', () => {
- const debouncedAlert = jest.fn();
- beforeEach(() => {
- const { datasource } = setupMockedDataSource({
- variables: [
- { ...namespaceVariable, multi: true },
- { ...metricVariable, multi: true },
- ],
- });
- datasource.debouncedCustomAlert = debouncedAlert;
- datasource.performTimeSeriesQuery = jest.fn().mockResolvedValue([]);
- datasource.query({
- targets: [
- {
- queryMode: 'Metrics',
- id: '',
- region: 'us-east-2',
- namespace: namespaceVariable.id,
- metricName: metricVariable.id,
- period: '',
- alias: '',
- dimensions: {},
- matchExact: true,
- statistic: '',
- refId: '',
- expression: 'x * 2',
- metricQueryType: MetricQueryType.Search,
- metricEditorMode: MetricEditorMode.Code,
- },
- ],
- } as any);
- });
- it('should show debounced alert for namespace and metric name', async () => {
- expect(debouncedAlert).toHaveBeenCalledWith(
- 'CloudWatch templating error',
- 'Multi template variables are not supported for namespace'
- );
- expect(debouncedAlert).toHaveBeenCalledWith(
- 'CloudWatch templating error',
- 'Multi template variables are not supported for metric name'
- );
- });
- it('should not show debounced alert for region', async () => {
- expect(debouncedAlert).not.toHaveBeenCalledWith(
- 'CloudWatch templating error',
- 'Multi template variables are not supported for region'
- );
- });
- });
- });
- describe('filterMetricsQuery', () => {
- const datasource = setupMockedDataSource().datasource;
- let baseQuery: CloudWatchMetricsQuery;
- beforeEach(() => {
- baseQuery = {
- id: '',
- region: 'us-east-2',
- namespace: '',
- period: '',
- alias: '',
- metricName: '',
- dimensions: {},
- matchExact: true,
- statistic: '',
- expression: '',
- refId: '',
- };
- });
- it('should error if invalid mode', async () => {
- expect(() => datasource.filterMetricQuery(baseQuery)).toThrowError('invalid metric editor mode');
- });
- describe('metric search queries', () => {
- beforeEach(() => {
- baseQuery = {
- ...baseQuery,
- namespace: 'AWS/EC2',
- metricName: 'CPUUtilization',
- statistic: 'Average',
- metricQueryType: MetricQueryType.Search,
- metricEditorMode: MetricEditorMode.Builder,
- };
- });
- it('should not allow builder queries that dont have namespace, metric or statistic', async () => {
- expect(datasource.filterMetricQuery({ ...baseQuery, statistic: undefined })).toBeFalsy();
- expect(datasource.filterMetricQuery({ ...baseQuery, metricName: undefined })).toBeFalsy();
- expect(datasource.filterMetricQuery({ ...baseQuery, namespace: '' })).toBeFalsy();
- });
- it('should allow builder queries that have namespace, metric or statistic', async () => {
- expect(datasource.filterMetricQuery(baseQuery)).toBeTruthy();
- });
- it('should not allow code queries that dont have an expression', async () => {
- expect(
- datasource.filterMetricQuery({
- ...baseQuery,
- expression: undefined,
- metricEditorMode: MetricEditorMode.Code,
- })
- ).toBeFalsy();
- });
- it('should allow code queries that have an expression', async () => {
- expect(
- datasource.filterMetricQuery({ ...baseQuery, expression: 'x * 2', metricEditorMode: MetricEditorMode.Code })
- ).toBeTruthy();
- });
- });
- describe('metric search expression queries', () => {
- beforeEach(() => {
- baseQuery = {
- ...baseQuery,
- metricQueryType: MetricQueryType.Search,
- metricEditorMode: MetricEditorMode.Code,
- };
- });
- it('should not allow queries that dont have an expression', async () => {
- const valid = datasource.filterMetricQuery(baseQuery);
- expect(valid).toBeFalsy();
- });
- it('should allow queries that have an expression', async () => {
- baseQuery.expression = 'SUM([a,x])';
- const valid = datasource.filterMetricQuery(baseQuery);
- expect(valid).toBeTruthy();
- });
- });
- describe('metric query queries', () => {
- beforeEach(() => {
- baseQuery = {
- ...baseQuery,
- metricQueryType: MetricQueryType.Query,
- metricEditorMode: MetricEditorMode.Code,
- };
- });
- it('should not allow queries that dont have a sql expresssion', async () => {
- const valid = datasource.filterMetricQuery(baseQuery);
- expect(valid).toBeFalsy();
- });
- it('should allow queries that have a sql expresssion', async () => {
- baseQuery.sqlExpression = 'select SUM(CPUUtilization) from "AWS/EC2"';
- const valid = datasource.filterMetricQuery(baseQuery);
- expect(valid).toBeTruthy();
- });
- });
- });
- describe('resource requests', () => {
- it('should map resource response to metric response', async () => {
- const datasource = setupMockedDataSource().datasource;
- datasource.doMetricResourceRequest = jest.fn().mockResolvedValue([
- {
- text: 'AWS/EC2',
- value: 'CPUUtilization',
- },
- {
- text: 'AWS/Redshift',
- value: 'CPUPercentage',
- },
- ]);
- const allMetrics = await datasource.getAllMetrics('us-east-2');
- expect(allMetrics[0].metricName).toEqual('CPUUtilization');
- expect(allMetrics[0].namespace).toEqual('AWS/EC2');
- expect(allMetrics[1].metricName).toEqual('CPUPercentage');
- expect(allMetrics[1].namespace).toEqual('AWS/Redshift');
- });
- });
- describe('performTimeSeriesQuery', () => {
- it('should return the same length of data as result', async () => {
- const { datasource } = setupMockedDataSource({
- data: {
- results: {
- a: { refId: 'a', series: [{ name: 'cpu', points: [1, 1] }], meta: {} },
- b: { refId: 'b', series: [{ name: 'memory', points: [2, 2] }], meta: {} },
- },
- },
- });
- const observable = datasource.performTimeSeriesQuery(
- {
- queries: [
- { datasourceId: 1, refId: 'a' },
- { datasourceId: 1, refId: 'b' },
- ],
- } as any,
- { from: dateTime(), to: dateTime() } as any
- );
- await expect(observable).toEmitValuesWith((received) => {
- const response = received[0];
- expect(response.data.length).toEqual(2);
- });
- });
- it('sets fields.config.interval based on period', async () => {
- const { datasource } = setupMockedDataSource({
- data: {
- results: {
- a: {
- refId: 'a',
- series: [{ name: 'cpu', points: [1, 2], meta: { custom: { period: 60 } } }],
- },
- b: {
- refId: 'b',
- series: [{ name: 'cpu', points: [1, 2], meta: { custom: { period: 120 } } }],
- },
- },
- },
- });
- const observable = datasource.performTimeSeriesQuery(
- {
- queries: [{ datasourceId: 1, refId: 'a' }],
- } as any,
- { from: dateTime(), to: dateTime() } as any
- );
- await expect(observable).toEmitValuesWith((received) => {
- const response = received[0];
- expect(response.data[0].fields[0].config.interval).toEqual(60000);
- expect(response.data[1].fields[0].config.interval).toEqual(120000);
- });
- });
- });
- describe('describeLogGroup', () => {
- it('replaces region correctly in the query', async () => {
- const { datasource, fetchMock } = setupMockedDataSource();
- await datasource.describeLogGroups({ region: 'default' });
- expect(fetchMock.mock.calls[0][0].data.queries[0].region).toBe('us-west-1');
- await datasource.describeLogGroups({ region: 'eu-east' });
- expect(fetchMock.mock.calls[1][0].data.queries[0].region).toBe('eu-east');
- });
- });
- describe('getLogRowContext', () => {
- it('replaces parameters correctly in the query', async () => {
- const { datasource, fetchMock } = setupMockedDataSource();
- const row: LogRowModel = {
- entryFieldIndex: 0,
- rowIndex: 0,
- dataFrame: new MutableDataFrame({
- refId: 'B',
- fields: [
- { name: 'ts', type: FieldType.time, values: [1] },
- { name: LOG_IDENTIFIER_INTERNAL, type: FieldType.string, values: ['foo'], labels: {} },
- { name: LOGSTREAM_IDENTIFIER_INTERNAL, type: FieldType.string, values: ['bar'], labels: {} },
- ],
- }),
- entry: '4',
- labels: {},
- hasAnsi: false,
- hasUnescapedContent: false,
- raw: '4',
- logLevel: LogLevel.info,
- timeEpochMs: 4,
- timeEpochNs: '4000000',
- timeFromNow: '',
- timeLocal: '',
- timeUtc: '',
- uid: '1',
- };
- await datasource.getLogRowContext(row);
- expect(fetchMock.mock.calls[0][0].data.queries[0].endTime).toBe(4);
- expect(fetchMock.mock.calls[0][0].data.queries[0].region).toBe(undefined);
- await datasource.getLogRowContext(row, { direction: 'FORWARD' }, { ...validLogsQuery, region: 'eu-east' });
- expect(fetchMock.mock.calls[1][0].data.queries[0].startTime).toBe(4);
- expect(fetchMock.mock.calls[1][0].data.queries[0].region).toBe('eu-east');
- });
- });
- describe('template variable interpolation', () => {
- it('interpolates variables correctly', async () => {
- const { datasource, fetchMock } = setupMockedDataSource({
- variables: [namespaceVariable, metricVariable, labelsVariable, limitVariable],
- });
- datasource.handleMetricQueries(
- [
- {
- id: '',
- refId: 'a',
- region: 'us-east-2',
- namespace: '',
- period: '',
- alias: '',
- metricName: '',
- dimensions: {},
- matchExact: true,
- statistic: '',
- expression: '',
- metricQueryType: MetricQueryType.Query,
- metricEditorMode: MetricEditorMode.Code,
- sqlExpression: 'SELECT SUM($metric) FROM "$namespace" GROUP BY ${labels:raw} LIMIT $limit',
- },
- ],
- { range: { from: dateTime(), to: dateTime() } } as any
- );
- expect(fetchMock).toHaveBeenCalledWith(
- expect.objectContaining({
- data: expect.objectContaining({
- queries: expect.arrayContaining([
- expect.objectContaining({
- sqlExpression: `SELECT SUM(CPUUtilization) FROM "AWS/EC2" GROUP BY InstanceId,InstanceType LIMIT 100`,
- }),
- ]),
- }),
- })
- );
- });
- });
- describe('timezoneUTCOffset', () => {
- const testQuery = {
- id: '',
- refId: 'a',
- region: 'us-east-2',
- namespace: '',
- period: '',
- label: '${MAX_TIME_RELATIVE}',
- metricName: '',
- dimensions: {},
- matchExact: true,
- statistic: '',
- expression: '',
- metricQueryType: MetricQueryType.Query,
- metricEditorMode: MetricEditorMode.Code,
- sqlExpression: 'SELECT SUM($metric) FROM "$namespace" GROUP BY ${labels:raw} LIMIT $limit',
- };
- const testTable = [
- ['Europe/Stockholm', '+0200'],
- ['America/New_York', '-0400'],
- ['Asia/Tokyo', '+0900'],
- ['UTC', '+0000'],
- ];
- describe.each(testTable)('should use the right time zone offset', (ianaTimezone, expectedOffset) => {
- const { datasource, fetchMock } = setupMockedDataSource();
- datasource.handleMetricQueries([testQuery], {
- range: { from: dateTime(), to: dateTime() },
- timezone: ianaTimezone,
- } as any);
- expect(fetchMock).toHaveBeenCalledWith(
- expect.objectContaining({
- data: expect.objectContaining({
- queries: expect.arrayContaining([
- expect.objectContaining({
- timezoneUTCOffset: expectedOffset,
- }),
- ]),
- }),
- })
- );
- });
- });
- describe('interpolateMetricsQueryVariables', () => {
- it('interpolates dimensions correctly', () => {
- const testQuery = {
- id: 'a',
- refId: 'a',
- region: 'us-east-2',
- namespace: '',
- dimensions: { InstanceId: '$dimension' },
- };
- const ds = setupMockedDataSource({ variables: [dimensionVariable], mockGetVariableName: false });
- const result = ds.datasource.interpolateMetricsQueryVariables(testQuery, {
- dimension: { text: 'foo', value: 'foo' },
- });
- expect(result).toStrictEqual({
- alias: '',
- metricName: '',
- namespace: '',
- period: '',
- sqlExpression: '',
- dimensions: { InstanceId: ['foo'] },
- });
- });
- });
- describe('convertMultiFiltersFormat', () => {
- const ds = setupMockedDataSource({ variables: [labelsVariable, dimensionVariable], mockGetVariableName: false });
- it('converts keys and values correctly', () => {
- const filters = { $dimension: ['b'], a: ['$labels', 'bar'] };
- const result = ds.datasource.convertMultiFilterFormat(filters);
- expect(result).toStrictEqual({
- env: ['b'],
- a: ['InstanceId', 'InstanceType', 'bar'],
- });
- });
- });
- describe('getLogGroupFields', () => {
- it('passes region correctly', async () => {
- const { datasource, fetchMock } = setupMockedDataSource();
- fetchMock.mockReturnValueOnce(
- of({
- data: {
- results: {
- A: {
- frames: [
- dataFrameToJSON(
- new MutableDataFrame({
- fields: [
- { name: 'key', values: [] },
- { name: 'val', values: [] },
- ],
- })
- ),
- ],
- },
- },
- },
- })
- );
- await datasource.getLogGroupFields({ region: 'us-west-1', logGroupName: 'test' });
- expect(fetchMock.mock.calls[0][0].data.queries[0].region).toBe('us-west-1');
- });
- });
- });
- function setupForLogs() {
- function envelope(frame: DataFrame) {
- return { data: { results: { a: { refId: 'a', frames: [dataFrameToJSON(frame)] } } } };
- }
- const { datasource, fetchMock } = setupMockedDataSource();
- const startQueryFrame = new MutableDataFrame({ fields: [{ name: 'queryId', values: ['queryid'] }] });
- fetchMock.mockReturnValueOnce(of(envelope(startQueryFrame)));
- const logsFrame = new MutableDataFrame({
- fields: [
- {
- name: '@message',
- values: new ArrayVector(['something']),
- },
- {
- name: '@timestamp',
- values: new ArrayVector([1]),
- },
- {
- name: '@xrayTraceId',
- values: new ArrayVector(['1-613f0d6b-3e7cb34375b60662359611bd']),
- },
- ],
- meta: { custom: { Status: CloudWatchLogsQueryStatus.Complete } },
- });
- fetchMock.mockReturnValueOnce(of(envelope(logsFrame)));
- setDataSourceSrv({
- async get() {
- return {
- name: 'Xray',
- };
- },
- } as any);
- return { datasource, fetchMock };
- }
|