CloudWatch: fix log explorer context (#52118)

This commit is contained in:
Isabella Siu 2022-07-19 13:59:30 -04:00 committed by GitHub
parent e1785f4eb4
commit 146c90d59e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 160 additions and 9 deletions

View File

@ -34,8 +34,8 @@ type LogQueryJson struct {
SubType string SubType string
Limit *int64 Limit *int64
Time int64 Time int64
StartTime int64 StartTime *int64
EndTime int64 EndTime *int64
LogGroupName string LogGroupName string
LogGroupNames []string LogGroupNames []string
LogGroupNamePrefix string LogGroupNamePrefix string
@ -168,8 +168,13 @@ func (e *cloudWatchExecutor) handleGetLogEvents(ctx context.Context, logsClient
} }
queryRequest.SetLogStreamName(parameters.LogStreamName) queryRequest.SetLogStreamName(parameters.LogStreamName)
queryRequest.SetStartTime(parameters.StartTime) if parameters.StartTime != nil && *parameters.StartTime != 0 {
queryRequest.SetEndTime(parameters.EndTime) queryRequest.SetStartTime(*parameters.StartTime)
}
if parameters.EndTime != nil && *parameters.EndTime != 0 {
queryRequest.SetEndTime(*parameters.EndTime)
}
logEvents, err := logsClient.GetLogEventsWithContext(ctx, queryRequest) logEvents, err := logsClient.GetLogEventsWithContext(ctx, queryRequest)
if err != nil { if err != nil {

View File

@ -20,6 +20,92 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestQuery_GetLogEvents(t *testing.T) {
origNewCWLogsClient := NewCWLogsClient
t.Cleanup(func() {
NewCWLogsClient = origNewCWLogsClient
})
var cli fakeCWLogsClient
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
return &cli
}
const refID = "A"
testCases := map[string]struct {
query string
expectedInput []*cloudwatchlogs.GetLogEventsInput
}{
"Nil startTime": {
query: `{
"type": "logAction",
"subtype": "GetLogEvents",
"logGroupName": "foo",
"logStreamName": "bar",
"endTime": 1,
"startFromHead": false
}`,
expectedInput: []*cloudwatchlogs.GetLogEventsInput{
{
EndTime: aws.Int64(1),
Limit: aws.Int64(10),
LogGroupName: aws.String("foo"),
LogStreamName: aws.String("bar"),
StartFromHead: aws.Bool(false),
},
},
},
"Nil endTime": {
query: `{
"type": "logAction",
"subtype": "GetLogEvents",
"logGroupName": "foo",
"logStreamName": "bar",
"startTime": 1,
"startFromHead": true
}`,
expectedInput: []*cloudwatchlogs.GetLogEventsInput{
{
StartTime: aws.Int64(1),
Limit: aws.Int64(10),
LogGroupName: aws.String("foo"),
LogStreamName: aws.String("bar"),
StartFromHead: aws.Bool(true),
},
},
},
}
for name, test := range testCases {
t.Run(name, func(t *testing.T) {
cli = fakeCWLogsClient{}
im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return datasourceInfo{}, nil
})
executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
},
Queries: []backend.DataQuery{
{
RefID: refID,
TimeRange: backend.TimeRange{From: time.Unix(0, 0), To: time.Unix(1, 0)},
JSON: json.RawMessage(test.query),
},
},
})
require.NoError(t, err)
require.Len(t, cli.calls.getEventsWithContext, 1)
assert.Equal(t, test.expectedInput, cli.calls.getEventsWithContext)
})
}
}
func TestQuery_DescribeLogGroups(t *testing.T) { func TestQuery_DescribeLogGroups(t *testing.T) {
origNewCWLogsClient := NewCWLogsClient origNewCWLogsClient := NewCWLogsClient
t.Cleanup(func() { t.Cleanup(func() {

View File

@ -30,6 +30,7 @@ type fakeCWLogsClient struct {
type logsQueryCalls struct { type logsQueryCalls struct {
startQueryWithContext []*cloudwatchlogs.StartQueryInput startQueryWithContext []*cloudwatchlogs.StartQueryInput
getEventsWithContext []*cloudwatchlogs.GetLogEventsInput
} }
func (m *fakeCWLogsClient) GetQueryResultsWithContext(ctx context.Context, input *cloudwatchlogs.GetQueryResultsInput, option ...request.Option) (*cloudwatchlogs.GetQueryResultsOutput, error) { func (m *fakeCWLogsClient) GetQueryResultsWithContext(ctx context.Context, input *cloudwatchlogs.GetQueryResultsInput, option ...request.Option) (*cloudwatchlogs.GetQueryResultsOutput, error) {
@ -58,6 +59,14 @@ func (m *fakeCWLogsClient) GetLogGroupFieldsWithContext(ctx context.Context, inp
return &m.logGroupFields, nil return &m.logGroupFields, nil
} }
func (m *fakeCWLogsClient) GetLogEventsWithContext(ctx context.Context, input *cloudwatchlogs.GetLogEventsInput, option ...request.Option) (*cloudwatchlogs.GetLogEventsOutput, error) {
m.calls.getEventsWithContext = append(m.calls.getEventsWithContext, input)
return &cloudwatchlogs.GetLogEventsOutput{
Events: []*cloudwatchlogs.OutputLogEvent{},
}, nil
}
type fakeCWClient struct { type fakeCWClient struct {
cloudwatchiface.CloudWatchAPI cloudwatchiface.CloudWatchAPI
cloudwatch.GetMetricDataOutput cloudwatch.GetMetricDataOutput

View File

@ -1,7 +1,17 @@
import { lastValueFrom, of } from 'rxjs'; import { lastValueFrom, of } from 'rxjs';
import { toArray } from 'rxjs/operators'; import { toArray } from 'rxjs/operators';
import { ArrayVector, DataFrame, dataFrameToJSON, dateTime, Field, MutableDataFrame } from '@grafana/data'; import {
ArrayVector,
DataFrame,
dataFrameToJSON,
dateTime,
Field,
FieldType,
LogLevel,
LogRowModel,
MutableDataFrame,
} from '@grafana/data';
import { setDataSourceSrv } from '@grafana/runtime'; import { setDataSourceSrv } from '@grafana/runtime';
import { import {
@ -16,6 +26,7 @@ import {
regionVariable, regionVariable,
} from './__mocks__/CloudWatchDataSource'; } from './__mocks__/CloudWatchDataSource';
import { validLogsQuery, validMetricsQuery } from './__mocks__/queries'; import { validLogsQuery, validMetricsQuery } from './__mocks__/queries';
import { LOGSTREAM_IDENTIFIER_INTERNAL, LOG_IDENTIFIER_INTERNAL } from './datasource';
import { import {
CloudWatchLogsQueryStatus, CloudWatchLogsQueryStatus,
CloudWatchMetricsQuery, CloudWatchMetricsQuery,
@ -389,6 +400,43 @@ describe('datasource', () => {
}); });
}); });
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', () => { describe('template variable interpolation', () => {
it('interpolates variables correctly', async () => { it('interpolates variables correctly', async () => {
const { datasource, fetchMock } = setupMockedDataSource({ const { datasource, fetchMock } = setupMockedDataSource({

View File

@ -69,8 +69,8 @@ import { CloudWatchVariableSupport } from './variables';
const DS_QUERY_ENDPOINT = '/api/ds/query'; const DS_QUERY_ENDPOINT = '/api/ds/query';
// Constants also defined in tsdb/cloudwatch/cloudwatch.go // Constants also defined in tsdb/cloudwatch/cloudwatch.go
const LOG_IDENTIFIER_INTERNAL = '__log__grafana_internal__'; export const LOG_IDENTIFIER_INTERNAL = '__log__grafana_internal__';
const LOGSTREAM_IDENTIFIER_INTERNAL = '__logstream__grafana_internal__'; export const LOGSTREAM_IDENTIFIER_INTERNAL = '__logstream__grafana_internal__';
const displayAlert = (datasourceName: string, region: string) => const displayAlert = (datasourceName: string, region: string) =>
store.dispatch( store.dispatch(
@ -89,7 +89,7 @@ const displayCustomError = (title: string, message: string) =>
export class CloudWatchDatasource export class CloudWatchDatasource
extends DataSourceWithBackend<CloudWatchQuery, CloudWatchJsonData> extends DataSourceWithBackend<CloudWatchQuery, CloudWatchJsonData>
implements DataSourceWithLogsContextSupport implements DataSourceWithLogsContextSupport<CloudWatchLogsQuery>
{ {
proxyUrl: any; proxyUrl: any;
defaultRegion: any; defaultRegion: any;
@ -488,7 +488,8 @@ export class CloudWatchDatasource
getLogRowContext = async ( getLogRowContext = async (
row: LogRowModel, row: LogRowModel,
{ limit = 10, direction = 'BACKWARD' }: RowContextOptions = {} { limit = 10, direction = 'BACKWARD' }: RowContextOptions = {},
query?: CloudWatchLogsQuery
): Promise<{ data: DataFrame[] }> => { ): Promise<{ data: DataFrame[] }> => {
let logStreamField = null; let logStreamField = null;
let logField = null; let logField = null;
@ -510,6 +511,7 @@ export class CloudWatchDatasource
const requestParams: GetLogEventsRequest = { const requestParams: GetLogEventsRequest = {
limit, limit,
startFromHead: direction !== 'BACKWARD', startFromHead: direction !== 'BACKWARD',
region: query?.region,
logGroupName: parseLogGroupName(logField!.values.get(row.rowIndex)), logGroupName: parseLogGroupName(logField!.values.get(row.rowIndex)),
logStreamName: logStreamField!.values.get(row.rowIndex), logStreamName: logStreamField!.values.get(row.rowIndex),
}; };

View File

@ -199,6 +199,7 @@ export interface GetLogEventsRequest {
* If the value is true, the earliest log events are returned first. If the value is false, the latest log events are returned first. The default value is false. If you are using nextToken in this operation, you must specify true for startFromHead. * If the value is true, the earliest log events are returned first. If the value is false, the latest log events are returned first. The default value is false. If you are using nextToken in this operation, you must specify true for startFromHead.
*/ */
startFromHead?: boolean; startFromHead?: boolean;
region?: string;
} }
export interface GetQueryResultsResponse { export interface GetQueryResultsResponse {