mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
621 lines
20 KiB
TypeScript
621 lines
20 KiB
TypeScript
import { lastValueFrom, of } from 'rxjs';
|
|
|
|
import {
|
|
DataQueryRequest,
|
|
FieldType,
|
|
LogLevel,
|
|
LogRowContextQueryDirection,
|
|
LogRowModel,
|
|
MutableDataFrame,
|
|
} from '@grafana/data';
|
|
|
|
import { regionVariable } from '../__mocks__/CloudWatchDataSource';
|
|
import { setupMockedLogsQueryRunner } from '../__mocks__/LogsQueryRunner';
|
|
import { LogsRequestMock } from '../__mocks__/Request';
|
|
import { validLogsQuery } from '../__mocks__/queries';
|
|
import { CloudWatchLogsQuery } from '../types'; // Add this import statement
|
|
|
|
import { LOGSTREAM_IDENTIFIER_INTERNAL, LOG_IDENTIFIER_INTERNAL } from './CloudWatchLogsQueryRunner';
|
|
|
|
describe('CloudWatchLogsQueryRunner', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('getLogRowContext', () => {
|
|
it('replaces parameters correctly in the query', async () => {
|
|
const { runner, queryMock } = setupMockedLogsQueryRunner({ variables: [regionVariable] });
|
|
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 runner.getLogRowContext(row, undefined, queryMock);
|
|
expect(queryMock.mock.calls[0][0].targets[0].endTime).toBe(4);
|
|
// sets the default region if region is empty
|
|
expect(queryMock.mock.calls[0][0].targets[0].region).toBe('us-west-1');
|
|
|
|
await runner.getLogRowContext(row, { direction: LogRowContextQueryDirection.Forward }, queryMock, {
|
|
...validLogsQuery,
|
|
region: '$region',
|
|
});
|
|
expect(queryMock.mock.calls[1][0].targets[0].startTime).toBe(4);
|
|
expect(queryMock.mock.calls[1][0].targets[0].region).toBe('templatedRegion');
|
|
});
|
|
});
|
|
|
|
describe('handleLogQueries', () => {
|
|
it('should request to start each query and then request to get the query results', async () => {
|
|
const { runner } = setupMockedLogsQueryRunner();
|
|
|
|
const options: DataQueryRequest<CloudWatchLogsQuery> = {
|
|
...LogsRequestMock,
|
|
targets: rawLogQueriesStub,
|
|
};
|
|
|
|
const queryFn = jest
|
|
.fn()
|
|
.mockReturnValueOnce(of(startQuerySuccessResponseStub))
|
|
.mockReturnValueOnce(of(getQuerySuccessResponseStub));
|
|
|
|
const response = runner.handleLogQueries(rawLogQueriesStub, options, queryFn);
|
|
const results = await lastValueFrom(response);
|
|
expect(queryFn).toHaveBeenCalledTimes(2);
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'StartQuery' })]),
|
|
})
|
|
);
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'GetQueryResults' })]),
|
|
})
|
|
);
|
|
|
|
expect(results).toEqual({
|
|
...getQuerySuccessResponseStub,
|
|
errors: [],
|
|
key: 'test-key',
|
|
});
|
|
});
|
|
|
|
it('should call getQueryResults until the query returns with a status of complete', async () => {
|
|
const { runner } = setupMockedLogsQueryRunner();
|
|
|
|
const options: DataQueryRequest<CloudWatchLogsQuery> = {
|
|
...LogsRequestMock,
|
|
targets: rawLogQueriesStub,
|
|
};
|
|
|
|
const queryFn = jest
|
|
.fn()
|
|
.mockReturnValueOnce(of(startQuerySuccessResponseStub))
|
|
.mockReturnValueOnce(of(getQueryLoadingResponseStub))
|
|
.mockReturnValueOnce(of(getQueryLoadingResponseStub))
|
|
.mockReturnValueOnce(of(getQueryLoadingResponseStub))
|
|
.mockReturnValueOnce(of(getQuerySuccessResponseStub));
|
|
|
|
const response = runner.handleLogQueries(rawLogQueriesStub, options, queryFn);
|
|
const results = await lastValueFrom(response);
|
|
expect(queryFn).toHaveBeenCalledTimes(5);
|
|
|
|
// first call to start query
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'StartQuery' })]),
|
|
})
|
|
);
|
|
// second call we try to get the results
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'GetQueryResults' })]),
|
|
})
|
|
);
|
|
// after getting a loading response we wait and try again
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
3,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'GetQueryResults' })]),
|
|
})
|
|
);
|
|
// after getting a loading response we wait and try again
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
4,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'GetQueryResults' })]),
|
|
})
|
|
);
|
|
// after getting a loading response we wait and try again
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
5,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'GetQueryResults' })]),
|
|
})
|
|
);
|
|
|
|
expect(results).toEqual({
|
|
...getQuerySuccessResponseStub,
|
|
errors: [],
|
|
key: 'test-key',
|
|
});
|
|
});
|
|
|
|
it('should call getQueryResults until the query returns even if it the startQuery gets a rate limiting error from aws', async () => {
|
|
const { runner } = setupMockedLogsQueryRunner();
|
|
|
|
const options: DataQueryRequest<CloudWatchLogsQuery> = {
|
|
...LogsRequestMock,
|
|
targets: rawLogQueriesStub,
|
|
};
|
|
|
|
const queryFn = jest
|
|
.fn()
|
|
.mockReturnValueOnce(of(startQueryErrorWhenRateLimitedResponseStub))
|
|
.mockReturnValueOnce(of(startQuerySuccessResponseStub))
|
|
.mockReturnValueOnce(of(getQuerySuccessResponseStub));
|
|
|
|
const response = runner.handleLogQueries(rawLogQueriesStub, options, queryFn);
|
|
const results = await lastValueFrom(response);
|
|
expect(queryFn).toHaveBeenCalledTimes(3);
|
|
|
|
// first call
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'StartQuery' })]),
|
|
})
|
|
);
|
|
// we retry because the first call failed with the rate limiting error
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'StartQuery' })]),
|
|
})
|
|
);
|
|
// we get results because second call was successful
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
3,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'GetQueryResults' })]),
|
|
})
|
|
);
|
|
|
|
expect(results).toEqual({
|
|
...getQuerySuccessResponseStub,
|
|
errors: [],
|
|
key: 'test-key',
|
|
});
|
|
});
|
|
|
|
it('should call getQueryResults until the query returns even if it the startQuery gets a throttling error from aws', async () => {
|
|
const { runner } = setupMockedLogsQueryRunner();
|
|
|
|
const options: DataQueryRequest<CloudWatchLogsQuery> = {
|
|
...LogsRequestMock,
|
|
targets: rawLogQueriesStub,
|
|
};
|
|
|
|
const queryFn = jest
|
|
.fn()
|
|
.mockReturnValueOnce(of(startQueryErrorWhenThrottlingResponseStub))
|
|
.mockReturnValueOnce(of(startQuerySuccessResponseStub))
|
|
.mockReturnValueOnce(of(getQuerySuccessResponseStub));
|
|
|
|
const response = runner.handleLogQueries(rawLogQueriesStub, options, queryFn);
|
|
const results = await lastValueFrom(response);
|
|
expect(queryFn).toHaveBeenCalledTimes(3);
|
|
|
|
// first call
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'StartQuery' })]),
|
|
})
|
|
);
|
|
// we retry because the first call failed with the rate limiting error
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'StartQuery' })]),
|
|
})
|
|
);
|
|
// we get results because second call was successful
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
3,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'GetQueryResults' })]),
|
|
})
|
|
);
|
|
|
|
expect(results).toEqual({
|
|
...getQuerySuccessResponseStub,
|
|
errors: [],
|
|
key: 'test-key',
|
|
});
|
|
});
|
|
|
|
it('should return an error if it timesout before the start queries can get past a rate limiting error', async () => {
|
|
const { runner } = setupMockedLogsQueryRunner();
|
|
// first time timeout is called it will not be timed out, second time it will be timed out
|
|
const timeoutFunc = jest
|
|
.fn()
|
|
.mockImplementationOnce(() => false)
|
|
.mockImplementationOnce(() => true);
|
|
runner.createTimeoutFn = jest.fn(() => timeoutFunc);
|
|
|
|
const options: DataQueryRequest<CloudWatchLogsQuery> = {
|
|
...LogsRequestMock,
|
|
targets: rawLogQueriesStub,
|
|
};
|
|
|
|
// running query fn will always return the rate limit
|
|
const queryFn = jest.fn().mockReturnValue(of(startQueryErrorWhenRateLimitedResponseStub));
|
|
|
|
const response = runner.handleLogQueries(rawLogQueriesStub, options, queryFn);
|
|
const results = await lastValueFrom(response);
|
|
|
|
expect(queryFn).toHaveBeenCalledTimes(2);
|
|
|
|
// first call starts the query, but it fails with rate limiting error
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'StartQuery' })]),
|
|
})
|
|
);
|
|
|
|
// we retry because the first call failed with the rate limiting error and we haven't timed out yet
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'StartQuery' })]),
|
|
})
|
|
);
|
|
|
|
expect(results).toEqual({
|
|
...startQueryErrorWhenRateLimitedResponseStub,
|
|
key: 'test-key',
|
|
state: 'Done',
|
|
});
|
|
});
|
|
|
|
it('should return an error if the start query fails with an error that is not a rate limiting error', async () => {
|
|
const { runner } = setupMockedLogsQueryRunner();
|
|
|
|
const options: DataQueryRequest<CloudWatchLogsQuery> = {
|
|
...LogsRequestMock,
|
|
targets: rawLogQueriesStub,
|
|
};
|
|
|
|
const queryFn = jest.fn().mockReturnValueOnce(of(startQueryErrorWhenBadSyntaxResponseStub));
|
|
|
|
const response = runner.handleLogQueries(rawLogQueriesStub, options, queryFn);
|
|
const results = await lastValueFrom(response);
|
|
|
|
// only one query is made, it gets the error and returns the error
|
|
expect(queryFn).toHaveBeenCalledTimes(1);
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'StartQuery' })]),
|
|
})
|
|
);
|
|
expect(results).toEqual({
|
|
...startQueryErrorWhenBadSyntaxResponseStub,
|
|
key: 'test-key',
|
|
state: 'Done',
|
|
});
|
|
});
|
|
|
|
it('should return an error and stop querying if get query results has finished with errors', async () => {
|
|
const { runner } = setupMockedLogsQueryRunner();
|
|
|
|
const options: DataQueryRequest<CloudWatchLogsQuery> = {
|
|
...LogsRequestMock,
|
|
targets: rawLogQueriesStub,
|
|
};
|
|
|
|
const queryFn = jest
|
|
.fn()
|
|
.mockReturnValueOnce(of(startQuerySuccessResponseStub))
|
|
.mockReturnValueOnce(of(getQueryLoadingResponseStub))
|
|
.mockReturnValueOnce(of(getQueryErrorResponseStub))
|
|
.mockReturnValueOnce(of(stopQueryResponseStub));
|
|
|
|
const response = runner.handleLogQueries(rawLogQueriesStub, options, queryFn);
|
|
const results = await lastValueFrom(response);
|
|
|
|
expect(queryFn).toHaveBeenCalledTimes(4);
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'StartQuery' })]),
|
|
})
|
|
);
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'GetQueryResults' })]),
|
|
})
|
|
);
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
3,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'GetQueryResults' })]),
|
|
})
|
|
);
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
4,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'StopQuery' })]),
|
|
})
|
|
);
|
|
expect(results).toEqual({
|
|
...getQueryErrorResponseStub,
|
|
key: 'test-key',
|
|
state: 'Done',
|
|
});
|
|
});
|
|
|
|
it('should return an error and any partial data if it timesout before getting back all the results', async () => {
|
|
const { runner } = setupMockedLogsQueryRunner();
|
|
// mocking running for a while and then timing out
|
|
const timeoutFunc = jest
|
|
.fn()
|
|
.mockImplementationOnce(() => false)
|
|
.mockImplementationOnce(() => false)
|
|
.mockImplementationOnce(() => false)
|
|
.mockImplementationOnce(() => true);
|
|
runner.createTimeoutFn = jest.fn(() => timeoutFunc);
|
|
|
|
const queryFn = jest
|
|
.fn()
|
|
.mockReturnValueOnce(of(startQuerySuccessResponseStub))
|
|
.mockReturnValueOnce(of(getQueryLoadingResponseStub))
|
|
.mockReturnValueOnce(of(getQueryLoadingResponseStub))
|
|
.mockReturnValueOnce(of(getQueryLoadingResponseStub))
|
|
.mockReturnValueOnce(of(getQueryLoadingResponseStub))
|
|
.mockReturnValueOnce(of(stopQueryResponseStub));
|
|
|
|
const options: DataQueryRequest<CloudWatchLogsQuery> = {
|
|
...LogsRequestMock,
|
|
targets: rawLogQueriesStub,
|
|
};
|
|
const response = runner.handleLogQueries(rawLogQueriesStub, options, queryFn);
|
|
const results = await lastValueFrom(response);
|
|
expect(queryFn).toHaveBeenCalledTimes(6);
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'StartQuery' })]),
|
|
})
|
|
);
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'GetQueryResults' })]),
|
|
})
|
|
);
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
3,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'GetQueryResults' })]),
|
|
})
|
|
);
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
4,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'GetQueryResults' })]),
|
|
})
|
|
);
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
5,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'GetQueryResults' })]),
|
|
})
|
|
);
|
|
expect(queryFn).toHaveBeenNthCalledWith(
|
|
6,
|
|
expect.objectContaining({
|
|
targets: expect.arrayContaining([expect.objectContaining({ subtype: 'StopQuery' })]),
|
|
})
|
|
);
|
|
expect(results).toEqual({
|
|
...getQueryLoadingResponseStub,
|
|
errors: [
|
|
{
|
|
message:
|
|
'Error: Query hit timeout before completing after 3 attempts, partial results may be shown. To increase the timeout window update your datasource configuration.',
|
|
refId: 'A',
|
|
type: 'timeout',
|
|
},
|
|
],
|
|
key: 'test-key',
|
|
state: 'Done',
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
const rawLogQueriesStub: CloudWatchLogsQuery[] = [
|
|
{
|
|
refId: 'A',
|
|
id: '',
|
|
region: 'us-east-2',
|
|
logGroups: [
|
|
{
|
|
accountId: 'accountId',
|
|
arn: 'somearn',
|
|
name: 'nameOfLogGroup',
|
|
},
|
|
],
|
|
queryMode: 'Logs',
|
|
expression: 'fields @timestamp, @message |\n sort @timestamp desc |\n limit 20',
|
|
datasource: {
|
|
type: 'cloudwatch',
|
|
uid: 'ff87aa43-7618-42ee-ae9c-4a405378728b',
|
|
},
|
|
},
|
|
];
|
|
|
|
const startQuerySuccessResponseStub = {
|
|
data: [
|
|
{
|
|
name: 'A',
|
|
refId: 'A',
|
|
meta: {
|
|
typeVersion: [0, 0],
|
|
custom: { Region: 'us-east-2' },
|
|
},
|
|
fields: [
|
|
{
|
|
name: 'queryId',
|
|
type: 'string',
|
|
typeInfo: { frame: 'string' },
|
|
config: {},
|
|
values: ['123'],
|
|
entities: {},
|
|
},
|
|
],
|
|
length: 1,
|
|
state: 'Done',
|
|
},
|
|
],
|
|
};
|
|
|
|
const startQueryErrorWhenRateLimitedResponseStub = {
|
|
data: [],
|
|
errors: [
|
|
{
|
|
refId: 'A',
|
|
message:
|
|
'failed to execute log action with subtype: StartQuery: LimitExceededException: LimitExceededException: Account maximum query concurrency limit of [30] reached.',
|
|
status: 500,
|
|
},
|
|
],
|
|
};
|
|
|
|
const startQueryErrorWhenThrottlingResponseStub = {
|
|
data: [],
|
|
errors: [
|
|
{
|
|
refId: 'A',
|
|
message:
|
|
'failed to execute log action with subtype: StartQuery: ThrottlingException: ThrottlingException: Rate exceeded',
|
|
status: 500,
|
|
},
|
|
],
|
|
};
|
|
|
|
const startQueryErrorWhenBadSyntaxResponseStub = {
|
|
data: [],
|
|
state: 'Error',
|
|
errors: [
|
|
{
|
|
refId: 'A',
|
|
message:
|
|
'failed to execute log action with subtype: StartQuery: MalformedQueryException: unexpected symbol found bad at line 1 and position 843',
|
|
status: 500,
|
|
},
|
|
],
|
|
};
|
|
|
|
const getQuerySuccessResponseStub = {
|
|
data: [
|
|
{
|
|
name: 'A',
|
|
refId: 'A',
|
|
meta: {
|
|
custom: { Status: 'Complete' },
|
|
typeVersion: [0, 0],
|
|
stats: [
|
|
{ displayName: 'Bytes scanned', value: 1000 },
|
|
{ displayName: 'Records scanned', value: 1000 },
|
|
{ displayName: 'Records matched', value: 1000 },
|
|
],
|
|
},
|
|
fields: [
|
|
{
|
|
name: '@message',
|
|
type: 'string',
|
|
typeInfo: { frame: 'string' },
|
|
config: {},
|
|
values: ['some log'],
|
|
},
|
|
],
|
|
length: 1,
|
|
state: 'Done',
|
|
},
|
|
],
|
|
state: 'Done',
|
|
};
|
|
|
|
const getQueryLoadingResponseStub = {
|
|
data: [
|
|
{
|
|
name: 'A',
|
|
refId: 'A',
|
|
meta: {
|
|
custom: { Status: 'Running' },
|
|
typeVersion: [0, 0],
|
|
stats: [
|
|
{ displayName: 'Bytes scanned', value: 1 },
|
|
{ displayName: 'Records scanned', value: 1 },
|
|
{ displayName: 'Records matched', value: 1 },
|
|
],
|
|
},
|
|
fields: [
|
|
{
|
|
name: '@message',
|
|
type: 'string',
|
|
typeInfo: { frame: 'string' },
|
|
config: {},
|
|
values: ['some log'],
|
|
},
|
|
],
|
|
length: 1,
|
|
state: 'Done',
|
|
},
|
|
],
|
|
state: 'Done',
|
|
};
|
|
|
|
const getQueryErrorResponseStub = {
|
|
data: [],
|
|
errors: [
|
|
{
|
|
refId: 'A',
|
|
message: 'failed to execute log action with subtype: GetQueryResults: AWS is down',
|
|
status: 500,
|
|
},
|
|
],
|
|
state: 'Error',
|
|
};
|
|
|
|
const stopQueryResponseStub = {
|
|
state: 'Done',
|
|
};
|