The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.t...

620 lines
20 KiB

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',
};