mirror of
https://github.com/grafana/grafana.git
synced 2025-02-20 11:48:34 -06:00
629 lines
20 KiB
TypeScript
629 lines
20 KiB
TypeScript
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 };
|
|
}
|