diff --git a/.betterer.results b/.betterer.results index f62f4482efd..c754b1a4c5e 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5765,14 +5765,6 @@ exports[`no gf-form usage`] = { "public/app/plugins/datasource/cloudwatch/components/ConfigEditor/XrayLinkConfig.tsx:5381": [ [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] ], - "public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryEditor.tsx:5381": [ - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] - ], - "public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryField.tsx:5381": [ - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] - ], "public/app/plugins/datasource/cloudwatch/components/shared/LogGroups/LegacyLogGroupNamesSelection.tsx:5381": [ [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] diff --git a/docs/sources/datasources/aws-cloudwatch/query-editor/index.md b/docs/sources/datasources/aws-cloudwatch/query-editor/index.md index 0acd6b7e696..fe44e0d7542 100644 --- a/docs/sources/datasources/aws-cloudwatch/query-editor/index.md +++ b/docs/sources/datasources/aws-cloudwatch/query-editor/index.md @@ -226,17 +226,40 @@ The label field allows you to override the default name of the metric legend usi ## Query CloudWatch Logs The logs query editor helps you write CloudWatch Logs Query Language queries across defined regions and log groups. +It supports querying Cloudwatch logs with Logs Insights Query Language, OpenSearch PPL and OpenSearch SQL. ### Create a CloudWatch Logs query +1. Select the query language you would like to use in the Query Language dropdown. 1. Select the region and up to 20 log groups to query. -1. Use the main input area to write your query in [CloudWatch Logs Query Language](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CWL_QuerySyntax.html). + + {{< admonition type="note" >}} + Region and log groups are mandatory fields when querying with Logs Insights QL and OpenSearch PPL. Log group selection is not necessary when querying with OpenSearch SQL. However, selecting log groups simplifies writing logs queries by populating syntax suggestions with discovered log group fields. + {{< /admonition >}} + +1. Use the main input area to write your logs query. AWS Cloudwatch only supports a subset of OpenSearch SQL and PPL commands. To find out more about the syntax supported, consult [Amazon CloudWatch Logs documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CWL_AnalyzeLogData_Languages.html) + + #### Querying Log groups with OpenSearch SQL + + When querying log groups with OpenSearch SQL, the log group identifier or ARN _must_ be explicitly stated in the `FROM` clause: + + ```sql + SELECT window.start, COUNT(*) AS exceptionCount + FROM `log_group` + WHERE `@message` LIKE '%Exception%' + ``` + + or, when querying multiple log groups: + + ```sql + SELECT window.start, COUNT(*) AS exceptionCount + FROM `logGroups( logGroupIdentifier: ['LogGroup1', 'LogGroup2'])` + WHERE `@message` LIKE '%Exception%' + ``` You can also write queries returning time series data by using the [`stats` command](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CWL_Insights-Visualizing-Log-Data.html). When making `stats` queries in [Explore](ref:explore), make sure you are in Metrics Explore mode. -{{< figure src="/static/img/docs/v70/explore-mode-switcher.png" max-width="500px" class="docs-image--right" caption="Explore mode switcher" >}} - ## Cross-account observability The CloudWatch plugin allows monitoring and troubleshooting applications that span multiple accounts within a region. Using cross-account observability, you can seamlessly search, visualize, and analyze metrics and logs without worrying about account boundaries. diff --git a/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts index 007eb2a1f22..9c1ad367642 100644 --- a/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts @@ -218,6 +218,12 @@ export interface QueryEditorArrayExpression { export type QueryEditorExpression = (QueryEditorArrayExpression | QueryEditorPropertyExpression | QueryEditorGroupByExpression | QueryEditorFunctionExpression | QueryEditorFunctionParameterExpression | QueryEditorOperatorExpression); +export enum LogsQueryLanguage { + CWLI = 'CWLI', + PPL = 'PPL', + SQL = 'SQL', +} + /** * Shape of a CloudWatch Logs query */ @@ -235,6 +241,10 @@ export interface CloudWatchLogsQuery extends common.DataQuery { * Log groups to query */ logGroups?: Array; + /** + * Language used for querying logs, can be CWLI, SQL, or PPL. If empty, the default language is CWLI. + */ + queryLanguage?: LogsQueryLanguage; /** * Whether a query is a Metrics, Logs, or Annotations query */ diff --git a/pkg/tsdb/cloudwatch/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/cloudwatch/kinds/dataquery/types_dataquery_gen.go index 93a14021487..2b9acf11dec 100644 --- a/pkg/tsdb/cloudwatch/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/cloudwatch/kinds/dataquery/types_dataquery_gen.go @@ -16,6 +16,13 @@ const ( CloudWatchQueryModeMetrics CloudWatchQueryMode = "Metrics" ) +// Defines values for LogsQueryLanguage. +const ( + LogsQueryLanguageCWLI LogsQueryLanguage = "CWLI" + LogsQueryLanguagePPL LogsQueryLanguage = "PPL" + LogsQueryLanguageSQL LogsQueryLanguage = "SQL" +) + // Defines values for MetricEditorMode. const ( MetricEditorModeN0 MetricEditorMode = 0 @@ -194,8 +201,9 @@ type CloudWatchLogsQuery struct { LogGroupNames []string `json:"logGroupNames,omitempty"` // Log groups to query - LogGroups []LogGroup `json:"logGroups,omitempty"` - QueryMode *CloudWatchQueryMode `json:"queryMode,omitempty"` + LogGroups []LogGroup `json:"logGroups,omitempty"` + QueryLanguage *LogsQueryLanguage `json:"queryLanguage,omitempty"` + QueryMode *CloudWatchQueryMode `json:"queryMode,omitempty"` // Specify the query flavor // TODO make this required and give it a default @@ -325,6 +333,9 @@ type LogGroup struct { Name string `json:"name"` } +// LogsQueryLanguage defines model for LogsQueryLanguage. +type LogsQueryLanguage string + // MetricEditorMode defines model for MetricEditorMode. type MetricEditorMode int diff --git a/pkg/tsdb/cloudwatch/log_actions.go b/pkg/tsdb/cloudwatch/log_actions.go index 28616531d5b..2aa6d907095 100644 --- a/pkg/tsdb/cloudwatch/log_actions.go +++ b/pkg/tsdb/cloudwatch/log_actions.go @@ -12,6 +12,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/service/cloudwatchlogs" "github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface" "github.com/grafana/grafana-plugin-sdk-go/backend" @@ -20,6 +21,7 @@ import ( "golang.org/x/sync/errgroup" "github.com/grafana/grafana/pkg/tsdb/cloudwatch/features" + "github.com/grafana/grafana/pkg/tsdb/cloudwatch/kinds/dataquery" "github.com/grafana/grafana/pkg/tsdb/cloudwatch/models" ) @@ -42,6 +44,45 @@ func (e *AWSError) Error() string { return fmt.Sprintf("CloudWatch error: %s: %s", e.Code, e.Message) } +// StartQueryInputWithLanguage copies the StartQueryInput struct from aws-sdk-go@v1.55.5 +// (https://github.com/aws/aws-sdk-go/blob/7112c0a0c2d01713a9db2d57f0e5722225baf5b5/service/cloudwatchlogs/api.go#L19541) +// to add support for the new QueryLanguage parameter, which is unlikely to be backported +// since v1 of the aws-sdk-go is in maintenance mode. We've removed the comments for +// clarity. +type StartQueryInputWithLanguage struct { + _ struct{} `type:"structure"` + + EndTime *int64 `locationName:"endTime" type:"long" required:"true"` + Limit *int64 `locationName:"limit" min:"1" type:"integer"` + LogGroupIdentifiers []*string `locationName:"logGroupIdentifiers" type:"list"` + LogGroupName *string `locationName:"logGroupName" min:"1" type:"string"` + LogGroupNames []*string `locationName:"logGroupNames" type:"list"` + QueryString *string `locationName:"queryString" type:"string" required:"true"` + // QueryLanguage is the only change here from the original code. + QueryLanguage *string `locationName:"queryLanguage" type:"string"` + StartTime *int64 `locationName:"startTime" type:"long" required:"true"` +} +type WithQueryLanguageFunc func(language *dataquery.LogsQueryLanguage) func(*request.Request) + +// WithQueryLanguage assigns the function to a variable in order to mock it in log_actions_test.go +var WithQueryLanguage WithQueryLanguageFunc = withQueryLanguage + +func withQueryLanguage(language *dataquery.LogsQueryLanguage) func(request *request.Request) { + return func(request *request.Request) { + sqi := request.Params.(*cloudwatchlogs.StartQueryInput) + request.Params = &StartQueryInputWithLanguage{ + EndTime: sqi.EndTime, + Limit: sqi.Limit, + LogGroupIdentifiers: sqi.LogGroupIdentifiers, + LogGroupName: sqi.LogGroupName, + LogGroupNames: sqi.LogGroupNames, + QueryString: sqi.QueryString, + QueryLanguage: (*string)(language), + StartTime: sqi.StartTime, + } + } +} + func (e *cloudWatchExecutor) executeLogActions(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { resp := backend.NewQueryDataResponse() @@ -191,13 +232,21 @@ func (e *cloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient c if !startTime.Before(endTime) { return nil, errorsource.DownstreamError(fmt.Errorf("invalid time range: start time must be before end time"), false) } + if logsQuery.QueryLanguage == nil { + cwli := dataquery.LogsQueryLanguageCWLI + logsQuery.QueryLanguage = &cwli + } + finalQueryString := logsQuery.QueryString + // Only for CWLI queries // The fields @log and @logStream are always included in the results of a user's query // so that a row's context can be retrieved later if necessary. // The usage of ltrim around the @log/@logStream fields is a necessary workaround, as without it, // CloudWatch wouldn't consider a query using a non-alised @log/@logStream valid. - modifiedQueryString := "fields @timestamp,ltrim(@log) as " + logIdentifierInternal + ",ltrim(@logStream) as " + - logStreamIdentifierInternal + "|" + logsQuery.QueryString + if *logsQuery.QueryLanguage == dataquery.LogsQueryLanguageCWLI { + finalQueryString = "fields @timestamp,ltrim(@log) as " + logIdentifierInternal + ",ltrim(@logStream) as " + + logStreamIdentifierInternal + "|" + logsQuery.QueryString + } startQueryInput := &cloudwatchlogs.StartQueryInput{ StartTime: aws.Int64(startTime.Unix()), @@ -207,20 +256,23 @@ func (e *cloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient c // and also a little bit more but as CW logs accept only seconds as integers there is not much to do about // that. EndTime: aws.Int64(int64(math.Ceil(float64(endTime.UnixNano()) / 1e9))), - QueryString: aws.String(modifiedQueryString), + QueryString: aws.String(finalQueryString), } - if len(logsQuery.LogGroups) > 0 && features.IsEnabled(ctx, features.FlagCloudWatchCrossAccountQuerying) { - var logGroupIdentifiers []string - for _, lg := range logsQuery.LogGroups { - arn := lg.Arn - // due to a bug in the startQuery api, we remove * from the arn, otherwise it throws an error - logGroupIdentifiers = append(logGroupIdentifiers, strings.TrimSuffix(arn, "*")) + // log group identifiers can be left out if the query is an SQL query + if *logsQuery.QueryLanguage != dataquery.LogsQueryLanguageSQL { + if len(logsQuery.LogGroups) > 0 && features.IsEnabled(ctx, features.FlagCloudWatchCrossAccountQuerying) { + var logGroupIdentifiers []string + for _, lg := range logsQuery.LogGroups { + arn := lg.Arn + // due to a bug in the startQuery api, we remove * from the arn, otherwise it throws an error + logGroupIdentifiers = append(logGroupIdentifiers, strings.TrimSuffix(arn, "*")) + } + startQueryInput.LogGroupIdentifiers = aws.StringSlice(logGroupIdentifiers) + } else { + // even though log group names are being phased out, we still need to support them for backwards compatibility and alert queries + startQueryInput.LogGroupNames = aws.StringSlice(logsQuery.LogGroupNames) } - startQueryInput.LogGroupIdentifiers = aws.StringSlice(logGroupIdentifiers) - } else { - // even though log group names are being phased out, we still need to support them for backwards compatibility and alert queries - startQueryInput.LogGroupNames = aws.StringSlice(logsQuery.LogGroupNames) } if logsQuery.Limit != nil { @@ -228,7 +280,7 @@ func (e *cloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient c } e.logger.FromContext(ctx).Debug("Calling startquery with context with input", "input", startQueryInput) - resp, err := logsClient.StartQueryWithContext(ctx, startQueryInput) + resp, err := logsClient.StartQueryWithContext(ctx, startQueryInput, WithQueryLanguage(logsQuery.QueryLanguage)) if err != nil { var awsErr awserr.Error if errors.As(err, &awsErr) && awsErr.Code() == "LimitExceededException" { diff --git a/pkg/tsdb/cloudwatch/log_actions_test.go b/pkg/tsdb/cloudwatch/log_actions_test.go index 201f282c445..ae06b447820 100644 --- a/pkg/tsdb/cloudwatch/log_actions_test.go +++ b/pkg/tsdb/cloudwatch/log_actions_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/cloudwatchlogs" "github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface" @@ -17,6 +18,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/tsdb/cloudwatch/features" + "github.com/grafana/grafana/pkg/tsdb/cloudwatch/kinds/dataquery" "github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks" "github.com/grafana/grafana/pkg/tsdb/cloudwatch/models" "github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils" @@ -309,6 +311,26 @@ func TestQuery_StartQuery(t *testing.T) { }) } +type withQueryLanguageMock struct { + capturedLanguage *dataquery.LogsQueryLanguage + mockWithQueryLanguage func(language *dataquery.LogsQueryLanguage) func(request *request.Request) +} + +func newWithQueryLanguageMock() *withQueryLanguageMock { + mock := &withQueryLanguageMock{ + capturedLanguage: new(dataquery.LogsQueryLanguage), + } + + mock.mockWithQueryLanguage = func(language *dataquery.LogsQueryLanguage) func(request *request.Request) { + *mock.capturedLanguage = *language + return func(req *request.Request) { + + } + } + + return mock +} + func Test_executeStartQuery(t *testing.T) { origNewCWLogsClient := NewCWLogsClient t.Cleanup(func() { @@ -321,40 +343,135 @@ func Test_executeStartQuery(t *testing.T) { return &cli } - t.Run("successfully parses information from JSON to StartQueryWithContext", func(t *testing.T) { - cli = fakeCWLogsClient{} - im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil - }) - executor := newExecutor(im, log.NewNullLogger()) - - _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ - PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, - Queries: []backend.DataQuery{ - { + t.Run("successfully parses information from JSON to StartQueryWithContext for language", func(t *testing.T) { + testCases := map[string]struct { + queries []backend.DataQuery + expectedOutput []*cloudwatchlogs.StartQueryInput + queryLanguage dataquery.LogsQueryLanguage + }{ + "not defined": { + queries: []backend.DataQuery{ + { + RefID: "A", + TimeRange: backend.TimeRange{From: time.Unix(0, 0), To: time.Unix(1, 0)}, + JSON: json.RawMessage(`{ + "type": "logAction", + "subtype": "StartQuery", + "limit": 12, + "queryString":"fields @message", + "logGroupNames":["some name","another name"] + }`), + }, + }, + expectedOutput: []*cloudwatchlogs.StartQueryInput{{ + StartTime: aws.Int64(0), + EndTime: aws.Int64(1), + Limit: aws.Int64(12), + QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"), + LogGroupNames: []*string{aws.String("some name"), aws.String("another name")}, + }}, + queryLanguage: dataquery.LogsQueryLanguageCWLI, + }, + "CWLI": { + queries: []backend.DataQuery{{ RefID: "A", TimeRange: backend.TimeRange{From: time.Unix(0, 0), To: time.Unix(1, 0)}, JSON: json.RawMessage(`{ "type": "logAction", "subtype": "StartQuery", "limit": 12, + "queryLanguage": "CWLI", "queryString":"fields @message", "logGroupNames":["some name","another name"] }`), + }}, + expectedOutput: []*cloudwatchlogs.StartQueryInput{ + { + StartTime: aws.Int64(0), + EndTime: aws.Int64(1), + Limit: aws.Int64(12), + QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"), + LogGroupNames: []*string{aws.String("some name"), aws.String("another name")}, + }, }, + queryLanguage: dataquery.LogsQueryLanguageCWLI, }, - }) + "PPL": { + queries: []backend.DataQuery{{ + RefID: "A", + TimeRange: backend.TimeRange{From: time.Unix(0, 0), To: time.Unix(1, 0)}, + JSON: json.RawMessage(`{ + "type": "logAction", + "subtype": "StartQuery", + "limit": 12, + "queryLanguage": "PPL", + "queryString":"source logs | fields @message", + "logGroupNames":["some name","another name"] + }`), + }}, + expectedOutput: []*cloudwatchlogs.StartQueryInput{ + { + StartTime: aws.Int64(0), + EndTime: aws.Int64(1), + Limit: aws.Int64(12), + QueryString: aws.String("source logs | fields @message"), + LogGroupNames: []*string{aws.String("some name"), aws.String("another name")}, + }, + }, + queryLanguage: dataquery.LogsQueryLanguagePPL, + }, + "SQL": { + queries: []backend.DataQuery{ + { + RefID: "A", + TimeRange: backend.TimeRange{From: time.Unix(0, 0), To: time.Unix(1, 0)}, + JSON: json.RawMessage(`{ + "type": "logAction", + "subtype": "StartQuery", + "limit": 12, + "queryLanguage": "SQL", + "queryString":"SELECT * FROM logs", + "logGroupNames":["some name","another name"] + }`), + }, + }, + expectedOutput: []*cloudwatchlogs.StartQueryInput{ + { + StartTime: aws.Int64(0), + EndTime: aws.Int64(1), + Limit: aws.Int64(12), + QueryString: aws.String("SELECT * FROM logs"), + LogGroupNames: nil, + }, + }, + queryLanguage: dataquery.LogsQueryLanguageSQL, + }, + } + for name, test := range testCases { + t.Run(name, func(t *testing.T) { + cli = fakeCWLogsClient{} + im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil + }) + executor := newExecutor(im, log.NewNullLogger()) - assert.NoError(t, err) - assert.Equal(t, []*cloudwatchlogs.StartQueryInput{ - { - StartTime: aws.Int64(0), - EndTime: aws.Int64(1), - Limit: aws.Int64(12), - QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"), - LogGroupNames: []*string{aws.String("some name"), aws.String("another name")}, - }, - }, cli.calls.startQueryWithContext) + languageMock := newWithQueryLanguageMock() + originalWithQueryLanguage := WithQueryLanguage + WithQueryLanguage = languageMock.mockWithQueryLanguage + defer func() { + WithQueryLanguage = originalWithQueryLanguage + }() + + _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ + PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, + Queries: test.queries, + }) + + assert.NoError(t, err) + assert.Equal(t, test.expectedOutput, cli.calls.startQueryWithContext) + assert.Equal(t, &test.queryLanguage, languageMock.capturedLanguage) + }) + } }) t.Run("does not populate StartQueryInput.limit when no limit provided", func(t *testing.T) { @@ -400,6 +517,7 @@ func Test_executeStartQuery(t *testing.T) { "type": "logAction", "subtype": "StartQuery", "limit": 12, + "queryLanguage": "CWLI", "queryString":"fields @message", "logGroups":[{"arn": "fakeARN"}] }`), diff --git a/pkg/tsdb/cloudwatch/log_query.go b/pkg/tsdb/cloudwatch/log_query.go index 575421f23c2..50e77bda75f 100644 --- a/pkg/tsdb/cloudwatch/log_query.go +++ b/pkg/tsdb/cloudwatch/log_query.go @@ -54,8 +54,10 @@ func logsResultsToDataframes(response *cloudwatchlogs.GetQueryResultsOutput) (*d if _, exists := fieldValues[*resultField.Field]; !exists { fieldNames = append(fieldNames, *resultField.Field) - // Check if it's a time field - if _, err := time.Parse(cloudWatchTSFormat, *resultField.Value); err == nil { + // Check if it's a cloudWatchTSFormat field or one of the known timestamp fields: + // https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CWL_AnalyzeLogData-discoverable-fields.html + // which can be in a millisecond format as well as cloudWatchTSFormat string format + if _, err := time.Parse(cloudWatchTSFormat, *resultField.Value); err == nil || isTimestampField(*resultField.Field) { fieldValues[*resultField.Field] = make([]*time.Time, rowCount) } else if _, err := strconv.ParseFloat(*resultField.Value, 64); err == nil { fieldValues[*resultField.Field] = make([]*float64, rowCount) @@ -67,9 +69,13 @@ func logsResultsToDataframes(response *cloudwatchlogs.GetQueryResultsOutput) (*d if timeField, ok := fieldValues[*resultField.Field].([]*time.Time); ok { parsedTime, err := time.Parse(cloudWatchTSFormat, *resultField.Value) if err != nil { - return nil, err + unixTimeMs, err := strconv.ParseInt(*resultField.Value, 10, 64) + if err == nil { + parsedTime = time.Unix(unixTimeMs/1000, (unixTimeMs%1000)*int64(time.Millisecond)) + } else { + return nil, err + } } - timeField[i] = &parsedTime } else if numericField, ok := fieldValues[*resultField.Field].([]*float64); ok { parsedFloat, err := strconv.ParseFloat(*resultField.Value, 64) @@ -313,3 +319,7 @@ func numericFieldToStringField(field *data.Field) (*data.Field, error) { return newField, nil } + +func isTimestampField(fieldName string) bool { + return fieldName == "@timestamp" || fieldName == "@ingestionTime" +} diff --git a/pkg/tsdb/cloudwatch/log_query_test.go b/pkg/tsdb/cloudwatch/log_query_test.go index 1a72ded1e09..bfec8583f90 100644 --- a/pkg/tsdb/cloudwatch/log_query_test.go +++ b/pkg/tsdb/cloudwatch/log_query_test.go @@ -1,6 +1,7 @@ package cloudwatch import ( + "fmt" "testing" "time" @@ -277,6 +278,72 @@ func TestLogsResultsToDataframes_MixedTypes_NumericValuesMixedWithStringFallBack assert.ElementsMatch(t, expectedDataframe.Fields, dataframes.Fields) } +func TestLogsResultsToDataframes_With_Millisecond_Timestamps(t *testing.T) { + stringTimeField := "2020-03-02 15:04:05.000" + timestampField := int64(1732749534876) + ingestionTimeField := int64(1732790372916) + + dataframes, err := logsResultsToDataframes(&cloudwatchlogs.GetQueryResultsOutput{ + Results: [][]*cloudwatchlogs.ResultField{ + { + &cloudwatchlogs.ResultField{ + Field: aws.String("@timestamp"), + Value: aws.String(fmt.Sprintf("%d", timestampField)), + }, + &cloudwatchlogs.ResultField{ + Field: aws.String("@ingestionTime"), + Value: aws.String(fmt.Sprintf("%d", ingestionTimeField)), + }, + &cloudwatchlogs.ResultField{ + Field: aws.String("stringTimeField"), + Value: aws.String(stringTimeField), + }, + &cloudwatchlogs.ResultField{ + Field: aws.String("message"), + Value: aws.String("log message"), + }, + }, + }, + Status: aws.String("ok"), + }) + require.NoError(t, err) + + timeStampResult := time.Unix(timestampField/1000, (timestampField%1000)*int64(time.Millisecond)) + ingestionTimeResult := time.Unix(ingestionTimeField/1000, (ingestionTimeField%1000)*int64(time.Millisecond)) + stringTimeFieldResult, err := time.Parse(cloudWatchTSFormat, stringTimeField) + require.NoError(t, err) + + expectedDataframe := &data.Frame{ + Name: "CloudWatchLogsResponse", + Fields: []*data.Field{ + data.NewField("@timestamp", nil, []*time.Time{ + &timeStampResult, + }), + data.NewField("@ingestionTime", nil, []*time.Time{ + &ingestionTimeResult, + }), + data.NewField("stringTimeField", nil, []*time.Time{ + &stringTimeFieldResult, + }), + data.NewField("message", nil, []*string{ + aws.String("log message"), + }), + }, + RefID: "", + Meta: &data.FrameMeta{ + Custom: map[string]any{ + "Status": "ok", + }, + }, + } + expectedDataframe.Fields[0].SetConfig(&data.FieldConfig{DisplayName: "Time"}) + + assert.Equal(t, expectedDataframe.Name, dataframes.Name) + assert.Equal(t, expectedDataframe.RefID, dataframes.RefID) + assert.Equal(t, expectedDataframe.Meta, dataframes.Meta) + assert.ElementsMatch(t, expectedDataframe.Fields, dataframes.Fields) +} + func TestGroupKeyGeneration(t *testing.T) { logField := data.NewField("@log", data.Labels{}, []*string{ aws.String("fakelog-a"), diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts index e698004926a..2e3d5a4401e 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts @@ -127,6 +127,7 @@ export function setupMockedDataSource({ datasource.resources.getMetrics = jest.fn().mockResolvedValue([]); datasource.resources.getAccounts = jest.fn().mockResolvedValue([]); datasource.resources.getLogGroups = jest.fn().mockResolvedValue([]); + datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(false); const fetchMock = jest.fn().mockReturnValue(of({})); setBackendSrv({ ...getBackendSrv(), diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-logs-sql-test-data/commentOnlyQuery.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-logs-sql-test-data/commentOnlyQuery.ts new file mode 100644 index 00000000000..b69f94efdb4 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-logs-sql-test-data/commentOnlyQuery.ts @@ -0,0 +1,8 @@ +import { monacoTypes } from '@grafana/ui'; + +export const commentOnlyQuery = { + query: `-- comment ending with whitespace `, + tokens: [ + [{ offset: 0, type: 'comment.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }], + ] as monacoTypes.Token[][], +}; diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-logs-sql-test-data/multiLineFullQuery.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-logs-sql-test-data/multiLineFullQuery.ts new file mode 100644 index 00000000000..907a01985af --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-logs-sql-test-data/multiLineFullQuery.ts @@ -0,0 +1,134 @@ +import { monacoTypes } from '@grafana/ui'; + +export const multiLineFullQuery = { + query: `SELECT + length(\`@message\`) as msg_length, + COUNT(*) as count, + MIN(\`@message\`) as sample_message +FROM \`LogGroupA\` +WHERE \`startTime\` >= date_sub(current_timestamp(), 1) +GROUP BY length(\`@message\`) +HAVING count > 10 +ORDER BY msg_length DESC +/* +a +comment +over +multiple +lines +*/ +LIMIT 10`, + tokens: [ + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 6, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + [ + { offset: 0, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 2, type: 'predefined.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 8, type: 'delimiter.parenthesis.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 9, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 19, type: 'delimiter.parenthesis.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 20, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 21, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 23, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 24, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 34, type: 'delimiter.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 35, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + [ + { offset: 0, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 2, type: 'predefined.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 7, type: 'delimiter.parenthesis.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 8, type: 'operator.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 9, type: 'delimiter.parenthesis.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 10, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 11, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 13, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 14, type: 'predefined.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 19, type: 'delimiter.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 20, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + [ + { offset: 0, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 2, type: 'predefined.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 5, type: 'delimiter.parenthesis.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 6, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 16, type: 'delimiter.parenthesis.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 17, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 18, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 20, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 21, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 35, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 4, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 5, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 16, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 5, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 6, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 17, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 18, type: 'operator.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 20, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 21, type: 'predefined.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 29, type: 'delimiter.parenthesis.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 30, type: 'predefined.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 47, type: 'delimiter.parenthesis.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 49, type: 'delimiter.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 50, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 51, type: 'number.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 52, type: 'delimiter.parenthesis.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 53, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 5, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 6, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 8, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 9, type: 'predefined.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 15, type: 'delimiter.parenthesis.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 16, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 26, type: 'delimiter.parenthesis.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 27, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 6, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 7, type: 'predefined.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 12, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 13, type: 'operator.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 14, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 15, type: 'number.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 17, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 5, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 6, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 8, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 9, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 19, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 20, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 24, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + [ + { offset: 0, type: 'comment.quote.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 2, type: 'comment.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + [{ offset: 0, type: 'comment.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }], + [{ offset: 0, type: 'comment.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }], + [{ offset: 0, type: 'comment.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }], + [{ offset: 0, type: 'comment.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }], + [{ offset: 0, type: 'comment.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }], + [{ offset: 0, type: 'comment.quote.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }], + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 5, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 6, type: 'number.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + ] as monacoTypes.Token[][], +}; diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-logs-sql-test-data/multiLineFullQueryWithCaseClause.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-logs-sql-test-data/multiLineFullQueryWithCaseClause.ts new file mode 100644 index 00000000000..cb615d75227 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-logs-sql-test-data/multiLineFullQueryWithCaseClause.ts @@ -0,0 +1,148 @@ +import { monacoTypes } from '@grafana/ui'; + +export const multiLineFullQueryWithCaseClause = { + query: `SELECT id, +CASE id +WHEN 100 THEN 'big' +WHEN id > 300 THEN 'biggest' +ELSE 'small' +END as size +FROM LogGroupA +WHERE +CASE 1 = 1 +WHEN 100 THEN 'long' +WHEN 200 THEN 'longest' +ELSE 'short' +END = 'short' +ORDER BY message_count DESC`, + tokens: [ + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 6, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 7, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 9, type: 'delimiter.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 10, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 4, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 5, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 7, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 4, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 5, type: 'number.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 8, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 9, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 13, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 14, type: 'string.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 15, type: 'string.escape.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 18, type: 'string.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 19, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 4, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 5, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 7, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 8, type: 'operator.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 9, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 10, type: 'number.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 13, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 14, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 18, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 19, type: 'string.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 20, type: 'string.escape.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 27, type: 'string.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 28, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 4, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 5, type: 'string.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 6, type: 'string.escape.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 11, type: 'string.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 12, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 3, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 4, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 6, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 7, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 11, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 4, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 5, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 14, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 5, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 4, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 5, type: 'number.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 6, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 7, type: 'operator.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 8, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 9, type: 'number.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 10, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 4, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 5, type: 'number.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 8, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 9, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 13, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 14, type: 'string.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 15, type: 'string.escape.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 19, type: 'string.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 20, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 4, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 5, type: 'number.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 8, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 9, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 13, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 14, type: 'string.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 15, type: 'string.escape.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 22, type: 'string.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 23, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 4, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 5, type: 'string.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 6, type: 'string.escape.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 11, type: 'string.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 12, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 3, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 4, type: 'operator.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 5, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 6, type: 'string.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 7, type: 'string.escape.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 12, type: 'string.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 13, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 5, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 6, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 8, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 9, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 22, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 23, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + ] as monacoTypes.Token[][], +}; diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-logs-sql-test-data/partialQueryWithFunction.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-logs-sql-test-data/partialQueryWithFunction.ts new file mode 100644 index 00000000000..8d615047a0f --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-logs-sql-test-data/partialQueryWithFunction.ts @@ -0,0 +1,13 @@ +import { monacoTypes } from '@grafana/ui'; + +export const partialQueryWithFunction = { + query: `SELECT length()`, + tokens: [ + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 6, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 7, type: 'predefined.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 13, type: 'delimiter.parenthesis.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + ] as monacoTypes.Token[][], +}; diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-logs-sql-test-data/partialQueryWithSubquery.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-logs-sql-test-data/partialQueryWithSubquery.ts new file mode 100644 index 00000000000..af21678ddb6 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-logs-sql-test-data/partialQueryWithSubquery.ts @@ -0,0 +1,24 @@ +import { monacoTypes } from '@grafana/ui'; + +export const partialQueryWithSubquery = { + query: 'SELECT requestId FROM LogGroupA WHERE requestId IN ()', + tokens: [ + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 6, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 7, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 16, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 17, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 21, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 22, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 31, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 32, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 37, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 38, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 47, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 48, type: 'operator.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 50, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 51, type: 'delimiter.parenthesis.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + ] as monacoTypes.Token[][], +}; diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-logs-sql-test-data/singleLineFullQuery.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-logs-sql-test-data/singleLineFullQuery.ts new file mode 100644 index 00000000000..32a519da669 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-logs-sql-test-data/singleLineFullQuery.ts @@ -0,0 +1,85 @@ +import { monacoTypes } from '@grafana/ui'; +export const singleLineFullQuery = { + query: + "SELECT A.transaction_id AS txn_id_a, A.user_id, A.instance_id AS inst_id_a, B.instance_id AS inst_id_b FROM `LogGroupA` AS A INNER JOIN `LogGroupB` AS B ON A.userId = B.userId WHERE B.Status = 'ERROR' -- comment at the end", + tokens: [ + [ + { offset: 0, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 6, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 7, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 8, type: 'delimiter.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 9, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 23, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 24, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 26, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 27, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 35, type: 'delimiter.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 36, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 37, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 38, type: 'delimiter.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 39, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 46, type: 'delimiter.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 47, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 48, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 49, type: 'delimiter.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 50, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 61, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 62, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 64, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 65, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 74, type: 'delimiter.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 75, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 76, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 77, type: 'delimiter.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 78, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 89, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 90, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 92, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 93, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 102, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 103, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 107, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 108, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 119, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 120, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 122, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 123, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 124, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 125, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 130, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 131, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 135, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 136, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 147, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 148, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 150, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 151, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 152, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 153, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 155, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 156, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 157, type: 'delimiter.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 158, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 164, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 165, type: 'operator.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 166, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 167, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 168, type: 'delimiter.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 169, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 175, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 176, type: 'keyword.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 181, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 182, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 183, type: 'delimiter.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 184, type: 'identifier.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 190, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 191, type: 'operator.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 192, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 193, type: 'string.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 194, type: 'string.escape.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 199, type: 'string.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 200, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + { offset: 201, type: 'comment.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }, + ], + ] as monacoTypes.Token[][], +}; diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-logs-sql-test-data/whitespaceQuery.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-logs-sql-test-data/whitespaceQuery.ts new file mode 100644 index 00000000000..d4394bd0098 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-logs-sql-test-data/whitespaceQuery.ts @@ -0,0 +1,8 @@ +import { monacoTypes } from '@grafana/ui'; + +export const whitespaceQuery = { + query: ' ', + tokens: [ + [{ offset: 0, type: 'white.cloudwatch-logs-sql', language: 'cloudwatch-logs-sql' }], + ] as monacoTypes.Token[][], +}; diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-ppl-test-data/multilineQueries.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-ppl-test-data/multilineQueries.ts new file mode 100644 index 00000000000..4f5ff1372b1 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-ppl-test-data/multilineQueries.ts @@ -0,0 +1,294 @@ +import { monacoTypes } from '@grafana/ui'; + +import { CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID } from '../../language/cloudwatch-ppl/language'; +import { PPLTokenTypes } from '../../language/cloudwatch-ppl/tokenTypes'; + +export const multiLineNewCommandQuery = { + query: `fields ingestionTime, level + | `, + tokens: [ + [ + { offset: 0, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, + { offset: 7, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, + { offset: 8, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, + { offset: 21, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, + { offset: 22, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, + { offset: 23, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, + ], + [ + { offset: 0, type: PPLTokenTypes.Pipe, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, + { offset: 1, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, + ], + ] as monacoTypes.Token[][], +}; +export const multiLineFullQuery = { + query: `fields ingestionTime, level +| WHERE like(\`@message\`, "%Exception%") AND not like(server, "test") +| FIELDS + \`@ingestionTime\`, timestamp, table +| stats avg(timestamp) as exceptionCount by span(\`@timestamp\`, 1h) +| EVENTSTATS avg(timestamp) as exceptionCount by span(\`@timestamp\`, 1h) +| sort - DisconnectReason, + timestamp, server +| sort - AUTO(DisconnectReason) +| DEDUP 5 timestamp, ingestionTime, \`@query\` keepempty = true consecutive = false +| DEDUP timestamp, ingestionTime, \`@query\` +| TOP 100 ingestionTime, timestamp by server, region +| HEAD 10 from 1500 +| RARE server, ingestionTime by region, user +| EVAL total_revenue = price * quantity, discount_price = price >= 0.9, revenue_category = IF(price > 100, 'high', 'low') +| parse email ".+@(?.+)"'`, + tokens: [ + [ + { offset: 0, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, + { offset: 6, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, + { offset: 7, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, + { offset: 20, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, + { offset: 21, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, + { offset: 22, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, + ], + [ + { offset: 0, type: PPLTokenTypes.Pipe, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "|" + { offset: 1, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 2, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "WHERE" + { offset: 7, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 8, type: PPLTokenTypes.Function, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "like" + { offset: 12, type: PPLTokenTypes.Parenthesis, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "(" + { offset: 13, type: PPLTokenTypes.Backtick, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "`@message`" + { offset: 23, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 24, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 25, type: PPLTokenTypes.String, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "%Exception" + { offset: 38, type: PPLTokenTypes.Parenthesis, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // ")" + { offset: 39, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 40, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "and" + { offset: 43, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 44, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "not" + { offset: 47, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 48, type: PPLTokenTypes.Function, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "like" + ], + [ + { offset: 0, type: PPLTokenTypes.Pipe, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "|" + { offset: 1, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 2, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "fields" + { offset: 8, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 9, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "+" + { offset: 10, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 11, type: PPLTokenTypes.Backtick, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "@ingestionTime" + { offset: 27, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 28, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " ", + { offset: 29, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "timestamp", + { offset: 38, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 39, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 40, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "table" + ], + [ + { offset: 0, type: PPLTokenTypes.Pipe, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "|" + { offset: 1, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 2, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "stats" + { offset: 7, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 8, type: PPLTokenTypes.Function, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "avg" + { offset: 11, type: PPLTokenTypes.Parenthesis, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "(" + { offset: 12, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "timestamp", + { offset: 21, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // ")", + { offset: 22, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 23, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "as" + { offset: 25, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 26, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "exceptionCount" + { offset: 40, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 41, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "by" + { offset: 43, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 44, type: PPLTokenTypes.Function, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "span" + { offset: 48, type: PPLTokenTypes.Parenthesis, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "(" + { offset: 49, type: PPLTokenTypes.Backtick, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "@timestmap" + { offset: 61, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 62, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 63, type: PPLTokenTypes.Number, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "1" + { offset: 64, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, //"h" + ], + [ + { offset: 0, type: PPLTokenTypes.Pipe, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "|" + { offset: 1, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 2, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "eventstats" + { offset: 12, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 13, type: PPLTokenTypes.Function, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "avg" + { offset: 16, type: PPLTokenTypes.Parenthesis, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "(" + { offset: 17, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "timestamp", + { offset: 26, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // ")", + { offset: 27, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 28, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "as" + { offset: 30, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 31, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "exceptionCount" + { offset: 45, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 46, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "by" + { offset: 48, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 49, type: PPLTokenTypes.Function, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "span" + { offset: 53, type: PPLTokenTypes.Parenthesis, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "(" + { offset: 54, type: PPLTokenTypes.Backtick, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "@timestmap" + { offset: 66, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 67, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 68, type: PPLTokenTypes.Number, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "1" + ], + [ + { offset: 0, type: PPLTokenTypes.Pipe, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "|" + { offset: 1, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 2, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "sort" + { offset: 6, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 7, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "-" + { offset: 8, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 9, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "DisconnectReason" + { offset: 25, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 26, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 27, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "+" + { offset: 28, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 29, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "timestamp" + { offset: 38, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 39, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 40, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "server" + ], + [ + { offset: 0, type: PPLTokenTypes.Pipe, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "|" + { offset: 1, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 2, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "sort" + { offset: 6, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 7, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "-" + { offset: 8, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 9, type: PPLTokenTypes.Function, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "AUTO" + { offset: 13, type: PPLTokenTypes.Parenthesis, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "(" + { offset: 14, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "DisconnectReason" + { offset: 30, type: PPLTokenTypes.Parenthesis, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // ")" + ], + [ + { offset: 0, type: PPLTokenTypes.Pipe, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "|" + { offset: 1, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 2, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "dedup" + { offset: 7, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 8, type: PPLTokenTypes.Number, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "5" + { offset: 9, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 10, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "timestamp" + { offset: 19, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 20, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 21, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "ingestionTime" + { offset: 34, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 35, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 36, type: PPLTokenTypes.Backtick, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "`@query`" + { offset: 44, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 45, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "keepempty" + { offset: 54, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 55, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "=" + { offset: 56, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 57, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "true" + { offset: 61, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 62, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "consecutive" + { offset: 73, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 74, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "=" + { offset: 75, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 76, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "false" + ], + [ + { offset: 0, type: PPLTokenTypes.Pipe, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "|" + { offset: 1, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 2, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "dedup" + { offset: 7, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 8, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "timestamp" + { offset: 17, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 18, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 19, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "ingestionTime" + { offset: 32, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 33, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 34, type: PPLTokenTypes.Backtick, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "`@query`" + ], + [ + { offset: 0, type: PPLTokenTypes.Pipe, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "|" + { offset: 1, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 2, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "top" + { offset: 5, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 6, type: PPLTokenTypes.Number, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "100" + { offset: 9, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 10, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "ingestionTime" + { offset: 23, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 24, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 25, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "timestamp" + { offset: 34, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 35, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "by" + { offset: 37, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 38, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "server" + { offset: 44, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 45, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 46, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "region" + ], + [ + { offset: 0, type: PPLTokenTypes.Pipe, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "|" + { offset: 1, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 2, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "head" + { offset: 6, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 7, type: PPLTokenTypes.Number, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "10" + { offset: 9, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 10, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "from" + { offset: 14, type: PPLTokenTypes.Number, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 15, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "1500" + ], + [ + { offset: 0, type: PPLTokenTypes.Pipe, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "|" + { offset: 1, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 2, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "rare" + { offset: 6, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 7, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "server" + { offset: 13, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 14, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 15, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "ingestionTime" + { offset: 28, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // + { offset: 29, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "by" + { offset: 31, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 32, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "region" + { offset: 38, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 39, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 40, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "user" + ], + [ + { offset: 0, type: PPLTokenTypes.Pipe, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "|" + { offset: 1, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 2, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "eval" + { offset: 6, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 7, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "total_revenue" + { offset: 20, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 21, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "=" + { offset: 22, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 23, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "price" + { offset: 28, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 29, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "*" + { offset: 30, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 31, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "quantity" + { offset: 39, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 40, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 41, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "discount_price" + { offset: 55, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 56, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "=" + { offset: 57, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 58, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "price" + { offset: 63, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 64, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // ">=" + { offset: 66, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 67, type: PPLTokenTypes.Number, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "0.9" + { offset: 70, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 71, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 72, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "revenue_category" + { offset: 88, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 89, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "=" + { offset: 90, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 91, type: PPLTokenTypes.Function, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "IF" + { offset: 93, type: PPLTokenTypes.Parenthesis, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "(" + { offset: 94, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "price" + { offset: 99, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 100, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // ">" + { offset: 101, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 102, type: PPLTokenTypes.Number, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "100" + ], + [ + { offset: 0, type: PPLTokenTypes.Pipe, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "|" + { offset: 1, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 2, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "parse" + { offset: 7, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 8, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "email" + { offset: 13, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 14, type: PPLTokenTypes.String, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // '".+@(?.+)" + ], + ] as monacoTypes.Token[][], +}; diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-ppl-test-data/newCommandQuery.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-ppl-test-data/newCommandQuery.ts new file mode 100644 index 00000000000..a9a12e6be30 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-ppl-test-data/newCommandQuery.ts @@ -0,0 +1,18 @@ +import { monacoTypes } from '@grafana/ui'; + +import { CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID } from '../../language/cloudwatch-ppl/language'; +import { PPLTokenTypes } from '../../language/cloudwatch-ppl/tokenTypes'; + +export const newCommandQuery = { + query: `fields timestamp | `, + tokens: [ + [ + { offset: 0, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, + { offset: 6, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, + { offset: 7, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, + { offset: 16, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, + { offset: 17, type: PPLTokenTypes.Pipe, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, + { offset: 18, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, + ], + ] as monacoTypes.Token[][], +}; diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-ppl-test-data/singleLineQueries.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-ppl-test-data/singleLineQueries.ts new file mode 100644 index 00000000000..bfef98674fb --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-ppl-test-data/singleLineQueries.ts @@ -0,0 +1,420 @@ +import { monacoTypes } from '@grafana/ui'; + +import { CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID } from '../../language/cloudwatch-ppl/language'; +import { PPLTokenTypes } from '../../language/cloudwatch-ppl/tokenTypes'; + +export const emptyQuery = { + query: '', + tokens: [], +}; + +export const whitespaceOnlyQuery = { + query: ' ', + tokens: [ + [{ offset: 0, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }], + ] as monacoTypes.Token[][], +}; + +export const whereQuery = { + query: 'WHERE like(`@message`, "%Exception%") AND not like(server, "test") in ()', + tokens: [ + [ + { offset: 0, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "WHERE" + { offset: 5, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 6, type: PPLTokenTypes.Function, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "like" + { offset: 10, type: PPLTokenTypes.Parenthesis, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "(" + { offset: 11, type: PPLTokenTypes.Backtick, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "`@message`" + { offset: 21, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 22, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 23, type: PPLTokenTypes.String, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "%Exception" + { offset: 36, type: PPLTokenTypes.Parenthesis, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // ")" + { offset: 37, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 38, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "and" + { offset: 41, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 42, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "not" + { offset: 45, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 46, type: PPLTokenTypes.Function, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "like" + { offset: 50, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "(" + { offset: 51, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "server" + { offset: 57, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 58, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 59, type: PPLTokenTypes.String, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "'test'" + { offset: 55, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // ")" + { offset: 66, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 67, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "in" + { offset: 69, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 70, type: PPLTokenTypes.Parenthesis, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "(" + ], + ] as monacoTypes.Token[][], +}; +export const fieldsQuery = { + query: 'FIELDS + `@ingestionTime`, timestamp, table', + tokens: [ + [ + { offset: 0, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "fields" + { offset: 6, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 7, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "+" + { offset: 8, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 9, type: PPLTokenTypes.Backtick, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "@ingestionTime" + { offset: 25, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 26, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " ", + { offset: 27, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "timestamp", + { offset: 36, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 37, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 38, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "table" + ], + ] as monacoTypes.Token[][], +}; + +export const statsQuery = { + query: 'stats avg(timestamp) as exceptionCount by span(`@timestamp`, 1h)', + tokens: [ + [ + { offset: 0, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "stats" + { offset: 5, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 6, type: PPLTokenTypes.Function, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "avg" + { offset: 9, type: PPLTokenTypes.Parenthesis, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "(" + { offset: 10, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "timestamp", + { offset: 19, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // ")", + { offset: 20, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 21, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "as" + { offset: 23, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 24, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "exceptionCount" + { offset: 38, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 39, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "by" + { offset: 41, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 42, type: PPLTokenTypes.Function, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "span" + { offset: 46, type: PPLTokenTypes.Parenthesis, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "(" + { offset: 47, type: PPLTokenTypes.Backtick, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "@timestmap" + { offset: 59, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 60, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 61, type: PPLTokenTypes.Number, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "1h" + ], + ] as monacoTypes.Token[][], +}; + +export const eventStatsQuery = { + query: 'EVENTSTATS avg(timestamp) as exceptionCount by span(`@timestamp`, 1h)', + tokens: [ + [ + { offset: 0, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "eventstats" + { offset: 10, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 11, type: PPLTokenTypes.Function, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "avg" + { offset: 14, type: PPLTokenTypes.Parenthesis, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "(" + { offset: 15, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "timestamp", + { offset: 24, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // ")", + { offset: 25, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 26, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "as" + { offset: 28, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 29, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "exceptionCount" + { offset: 43, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 44, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "by" + { offset: 46, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 47, type: PPLTokenTypes.Function, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "span" + { offset: 51, type: PPLTokenTypes.Parenthesis, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "(" + { offset: 52, type: PPLTokenTypes.Backtick, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "@timestmap" + { offset: 64, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 65, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 66, type: PPLTokenTypes.Number, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "1h" + ], + ] as monacoTypes.Token[][], +}; + +export const sortQuery = { + query: 'sort - DisconnectReason, + timestamp, server', + tokens: [ + [ + { offset: 0, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "sort" + { offset: 4, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 5, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "-" + { offset: 6, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 7, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "DisconnectReason" + { offset: 23, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 24, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 25, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "+" + { offset: 26, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 27, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "timestamp" + { offset: 36, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 37, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 38, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "server" + ], + ] as monacoTypes.Token[][], +}; +export const sortQueryWithFunctions = { + query: 'sort - AUTO(DisconnectReason)', + tokens: [ + [ + { offset: 0, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "sort" + { offset: 4, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 5, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "-" + { offset: 6, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 7, type: PPLTokenTypes.Function, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "AUTO" + { offset: 11, type: PPLTokenTypes.Parenthesis, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "(" + { offset: 12, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "DisconnectReason" + ], + ] as monacoTypes.Token[][], +}; + +export const dedupQueryWithOptionalArgs = { + query: 'DEDUP 5 timestamp, ingestionTime, `@query` keepempty = true consecutive = false', + tokens: [ + [ + { offset: 0, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "dedup" + { offset: 5, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 6, type: PPLTokenTypes.Number, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "5" + { offset: 7, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 8, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "timestamp" + { offset: 17, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 18, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 19, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "ingestionTime" + { offset: 32, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 33, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 34, type: PPLTokenTypes.Backtick, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "`@query`" + { offset: 42, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 43, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "keepempty" + { offset: 52, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 53, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "=" + { offset: 54, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 55, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "true" + { offset: 59, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 60, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "consecutive" + { offset: 71, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 72, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "=" + { offset: 73, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 74, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "false" + ], + ] as monacoTypes.Token[][], +}; + +export const dedupQueryWithoutOptionalArgs = { + query: 'DEDUP timestamp, ingestionTime, `@query`', + tokens: [ + [ + { offset: 0, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "dedup" + { offset: 5, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 6, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "timestamp" + { offset: 15, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 16, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 17, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "ingestionTime" + { offset: 30, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 31, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 32, type: PPLTokenTypes.Backtick, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "`@query`" + ], + ] as monacoTypes.Token[][], +}; + +export const topQuery = { + query: 'TOP 100 ingestionTime, timestamp by server, region', + tokens: [ + [ + { offset: 0, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "top" + { offset: 3, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 4, type: PPLTokenTypes.Number, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "100" + { offset: 7, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 8, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "ingestionTime" + { offset: 21, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 22, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 23, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "timestamp" + { offset: 32, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 33, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "by" + { offset: 35, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 36, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "server" + { offset: 42, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 43, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 44, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "region" + ], + ] as monacoTypes.Token[][], +}; + +export const headQuery = { + query: 'HEAD 10 from 1500', + tokens: [ + [ + { offset: 0, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "head" + { offset: 4, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 5, type: PPLTokenTypes.Number, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "10" + { offset: 7, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 8, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "from" + { offset: 12, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 13, type: PPLTokenTypes.Number, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "1500" + ], + ] as monacoTypes.Token[][], +}; +export const rareQuery = { + query: 'RARE server, ingestionTime by region, user', + tokens: [ + [ + { offset: 0, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "rare" + { offset: 4, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 5, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "server" + { offset: 11, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 12, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 13, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "ingestionTime" + { offset: 26, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 27, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "by" + { offset: 29, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 30, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "region" + { offset: 36, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 37, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 38, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "user" + ], + ] as monacoTypes.Token[][], +}; + +export const evalQuery = { + query: + 'EVAL total_revenue = price * quantity, discount_price = price >= 0.9, revenue_category = IF(price BETWEEN 100 AND 200, "high", "low")', + tokens: [ + [ + { offset: 0, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "eval" + { offset: 4, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 5, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "total_revenue" + { offset: 18, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 19, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "=" + { offset: 20, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 21, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "price" + { offset: 26, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 27, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "*" + { offset: 28, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 29, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "quantity" + { offset: 37, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 38, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 39, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "discount_price" + { offset: 53, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 54, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "=" + { offset: 55, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 56, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "price" + { offset: 61, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 62, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // ">=" + { offset: 64, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 65, type: PPLTokenTypes.Number, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "0.9" + { offset: 68, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 69, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 70, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "revenue_category" + { offset: 86, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 87, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "=" + { offset: 88, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 89, type: PPLTokenTypes.Function, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "IF" + { offset: 91, type: PPLTokenTypes.Parenthesis, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "(" + { offset: 92, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "price" + { offset: 97, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 98, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "between" + { offset: 105, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 106, type: PPLTokenTypes.Number, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "100" + ], + ] as monacoTypes.Token[][], +}; + +export const parseQuery = { + query: 'parse email ".+@(?.+)"', + tokens: [ + [ + { offset: 0, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "parse" + { offset: 5, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 6, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "email" + { offset: 11, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 12, type: PPLTokenTypes.String, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // '".+@(?.+)" + ], + ] as monacoTypes.Token[][], +}; +export const queryWithArithmeticOps = { + query: 'where price * discount >= 200', + tokens: [ + [ + { offset: 0, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "where" + { offset: 5, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 6, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "price" + { offset: 11, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 12, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "*" + { offset: 13, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 14, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "discount" + { offset: 22, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 23, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // ">=" + { offset: 25, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 26, type: PPLTokenTypes.Number, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "200" + ], + ] as monacoTypes.Token[][], +}; +export const queryWithLogicalExpression = { + query: 'where orders = "shipped" OR NOT /returned/ AND price > 20', + tokens: [ + [ + { offset: 0, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "where" + { offset: 5, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 6, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "orders" + { offset: 12, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 13, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "=" + { offset: 14, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 15, type: PPLTokenTypes.String, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "'shipped'" + { offset: 24, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 25, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "OR" + { offset: 27, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 28, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "NOT" + { offset: 31, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 32, type: PPLTokenTypes.Regexp, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "/returned/" + { offset: 42, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 43, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "AND" + { offset: 46, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 47, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "price" + { offset: 52, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 53, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // ">" + { offset: 54, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 55, type: PPLTokenTypes.Number, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "20" + ], + ] as monacoTypes.Token[][], +}; +export const queryWithFieldList = { + query: 'fields ingestionTime, timestamp, `@server`, bytesReceived', + tokens: [ + [ + { offset: 0, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "fields" + { offset: 6, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 7, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "ingestionTime" + { offset: 20, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 21, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 22, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "timestamp" + { offset: 31, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 32, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 33, type: PPLTokenTypes.Backtick, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "`@server`" + { offset: 42, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 43, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 44, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "bytesReceived" + ], + ] as monacoTypes.Token[][], +}; + +export const queryWithFunctionCalls = { + query: 'where like(dstAddr, ) where logType = "Tracing"| where cos(`duration`), right(`duration`)', + tokens: [ + [ + { offset: 0, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "where" + { offset: 5, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 6, type: PPLTokenTypes.Function, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "like" + { offset: 10, type: PPLTokenTypes.Parenthesis, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "(" + { offset: 11, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "dstAddr" + { offset: 18, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 19, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 20, type: PPLTokenTypes.Parenthesis, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // ")" + { offset: 21, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 22, type: PPLTokenTypes.Keyword, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // where + { offset: 27, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 28, type: PPLTokenTypes.Identifier, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // logType + { offset: 35, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 36, type: PPLTokenTypes.Operator, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // = + { offset: 37, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 38, type: PPLTokenTypes.String, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "Tracing" + { offset: 47, type: PPLTokenTypes.Pipe, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "|" + { offset: 48, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "" + { offset: 49, type: PPLTokenTypes.Command, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "where" + { offset: 54, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 55, type: PPLTokenTypes.Function, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "cos" + { offset: 58, type: PPLTokenTypes.Parenthesis, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "(" + { offset: 59, type: PPLTokenTypes.Backtick, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "`duration`" + { offset: 69, type: PPLTokenTypes.Parenthesis, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // ")" + { offset: 70, type: PPLTokenTypes.Delimiter, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "," + { offset: 71, type: PPLTokenTypes.Whitespace, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // " " + { offset: 72, type: PPLTokenTypes.Function, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "right" + { offset: 77, type: PPLTokenTypes.Parenthesis, language: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID }, // "(" + ], + ] as monacoTypes.Token[][], +}; diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/monarch/Monaco.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/monarch/Monaco.ts index 3f7e5438d1e..09fda988e8e 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/monarch/Monaco.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/monarch/Monaco.ts @@ -1,7 +1,20 @@ import { monacoTypes } from '@grafana/ui'; +import { CLOUDWATCH_LOGS_SQL_LANGUAGE_DEFINITION_ID } from '../../language/cloudwatch-logs-sql/definition'; +import { CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID } from '../../language/cloudwatch-ppl/language'; +import { CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID } from '../../language/logs/definition'; import { Monaco } from '../../language/monarch/types'; +import { commentOnlyQuery as cloudwatchLogsSQLCommentOnlyQuery } from '../cloudwatch-logs-sql-test-data/commentOnlyQuery'; +import { multiLineFullQuery as cloudwatchLogsSQLMultiLineFullQuery } from '../cloudwatch-logs-sql-test-data/multiLineFullQuery'; +import { multiLineFullQueryWithCaseClause as cloudwatchLogsSQLMultiLineFullQueryWithCaseClause } from '../cloudwatch-logs-sql-test-data/multiLineFullQueryWithCaseClause'; +import { partialQueryWithFunction as cloudwatchLogsSQLPartialQueryWithFunction } from '../cloudwatch-logs-sql-test-data/partialQueryWithFunction'; +import { partialQueryWithSubquery as cloudwatchLogsSQLPartialQueryWithSubquery } from '../cloudwatch-logs-sql-test-data/partialQueryWithSubquery'; +import { singleLineFullQuery as cloudwatchLogsSQLSingleLineFullQuery } from '../cloudwatch-logs-sql-test-data/singleLineFullQuery'; +import { whitespaceQuery as cloudwatchLogsSQLWhitespaceQuery } from '../cloudwatch-logs-sql-test-data/whitespaceQuery'; import * as CloudwatchLogsTestData from '../cloudwatch-logs-test-data'; +import * as PPLMultilineQueries from '../cloudwatch-ppl-test-data/multilineQueries'; +import { newCommandQuery as PPLNewCommandQuery } from '../cloudwatch-ppl-test-data/newCommandQuery'; +import * as PPLSingleLineQueries from '../cloudwatch-ppl-test-data/singleLineQueries'; import * as SQLTestData from '../cloudwatch-sql-test-data'; import * as DynamicLabelTestData from '../dynamic-label-test-data'; import * as MetricMathTestData from '../metric-math-test-data'; @@ -40,7 +53,7 @@ const MonacoMock: Monaco = { }; return TestData[value]; } - if (languageId === 'cloudwatch-logs') { + if (languageId === CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID) { const TestData = { [CloudwatchLogsTestData.emptyQuery.query]: CloudwatchLogsTestData.emptyQuery.tokens, [CloudwatchLogsTestData.whitespaceOnlyQuery.query]: CloudwatchLogsTestData.whitespaceOnlyQuery.tokens, @@ -53,6 +66,49 @@ const MonacoMock: Monaco = { }; return TestData[value]; } + if (languageId === CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID) { + const TestData = { + [PPLSingleLineQueries.emptyQuery.query]: PPLSingleLineQueries.emptyQuery.tokens, + [PPLSingleLineQueries.whitespaceOnlyQuery.query]: PPLSingleLineQueries.whitespaceOnlyQuery.tokens, + [PPLNewCommandQuery.query]: PPLNewCommandQuery.tokens, + [PPLMultilineQueries.multiLineFullQuery.query]: PPLMultilineQueries.multiLineFullQuery.tokens, + [PPLMultilineQueries.multiLineNewCommandQuery.query]: PPLMultilineQueries.multiLineNewCommandQuery.tokens, + [PPLSingleLineQueries.whereQuery.query]: PPLSingleLineQueries.whereQuery.tokens, + [PPLSingleLineQueries.fieldsQuery.query]: PPLSingleLineQueries.fieldsQuery.tokens, + [PPLSingleLineQueries.statsQuery.query]: PPLSingleLineQueries.statsQuery.tokens, + [PPLSingleLineQueries.eventStatsQuery.query]: PPLSingleLineQueries.eventStatsQuery.tokens, + [PPLSingleLineQueries.dedupQueryWithOptionalArgs.query]: + PPLSingleLineQueries.dedupQueryWithOptionalArgs.tokens, + [PPLSingleLineQueries.dedupQueryWithoutOptionalArgs.query]: + PPLSingleLineQueries.dedupQueryWithoutOptionalArgs.tokens, + [PPLSingleLineQueries.sortQuery.query]: PPLSingleLineQueries.sortQuery.tokens, + [PPLSingleLineQueries.sortQueryWithFunctions.query]: PPLSingleLineQueries.sortQueryWithFunctions.tokens, + [PPLSingleLineQueries.headQuery.query]: PPLSingleLineQueries.headQuery.tokens, + [PPLSingleLineQueries.topQuery.query]: PPLSingleLineQueries.topQuery.tokens, + [PPLSingleLineQueries.rareQuery.query]: PPLSingleLineQueries.rareQuery.tokens, + [PPLSingleLineQueries.evalQuery.query]: PPLSingleLineQueries.evalQuery.tokens, + [PPLSingleLineQueries.parseQuery.query]: PPLSingleLineQueries.parseQuery.tokens, + [PPLSingleLineQueries.queryWithArithmeticOps.query]: PPLSingleLineQueries.queryWithArithmeticOps.tokens, + [PPLSingleLineQueries.queryWithLogicalExpression.query]: + PPLSingleLineQueries.queryWithLogicalExpression.tokens, + [PPLSingleLineQueries.queryWithFieldList.query]: PPLSingleLineQueries.queryWithFieldList.tokens, + [PPLSingleLineQueries.queryWithFunctionCalls.query]: PPLSingleLineQueries.queryWithFunctionCalls.tokens, + }; + return TestData[value]; + } + if (languageId === CLOUDWATCH_LOGS_SQL_LANGUAGE_DEFINITION_ID) { + const TestData = { + [cloudwatchLogsSQLCommentOnlyQuery.query]: cloudwatchLogsSQLCommentOnlyQuery.tokens, + [cloudwatchLogsSQLMultiLineFullQuery.query]: cloudwatchLogsSQLMultiLineFullQuery.tokens, + [cloudwatchLogsSQLMultiLineFullQueryWithCaseClause.query]: + cloudwatchLogsSQLMultiLineFullQueryWithCaseClause.tokens, + [cloudwatchLogsSQLPartialQueryWithFunction.query]: cloudwatchLogsSQLPartialQueryWithFunction.tokens, + [cloudwatchLogsSQLPartialQueryWithSubquery.query]: cloudwatchLogsSQLPartialQueryWithSubquery.tokens, + [cloudwatchLogsSQLSingleLineFullQuery.query]: cloudwatchLogsSQLSingleLineFullQuery.tokens, + [cloudwatchLogsSQLWhitespaceQuery.query]: cloudwatchLogsSQLWhitespaceQuery.tokens, + }; + return TestData[value]; + } return []; }, }, diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/queries.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/queries.ts index 36b3a2df017..82e6e3ef1ae 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/queries.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/queries.ts @@ -84,7 +84,10 @@ export const validMetricQueryCodeQuery: CloudWatchMetricsQuery = { export const validLogsQuery: CloudWatchLogsQuery = { queryMode: 'Logs', - logGroupNames: ['group-A', 'group-B'], + logGroups: [ + { arn: 'group-A', name: 'A' }, + { arn: 'group-B', name: 'B' }, + ], hide: false, id: '', region: 'us-east-2', diff --git a/public/app/plugins/datasource/cloudwatch/components/CheatSheet/LogsCheatSheet.tsx b/public/app/plugins/datasource/cloudwatch/components/CheatSheet/LogsCheatSheet.tsx index 91929e6bc6f..3c3448c419d 100644 --- a/public/app/plugins/datasource/cloudwatch/components/CheatSheet/LogsCheatSheet.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/CheatSheet/LogsCheatSheet.tsx @@ -1,339 +1,58 @@ -import { css, cx } from '@emotion/css'; -import { stripIndent, stripIndents } from 'common-tags'; +import { css } from '@emotion/css'; import Prism from 'prismjs'; import { useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Collapse, useStyles2 } from '@grafana/ui'; +import { Collapse, useStyles2, Text } from '@grafana/ui'; import { flattenTokens } from '@grafana/ui/src/slate-plugins/slate-prism'; -import tokenizer from '../../language/cloudwatch-logs/syntax'; -import { CloudWatchQuery } from '../../types'; +import { CloudWatchLogsQuery, CloudWatchQuery, LogsQueryLanguage } from '../../types'; +import * as sampleQueries from './sampleQueries'; +import { cwliTokenizer, pplTokenizer, sqlTokenizer } from './tokenizer'; interface QueryExample { category: string; - examples: Array<{ - title?: string; - description?: string; - expr: string; - }>; + examples: sampleQueries.SampleQuery[]; } const QUERIES: QueryExample[] = [ + { + category: 'General queries', + examples: sampleQueries.generalQueries, + }, { category: 'Lambda', - examples: [ - { - title: 'View latency statistics for 5-minute intervals', - expr: stripIndents`filter @type = "REPORT" | - stats avg(@duration), max(@duration), min(@duration) by bin(5m)`, - }, - { - title: 'Determine the amount of overprovisioned memory', - expr: stripIndent`filter @type = "REPORT" - | stats max(@memorySize / 1000 / 1000) as provisionedMemoryMB, - min(@maxMemoryUsed / 1000 / 1000) as smallestMemoryRequestMB, - avg(@maxMemoryUsed / 1000 / 1000) as avgMemoryUsedMB, - max(@maxMemoryUsed / 1000 / 1000) as maxMemoryUsedMB, - provisionedMemoryMB - maxMemoryUsedMB as overProvisionedMB - `, - }, - { - title: 'Find the most expensive requests', - expr: stripIndents`filter @type = "REPORT" - | fields @requestId, @billedDuration - | sort by @billedDuration desc`, - }, - ], + examples: sampleQueries.lambdaSamples, }, { category: 'VPC Flow Logs', - examples: [ - { - title: 'Average, min, and max byte transfers by source and destination IP addresses', - expr: `stats avg(bytes), min(bytes), max(bytes) by srcAddr, dstAddr`, - }, - { - title: 'IP addresses using UDP transfer protocol', - expr: 'filter protocol=17 | stats count(*) by srcAddr', - }, - { - title: 'Top 10 byte transfers by source and destination IP addresses', - expr: stripIndents`stats sum(bytes) as bytesTransferred by srcAddr, dstAddr | - sort bytesTransferred desc | - limit 10`, - }, - { - title: 'Top 20 source IP addresses with highest number of rejected requests', - expr: stripIndents`filter action="REJECT" | - stats count(*) as numRejections by srcAddr | - sort numRejections desc | - limit 20`, - }, - { - title: 'Find the top 15 packet transfers across hosts', - expr: stripIndents`stats sum(packets) as packetsTransferred by srcAddr, dstAddr - | sort packetsTransferred desc - | limit 15`, - }, - { - title: 'Find the IP addresses where flow records were skipped during the capture window', - expr: stripIndents`filter logStatus="SKIPDATA" - | stats count(*) by bin(1h) as t - | sort t`, - }, - ], + examples: sampleQueries.vpcSamples, }, { - category: 'CloudTrail', - examples: [ - { - title: 'Number of log entries by service, event type, and region', - expr: 'stats count(*) by eventSource, eventName, awsRegion', - }, - - { - title: 'Number of log entries by region and EC2 event type', - expr: stripIndents`filter eventSource="ec2.amazonaws.com" | - stats count(*) as eventCount by eventName, awsRegion | - sort eventCount desc`, - }, - - { - title: 'Regions, usernames, and ARNs of newly created IAM users', - expr: stripIndents`filter eventName="CreateUser" | - fields awsRegion, requestParameters.userName, responseElements.user.arn`, - }, - { - title: 'Find EC2 hosts that were started or stopped in a given AWS Region', - expr: stripIndents`filter (eventName="StartInstances" or eventName="StopInstances") and region="us-east-2"`, - }, - { - title: 'Find the number of records where an exception occurred while invoking the UpdateTrail API', - expr: stripIndents`filter eventName="UpdateTrail" and ispresent(errorCode) | stats count(*) by errorCode, errorMessage`, - }, - { - title: 'Find log entries where TLS 1.0 or 1.1 was used', - expr: stripIndents`filter tlsDetails.tlsVersion in [ "TLSv1", "TLSv1.1" ] - | stats count(*) as numOutdatedTlsCalls by userIdentity.accountId, recipientAccountId, eventSource, eventName, awsRegion, tlsDetails.tlsVersion, tlsDetails.cipherSuite, userAgent - | sort eventSource, eventName, awsRegion, tlsDetails.tlsVersion`, - }, - { - title: 'Find the number of calls per service that used TLS versions 1.0 or 1.1', - expr: stripIndents`filter tlsDetails.tlsVersion in [ "TLSv1", "TLSv1.1" ] - | stats count(*) as numOutdatedTlsCalls by eventSource - | sort numOutdatedTlsCalls desc`, - }, - ], + category: 'CloudTrail Logs', + examples: sampleQueries.cloudtrailSamples, }, { - category: 'Common Queries', - examples: [ - { - title: '25 most recently added log events', - expr: stripIndents`fields @timestamp, @message | - sort @timestamp desc | - limit 25`, - }, - { - title: 'Number of exceptions logged every 5 minutes', - expr: stripIndents`filter @message like /Exception/ | - stats count(*) as exceptionCount by bin(5m) | - sort exceptionCount desc`, - }, - { - title: 'List of log events that are not exceptions', - expr: 'fields @message | filter @message not like /Exception/', - }, - { - title: 'To parse and count fields', - expr: stripIndents`fields @timestamp, @message - | filter @message like /User ID/ - | parse @message "User ID: *" as @userId - | stats count(*) by @userId`, - }, - { - title: 'To Identify faults on any API calls', - expr: stripIndents`filter Operation = AND Fault > 0 - | fields @timestamp, @logStream as instanceId, ExceptionMessage`, - }, - { - title: - 'To get the number of exceptions logged every 5 minutes using regex where exception is not case sensitive', - expr: stripIndents`filter @message like /(?i)Exception/ - | stats count(*) as exceptionCount by bin(5m) - | sort exceptionCount desc`, - }, - { - title: 'To parse ephemeral fields using a glob expression', - expr: stripIndents`parse @message "user=*, method:*, latency := *" as @user, @method, @latency - | stats avg(@latency) by @method, @user`, - }, - { - title: 'To parse ephemeral fields using a glob expression using regular expression', - expr: stripIndents`parse @message /user=(?.*?), method:(?.*?), latency := (?.*?)/ - | stats avg(latency2) by @method2, @user2`, - }, - { - title: 'To extract ephemeral fields and display field for events that contain an ERROR string', - expr: stripIndents`fields @message - | parse @message "* [*] *" as loggingTime, loggingType, loggingMessage - | filter loggingType IN ["ERROR"] - | display loggingMessage, loggingType = "ERROR" as isError`, - }, - { - title: 'To trim whitespaces from query results', - expr: stripIndents`fields trim(@message) as trimmedMessage - | parse trimmedMessage "[*] * * Retrieving CloudWatch Metrics for AccountID : *, CloudWatch Metric : *, Resource Type : *, ResourceID : *" as level, time, logId, accountId, metric, type, resourceId - | display level, time, logId, accountId, metric, type, resourceId - | filter level like "INFO"`, - }, - ], + category: 'NAT Gateway', + examples: sampleQueries.natSamples, }, { - category: 'Route 53', - examples: [ - { - title: 'Number of requests received every 10 minutes by edge location', - expr: 'stats count(*) by queryType, bin(10m)', - }, - { - title: 'Number of unsuccessful requests by domain', - expr: 'filter responseCode="SERVFAIL" | stats count(*) by queryName', - }, - { - title: 'Top 10 DNS resolver IPs with highest number of requests', - expr: 'stats count(*) as numRequests by resolverIp | sort numRequests desc | limit 10', - }, - ], + category: 'AWS App Sync', + examples: sampleQueries.appSyncSamples, }, { - category: 'AWS AppSync', - examples: [ - { - title: 'Number of unique HTTP status codes', - expr: stripIndents`fields ispresent(graphQLAPIId) as isApi | - filter isApi | - filter logType = "RequestSummary" | - stats count() as statusCount by statusCode | - sort statusCount desc`, - }, - { - title: 'Top 10 resolvers with maximum latency', - expr: stripIndents`fields resolverArn, duration | - filter logType = "Tracing" | - sort duration desc | - limit 10`, - }, - { - title: 'Most frequently invoked resolvers', - expr: stripIndents`fields ispresent(resolverArn) as isRes | - stats count() as invocationCount by resolverArn | - filter isRes | - filter logType = "Tracing" | - sort invocationCount desc | - limit 10`, - }, - { - title: 'Resolvers with most errors in mapping templates', - expr: stripIndents`fields ispresent(resolverArn) as isRes | - stats count() as errorCount by resolverArn, logType | - filter isRes and (logType = "RequestMapping" or logType = "ResponseMapping") and fieldInError | - sort errorCount desc | - limit 10`, - }, - { - title: 'Field latency statistics', - expr: stripIndents`fields requestId, latency | - filter logType = "RequestSummary" | - sort latency desc | - limit 10`, - }, - { - title: 'Resolver latency statistics', - expr: stripIndents`fields ispresent(resolverArn) as isRes | - filter isRes | - filter logType = "Tracing" | - stats min(duration), max(duration), avg(duration) as avgDur by resolverArn | - sort avgDur desc | - limit 10`, - }, - { - title: 'Top 10 requests with maximum latency', - expr: stripIndents`fields requestId, latency | - filter logType = "RequestSummary" | - sort latency desc | - limit 10`, - }, - ], + category: 'IOT queries', + examples: sampleQueries.iotSamples, }, ]; -const COMMANDS: QueryExample[] = [ - { - category: 'fields', - examples: [ - { - description: - 'Retrieve one or more log fields. You can also use functions and operations such as abs(a+b), sqrt(a/b), log(a)+log(b), strlen(trim()), datefloor(), isPresent(), and others in this command.', - expr: 'fields @log, @logStream, @message, @timestamp', - }, - ], - }, - { - category: 'filter', - examples: [ - { - description: - 'Retrieve log fields based on one or more conditions. You can use comparison operators such as =, !=, >, >=, <, <=, boolean operators such as and, or, and not, and regular expressions in this command.', - expr: 'filter @message like /(?i)(Exception|error|fail|5dd)/', - }, - ], - }, - { - category: 'stats', - examples: [ - { - description: 'Calculate aggregate statistics such as sum(), avg(), count(), min() and max() for log fields.', - expr: 'stats count() by bin(5m)', - }, - ], - }, - { - category: 'sort', - examples: [ - { - description: 'Sort the log fields in ascending or descending order.', - expr: 'sort @timestamp asc', - }, - ], - }, - { - category: 'limit', - examples: [ - { - description: 'Limit the number of log events returned by a query.', - expr: 'limit 10', - }, - ], - }, - { - category: 'parse', - examples: [ - { - description: - 'Create one or more ephemeral fields, which can be further processed by the query. The following example will extract the ephemeral fields host, identity, dateTimeString, httpVerb, url, protocol, statusCode, bytes from @message, and return the url, max(bytes), and avg(bytes) fields sorted by max(bytes) in descending order.', - expr: stripIndents`parse '* - * [*] "* * *" * *' as host, identity, dateTimeString, httpVerb, url, protocol, statusCode, bytes - | stats max(bytes) as maxBytes, avg(bytes) by url - | sort maxBytes desc`, - }, - ], - }, -]; - -function renderHighlightedMarkup(code: string, keyPrefix: string) { - const grammar = tokenizer; +function renderHighlightedMarkup( + code: string, + keyPrefix: string, + queryLanugage: LogsQueryLanguage = LogsQueryLanguage.CWLI +) { + const grammar = getGrammarForLanguage(queryLanugage); const tokens = flattenTokens(Prism.tokenize(code, grammar)); const spans = tokens .filter((token) => typeof token !== 'string') @@ -351,91 +70,73 @@ function renderHighlightedMarkup(code: string, keyPrefix: string) { return
{spans}
; } +interface CollapseProps { + key?: string; + label: string; + children: React.ReactNode; +} +const CheatSheetCollapse = (props: CollapseProps) => { + const [isOpen, setIsOpen] = useState(false); + return ( + + {props.children} + + ); +}; + type Props = { onClickExample: (query: CloudWatchQuery) => void; query: CloudWatchQuery; }; - +const isLogsQuery = (query: CloudWatchQuery): query is CloudWatchLogsQuery => query.queryMode === 'Logs'; const LogsCheatSheet = (props: Props) => { - const [isCommandsOpen, setIsCommandsOpen] = useState(false); - const [isQueriesOpen, setIsQueriesOpen] = useState(false); const styles = useStyles2(getStyles); + const queryLanugage: LogsQueryLanguage = + (isLogsQuery(props.query) && props.query.queryLanguage) || LogsQueryLanguage.CWLI; return (
-

CloudWatch Logs cheat sheet

- setIsCommandsOpen(isOpen)} - > - <> - {COMMANDS.map((cat, i) => ( -
-
{cat.category}
- {cat.examples.map((item, j) => ( -
-

{item.description}

- -
- ))} -
- ))} - -
- setIsQueriesOpen(isOpen)} - > - {QUERIES.map((cat, i) => ( +
+ + CloudWatch Logs cheat sheet + +
+ {QUERIES.map((query, i) => ( +
-
{cat.category}
- {cat.examples.map((item, j) => ( -
-

{item.title}

- -
+ {query.examples.map((item, j) => ( + <> + {item.expr[queryLanugage] && ( + <> + + {item.title} + + + + )} + ))}
- ))} -
+ + ))}
Note: If you are seeing masked data, you may have CloudWatch logs data protection enabled.{' '} { export default LogsCheatSheet; const getStyles = (theme: GrafanaTheme2) => ({ - exampleCategory: css({ - marginTop: '5px', + heading: css({ + marginBottom: theme.spacing(2), }), link: css({ textDecoration: 'underline', }), - cheatSheetItem: css({ - margin: theme.spacing(3, 0), - }), - cheatSheetItemTitle: css({ - fontSize: theme.typography.h3.fontSize, - }), cheatSheetExample: css({ margin: theme.spacing(0.5, 0), // element is interactive, clear button styles @@ -476,3 +171,14 @@ const getStyles = (theme: GrafanaTheme2) => ({ display: 'block', }), }); + +const getGrammarForLanguage = (queryLanugage: LogsQueryLanguage) => { + switch (queryLanugage) { + case LogsQueryLanguage.CWLI: + return cwliTokenizer; + case LogsQueryLanguage.PPL: + return pplTokenizer; + case LogsQueryLanguage.SQL: + return sqlTokenizer; + } +}; diff --git a/public/app/plugins/datasource/cloudwatch/components/CheatSheet/sampleQueries.ts b/public/app/plugins/datasource/cloudwatch/components/CheatSheet/sampleQueries.ts new file mode 100644 index 00000000000..4c67fe68c9b --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/components/CheatSheet/sampleQueries.ts @@ -0,0 +1,1035 @@ +import { stripIndents } from 'common-tags'; + +import { LogsQueryLanguage } from '../../types'; + +export interface SampleQuery { + title: string; + expr: Partial>; +} + +const sqlOnlyGeneralQueries: SampleQuery[] = [ + { + title: + 'Use the JOIN command to match events between two log groups (LogGroupA, LogGroupB), based on common user IDs across the logs. ', + expr: { + SQL: stripIndents`SELECT A.transaction_id as txn_id_a, A.userId , A.instance_id as inst_id_a, B.instance_id as inst_id_b FROM \`LogGroupA\` as A INNER JOIN \`LogGroupB\` as B ON A.userId = B.userId WHERE B.Status='ERROR'`, + }, + }, + { + title: 'Find logs where duration is greater than the average duration of all log groups, using a sub-query', + expr: { + SQL: stripIndents`SELECT \`@duration\` FROM \`LogGroupA\` + WHERE \`@duration\` > ( + SELECT avg(\`@duration\`) FROM \`LogGroupA\` + WHERE \`@type\` = 'REPORT')`, + }, + }, + { + title: + 'Find all logs relating to a Lambda function where level is ERROR, and order these logs by request id using a sub-query', + expr: { + SQL: stripIndents`select requestId, level, \`@timestamp\`, \`@message\` from \`LogGroupA\` where requestId IN (SELECT distinct + requestId FROM \`LogGroupA\` where level = 'ERROR') order by requestId`, + }, + }, + { + title: 'Find error logs from high-volume log streams using a sub-query. ', + expr: { + SQL: stripIndents`SELECT + \`@logStream\`, + COUNT(*) as error_count + FROM \`LogGroupA\` + WHERE + \`start\` >= date_sub(current_timestamp(), 1) + AND lower(\`@message\`) LIKE '%error%' + AND \`@logStream\` IN ( + SELECT \`@logStream\` + FROM \`logs\` + GROUP BY \`@logStream\` + HAVING COUNT(*) > 1000 + ) + GROUP BY \`@logStream\` + ORDER BY error_count DESC`, + }, + }, + { + title: 'Extract parameter values from a JSON log group', + expr: { + SQL: stripIndents`SELECT query_name, get_json_object(\`@message\`, '$.answers[*].Rdata') + AS answers FROM \`LogGroupA\` Where query_type = 'A'`, + }, + }, + { + title: 'Find the intersection of elements for two columns based on eventName.', + expr: { + SQL: stripIndents`SELECT array_intersect( + array(get_json_object(\`column1\`, '$.eventName')), + array(get_json_object(\`column2\`, '$.eventName')) + ) as matching_events + FROM \`LogGroupA\`;`, + }, + }, + { + title: 'Return the top 25 most recently added log events.', + expr: { + SQL: 'SELECT `@timestamp`, `@message` FROM `LogGroupA` ORDER BY `@timestamp` DESC LIMIT 25;', + }, + }, + { + title: 'Find the number of exceptions logged every five minutes.', + expr: { + SQL: `SELECT window.start, COUNT(*) AS exceptionCount FROM \`LogGroupA\` WHERE \`@message\` LIKE '%Exception%' GROUP BY window(\`@timestamp\`, '5 minute') ORDER BY exceptionCount DESC`, + }, + }, + { + title: 'Return a list of log events that are not exceptions.', + expr: { + SQL: `SELECT \`@message\` FROM \`LogGroupA\` WHERE \`@message\` NOT LIKE '%Exception%'`, + }, + }, + { + title: 'Identify faults on API calls.', + expr: { + SQL: 'Select @timestamp, @logStream as instanceId, ExceptionMessage from `LogGroupA` where Operation = "x" and Fault > 0', + }, + }, + { + title: + 'Return the number of exceptions logged every five minutes using regex where exception is not case sensitive.', + expr: { + SQL: `SELECT window.start, COUNT(*) AS exceptionCount FROM \`LogGroupA\` WHERE \`@message\` LIKE '%Exception%' GROUP BY window(\`@timestamp\`, '5 minute') ORDER BY exceptionCount DESC`, + }, + }, + { + title: + 'Count the number of logs per minute over the last 24 hours, grouping them into one-minute time buckets and sorting from newest to oldest, and only consider those groups that have error_count greater than zero.', + expr: { + SQL: stripIndents`SELECT + date(\`@timestamp\`) as log_date, + \`@logStream\`, + COUNT(*) as total_messages, + SUM(CASE WHEN lower(\`@message\`) LIKE '%error%' THEN 1 ELSE 0 END) as error_count, + SUM(CASE WHEN lower(\`@message\`) LIKE '%warn%' THEN 1 ELSE 0 END) as warning_count + FROM \`LogGroupA\` + WHERE \`@timestamp\` >= date_sub(current_timestamp(), 7) + GROUP BY date(\`startTime\`), \`@logStream\` + HAVING error_count > 0 + ORDER BY error_count DESC`, + }, + }, + { + title: + 'Calculate the total count of logs and unique streams, along with the earliest and latest timestamps for all logs from the past 24 hours.', + expr: { + SQL: stripIndents`SELECT + COUNT(*) as total_logs, + COUNT(DISTINCT \`@logStream\`) as unique_streams, + MIN(\`@timestamp\`) as earliest_log, + MAX(\`startTime\`) as latest_log + FROM \`LogGroupA\` + WHERE \`startTime\` >= date_sub(current_timestamp(), 1)`, + }, + }, + { + title: + "Show the top 10 most active log streams from the past 24 hours, displaying each stream's total log count and its first and last log timestamps, sorted by highest log count first.", + expr: { + SQL: stripIndents`SELECT \`@logStream\`, COUNT(*) as log_count, MIN(\`@timestamp\`) as first_seen, MAX(\`@timestamp\`) as last_seen FROM \`LogGroupA\`WHERE \`startTime\` >= date_sub(current_timestamp(), 24)GROUP BY \`@logStream\`ORDER BY log_count DESC LIMIT 10`, + }, + }, + { + title: 'Count the number of error messages per hour over the last 24 hours, sorted chronologically by hour.', + expr: { + SQL: stripIndents`SELECT + hour(\`@timestamp\`) as hour_of_day, + COUNT(*) as error_count + FROM \`LogGroupA\` + WHERE lower(\`@message\`) LIKE '%error%' + AND \`@timestamp\` >= date_sub(current_timestamp(), 24) + GROUP BY hour(\`@timestamp\`) + ORDER BY hour_of_day`, + }, + }, + { + title: + 'Categorize and count all log messages from the last 24 hours into different log levels (ERROR, WARNING, INFO, OTHER), based on message content.', + expr: { + SQL: stripIndents`SELECT + CASE + WHEN lower(\`@message\`) LIKE '%error%' THEN 'ERROR' + WHEN lower(\`@message\`) LIKE '%warn%' THEN 'WARNING' + WHEN lower(\`@message\`) LIKE '%info%' THEN 'INFO' + ELSE 'OTHER' + END as log_level, + COUNT(*) as message_count + FROM \`LogGroupA\` + WHERE \`startTime\` >= date_sub(current_timestamp(), 1) + GROUP BY CASE + WHEN lower(\`@message\`) LIKE '%error%' THEN 'ERROR' + WHEN lower(\`@message\`) LIKE '%warn%' THEN 'WARNING' + WHEN lower(\`@message\`) LIKE '%info%' THEN 'INFO' + ELSE 'OTHER' + END + ORDER BY message_count DESC`, + }, + }, + { + title: + 'Count the number of logs per minute over the last 24 hours, and group them into one-minute time buckets and sort from newest to oldest.', + expr: { + SQL: stripIndents`SELECT + date_trunc('minute', startTime) as time_bucket, + COUNT(*) as log_count + FROM \`LogGroupA\` + WHERE startTime >= date_sub(current_timestamp(), 1) + GROUP BY date_trunc('minute', \`startTime\`) + ORDER BY time_bucket DESC`, + }, + }, + { + title: + 'Find log messages that were truncated, based on analysis of the length of the @message field in the log events.', + expr: { + SQL: stripIndents`SELECT + length(\`@message\`) as msg_length, + COUNT(*) as count, + MIN(\`@message\`) as sample_message + FROM \`LogGroupA\` + WHERE \`startTime\` >= date_sub(current_timestamp(), 1) + GROUP BY length(\`@message\`) + HAVING count > 10 + ORDER BY msg_length DESC + LIMIT 10`, + }, + }, + { + title: + 'Show the top 10 most common message lengths from the last 24 hours. It displays the length, count, and a sample message for each message length that appears more than 10 times, sorted by longest messages first.', + expr: { + SQL: 'SELECT `@logStream`, MAX(`startTime`) as last_log_time, UNIX_TIMESTAMP() - UNIX_TIMESTAMP(MAX(`startTime`)) as seconds_since_last_log FROM `LogGroupA`GROUP BY `@logStream`HAVING seconds_since_last_log > 3600 ORDER BY seconds_since_last_log DESC', + }, + }, + { + title: + 'Find duplicate log messages that occurred more than 10 times in the last 24 hours, showing their count, first and last occurrence times, and number of streams they appeared in, sorted by most frequent messages first', + expr: { + SQL: stripIndents`SELECT + \`@message\`, + COUNT(*) as occurrence_count, + MIN(\`@timestamp\`) as first_seen, + MAX(\`@timestamp\`) as last_seen, + COUNT(DISTINCT \`@logStream\`) as stream_count + FROM \`LogGroupA\` + WHERE \`@timestamp\` >= date_sub(current_timestamp(), 1) + GROUP BY \`@message\` + HAVING occurrence_count > 10 + ORDER BY occurrence_count DESC"`, + }, + }, + { + title: + 'Count unique message patterns per hour over the last 24 hours. When doing this, it considers only the first 50 characters of longer messages. Results are sorted from most recent hour to oldest.', + expr: { + SQL: stripIndents`SSELECT + date_trunc('hour', startTime) as hour_window, + COUNT(DISTINCT + CASE + WHEN length(\`@message\`) < 50 THEN substr(\`@message\`, 1, length(\`@message\`)) + ELSE substr(\`@message\`, 1, 50) + END + ) as unique_patterns + FROM \`LogGroupA\` + WHERE startTime >= date_sub(current_timestamp(), 24) + GROUP BY date_trunc('hour', startTime) + ORDER BY hour_window DESC"`, + }, + }, + { + title: + 'Calculate the success and failure rates of requests, based on occurrence of success or failure keywords in the log.', + expr: { + SQL: stripIndents`SELECT + date_trunc('minute', \`@timestamp\`) as minute, + COUNT(*) as total_requests, + SUM(CASE WHEN lower(\`@message\`) LIKE '%success%' THEN 1 ELSE 0 END) as successful_requests, + SUM(CASE WHEN lower(\`@message\`) LIKE '%fail%' OR lower(\`@message\`) LIKE '%error%' THEN 1 ELSE 0 END) as failed_requests, + ROUND(SUM(CASE WHEN lower(\`@message\`) LIKE '%success%' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2) as success_rate + FROM \`LogGroupA\` + WHERE startTime >= date_sub(current_timestamp(), 1) + GROUP BY date_trunc('minute', startTime) + ORDER BY minute DESC"`, + }, + }, + { + title: 'Identify and extract specific patterns from messages.', + expr: { + SQL: stripIndents`SELECT + \`@logStream\`, + regexp_extract(\`@message\`, '([A-Z0-9]{8})', 1) as id_pattern, + substring_index(\`@message\`, ' ', 2) as first_two_words, + left(\`@message\`, 10) as message_start, + right(\`@message\`, 10) as message_end, + length(trim(\`@message\`)) - length(replace(lower(\`@message\`), ' ', '')) + 1 as word_count + FROM \`LogGroupA\` + WHERE startTime >= date_sub(current_timestamp(), 1)"`, + }, + }, + { + title: 'Mask numbers in the log events, replacing them with asterisks.', + expr: { + SQL: stripIndents`SELECT + \`@logStream\`, + translate(\`@message\`, '{}[]()',' ') as cleaned_message, + regexp_replace(\`@message\`, '[0-9]', '*') as numbers_masked, + concat_ws(' - ', \`@logStream\`, substr(\`@message\`, 1, 50)) as combined_log, + repeat('*', length(\`@message\`)) as message_mask + FROM \`LogGroupA\` + WHERE startTime >= date_sub(current_timestamp(), 1)"`, + }, + }, + { + title: 'Find log streams that have more than 50 error logs in the last 24 hours', + expr: { + SQL: stripIndents`SELECT + \`@logStream\`, + COUNT(*) as total_logs, + COUNT(CASE WHEN lower(\`@message\`) LIKE '%error%' THEN 1 END) as error_count + FROM \`LogGroupA\` + WHERE \`@timestamp\` >= date_sub(current_timestamp(), 1) + GROUP BY \`@logStream\` + HAVING error_count > 50 + ORDER BY error_count DESC"`, + }, + }, +]; + +const pplOnlyGeneralQueries: SampleQuery[] = [ + { + title: + 'Calculate the total message length over five-minute intervals, and then find the average value across the five-minute intervals. ', + expr: { + PPL: 'eval len_message = length(`@message`) | stats count(len_message) as log_bytes by span(`@timestamp`, 5m) | stats avg(log_bytes) | head 10', + }, + }, + { + title: 'Return the top 25 most recently added log events. ', + expr: { + PPL: 'fields `@timestamp`, `@message` | sort - `@timestamp` | head 25', + }, + }, + { + title: 'Return a list of log events that are not exceptions. ', + expr: { + PPL: "eval result = LIKE(`@message`, '%Exception%') | where result = false", + }, + }, + { + title: 'Identify faults on API calls. ', + expr: { + PPL: stripIndents`"where Operation = AND Fault > 0 + | fields \`@timestamp\`, \`@logStream\` as instanceId, ExceptionMessage"`, + }, + }, + { + title: + 'Return the number of exceptions logged every five minutes using regex where exception is not case sensitive. ', + expr: { + PPL: 'eval result = LIKE(`@message`, \'%Exception%\') | where result = true | stats count() as exceptionCount by span(`@timestamp`, "5m") | sort -exceptionCount', + }, + }, + { + title: 'Parse the data and counts the number of fields. ', + expr: { + PPL: stripIndents`"eval result = LIKE(\`@message\`, 'EndTime') | eval result = true | + parse \`@message\` '.+=(?[A-Za-z]{3}), \d+' | stats count() by day | head 25"`, + }, + }, + { + title: + 'Examine message length patterns per log stream to identify potential truncation issues or abnormal logging behavior that might indicate problems. ', + expr: { + PPL: stripIndents`eval msg_length = length(\`@message\`)| stats avg(msg_length) as avg_length, max(msg_length) as max_length, min(msg_length) as min_length by \`@logStream\`| sort - avg_length`, + }, + }, + { + title: 'Analyze log volume trends over time to identify patterns and potential issues in system behavior. ', + expr: { + PPL: stripIndents`"eval date = ADDDATE(CURRENT_DATE(), -1) | eval result = TIMESTAMP(date) | where \`@timestamp\` > result | + stats count() as log_count by span(\`@timestamp\`, 1h) + | sort - log_count + | head 10"`, + }, + }, + { + title: 'Group and count error messages to identify the most frequent issues affecting the system. ', + expr: { + PPL: stripIndents`eval result = LIKE(\`@message\`, "%Error%") | where result = true | stats count() by \`@logStream\` | head 10`, + }, + }, + { + title: 'Find the top causes of error logs. ', + expr: { + PPL: stripIndents`eval result = LIKE(\`@message\`, "%Error%") | where result = true | top 2 \`@logStream\` | head 10`, + }, + }, + { + title: 'Find the log streams that contribute the least error log events. ', + expr: { + PPL: stripIndents`eval result = LIKE(\`@message\`, "%Error%") | where result = true | rare \`@logStream\` | head 10`, + }, + }, + { + title: + 'Calculate the total message length over five-minute intervals, and then find the average value across the five-minute intervals. ', + expr: { + PPL: stripIndents`eval len_message = length(\`@message\`) | stats count(len_message) as log_bytes by span(\`@timestamp\`, 5m) | stats avg(log_bytes) | head 10`, + }, + }, + { + title: 'Find the log events that are not exceptions. ', + expr: { + PPL: stripIndents`eval isException = LIKE(\`@message\`, '%exception%') | where isException = false | fields \`@logStream\`, \`@message\` | head 10`, + }, + }, + { + title: 'Return the top 25 log events sorted by timestamp. ', + expr: { + PPL: stripIndents`fields \`@logStream\`, \`@message\` | sort -\`@timestamp\` | head 25`, + }, + }, + { + title: 'Find and display the error count. ', + expr: { + PPL: stripIndents`where Operation = "x" and Fault > 0 | fields \`@timestamp\`, \`@logStream\`, ExceptionMessage | head 20`, + }, + }, +]; + +export const generalQueries: SampleQuery[] = [ + { + title: 'Find the 25 most recently added log events', + expr: { + CWLI: 'fields @timestamp, @message | sort @timestamp desc | limit 25', + SQL: stripIndents`SELECT \`@timestamp\`, \`@message\` + FROM \`log_group\` + ORDER BY \`@timestamp\` DESC + LIMIT 25`, + PPL: stripIndents`fields \`@timestamp\`, \`@message\` +| sort - \`@timestamp\` +| head 25 +`, + }, + }, + { + title: 'Get a list of the number of exceptions per hour', + expr: { + CWLI: stripIndents`filter @message like /Exception/ +| stats count(*) as exceptionCount by bin(1h) +| sort exceptionCount desc`, + SQL: stripIndents`SELECT window.start, COUNT(*) AS exceptionCount +FROM \`log_group\` +WHERE \`@message\` LIKE '%Exception%' +GROUP BY window(\`@timestamp\`, '1 hour') +ORDER BY exceptionCount DESC`, + PPL: stripIndents`where abs(\`@message\`, "%Exception%") +| stats count() as exceptionCount by span(\`@timestamp\`, 1h) +| sort - exceptionCount`, + }, + }, + { + title: "Get a list of log events that aren't exceptions.", + expr: { + CWLI: 'fields @message | filter @message not like /Exception/', + SQL: stripIndents`SELECT \`@message\` +FROM \`log_group\` +WHERE \`@message\` NOT LIKE '%Exception%'`, + PPL: stripIndents`fields \`@message\` +| where not like(\`@message\`, "%Exception%")`, + }, + }, + { + title: 'Get the most recent log event for each unique value of the server field', + expr: { + CWLI: stripIndents`fields @timestamp, server, severity, message +| sort @timestamp asc +| dedup server`, + PPL: stripIndents`fields \`@timestamp\`, server, severity, message +| sort \`@timestamp\` +| dedup server`, + }, + }, + { + title: 'Get the most recent log event for each unique value of the server field for each severity type', + expr: { + CWLI: stripIndents`fields @timestamp, server, severity, message +| sort @timestamp desc +| dedup server, severity`, + PPL: stripIndents`fields \`@timestamp\`, server, severity, message +| sort - \`@timestamp\` +| dedup server`, + }, + }, + { + title: 'Number of exceptions logged every 5 minutes', + expr: { + CWLI: 'filter @message like /Exception/ | stats count(*) as exceptionCount by bin(5m) | sort exceptionCount desc', + SQL: stripIndents`SELECT window.start, COUNT(*) AS exceptionCount +FROM \`log_group\` +WHERE \`@message\` LIKE '%Exception%' +GROUP BY window(\`@timestamp\`, '5 minute') +ORDER BY exceptionCount DESC`, + PPL: stripIndents`where like(\`@message\`, "%Exception%") +| stats count() as exceptionCount by span(\`@timestamp\`, 5m) +| sort - exceptionCount`, + }, + }, + ...sqlOnlyGeneralQueries, + ...pplOnlyGeneralQueries, +]; + +export const lambdaSamples: SampleQuery[] = [ + { + title: 'View latency statistics for 5-minute intervals', + expr: { + CWLI: stripIndents`filter @type = "REPORT" | + stats avg(@duration), max(@duration), min(@duration) by bin(5m)`, + SQL: stripIndents`SELECT window.start, AVG(\`@duration\`) AS averageDuration, + MAX(\`@duration\`) AS maxDuration, + MIN(\`@duration\`) AS minDuration + FROM \`log_group\` + WHERE \`@type\` = 'REPORT' + GROUP BY window(\`@timestamp\`, '5 minute')`, + }, + }, + { + title: 'Determine the amount of overprovisioned memory', + expr: { + CWLI: stripIndents`filter @type = "REPORT" + | stats max(@memorySize / 1000 / 1000) as provisionedMemoryMB, + min(@maxMemoryUsed / 1000 / 1000) as smallestMemoryRequestMB, + avg(@maxMemoryUsed / 1000 / 1000) as avgMemoryUsedMB, + max(@maxMemoryUsed / 1000 / 1000) as maxMemoryUsedMB, + provisionedMemoryMB - maxMemoryUsedMB as overProvisionedMB + `, + SQL: stripIndents`SELECT MAX(\`@memorySize\` / 1000 / 1000) AS provisonedMemoryMB, + MIN(\`@maxMemoryUsed\` / 1000 / 1000) AS smallestMemoryRequestMB, + AVG(\`@maxMemoryUsed\` / 1000 / 1000) AS avgMemoryUsedMB, + MAX(\`@maxMemoryUsed\` / 1000 / 1000) AS maxMemoryUsedMB, + MAX(\`@memorySize\` / 1000 / 1000) - MAX(\`@maxMemoryUsed\` / 1000 / 1000) AS overProvisionedMB + FROM \`log_group\` + WHERE \`@type\` = 'REPORT'`, + }, + }, + { + title: 'Find the most expensive requests', + expr: { + CWLI: stripIndents`filter @type = "REPORT" + | fields @requestId, @billedDuration + | sort by @billedDuration desc`, + SQL: stripIndents`SELECT\`@requestId\`, \`@billedDuration\` + FROM \`log_group\` + WHERE \`@type\` = 'REPORT' + ORDER BY \`@billedDuration\` DESC`, + PPL: stripIndents`where \`@type\` = 'REPORT' + | fields \`@requestId\`, \`@billedDuration\` + | sort - \`@billedDuration\``, + }, + }, +]; + +export const vpcSamples: SampleQuery[] = [ + { + title: 'Find the top 15 packet transfers across hosts', + expr: { + CWLI: stripIndents`stats sum(packets) as packetsTransferred by srcAddr, dstAddr + | sort packetsTransferred desc + | limit 15`, + SQL: stripIndents`SELECT \`srcAddr\`, \`dstAddr\`, SUM(\`packets\`) AS packetsTransferred + FROM \`log_group\` + GROUP BY \`srcAddr\`, \`dstAddr\` + ORDER BY packetsTransferred DESC + LIMIT 15;`, + }, + }, + + { + title: 'Find the IP addresses that use UDP as a data transfer protocol', + expr: { + CWLI: 'filter protocol=17 | stats count(*) by srcAddr', + SQL: stripIndents`SELECT \`srcAddr\`, COUNT(*) AS totalCount + FROM \`log_group\` + WHERE \`protocol\` = 17 + GROUP BY srcAddr;`, + }, + }, + { + title: 'Find the IP addresses where flow records were skipped during the capture window', + expr: { + CWLI: stripIndents`filter logStatus="SKIPDATA" + | stats count(*) by bin(1h) as t + | sort t`, + SQL: stripIndents`SELECT window.start, COUNT(*) AS totalCount + FROM \`log_group\` + WHERE \`logStatus\` = 'SKIPDATA' + GROUP BY window(\`@timestamp\`, '1 minute') + ORDER BY window.start`, + PPL: stripIndents`where logStatus="SKIPDATA" + | stats count() by span(\`@timestamp\`, 1h) as t + | sort t`, + }, + }, + { + title: 'Average, min, and max byte transfers by source and destination IP addresses', + expr: { + CWLI: 'stats sum(bytes) as bytesTransferred by srcAddr, dstAddr | sort bytesTransferred desc | limit 10', + SQL: stripIndents`SELECT \`srcAddr\`, \`dstAddr\`, AVG(\`bytes\`), + MIN(\`bytes\`), MAX(\`bytes\`) + FROM \`log_group\` + GROUP BY \`srcAddr\`, \`dstAddr\``, + }, + }, + + { + title: 'Top 10 byte transfers by source and destination IP addresses', + expr: { + CWLI: 'stats sum(bytes) as bytesTransferred by srcAddr, dstAddr | sort bytesTransferred desc | limit 10', + SQL: stripIndents`SELECT \`srcAddr\`, \`dstAddr\`, SUM(\`bytes\`) as bytesTransferred + FROM \`log_group\` + GROUP BY \`srcAddr\`, \`dstAddr\` + ORDER BY bytesTransferred DESC + LIMIT 10`, + }, + }, + { + title: 'Top 20 source IP addresses with highest number of rejected requests', + expr: { + CWLI: 'filter action="REJECT" | stats count(*) as numRejections by srcAddr | sort numRejections desc | limit 20', + SQL: stripIndents`SELECT \`srcAddr\`, COUNT(*) AS numRejections + FROM \`log_group\` + WHERE \`action\` = 'REJECT' + GROUP BY \`srcAddr\` + ORDER BY numRejections DESC + LIMIT 20`, + }, + }, + { + title: 'Find the 10 DNS resolvers with the highest number of requests.', + expr: { + CWLI: stripIndents`stats count(*) as numRequests by resolverIp + | sort numRequests desc + | limit 10`, + SQL: stripIndents`SELECT \`resolverIp\`, COUNT(*) AS numRequests + FROM \`log_group\` + GROUP BY \`resolverIp\` + ORDER BY numRequests DESC + LIMIT 10`, + }, + }, + { + title: 'Find the number of records by domain and subdomain where the server failed to complete the DNS request.', + expr: { + CWLI: stripIndents`filter responseCode="SERVFAIL" | stats count(*) by queryName`, + SQL: stripIndents`SELECT \`queryName\`, COUNT(*) + FROM \`log_group\` + WHERE \`responseCode\` = 'SERVFAIL' + GROUP BY \`queryName\``, + PPL: stripIndents`where \`responseCode\` = 'SERVFAIL' + | stats count() by \`queryName\``, + }, + }, + { + title: 'Number of requests received every 10 minutes by edge location', + expr: { + CWLI: 'stats count(*) by queryType, bin(10m)', + SQL: stripIndents`SELECT window.start, \`queryType\`, + COUNT(*) AS totalCount + FROM \`log_group\` + GROUP BY window(\`@timestamp\`, '10 minute'), \`queryType\``, + PPL: 'stats count() by queryType, span(`@timestamp`, 10m)', + }, + }, +]; + +export const cloudtrailSamples: SampleQuery[] = [ + { + title: 'Find the number of log entries for each service, event type, and AWS Region', + expr: { + CWLI: 'stats count(*) by eventSource, eventName, awsRegion', + PPL: 'stats count() by `eventSource`, `eventName`, `awsRegion`', + SQL: stripIndents`SELECT \`eventSource\`, \`eventName\`, + \`awsRegion\`, COUNT(*) + FROM \`log_group\` + GROUP BY \`eventSource\`, \`eventName\`, + \`awsRegion\``, + }, + }, + { + title: 'Find the Amazon EC2 hosts that were started or stopped in a given AWS Region', + expr: { + CWLI: 'filter (eventName="StartInstances" or eventName="StopInstances") and awsRegion="us-east-2', + PPL: stripIndents`where \`eventName\` = 'StartInstances' + OR \`eventName\` = 'StopInstances' + AND \`awsRegion\` = 'us-east-2'`, + SQL: stripIndents`SELECT \`@timestamp\`, \`@message\` + FROM \`log_group\` + WHERE \`eventName\` = 'StartInstances' + OR \`eventName\` = 'StopInstances' + AND \`awsRegion\` = 'us-east-2'`, + }, + }, + { + title: 'Find the AWS Regions, user names, and ARNs of newly created IAM users', + expr: { + CWLI: stripIndents`filter eventName="CreateUser" + | fields awsRegion, requestParameters.userName, responseElements.user.arn`, + PPL: stripIndents`where \`eventName\` = 'CreateUser' + | fields \`awsRegion\`, \`requestParameters.userName\`, \`responseElements.user.arn\``, + SQL: stripIndents`SELECT \`awsRegion\`, \`requestParameters.userName\`, + \`responseElements.user.arn\` + FROM \`log_group\` + WHERE \`eventName\` = 'CreateUser'`, + }, + }, + { + title: 'Find the number of records where an exception occurred while invoking the API UpdateTrail', + expr: { + CWLI: stripIndents`filter eventName="UpdateTrail" and ispresent(errorCode) + | stats count(*) by errorCode, errorMessage`, + PPL: stripIndents`where eventName = "UpdateTrail" and isnotnull(errorCode) + | stats count() by errorCode, errorMessage`, + SQL: stripIndents`SELECT \`errorCode\`, \`errorMessage\`, COUNT(*) + FROM \`log_group\` + WHERE \`eventName\` = 'UpdateTrail' + AND \`errorCode\` IS NOT NULL + GROUP BY \`errorCode\`, \`errorMessage\``, + }, + }, + { + title: 'Find log entries where TLS 1.0 or 1.1 was used', + expr: { + CWLI: stripIndents`filter tlsDetails.tlsVersion in [ "TLSv1", "TLSv1.1" ] + | stats count(*) as numOutdatedTlsCalls by userIdentity.accountId, recipientAccountId, eventSource, eventName, awsRegion, tlsDetails.tlsVersion, tlsDetails.cipherSuite, userAgent + | sort eventSource, eventName, awsRegion, tlsDetails.tlsVersion`, + PPL: stripIndents`where tlsDetails.tlsVersion in ('TLSv1', 'TLSv1.1') + | stats count() as numOutdatedTlsCalls by + \`userIdentity.accountId\`, \`recipientAccountId\`, + \`eventSource\`, \`eventName\`, \`awsRegion\` + \`tlsDetails.tlsVersion\`, \`tlsDetails.cipherSuite\` + \`userAgent\` + | sort \`eventSource\`, \`eventName\`, \`awsRegion\`, \`tlsDetails.tlsVersion\``, + SQL: stripIndents`SELECT \`userIdentity.accountId\`, \`recipientAccountId\`, \`eventSource\`, + \`eventName\`, \`awsRegion\`, \`tlsDetails.tlsVersion\`, + \`tlsDetails.cipherSuite\`, \`userAgent\`, COUNT(*) AS numOutdatedTlsCalls + FROM \`log_group\` + WHERE \`tlsDetails.tlsVersion\` IN ('TLSv1', 'TLSv1.1') + GROUP BY \`userIdentity.accountId\`, \`recipientAccountId\`, \`eventSource\`, + \`eventName\`, \`awsRegion\`, \`tlsDetails.tlsVersion\`, + \`tlsDetails.cipherSuite\`, \`userAgent\` + ORDER BY \`eventSource\`, \`eventName\`, \`awsRegion\`, \`tlsDetails.tlsVersion\``, + }, + }, + { + title: 'Find the number of calls per service that used TLS versions 1.0 or 1.1', + expr: { + CWLI: stripIndents`filter tlsDetails.tlsVersion in [ "TLSv1", "TLSv1.1" ] + | stats count(*) as numOutdatedTlsCalls by eventSource + | sort numOutdatedTlsCalls desc`, + PPL: stripIndents`where \`tlsDetails.tlsVersion\` in ('TLSv1', 'TLSv1.1') + | stats count() as numOutdatedTlsCalls by \`eventSource\` + | sort - numOutdatedTlsCalls`, + SQL: stripIndents`SELECT \`eventSource\`, COUNT(*) AS numOutdatedTlsCalls + FROM \`log_group\` + WHERE \`tlsDetails.tlsVersion\` IN ('TLSv1', 'TLSv1.1') + GROUP BY \`eventSource\` + ORDER BY numOutdatedTlsCalls DESC`, + }, + }, + { + title: 'Number of log entries by region and EC2 event type', + expr: { + CWLI: 'filter eventSource="ec2.amazonaws.com" | stats count(*) as eventCount by eventName, awsRegion | sort eventCount desc', + PPL: stripIndents`where \`eventSource\` = 'ec2.amazonaws.com' + | stats count() as eventCount by \`eventName\`, \`awsRegion\` + | sort - eventCount + `, + SQL: stripIndents`SELECT \`eventName\`, \`awsRegion\`, + COUNT(*) AS eventCount + FROM \`log_group\` + WHERE \`eventSource\` = 'ec2.amazonaws.com' + GROUP BY \`eventName\`, \`awsRegion\` + ORDER BY eventCount DESC`, + }, + }, +]; +export const natSamples: SampleQuery[] = [ + { + title: 'Find the instances that are sending the most traffic through your NAT gateway', + expr: { + CWLI: stripIndents`filter (dstAddr like 'x.x.x.x' and srcAddr like 'y.y.') + | stats sum(bytes) as bytesTransferred by srcAddr, dstAddr + | sort bytesTransferred desc + | limit 10`, + PPL: stripIndents`where like(dstAddr, "x.x.x.x") and like(srcAddr like "y.y.") + | stats sum(bytes) as bytesTransferred by srcAddr, dstAddr + | sort - bytesTransferred + | head 10`, + SQL: stripIndents`SELECT \`srcAddr\`, \`dstAddr\`, + SUM(\`bytes\`) AS bytesTransferred + FROM \`log_group\` + WHERE \`dstAddr\` LIKE 'x.x.x.x' + AND \`srcAddr\` LIKE \`y.y.%\` + GROUP BY \`srcAddr\`, \`dstAddr\` + ORDER BY bytesTransferred DESC + LIMIT 10`, + }, + }, + { + title: "Determine the traffic that's going to and from the instances in your NAT gateways", + expr: { + CWLI: stripIndents`filter (dstAddr like 'x.x.x.x' and srcAddr like 'y.y.') or (srcAddr like 'xxx.xx.xx.xx' and dstAddr like 'y.y.') + | stats sum(bytes) as bytesTransferred by srcAddr, dstAddr + | sort bytesTransferred desc + | limit 10`, + PPL: stripIndents`where (like(dstAddr, "x.x.x.x") and like(srcAddr, "y.y.")) or (like(srcAddr, "xxx.xx.xx.xx") and like(dstAddr, "y.y.") + | stats sum(bytes) as bytesTransferred by srcAddr, dstAddr + | sort - bytesTransferred + | limit 10`, + SQL: stripIndents`SELECT \`srcAddr\`, \`dstAddr\`, + SUM (\`bytes\`) AS bytesTransferred + FROM \`log_group\` + WHERE (\`dstAddr\` LIKE 'x.x.x.x' AND \`srcAddr\` LIKE 'y.y.%') + OR (\`srcAddr\` LIKE 'xxx.xx.xx.xx' AND \`dstAddr\` LIKE 'y.y.%') + GROUP BY \`srcAddr\`, \`dstAddr\` + ORDER BY \`bytesTransferred\` DESC + LIMIT 10`, + }, + }, + { + title: + 'Determine the internet destinations that the instances in your VPC communicate with most often for uploads and downloads - for uploads', + expr: { + CWLI: stripIndents`filter (srcAddr like 'x.x.x.x' and dstAddr not like 'y.y.') + | stats sum(bytes) as bytesTransferred by srcAddr, dstAddr + | sort bytesTransferred desc + | limit 10`, + PPL: stripIndents`where like(srcAddr like "y.y.") and not like(dstAddr, "x.x.x.x") + | stats sum(bytes) as bytesTransferred by srcAddr, dstAddr + | sort - bytesTransferred + | head 10`, + SQL: stripIndents`SELECT \`srcAddr\`, \`dstAddr\`, + SUM(\`bytes\`) AS bytesTransferred + FROM \`log_group\` + WHERE \`srcAddr\` LIKE 'x.x.x.x' + AND \`dstAddr\` NOT LIKE \`y.y.%\` + GROUP BY \`srcAddr\`, \`dstAddr\` + ORDER BY bytesTransferred DESC + LIMIT 10`, + }, + }, + { + title: + 'Determine the internet destinations that the instances in your VPC communicate with most often for uploads and downloads - for downloads', + expr: { + CWLI: stripIndents`filter (dstAddr like 'x.x.x.x' and srcAddr not like 'y.y.') + | stats sum(bytes) as bytesTransferred by srcAddr, dstAddr + | sort bytesTransferred desc + | limit 10`, + PPL: stripIndents`where like(dstAddr, "x.x.x.x") and not like(srcAddr like "y.y.") + | stats sum(bytes) as bytesTransferred by srcAddr, dstAddr + | sort - bytesTransferred + | head 10`, + SQL: stripIndents`SELECT \`srcAddr\`, \`dstAddr\`, + SUM(\`bytes\`) AS bytesTransferred + FROM \`log_group\` + WHERE \`dstAddr\` LIKE 'x.x.x.x' + AND \`srcAddr\` NOT LIKE \`y.y.%\` + GROUP BY \`srcAddr\`, \`dstAddr\` + ORDER BY bytesTransferred DESC + LIMIT 10`, + }, + }, +]; + +export const appSyncSamples: SampleQuery[] = [ + { + title: 'Number of unique HTTP status codes', + expr: { + CWLI: 'fields ispresent(graphQLAPIId) as isApi | filter isApi | filter logType = "RequestSummary" | stats count() as statusCount by statusCode | sort statusCount desc', + SQL: stripIndents`SELECT \`graphQLAPIId\`, \`statusCode\`, + COUNT(*) AS statusCount + FROM \`log_group\` + WHERE \`logType\` = 'RequestSummary' + AND \`graphQLAPIId\` IS NOT NULL + GROUP BY \`graphQLAPIId\`, \`statusCode\` + ORDER BY statusCount DESC`, + }, + }, + { + title: 'Most frequently invoked resolvers', + expr: { + CWLI: 'fields ispresent(resolverArn) as isRes | stats count() as invocationCount by resolverArn | filter isRes | filter logType = "Tracing" | sort invocationCount desc | limit 10', + PPL: stripIndents`where \`logType\` = 'Tracing' + | fields \`resolverArn\`, \`duration\` + | sort - duration + | head 10`, + SQL: stripIndents`SELECT \`resolverArn\`, COUNT(*) AS invocationCount + FROM \`log_group\` + WHERE \`logType\` = 'Tracing' + AND \`resolverArn\` IS NOT NULL + GROUP BY \`resolverArn\` + ORDER BY invocationCount DESC + LIMIT 10`, + }, + }, + { + title: 'Top 10 resolvers with maximum latency', + expr: { + CWLI: 'fields resolverArn, duration | filter logType = "Tracing" | sort duration desc | limit 10', + PPL: stripIndents`where \`logType\` = 'Tracing' + | fields \`resolverArn\`, \`duration\` + | sort - duration + | head 10`, + SQL: stripIndents`SELECT \`resolverArn\`, \`duration\` + FROM \`log_group\` + WHERE \`logType\` = 'Tracing' + ORDER BY \`duration\` DESC + LIMIT 10`, + }, + }, + { + title: 'Resolvers with most errors in mapping templates', + expr: { + CWLI: 'fields ispresent(resolverArn) as isRes | stats count() as errorCount by resolverArn, logType | filter isRes and (logType = "RequestMapping" or logType = "ResponseMapping") and fieldInError | sort errorCount desc | limit 10', + SQL: stripIndents`SELECT resolverArn, COUNT(*) AS errorCount + FROM \`log_group\` + WHERE ISNOTNULL(resolverArn) AND (logType = "RequestMapping" OR logType = "ResponseMapping") AND fieldInError + GROUP BY resolverArn + ORDER BY errorCount DESC + LIMIT 10`, + }, + }, + { + title: 'Field latency statistics', + expr: { + CWLI: `stats min(duration), max(duration), avg(duration) as avgDur by concat(parentType, '/', fieldName) as fieldKey | filter logType = "Tracing" | sort avgDur desc | limit 10`, + SQL: stripIndents`SELECT CONCAT(parentType, "/", fieldName) AS fieldKey, MIN(duration), MAX(duration), AVG(duration) as avgDur + FROM \`log_group\` + ORDER BY fieldKey + WHERE logType="Tracing" + SORTY BY avgDur DESC + LIMIT 10`, + }, + }, + { + title: 'Resolver latency statistics', + expr: { + CWLI: 'fields ispresent(resolverArn) as isRes | filter isRes | filter logType = "Tracing" | stats min(duration), max(duration), avg(duration) as avgDur by resolverArn | sort avgDur desc | limit 10 ', + SQL: stripIndents`SELECT \`resolverArn\`, MIN(\`duration\`), + MAX(\`duration\`), AVG(\`duration\`) as avgDur + FROM \`log_group\` + WHERE \`resolverArn\` IS NOT NULL + AND \`logType\` = 'Tracing' + GROUP BY \`resolverArn\` + ORDER BY avgDur DESC + LIMIT 10`, + }, + }, + { + title: 'Top 10 requests with maximum latency', + expr: { + CWLI: 'fields requestId, latency | filter logType = "RequestSummary" | sort latency desc | limit 10', + PPL: stripIndents`where \`logType\` = 'RequestSummary' + | fields \`requestId\`, \`latency\` + | sort - \`latency\` + | head 10`, + SQL: stripIndents`SELECT \`requestId\`, \`latency\` + FROM \`log_group\` + WHERE \`logType\` = 'RequestSummary' + ORDER BY \`latency\` DESC + LIMIT 10`, + }, + }, +]; + +export const iotSamples = [ + { + title: 'Count IoT Events and status including errors', + expr: { + CWLI: 'fields @timestamp, @message | stats count(*) by eventType, status', + SQL: stripIndents`SELECT \`eventType\`, \`status\`, COUNT(*) + FROM \`log_group\` + GROUP BY \`eventType\`, \`status\``, + }, + }, + { + title: 'Count of Disconnect reasons', + expr: { + CWLI: 'filter eventType="Disconnect" | stats count(*) by disconnectReason | sort disconnectReason desc', + PPL: stripIndents`where \`eventType\` = \`Disconnect\` + | stats count() by \`disconnectReason\` + | sort - \`disconnectReason\``, + SQL: stripIndents`SELECT \`disconnectReason\`, COUNT(*) + FROM \`log_group\` + WHERE \`eventType\` = 'Disconnect' + GROUP BY \`disconnectReason\` + ORDER BY \`disconnectReason\` DESC`, + }, + }, + { + title: 'Top 50 devices with Duplicate ClientId disconnect error', + expr: { + CWLI: 'filter eventType="Disconnect" and disconnectReason="DUPLICATE_CLIENTID" | stats count(*) by clientId | sort numPublishIn desc | limit 50', + SQL: stripIndents`SELECT \`clientId\`, COUNT(*) AS duplicateCount + FROM \`log_group\` + WHERE \`eventType\` = 'Disconnect' + AND \`disconnectReason\` = 'DUPLICATE_CLIENTID' + GROUP BY \`clientId\` + ORDER BY duplicateCount DESC + LIMIT 50`, + }, + }, + { + title: 'Top 10 failed connections by ClientId', + expr: { + CWLI: 'filter eventType="Connect" and status="Failure" | stats count(*) by clientId | sort numPublishIn desc | limit 10', + SQL: stripIndents`SELECT \`clientId\`, COUNT(*) AS failedConnectionCount + FROM \`log_group\` + WHERE \`eventType\` = 'Connect' + AND \`status\` = 'Failure' + GROUP BY \`clientId\` + ORDER BY failedConnectionCount DESC + LIMIT 10`, + }, + }, + { + title: 'Connectivity activity for a device', + expr: { + CWLI: 'fields @timestamp, eventType, reason, clientId | filter clientId like /sampleClientID/ | filter eventType like /Connect|Disconnect/ | sort @timestamp desc | limit 20', + PPL: stripIndents`fields \`@timestamp\`, eventType, reason, clientId + | where like(clientId, "%sampleClientID%") + | where like(eventType, "%Connect%") or like(eventType, "%Disconnect%") + | sort - \`@timestamp\` + | head 20`, + SQL: stripIndents`SELECT \`@timestamp\`, \`eventType\`, + \`reason\`, \`clientId\` + FROM \`log_group\` + WHERE \`clientId\` LIKE '%sampleClientID%' + AND \`eventType\` LIKE ANY ('%Connect%', '%Disconnect%') + ORDER BY \`@timestamp\` DESC + LIMIT 20`, + }, + }, + { + title: 'View messages published to a topic', + expr: { + CWLI: 'fields @timestamp, @message | sort @timestamp desc | filter ( eventType="Publish-In" ) and topicName like \'your/topic/here\'', + PPL: stripIndents`fields \`@timestamp\`, \`@message\` + | where eventType = "Publish-In" and like(topicName, "%your/topic/here%") + | sort - \`@timestamp\``, + SQL: stripIndents`SELECT \`@timestamp\`, \`@message\` + FROM \`log_group\` + WHERE \`eventType\` = 'Publish-In' + AND \`topicName\` LIKE '%your/topic/here%'`, + }, + }, +]; diff --git a/public/app/plugins/datasource/cloudwatch/components/CheatSheet/tokenizer.ts b/public/app/plugins/datasource/cloudwatch/components/CheatSheet/tokenizer.ts new file mode 100644 index 00000000000..3eab6d400c0 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/components/CheatSheet/tokenizer.ts @@ -0,0 +1,94 @@ +// import { Grammar } from 'prismjs'; + +import { Grammar } from 'prismjs'; + +import { FUNCTIONS, KEYWORDS, QUERY_COMMANDS } from '../../language/cloudwatch-logs/syntax'; +import * as sql from '../../language/cloudwatch-logs-sql/language'; +import * as ppl from '../../language/cloudwatch-ppl/language'; + +export const baseTokenizer = (languageSpecificFeatures: Grammar): Grammar => ({ + comment: { + pattern: /^#.*/, + greedy: true, + }, + backticks: { + pattern: /`.*?`/, + alias: 'string', + greedy: true, + }, + quote: { + pattern: /[\"'].*?[\"']/, + alias: 'string', + greedy: true, + }, + regex: { + pattern: /\/.*?\/(?=\||\s*$|,)/, + greedy: true, + }, + ...languageSpecificFeatures, + + 'field-name': { + pattern: /(@?[_a-zA-Z]+[_.0-9a-zA-Z]*)|(`((\\`)|([^`]))*?`)/, + greedy: true, + }, + number: /\b-?\d+((\.\d*)?([eE][+-]?\d+)?)?\b/, + 'command-separator': { + pattern: /\|/, + alias: 'punctuation', + }, + 'comparison-operator': { + pattern: /([<>]=?)|(!?=)/, + }, + punctuation: /[{}()`,.]/, + whitespace: /\s+/, +}); + +export const cwliTokenizer: Grammar = { + ...baseTokenizer({ + 'query-command': { + pattern: new RegExp(`\\b(?:${QUERY_COMMANDS.map((command) => command.label).join('|')})\\b`, 'i'), + alias: 'function', + }, + function: { + pattern: new RegExp(`\\b(?:${FUNCTIONS.map((f) => f.label).join('|')})\\b`, 'i'), + }, + keyword: { + pattern: new RegExp(`(\\s+)(${KEYWORDS.join('|')})(?=\\s+)`, 'i'), + lookbehind: true, + }, + }), +}; + +export const pplTokenizer: Grammar = { + ...baseTokenizer({ + 'query-command': { + pattern: new RegExp(`\\b(?:${ppl.PPL_COMMANDS.join('|')})\\b`, 'i'), + alias: 'function', + }, + function: { + pattern: new RegExp(`\\b(?:${ppl.ALL_FUNCTIONS.join('|')})\\b`, 'i'), + }, + keyword: { + pattern: new RegExp(`(\\s+)(${ppl.ALL_KEYWORDS.join('|')})(?=\\s+)`, 'i'), + lookbehind: true, + }, + operator: { + pattern: new RegExp(`\\b(?:${ppl.PPL_OPERATORS.map((operator) => `\\${operator}`).join('|')})\\b`, 'i'), + }, + }), +}; + +export const sqlTokenizer = { + ...baseTokenizer({ + function: { + pattern: new RegExp(`\\b(?:${sql.ALL_FUNCTIONS.join('|')})\\b(?!\\.)`, 'i'), + }, + keyword: { + pattern: new RegExp(`\\b(?:${sql.ALL_KEYWORDS.join('|')})\\b(?=\\s)`, 'i'), + lookbehind: true, + }, + operator: { + pattern: new RegExp(`\\b(?:${sql.ALL_OPERATORS.map((operator) => `\\${operator}`).join('|')})\\b`, 'i'), + }, + }), +}; diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/CloudWatchLink.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/CloudWatchLink.tsx index 5d07b54aed5..957fe7981ca 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/CloudWatchLink.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/CloudWatchLink.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { usePrevious } from 'react-use'; import { PanelData } from '@grafana/data'; -import { Icon } from '@grafana/ui'; +import { LinkButton } from '@grafana/ui'; import { AwsUrl, encodeUrl } from '../../../aws_url'; import { CloudWatchDatasource } from '../../../datasource'; @@ -45,8 +45,8 @@ export function CloudWatchLink({ panelData, query, datasource }: Props) { }, [panelData, prevPanelData, datasource, query]); return ( - - CloudWatch Logs Insights - + + CloudWatch Logs Insights + ); } diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryEditor.tsx index d98522b8990..4374633807a 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryEditor.tsx @@ -1,37 +1,102 @@ -import { css } from '@emotion/css'; -import { memo } from 'react'; +import { memo, useCallback, useEffect, useState } from 'react'; +import { useEffectOnce } from 'react-use'; -import { QueryEditorProps } from '@grafana/data'; -import { InlineFormLabel } from '@grafana/ui'; +import { QueryEditorProps, SelectableValue } from '@grafana/data'; +import { InlineSelect } from '@grafana/experimental'; import { CloudWatchDatasource } from '../../../datasource'; -import { CloudWatchJsonData, CloudWatchLogsQuery, CloudWatchQuery } from '../../../types'; +import { DEFAULT_CWLI_QUERY_STRING, DEFAULT_PPL_QUERY_STRING, DEFAULT_SQL_QUERY_STRING } from '../../../defaultQueries'; +import { CloudWatchJsonData, CloudWatchLogsQuery, CloudWatchQuery, LogsQueryLanguage } from '../../../types'; import { CloudWatchLink } from './CloudWatchLink'; -import CloudWatchLogsQueryField from './LogsQueryField'; +import { CloudWatchLogsQueryField } from './LogsQueryField'; type Props = QueryEditorProps & { query: CloudWatchLogsQuery; + extraHeaderElementLeft?: React.Dispatch; }; -const labelClass = css({ - marginLeft: '3px', - flexGrow: 0, -}); +const logsQueryLanguageOptions: Array> = [ + { label: 'Logs Insights QL', value: LogsQueryLanguage.CWLI }, + { label: 'OpenSearch SQL', value: LogsQueryLanguage.SQL }, + { label: 'OpenSearch PPL', value: LogsQueryLanguage.PPL }, +]; export const CloudWatchLogsQueryEditor = memo(function CloudWatchLogsQueryEditor(props: Props) { - const { query, data, datasource } = props; + const { query, data, datasource, onChange, extraHeaderElementLeft } = props; + + const [isQueryNew, setIsQueryNew] = useState(true); + + const onQueryLanguageChange = useCallback( + (language: LogsQueryLanguage | undefined) => { + if (isQueryNew) { + onChange({ + ...query, + expression: getDefaultQueryString(language), + queryLanguage: language ?? LogsQueryLanguage.CWLI, + }); + } else { + onChange({ ...query, queryLanguage: language ?? LogsQueryLanguage.CWLI }); + } + }, + [isQueryNew, onChange, query] + ); + + // if the query has already been saved from before, we shouldn't replace it with a default one + useEffectOnce(() => { + if (query.expression) { + setIsQueryNew(false); + } + }); + + useEffect(() => { + // if it's a new query, we should replace it with a default one + if (isQueryNew && !query.expression) { + onChange({ ...query, expression: getDefaultQueryString(query.queryLanguage) }); + } + }, [onChange, query, isQueryNew]); + + useEffect(() => { + extraHeaderElementLeft?.( + { + onQueryLanguageChange(value); + }} + /> + ); + + return () => { + extraHeaderElementLeft?.(undefined); + }; + }, [extraHeaderElementLeft, onChange, onQueryLanguageChange, query]); + + const onQueryStringChange = (query: CloudWatchQuery) => { + onChange(query); + setIsQueryNew(false); + }; return ( - - - } + onChange={onQueryStringChange} + ExtraFieldElement={} /> ); }); export default CloudWatchLogsQueryEditor; + +const getDefaultQueryString = (language: LogsQueryLanguage | undefined) => { + switch (language) { + case LogsQueryLanguage.SQL: + return DEFAULT_SQL_QUERY_STRING; + case LogsQueryLanguage.PPL: + return DEFAULT_PPL_QUERY_STRING; + case LogsQueryLanguage.CWLI: + default: + return DEFAULT_CWLI_QUERY_STRING; + } +}; diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryField.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryField.tsx index 2527565821e..111084dbb1e 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryField.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryField.tsx @@ -1,28 +1,26 @@ -import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api'; -import { ReactNode, useCallback, useRef } from 'react'; +import { css } from '@emotion/css'; +import { ReactNode, useCallback } from 'react'; -import { QueryEditorProps } from '@grafana/data'; -import { CodeEditor, Monaco, Themeable2, withTheme2 } from '@grafana/ui'; +import { GrafanaTheme2, QueryEditorProps } from '@grafana/data'; +import { useStyles2 } from '@grafana/ui'; import { CloudWatchDatasource } from '../../../datasource'; -import language from '../../../language/logs/definition'; -import { TRIGGER_SUGGEST } from '../../../language/monarch/commands'; -import { registerLanguage, reRegisterCompletionProvider } from '../../../language/monarch/register'; -import { CloudWatchJsonData, CloudWatchLogsQuery, CloudWatchQuery } from '../../../types'; -import { getStatsGroups } from '../../../utils/query/getStatsGroups'; +import { CloudWatchJsonData, CloudWatchLogsQuery, CloudWatchQuery, LogsQueryLanguage } from '../../../types'; import { LogGroupsFieldWrapper } from '../../shared/LogGroups/LogGroupsField'; +import { LogsQLCodeEditor } from './code-editors/LogsQLCodeEditor'; +import { PPLQueryEditor } from './code-editors/PPLQueryEditor'; +import { SQLQueryEditor } from './code-editors/SQLCodeEditor'; + export interface CloudWatchLogsQueryFieldProps - extends QueryEditorProps, - Themeable2 { + extends QueryEditorProps { ExtraFieldElement?: ReactNode; query: CloudWatchLogsQuery; } export const CloudWatchLogsQueryField = (props: CloudWatchLogsQueryFieldProps) => { const { query, datasource, onChange, ExtraFieldElement } = props; - const monacoRef = useRef(); - const disposalRef = useRef(); + const styles = useStyles2(getStyles); const onChangeLogs = useCallback( async (query: CloudWatchLogsQuery) => { @@ -31,51 +29,6 @@ export const CloudWatchLogsQueryField = (props: CloudWatchLogsQueryFieldProps) = [onChange] ); - const onFocus = useCallback(async () => { - disposalRef.current = await reRegisterCompletionProvider( - monacoRef.current!, - language, - datasource.logsCompletionItemProviderFunc({ - region: query.region, - logGroups: query.logGroups, - }), - disposalRef.current - ); - }, [datasource, query.logGroups, query.region]); - - const onChangeQuery = useCallback( - (value: string) => { - const nextQuery = { - ...query, - expression: value, - statsGroups: getStatsGroups(value), - }; - onChange(nextQuery); - }, - [onChange, query] - ); - const onEditorMount = useCallback( - (editor: monacoType.editor.IStandaloneCodeEditor, monaco: Monaco) => { - editor.onDidFocusEditorText(() => editor.trigger(TRIGGER_SUGGEST.id, TRIGGER_SUGGEST.id, {})); - editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => { - const text = editor.getValue(); - onChangeQuery(text); - }); - }, - [onChangeQuery] - ); - const onBeforeEditorMount = async (monaco: Monaco) => { - monacoRef.current = monaco; - disposalRef.current = await registerLanguage( - monaco, - language, - datasource.logsCompletionItemProviderFunc({ - region: query.region, - logGroups: query.logGroups, - }) - ); - }; - return ( <> -
-
- { - if (value !== query.expression) { - onChangeQuery(value); - } - disposalRef.current?.dispose(); - }} - onFocus={onFocus} - onBeforeEditorMount={onBeforeEditorMount} - onEditorDidMount={onEditorMount} - onEditorWillUnmount={() => disposalRef.current?.dispose()} - /> -
- {ExtraFieldElement} +
+ {getCodeEditor(query, datasource, onChange)} +
{ExtraFieldElement}
); }; -export default withTheme2(CloudWatchLogsQueryField); +const getStyles = (theme: GrafanaTheme2) => ({ + editor: css({ + marginTop: theme.spacing(1), + }), +}); + +const getCodeEditor = ( + query: CloudWatchLogsQuery, + datasource: CloudWatchDatasource, + onChange: (value: CloudWatchLogsQuery) => void +) => { + switch (query.queryLanguage) { + case LogsQueryLanguage.PPL: + return ; + case LogsQueryLanguage.SQL: + return ; + default: + return ; + } +}; diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/code-editors/LogsQLCodeEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/code-editors/LogsQLCodeEditor.tsx new file mode 100644 index 00000000000..dec250d42fd --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/code-editors/LogsQLCodeEditor.tsx @@ -0,0 +1,93 @@ +import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api'; +import { useCallback, useRef } from 'react'; + +import { CodeEditor, Monaco } from '@grafana/ui'; + +import { CloudWatchDatasource } from '../../../../datasource'; +import language from '../../../../language/logs/definition'; +import { TRIGGER_SUGGEST } from '../../../../language/monarch/commands'; +import { registerLanguage, reRegisterCompletionProvider } from '../../../../language/monarch/register'; +import { CloudWatchLogsQuery } from '../../../../types'; +import { getStatsGroups } from '../../../../utils/query/getStatsGroups'; + +import { codeEditorCommonProps } from './PPLQueryEditor'; + +interface CodeEditorProps { + query: CloudWatchLogsQuery; + datasource: CloudWatchDatasource; + onChange: (query: CloudWatchLogsQuery) => void; +} +export const LogsQLCodeEditor = (props: CodeEditorProps) => { + const { query, datasource, onChange } = props; + + const monacoRef = useRef(); + const disposalRef = useRef(); + + const onFocus = useCallback(async () => { + disposalRef.current = await reRegisterCompletionProvider( + monacoRef.current!, + language, + datasource.logsCompletionItemProviderFunc({ + region: query.region, + logGroups: query.logGroups, + }), + disposalRef.current + ); + }, [datasource, query.logGroups, query.region]); + + const onChangeQuery = useCallback( + (value: string) => { + const nextQuery = { + ...query, + expression: value, + statsGroups: getStatsGroups(value), + }; + onChange(nextQuery); + }, + [onChange, query] + ); + const onEditorMount = useCallback( + (editor: monacoType.editor.IStandaloneCodeEditor, monaco: Monaco) => { + editor.onDidFocusEditorText(() => editor.trigger(TRIGGER_SUGGEST.id, TRIGGER_SUGGEST.id, {})); + editor.onDidChangeModelContent(() => { + const model = editor.getModel(); + if (model?.getValue().trim() === '') { + editor.trigger(TRIGGER_SUGGEST.id, TRIGGER_SUGGEST.id, {}); + } + }); + editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => { + const text = editor.getValue(); + onChangeQuery(text); + }); + }, + [onChangeQuery] + ); + const onBeforeEditorMount = async (monaco: Monaco) => { + monacoRef.current = monaco; + disposalRef.current = await registerLanguage( + monaco, + language, + datasource.logsCompletionItemProviderFunc({ + region: query.region, + logGroups: query.logGroups, + }) + ); + }; + return ( + { + if (value !== query.expression) { + onChangeQuery(value); + } + disposalRef.current?.dispose(); + }} + onFocus={onFocus} + onBeforeEditorMount={onBeforeEditorMount} + onEditorDidMount={onEditorMount} + onEditorWillUnmount={() => disposalRef.current?.dispose()} + /> + ); +}; diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/code-editors/PPLQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/code-editors/PPLQueryEditor.tsx new file mode 100644 index 00000000000..f08e9587fb9 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/code-editors/PPLQueryEditor.tsx @@ -0,0 +1,115 @@ +import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api'; +import { useCallback, useRef } from 'react'; + +import { CodeEditor, Monaco } from '@grafana/ui'; +import { CodeEditorProps } from '@grafana/ui/src/components/Monaco/types'; + +import { CloudWatchDatasource } from '../../../../datasource'; +import language from '../../../../language/cloudwatch-ppl/definition'; +import { TRIGGER_SUGGEST } from '../../../../language/monarch/commands'; +import { registerLanguage, reRegisterCompletionProvider } from '../../../../language/monarch/register'; +import { CloudWatchLogsQuery } from '../../../../types'; +import { getStatsGroups } from '../../../../utils/query/getStatsGroups'; + +export const codeEditorCommonProps: Partial = { + height: '150px', + width: '100%', + showMiniMap: false, + monacoOptions: { + // without this setting, the auto-resize functionality causes an infinite loop, don't remove it! + scrollBeyondLastLine: false, + + // These additional options are style focused and are a subset of those in the query editor in Prometheus + fontSize: 14, + lineNumbers: 'off', + renderLineHighlight: 'none', + scrollbar: { + vertical: 'hidden', + horizontal: 'hidden', + }, + suggestFontSize: 12, + wordWrap: 'on', + padding: { + top: 6, + }, + }, +}; +interface LogsCodeEditorProps { + query: CloudWatchLogsQuery; + datasource: CloudWatchDatasource; + onChange: (query: CloudWatchLogsQuery) => void; +} +export const PPLQueryEditor = (props: LogsCodeEditorProps) => { + const { query, datasource, onChange } = props; + + const monacoRef = useRef(); + const disposalRef = useRef(); + + const onFocus = useCallback(async () => { + disposalRef.current = await reRegisterCompletionProvider( + monacoRef.current!, + language, + datasource.pplCompletionItemProviderFunc({ + region: query.region, + logGroups: query.logGroups, + }), + disposalRef.current + ); + }, [datasource, query.logGroups, query.region]); + + const onChangeQuery = useCallback( + (value: string) => { + const nextQuery = { + ...query, + expression: value, + statsGroups: getStatsGroups(value), + }; + onChange(nextQuery); + }, + [onChange, query] + ); + const onEditorMount = useCallback( + (editor: monacoType.editor.IStandaloneCodeEditor, monaco: Monaco) => { + editor.onDidFocusEditorText(() => editor.trigger(TRIGGER_SUGGEST.id, TRIGGER_SUGGEST.id, {})); + editor.onDidChangeModelContent(() => { + const model = editor.getModel(); + if (model?.getValue().trim() === '') { + editor.trigger(TRIGGER_SUGGEST.id, TRIGGER_SUGGEST.id, {}); + } + }); + editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => { + const text = editor.getValue(); + onChangeQuery(text); + }); + }, + [onChangeQuery] + ); + const onBeforeEditorMount = async (monaco: Monaco) => { + monacoRef.current = monaco; + disposalRef.current = await registerLanguage( + monaco, + language, + datasource.pplCompletionItemProviderFunc({ + region: query.region, + logGroups: query.logGroups, + }) + ); + }; + return ( + { + if (value !== query.expression) { + onChangeQuery(value); + } + disposalRef.current?.dispose(); + }} + onFocus={onFocus} + onBeforeEditorMount={onBeforeEditorMount} + onEditorDidMount={onEditorMount} + onEditorWillUnmount={() => disposalRef.current?.dispose()} + /> + ); +}; diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/code-editors/SQLCodeEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/code-editors/SQLCodeEditor.tsx new file mode 100644 index 00000000000..59abe051f76 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/code-editors/SQLCodeEditor.tsx @@ -0,0 +1,93 @@ +import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api'; +import { useCallback, useRef } from 'react'; + +import { CodeEditor, Monaco } from '@grafana/ui'; + +import { CloudWatchDatasource } from '../../../../datasource'; +import language from '../../../../language/cloudwatch-logs-sql/definition'; +import { TRIGGER_SUGGEST } from '../../../../language/monarch/commands'; +import { registerLanguage, reRegisterCompletionProvider } from '../../../../language/monarch/register'; +import { CloudWatchLogsQuery } from '../../../../types'; +import { getStatsGroups } from '../../../../utils/query/getStatsGroups'; + +import { codeEditorCommonProps } from './PPLQueryEditor'; + +interface SQLCodeEditorProps { + query: CloudWatchLogsQuery; + datasource: CloudWatchDatasource; + onChange: (query: CloudWatchLogsQuery) => void; +} +export const SQLQueryEditor = (props: SQLCodeEditorProps) => { + const { query, datasource, onChange } = props; + + const monacoRef = useRef(); + const disposalRef = useRef(); + + const onFocus = useCallback(async () => { + disposalRef.current = await reRegisterCompletionProvider( + monacoRef.current!, + language, + datasource.logsSqlCompletionItemProviderFunc({ + region: query.region, + logGroups: query.logGroups, + }), + disposalRef.current + ); + }, [datasource, query.logGroups, query.region]); + + const onChangeQuery = useCallback( + (value: string) => { + const nextQuery = { + ...query, + expression: value, + statsGroups: getStatsGroups(value), + }; + onChange(nextQuery); + }, + [onChange, query] + ); + const onEditorMount = useCallback( + (editor: monacoType.editor.IStandaloneCodeEditor, monaco: Monaco) => { + editor.onDidFocusEditorText(() => editor.trigger(TRIGGER_SUGGEST.id, TRIGGER_SUGGEST.id, {})); + editor.onDidChangeModelContent(() => { + const model = editor.getModel(); + if (model?.getValue().trim() === '') { + editor.trigger(TRIGGER_SUGGEST.id, TRIGGER_SUGGEST.id, {}); + } + }); + editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => { + const text = editor.getValue(); + onChangeQuery(text); + }); + }, + [onChangeQuery] + ); + const onBeforeEditorMount = async (monaco: Monaco) => { + monacoRef.current = monaco; + disposalRef.current = await registerLanguage( + monaco, + language, + datasource.logsSqlCompletionItemProviderFunc({ + region: query.region, + logGroups: query.logGroups, + }) + ); + }; + return ( + { + if (value !== query.expression) { + onChangeQuery(value); + } + disposalRef.current?.dispose(); + }} + onFocus={onFocus} + onBeforeEditorMount={onBeforeEditorMount} + onEditorDidMount={onEditorMount} + onEditorWillUnmount={() => disposalRef.current?.dispose()} + /> + ); +}; diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryEditor.test.tsx index ed093bed858..1816f46d0d8 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryEditor.test.tsx @@ -1,5 +1,6 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { selectOptionInTest } from 'test/helpers/selectOptionInTest'; import { QueryEditorProps } from '@grafana/data'; import { config } from '@grafana/runtime'; @@ -13,7 +14,8 @@ import { validMetricSearchCodeQuery, } from '../../__mocks__/queries'; import { CloudWatchDatasource } from '../../datasource'; -import { CloudWatchQuery, CloudWatchJsonData, MetricEditorMode, MetricQueryType } from '../../types'; +import { DEFAULT_CWLI_QUERY_STRING, DEFAULT_SQL_QUERY_STRING } from '../../defaultQueries'; +import { CloudWatchQuery, CloudWatchJsonData, MetricEditorMode, MetricQueryType, LogsQueryLanguage } from '../../types'; import { QueryEditor } from './QueryEditor'; @@ -23,11 +25,11 @@ const migratedFields = { metricEditorMode: MetricEditorMode.Builder, metricQueryType: MetricQueryType.Insights, }; - +const mockOnChange = jest.fn(); const props: QueryEditorProps = { datasource: setupMockedDataSource().datasource, onRunQuery: jest.fn(), - onChange: jest.fn(), + onChange: mockOnChange, query: {} as CloudWatchQuery, }; @@ -54,6 +56,13 @@ jest.mock('@grafana/runtime', () => ({ }, })); +jest.mock('@grafana/ui', () => ({ + ...jest.requireActual('@grafana/ui'), + CodeEditor: jest.fn().mockImplementation(() => { + return ; + }), +})); + export { SQLCodeEditor } from './MetricsQueryEditor/SQLCodeEditor'; describe('QueryEditor should render right editor', () => { @@ -352,3 +361,42 @@ describe('QueryEditor should render right editor', () => { }); }); }); +describe('LogsQueryEditor', () => { + const logsProps = { + ...props, + datasource: setupMockedDataSource().datasource, + query: validLogsQuery, + }; + describe('setting default query', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + it('should set default query expression if query is empty', async () => { + const emptyQuery = { ...logsProps.query, expression: '' }; + render(); + await waitFor(() => { + expect(mockOnChange).toHaveBeenLastCalledWith({ + ...logsProps.query, + expression: DEFAULT_CWLI_QUERY_STRING, + }); + }); + }); + it('should not change the query expression if not empty', async () => { + const nonEmptyQuery = { ...logsProps.query, expression: 'some expression' }; + render(); + await waitFor(() => { + expect(mockOnChange).not.toHaveBeenCalled(); + }); + }); + it('should set the correct default expression if query is new', async () => { + const emptyQuery = { ...logsProps.query, expression: '' }; + render(); + await selectOptionInTest(screen.getByLabelText(/Query language/), 'OpenSearch SQL'); + expect(mockOnChange).toHaveBeenCalledWith({ + ...logsProps.query, + queryLanguage: LogsQueryLanguage.SQL, + expression: DEFAULT_SQL_QUERY_STRING, + }); + }); + }); +}); diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryEditor.tsx index 90c4ff21a43..0f0c5f2fa32 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryEditor.tsx @@ -49,7 +49,14 @@ export const QueryEditor = (props: Props) => { extraHeaderElementRight={setExtraHeaderElementRight} /> )} - {isCloudWatchLogsQuery(query) && } + {isCloudWatchLogsQuery(query) && ( + + )} ); }; diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryHeader.test.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryHeader.test.tsx index 6c35f4d49dd..ddf208ebb95 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryHeader.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryHeader.test.tsx @@ -5,7 +5,8 @@ import { config } from '@grafana/runtime'; import { setupMockedDataSource } from '../../__mocks__/CloudWatchDataSource'; import { validLogsQuery, validMetricSearchBuilderQuery } from '../../__mocks__/queries'; -import { DEFAULT_LOGS_QUERY_STRING } from '../../defaultQueries'; +import { LogsQueryLanguage } from '../../dataquery.gen'; +import { DEFAULT_CWLI_QUERY_STRING } from '../../defaultQueries'; import QueryHeader from './QueryHeader'; @@ -111,32 +112,6 @@ describe('QueryHeader', () => { describe('when changing query mode', () => { const { datasource } = setupMockedDataSource(); - it('should set default log query when switching to log mode', async () => { - const onChange = jest.fn(); - datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(false); - render( - - ); - expect(await screen.findByText('CloudWatch Metrics')).toBeInTheDocument(); - await selectEvent.select(await screen.findByLabelText('Query mode'), 'CloudWatch Logs', { - container: document.body, - }); - expect(onChange).toHaveBeenCalledWith({ - ...validMetricSearchBuilderQuery, - logGroupNames: undefined, - logGroups: [], - queryMode: 'Logs', - sqlExpression: '', - expression: DEFAULT_LOGS_QUERY_STRING, - }); - }); - it('should set expression to empty when switching to metrics mode', async () => { const onChange = jest.fn(); datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(false); @@ -155,6 +130,7 @@ describe('QueryHeader', () => { }); expect(onChange).toHaveBeenCalledWith({ ...validMetricSearchBuilderQuery, + queryLanguage: LogsQueryLanguage.CWLI, logGroupNames: undefined, logGroups: [], sqlExpression: '', @@ -185,7 +161,7 @@ describe('QueryHeader', () => { render( ) => { if (value && value !== queryMode) { - // reset expression to a default string when the query mode changes - let expression = ''; - if (value === 'Logs') { - expression = DEFAULT_LOGS_QUERY_STRING; - } onChange({ ...datasource.getDefaultQuery(CoreApp.Unknown), ...query, - expression, queryMode: value, + expression: '', }); } }; diff --git a/public/app/plugins/datasource/cloudwatch/dataquery.cue b/public/app/plugins/datasource/cloudwatch/dataquery.cue index e1a5277f394..a7eb8261407 100644 --- a/public/app/plugins/datasource/cloudwatch/dataquery.cue +++ b/public/app/plugins/datasource/cloudwatch/dataquery.cue @@ -147,6 +147,8 @@ composableKinds: DataQuery: { #QueryEditorExpression: #QueryEditorArrayExpression | #QueryEditorPropertyExpression | #QueryEditorGroupByExpression | #QueryEditorFunctionExpression | #QueryEditorFunctionParameterExpression | #QueryEditorOperatorExpression @cuetsy(kind="type") + #LogsQueryLanguage: "CWLI" | "SQL" | "PPL" @cuetsy(kind="enum") + // Shape of a CloudWatch Logs query #CloudWatchLogsQuery: { common.DataQuery @@ -164,6 +166,8 @@ composableKinds: DataQuery: { logGroups?: [...#LogGroup] // @deprecated use logGroups logGroupNames?: [...string] + // Language used for querying logs, can be CWLI, SQL, or PPL. If empty, the default language is CWLI. + queryLanguage?: #LogsQueryLanguage } @cuetsy(kind="interface") #LogGroup: { // ARN of the log group diff --git a/public/app/plugins/datasource/cloudwatch/dataquery.gen.ts b/public/app/plugins/datasource/cloudwatch/dataquery.gen.ts index ff71dad3656..461ca94247e 100644 --- a/public/app/plugins/datasource/cloudwatch/dataquery.gen.ts +++ b/public/app/plugins/datasource/cloudwatch/dataquery.gen.ts @@ -216,6 +216,12 @@ export interface QueryEditorArrayExpression { export type QueryEditorExpression = (QueryEditorArrayExpression | QueryEditorPropertyExpression | QueryEditorGroupByExpression | QueryEditorFunctionExpression | QueryEditorFunctionParameterExpression | QueryEditorOperatorExpression); +export enum LogsQueryLanguage { + CWLI = 'CWLI', + PPL = 'PPL', + SQL = 'SQL', +} + /** * Shape of a CloudWatch Logs query */ @@ -233,6 +239,10 @@ export interface CloudWatchLogsQuery extends common.DataQuery { * Log groups to query */ logGroups?: Array; + /** + * Language used for querying logs, can be CWLI, SQL, or PPL. If empty, the default language is CWLI. + */ + queryLanguage?: LogsQueryLanguage; /** * Whether a query is a Metrics, Logs, or Annotations query */ diff --git a/public/app/plugins/datasource/cloudwatch/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/datasource.test.ts index 096c3ff5c5b..dd3d66c62eb 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.test.ts @@ -16,8 +16,10 @@ import { TimeRangeMock } from './__mocks__/timeRange'; import { CloudWatchDefaultQuery, CloudWatchLogsQuery, + CloudWatchLogsRequest, CloudWatchMetricsQuery, CloudWatchQuery, + LogsQueryLanguage, MetricEditorMode, MetricQueryType, } from './types'; @@ -28,10 +30,16 @@ describe('datasource', () => { jest.clearAllMocks(); }); describe('query', () => { - it('should not run a query if log groups is not specified', async () => { - const { datasource, queryMock } = setupMockedDataSource(); - await lastValueFrom( - datasource.query({ + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('query filtering', () => { + const testCases: Array<{ + targets: CloudWatchQuery[]; + queryLanguage: string | LogsQueryLanguage; + expectedOutput: Partial; + }> = [ + { targets: [ { queryMode: 'Logs', @@ -49,22 +57,104 @@ describe('datasource', () => { expression: 'some query string', }, ], - requestId: '', - interval: '', - intervalMs: 0, - range: TimeRangeMock, - scopedVars: {}, - timezone: '', - app: '', - startTime: 0, - }) - ); + expectedOutput: { + queryString: 'some query string', + logGroupNames: ['/some/group'], + region: 'us-west-1', + }, + queryLanguage: 'undefined', + }, + { + targets: [ + { + queryMode: 'Logs', + queryLanguage: LogsQueryLanguage.CWLI, + id: '', + refId: '', + region: '', + expression: 'some query string', // missing logGroups and logGroupNames, this query will be not be run + }, + { + queryMode: 'Logs', + id: '', + refId: '', + region: '', + logGroupNames: ['/some/group'], + expression: 'some query string', + }, + ], + expectedOutput: { + queryString: 'some query string', + logGroupNames: ['/some/group'], + region: 'us-west-1', + }, + queryLanguage: LogsQueryLanguage.CWLI, + }, + { + targets: [ + { + queryMode: 'Logs', + queryLanguage: LogsQueryLanguage.PPL, + id: '', + refId: '', + region: '', + expression: 'some query string', // missing logGroups and logGroupNames, this query will be not be run + }, + { + queryMode: 'Logs', + queryLanguage: LogsQueryLanguage.CWLI, + id: '', + refId: '', + region: '', + logGroupNames: ['/some/group'], + expression: 'some query string', + }, + ], + expectedOutput: { + queryString: 'some query string', + logGroupNames: ['/some/group'], + region: 'us-west-1', + }, + queryLanguage: LogsQueryLanguage.PPL, + }, + { + targets: [ + { + queryMode: 'Logs', + queryLanguage: LogsQueryLanguage.SQL, + id: '', + refId: '', + region: '', + expression: 'some query string', + }, + ], + expectedOutput: { + queryString: 'some query string', + region: 'us-west-1', + }, + queryLanguage: LogsQueryLanguage.SQL, + }, + ]; + testCases.forEach(async (testCase) => { + it(`should filter out query with no log groups when query language is ${testCase.queryLanguage}`, async () => { + const { datasource, queryMock } = setupMockedDataSource(); + await lastValueFrom( + datasource.query({ + targets: testCase.targets, + requestId: '', + interval: '', + intervalMs: 0, + range: TimeRangeMock, + scopedVars: {}, + timezone: '', + app: '', + startTime: 0, + }) + ); - expect(queryMock.mock.calls[0][0].targets).toHaveLength(1); - expect(queryMock.mock.calls[0][0].targets[0]).toMatchObject({ - queryString: 'some query string', - logGroupNames: ['/some/group'], - region: 'us-west-1', + expect(queryMock.mock.calls[0][0].targets).toHaveLength(1); + expect(queryMock.mock.calls[0][0].targets[0]).toMatchObject(testCase.expectedOutput); + }); }); }); @@ -365,5 +455,19 @@ describe('datasource', () => { ); expect((datasource.getDefaultQuery(CoreApp.PanelEditor) as CloudWatchDefaultQuery).matchExact).toEqual(true); }); + it('should set default values from logs query', () => { + const defaultLogGroups = [{ name: 'logName', arn: 'logARN' }]; + const { datasource } = setupMockedDataSource({ + customInstanceSettings: { + ...CloudWatchSettings, + jsonData: { ...CloudWatchSettings.jsonData, logGroups: defaultLogGroups }, + }, + }); + expect(datasource.getDefaultQuery(CoreApp.PanelEditor).region).toEqual('default'); + expect((datasource.getDefaultQuery(CoreApp.PanelEditor) as CloudWatchDefaultQuery).queryLanguage).toEqual('CWLI'); + expect((datasource.getDefaultQuery(CoreApp.PanelEditor) as CloudWatchDefaultQuery).logGroups).toEqual( + defaultLogGroups + ); + }); }); }); diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 3fdb1fa5b50..bb11d43809f 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -18,6 +18,14 @@ import { CloudWatchAnnotationSupport } from './annotationSupport'; import { DEFAULT_METRICS_QUERY, getDefaultLogsQuery } from './defaultQueries'; import { isCloudWatchAnnotationQuery, isCloudWatchLogsQuery, isCloudWatchMetricsQuery } from './guards'; import { CloudWatchLogsLanguageProvider } from './language/cloudwatch-logs/CloudWatchLogsLanguageProvider'; +import { + LogsSQLCompletionItemProvider, + LogsSQLCompletionItemProviderFunc, +} from './language/cloudwatch-logs-sql/completion/CompletionItemProvider'; +import { + PPLCompletionItemProvider, + PPLCompletionItemProviderFunc, +} from './language/cloudwatch-ppl/completion/PPLCompletionItemProvider'; import { SQLCompletionItemProvider } from './language/cloudwatch-sql/completion/CompletionItemProvider'; import { LogsCompletionItemProvider, @@ -46,8 +54,10 @@ export class CloudWatchDatasource languageProvider: CloudWatchLogsLanguageProvider; sqlCompletionItemProvider: SQLCompletionItemProvider; metricMathCompletionItemProvider: MetricMathCompletionItemProvider; - logsCompletionItemProviderFunc: (queryContext: queryContext) => LogsCompletionItemProvider; defaultLogGroups?: string[]; + logsSqlCompletionItemProviderFunc: (queryContext: queryContext) => LogsSQLCompletionItemProvider; + logsCompletionItemProviderFunc: (queryContext: queryContext) => LogsCompletionItemProvider; + pplCompletionItemProviderFunc: (queryContext: queryContext) => PPLCompletionItemProvider; type = 'cloudwatch'; @@ -65,14 +75,17 @@ export class CloudWatchDatasource this.resources = new ResourcesAPI(instanceSettings, templateSrv); this.languageProvider = new CloudWatchLogsLanguageProvider(this); this.sqlCompletionItemProvider = new SQLCompletionItemProvider(this.resources, this.templateSrv); - this.metricMathCompletionItemProvider = new MetricMathCompletionItemProvider(this.resources, this.templateSrv); this.metricsQueryRunner = new CloudWatchMetricsQueryRunner(instanceSettings, templateSrv); - this.logsCompletionItemProviderFunc = LogsCompletionItemProviderFunc(this.resources, this.templateSrv); this.logsQueryRunner = new CloudWatchLogsQueryRunner(instanceSettings, templateSrv); this.annotationQueryRunner = new CloudWatchAnnotationQueryRunner(instanceSettings, templateSrv); this.variables = new CloudWatchVariableSupport(this.resources); this.annotations = CloudWatchAnnotationSupport; this.defaultLogGroups = instanceSettings.jsonData.defaultLogGroups; + + this.metricMathCompletionItemProvider = new MetricMathCompletionItemProvider(this.resources, this.templateSrv); + this.logsCompletionItemProviderFunc = LogsCompletionItemProviderFunc(this.resources, this.templateSrv); + this.logsSqlCompletionItemProviderFunc = LogsSQLCompletionItemProviderFunc(this.resources, templateSrv); + this.pplCompletionItemProviderFunc = PPLCompletionItemProviderFunc(this.resources, this.templateSrv); } filterQuery(query: CloudWatchQuery) { diff --git a/public/app/plugins/datasource/cloudwatch/defaultQueries.ts b/public/app/plugins/datasource/cloudwatch/defaultQueries.ts index 2f09d8db926..bf66bf59151 100644 --- a/public/app/plugins/datasource/cloudwatch/defaultQueries.ts +++ b/public/app/plugins/datasource/cloudwatch/defaultQueries.ts @@ -3,6 +3,7 @@ import { CloudWatchLogsQuery, CloudWatchMetricsQuery, LogGroup, + LogsQueryLanguage, MetricEditorMode, MetricQueryType, VariableQuery, @@ -33,7 +34,10 @@ export const DEFAULT_ANNOTATIONS_QUERY: Omit statistic: 'Average', }; -export const DEFAULT_LOGS_QUERY_STRING = 'fields @timestamp, @message |\n sort @timestamp desc |\n limit 20'; +export const DEFAULT_CWLI_QUERY_STRING = 'fields @timestamp, @message |\nsort @timestamp desc |\nlimit 20'; +export const DEFAULT_PPL_QUERY_STRING = 'fields `@timestamp`, `@message`\n| sort - `@timestamp`\n| head 25s'; +export const DEFAULT_SQL_QUERY_STRING = + 'SELECT `@timestamp`, `@message`\nFROM `log_group`\nORDER BY `@timestamp` DESC\nLIMIT 25;'; export const getDefaultLogsQuery = ( defaultLogGroups?: LogGroup[], @@ -45,6 +49,7 @@ export const getDefaultLogsQuery = ( // the migration requires async backend calls, so we don't want to do it here as it would block the UI. logGroupNames: legacyDefaultLogGroups, logGroups: defaultLogGroups ?? [], + queryLanguage: LogsQueryLanguage.CWLI, }); export const DEFAULT_VARIABLE_QUERY: Partial = { diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/completion/CompletionItemProvider.test.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/completion/CompletionItemProvider.test.ts new file mode 100644 index 00000000000..6886d29d0cd --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/completion/CompletionItemProvider.test.ts @@ -0,0 +1,319 @@ +import { CustomVariableModel } from '@grafana/data'; +import { Monaco, monacoTypes } from '@grafana/ui'; + +import { setupMockedTemplateService, logGroupNamesVariable } from '../../../__mocks__/CloudWatchDataSource'; +import { multiLineFullQuery } from '../../../__mocks__/cloudwatch-logs-sql-test-data/multiLineFullQuery'; +import { multiLineFullQueryWithCaseClause } from '../../../__mocks__/cloudwatch-logs-sql-test-data/multiLineFullQueryWithCaseClause'; +import { partialQueryWithSubquery } from '../../../__mocks__/cloudwatch-logs-sql-test-data/partialQueryWithSubquery'; +import { singleLineFullQuery } from '../../../__mocks__/cloudwatch-logs-sql-test-data/singleLineFullQuery'; +import { whitespaceQuery } from '../../../__mocks__/cloudwatch-logs-sql-test-data/whitespaceQuery'; +import MonacoMock from '../../../__mocks__/monarch/Monaco'; +import TextModel from '../../../__mocks__/monarch/TextModel'; +import { ResourcesAPI } from '../../../resources/ResourcesAPI'; +import { ResourceResponse } from '../../../resources/types'; +import { LogGroup, LogGroupField } from '../../../types'; +import cloudWatchLogsLanguageDefinition from '../definition'; +import { + SELECT, + ALL, + DISTINCT, + ALL_FUNCTIONS, + FROM, + BY, + WHERE, + GROUP, + ORDER, + LIMIT, + INNER, + LEFT, + OUTER, + JOIN, + ON, + HAVING, + PREDICATE_OPERATORS, + LOGICAL_OPERATORS, + ASC, + DESC, + CASE, + WHEN, + THEN, + ELSE, + END, +} from '../language'; + +import { LogsSQLCompletionItemProvider } from './CompletionItemProvider'; + +jest.mock('monaco-editor/esm/vs/editor/editor.api', () => ({ + Token: jest.fn((offset, type, language) => ({ offset, type, language })), +})); + +const getSuggestions = async ( + value: string, + position: monacoTypes.IPosition, + variables: CustomVariableModel[] = [], + logGroups: LogGroup[] = [], + fields: Array> = [] +) => { + const setup = new LogsSQLCompletionItemProvider( + { + getActualRegion: () => 'us-east-2', + } as ResourcesAPI, + setupMockedTemplateService(variables), + { region: 'default', logGroups } + ); + + setup.resources.getLogGroupFields = jest.fn().mockResolvedValue(fields); + const monaco = MonacoMock as Monaco; + const provider = setup.getCompletionProvider(monaco, cloudWatchLogsLanguageDefinition); + const { suggestions } = await provider.provideCompletionItems( + TextModel(value) as monacoTypes.editor.ITextModel, + position + ); + return suggestions; +}; + +describe('LogsSQLCompletionItemProvider', () => { + describe('getSuggestions', () => { + it('returns select keyword for an empty query', async () => { + const suggestions = await getSuggestions(whitespaceQuery.query, { lineNumber: 1, column: 0 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining([SELECT])); + }); + + it('returns `ALL`, `DISTINCT`, `CASE`, keywords and all functions after select keyword', async () => { + const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 7 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining([ALL, DISTINCT, CASE, ...ALL_FUNCTIONS])); + }); + + it('returns `CASE` keyword and all functions for a select expression', async () => { + const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 37 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining([CASE, ...ALL_FUNCTIONS])); + }); + + it('returns `FROM`, `CASE` keywords and all functions after a select expression', async () => { + const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 103 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual( + expect.arrayContaining([FROM, `${FROM} \`logGroups(logGroupIdentifier: [...])\``, CASE, ...ALL_FUNCTIONS]) + ); + }); + + it('returns logGroups suggestion after from keyword', async () => { + const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 108 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining(['`logGroups(logGroupIdentifier: [...])`'])); + }); + + it('returns where, having, limit, group by, order by, and join suggestions after from arguments', async () => { + const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 125 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual( + expect.arrayContaining([ + WHERE, + HAVING, + LIMIT, + `${GROUP} ${BY}`, + `${ORDER} ${BY}`, + `${INNER} ${JOIN} ${ON} `, + `${LEFT} ${OUTER} ${JOIN} ${ON} `, + ]) + ); + }); + + it('returns `CASE` keyword and all functions for a where key', async () => { + const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 182 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining([CASE, ...ALL_FUNCTIONS])); + }); + + it('returns predicate operators after a where key', async () => { + const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 191 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining(PREDICATE_OPERATORS)); + }); + + it('returns all functions after a where comparison operator', async () => { + const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 193 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining(ALL_FUNCTIONS)); + }); + + it('returns logical operators, group by, order by, and limit keywords after a where value', async () => { + const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 201 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual( + expect.arrayContaining([...LOGICAL_OPERATORS, LIMIT, `${GROUP} ${BY}`, `${ORDER} ${BY}`]) + ); + }); + + it('returns all functions for a having key', async () => { + const suggestions = await getSuggestions(multiLineFullQuery.query, { lineNumber: 8, column: 7 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining(ALL_FUNCTIONS)); + }); + + it('returns predicate operators after a having key', async () => { + const suggestions = await getSuggestions(multiLineFullQuery.query, { lineNumber: 8, column: 13 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining(PREDICATE_OPERATORS)); + }); + + it('returns all functions after a having comparison operator', async () => { + const suggestions = await getSuggestions(multiLineFullQuery.query, { lineNumber: 8, column: 15 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining(ALL_FUNCTIONS)); + }); + + it('returns logical operators, order by, and limit keywords after a having value', async () => { + const suggestions = await getSuggestions(multiLineFullQuery.query, { lineNumber: 8, column: 18 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining([...LOGICAL_OPERATORS, LIMIT, `${ORDER} ${BY}`])); + }); + + it('returns all functions for an on key', async () => { + const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 156 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining(ALL_FUNCTIONS)); + }); + + it('returns predicate operators after an on key', async () => { + const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 165 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining(PREDICATE_OPERATORS)); + }); + + it('returns all functions after an on comparison operator', async () => { + const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 167 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining(ALL_FUNCTIONS)); + }); + + it('returns logical operators, group by, order by, and limit keywords after an on value', async () => { + const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 176 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual( + expect.arrayContaining([...LOGICAL_OPERATORS, LIMIT, `${GROUP} ${BY}`, `${ORDER} ${BY}`]) + ); + }); + + it('returns `WHEN` keyword and all functions for a case key', async () => { + const suggestions = await getSuggestions(multiLineFullQueryWithCaseClause.query, { lineNumber: 9, column: 5 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining([WHEN, ...ALL_FUNCTIONS])); + }); + + it('returns `WHEN` keyword and predicate operators after a case key', async () => { + const suggestions = await getSuggestions(multiLineFullQueryWithCaseClause.query, { lineNumber: 9, column: 7 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining([WHEN, ...PREDICATE_OPERATORS])); + }); + + it('returns all functions after a case comparison operator', async () => { + const suggestions = await getSuggestions(multiLineFullQueryWithCaseClause.query, { lineNumber: 9, column: 9 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining(ALL_FUNCTIONS)); + }); + + it('returns `WHEN` keyword after a case value', async () => { + const suggestions = await getSuggestions(multiLineFullQueryWithCaseClause.query, { lineNumber: 9, column: 11 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining([WHEN])); + }); + + it('returns all functions for a when key', async () => { + const suggestions = await getSuggestions(multiLineFullQueryWithCaseClause.query, { lineNumber: 4, column: 5 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining(ALL_FUNCTIONS)); + }); + + it('returns `THEN` keyword and predicate operators after a when key', async () => { + const suggestions = await getSuggestions(multiLineFullQueryWithCaseClause.query, { lineNumber: 4, column: 8 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining([THEN, ...PREDICATE_OPERATORS])); + }); + + it('returns all functions after a when comparison operator', async () => { + const suggestions = await getSuggestions(multiLineFullQueryWithCaseClause.query, { lineNumber: 4, column: 10 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining(ALL_FUNCTIONS)); + }); + + it('returns `THEN` keyword after a when value', async () => { + const suggestions = await getSuggestions(multiLineFullQueryWithCaseClause.query, { lineNumber: 4, column: 14 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining([THEN])); + }); + + it('returns all functions after a then keyword', async () => { + const suggestions = await getSuggestions(multiLineFullQueryWithCaseClause.query, { lineNumber: 4, column: 19 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining(ALL_FUNCTIONS)); + }); + + it('returns `WHEN`, `ELSE`, and `END` keywords after a then expression', async () => { + const suggestions = await getSuggestions(multiLineFullQueryWithCaseClause.query, { lineNumber: 4, column: 29 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining([WHEN, `${ELSE} ... ${END}`])); + }); + + it('returns all functions after an else keyword', async () => { + const suggestions = await getSuggestions(multiLineFullQueryWithCaseClause.query, { lineNumber: 5, column: 5 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining(ALL_FUNCTIONS)); + }); + + it('returns all functions after group by keywords', async () => { + const suggestions = await getSuggestions(multiLineFullQuery.query, { lineNumber: 7, column: 9 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining(ALL_FUNCTIONS)); + }); + + it('returns having, limit, order by suggestions after group by', async () => { + const suggestions = await getSuggestions(multiLineFullQuery.query, { lineNumber: 7, column: 27 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining([HAVING, LIMIT, `${ORDER} ${BY}`])); + }); + + it('returns order directions and limit keywords after order by keywords', async () => { + const suggestions = await getSuggestions(multiLineFullQuery.query, { lineNumber: 9, column: 9 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining([LIMIT, ASC, DESC])); + }); + + it('returns limit keyword after order by direction', async () => { + const suggestions = await getSuggestions(multiLineFullQuery.query, { lineNumber: 9, column: 26 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining([LIMIT])); + }); + + it('returns `SELECT` keyword and all functions at the start of a subquery', async () => { + const suggestions = await getSuggestions(partialQueryWithSubquery.query, { lineNumber: 1, column: 53 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining([SELECT, ...ALL_FUNCTIONS])); + }); + + it('returns template variables appended to list of suggestions', async () => { + const suggestions = await getSuggestions(whitespaceQuery.query, { lineNumber: 1, column: 0 }, [ + logGroupNamesVariable, + ]); + const suggestionLabels = suggestions.map((s) => s.label); + const expectedTemplateVariableLabel = `$${logGroupNamesVariable.name}`; + const expectedLabels = [SELECT, expectedTemplateVariableLabel]; + expect(suggestionLabels).toEqual(expect.arrayContaining(expectedLabels)); + }); + + it('fetches fields when logGroups are set', async () => { + const suggestions = await getSuggestions( + singleLineFullQuery.query, + { lineNumber: 1, column: 37 }, + [], + [{ arn: 'foo', name: 'bar' }], + [{ value: { name: '@field' } }] + ); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining(['@field'])); + }); + }); +}); diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/completion/CompletionItemProvider.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/completion/CompletionItemProvider.ts new file mode 100644 index 00000000000..0bb7db76d0d --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/completion/CompletionItemProvider.ts @@ -0,0 +1,314 @@ +import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; +import type { Monaco, monacoTypes } from '@grafana/ui'; + +import { ResourcesAPI } from '../../../resources/ResourcesAPI'; +import { LogGroup } from '../../../types'; +import { CompletionItemProvider } from '../../monarch/CompletionItemProvider'; +import { LinkedToken } from '../../monarch/LinkedToken'; +import { TRIGGER_SUGGEST } from '../../monarch/commands'; +import { SuggestionKind, CompletionItemPriority, StatementPosition } from '../../monarch/types'; +import { + ASC, + BY, + PREDICATE_OPERATORS, + DESC, + FROM, + ALL_FUNCTIONS, + ALL, + DISTINCT, + GROUP, + LIMIT, + INNER, + LEFT, + OUTER, + ON, + JOIN, + LOGICAL_OPERATORS, + ORDER, + SELECT, + WHERE, + HAVING, + CASE, + WHEN, + THEN, + ELSE, + END, +} from '../language'; + +import { getStatementPosition } from './statementPosition'; +import { getSuggestionKinds } from './suggestionKind'; +import { SQLTokenTypes } from './types'; + +type CompletionItem = monacoTypes.languages.CompletionItem; + +export type queryContext = { + logGroups?: LogGroup[]; + region: string; +}; + +export function LogsSQLCompletionItemProviderFunc( + resources: ResourcesAPI, + templateSrv: TemplateSrv = getTemplateSrv() +) { + return (queryContext: queryContext) => { + return new LogsSQLCompletionItemProvider(resources, templateSrv, queryContext); + }; +} + +export class LogsSQLCompletionItemProvider extends CompletionItemProvider { + region: string; + queryContext: queryContext; + + constructor(resources: ResourcesAPI, templateSrv: TemplateSrv = getTemplateSrv(), queryContext: queryContext) { + super(resources, templateSrv); + this.region = resources.getActualRegion() ?? ''; + this.getStatementPosition = getStatementPosition; + this.getSuggestionKinds = getSuggestionKinds; + this.tokenTypes = SQLTokenTypes; + this.queryContext = queryContext; + } + + async getSuggestions( + monaco: Monaco, + currentToken: LinkedToken | null, + suggestionKinds: SuggestionKind[], + statementPosition: StatementPosition, + position: monacoTypes.IPosition + ): Promise { + let suggestions: CompletionItem[] = []; + const invalidRangeToken = currentToken?.isWhiteSpace() || currentToken?.isParenthesis(); + const range = + invalidRangeToken || !currentToken?.range ? monaco.Range.fromPositions(position) : currentToken?.range; + + const toCompletionItem = (value: string, rest: Partial = {}) => { + const item: CompletionItem = { + label: value, + insertText: value, + kind: monaco.languages.CompletionItemKind.Field, + range, + sortText: CompletionItemPriority.Medium, + ...rest, + }; + return item; + }; + + function addSuggestion(value: string, rest: Partial = {}) { + suggestions = [...suggestions, toCompletionItem(value, rest)]; + } + + for (const suggestion of suggestionKinds) { + switch (suggestion) { + case SuggestionKind.SelectKeyword: + addSuggestion(SELECT, { + insertText: `${SELECT} $0`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + kind: monaco.languages.CompletionItemKind.Keyword, + command: TRIGGER_SUGGEST, + }); + break; + + case SuggestionKind.AfterSelectKeyword: + addSuggestion(ALL, { + insertText: `${ALL} `, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + command: TRIGGER_SUGGEST, + kind: monaco.languages.CompletionItemKind.Keyword, + }); + addSuggestion(DISTINCT, { + insertText: `${DISTINCT} `, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + command: TRIGGER_SUGGEST, + kind: monaco.languages.CompletionItemKind.Keyword, + }); + break; + + case SuggestionKind.FunctionsWithArguments: + ALL_FUNCTIONS.map((s) => + addSuggestion(s, { + insertText: `${s}($0)`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + command: TRIGGER_SUGGEST, + kind: monaco.languages.CompletionItemKind.Function, + }) + ); + break; + + case SuggestionKind.FromKeyword: + addSuggestion(FROM, { + insertText: `${FROM} $0`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + kind: monaco.languages.CompletionItemKind.Keyword, + command: TRIGGER_SUGGEST, + sortText: CompletionItemPriority.MediumHigh, + }); + addSuggestion(`${FROM} \`logGroups(logGroupIdentifier: [...])\``, { + insertText: `${FROM} \`logGroups(logGroupIdentifier: [$0])\``, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + kind: monaco.languages.CompletionItemKind.Keyword, + command: TRIGGER_SUGGEST, + sortText: CompletionItemPriority.MediumHigh, + }); + break; + + case SuggestionKind.AfterFromKeyword: + addSuggestion('`logGroups(logGroupIdentifier: [...])`', { + insertText: '`logGroups(logGroupIdentifier: [$0])`', + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + kind: monaco.languages.CompletionItemKind.Function, + command: TRIGGER_SUGGEST, + }); + break; + + case SuggestionKind.LogicalOperators: + LOGICAL_OPERATORS.map((o) => + addSuggestion(`${o}`, { + insertText: `${o} `, + command: TRIGGER_SUGGEST, + sortText: CompletionItemPriority.MediumHigh, + }) + ); + break; + + case SuggestionKind.WhereKeyword: + addSuggestion(`${WHERE}`, { + insertText: `${WHERE} `, + command: TRIGGER_SUGGEST, + sortText: CompletionItemPriority.High, + }); + break; + + case SuggestionKind.HavingKeywords: + addSuggestion(`${HAVING}`, { + insertText: `${HAVING} `, + command: TRIGGER_SUGGEST, + }); + break; + + case SuggestionKind.ComparisonOperators: + PREDICATE_OPERATORS.map((o) => addSuggestion(`${o}`, { insertText: `${o} `, command: TRIGGER_SUGGEST })); + break; + + case SuggestionKind.CaseKeyword: + addSuggestion(CASE, { + insertText: `${CASE} `, + kind: monaco.languages.CompletionItemKind.Keyword, + command: TRIGGER_SUGGEST, + }); + break; + + case SuggestionKind.WhenKeyword: + addSuggestion(WHEN, { + insertText: `${WHEN} `, + kind: monaco.languages.CompletionItemKind.Keyword, + command: TRIGGER_SUGGEST, + }); + break; + + case SuggestionKind.ThenKeyword: + addSuggestion(THEN, { + insertText: `${THEN} `, + kind: monaco.languages.CompletionItemKind.Keyword, + command: TRIGGER_SUGGEST, + }); + break; + + case SuggestionKind.AfterThenExpression: + addSuggestion(`${ELSE} ... ${END}`, { + insertText: `${ELSE} $0 ${END}`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + kind: monaco.languages.CompletionItemKind.Keyword, + command: TRIGGER_SUGGEST, + }); + break; + + case SuggestionKind.GroupByKeywords: + addSuggestion(`${GROUP} ${BY}`, { + insertText: `${GROUP} ${BY} `, + command: TRIGGER_SUGGEST, + sortText: CompletionItemPriority.MediumHigh, + }); + break; + + case SuggestionKind.OrderByKeywords: + addSuggestion(`${ORDER} ${BY}`, { + insertText: `${ORDER} ${BY} `, + command: TRIGGER_SUGGEST, + sortText: CompletionItemPriority.Medium, + }); + break; + + case SuggestionKind.JoinKeywords: + addSuggestion(`${INNER} ${JOIN} ${ON} `, { + insertText: `${INNER} ${JOIN} $1 ${ON} $2`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + kind: monaco.languages.CompletionItemKind.Keyword, + command: TRIGGER_SUGGEST, + sortText: CompletionItemPriority.MediumLow, + }); + addSuggestion(`${LEFT} ${OUTER} ${JOIN} ${ON} `, { + insertText: `${LEFT} ${OUTER} ${JOIN} $1 ${ON} $2`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + kind: monaco.languages.CompletionItemKind.Keyword, + command: TRIGGER_SUGGEST, + sortText: CompletionItemPriority.MediumLow, + }); + break; + + case SuggestionKind.LimitKeyword: + addSuggestion(LIMIT, { insertText: `${LIMIT} ` }); + break; + + case SuggestionKind.SortOrderDirectionKeyword: + [ASC, DESC].map((s) => + addSuggestion(s, { + insertText: `${s} `, + command: TRIGGER_SUGGEST, + }) + ); + break; + + case SuggestionKind.Field: + const fields = await this.fetchFields(this.queryContext.logGroups || [], this.queryContext.region); + fields.forEach((field) => { + if (field !== '') { + addSuggestion(field, { + label: field, + insertText: `\`${field}\``, + kind: monaco.languages.CompletionItemKind.Field, + }); + } + }); + break; + } + } + + this.templateSrv.getVariables().map((v) => { + const variable = `$${v.name}`; + addSuggestion(variable, { + range, + label: variable, + insertText: variable, + kind: monaco.languages.CompletionItemKind.Variable, + sortText: CompletionItemPriority.Low, + }); + }); + + return suggestions; + } + + private fetchFields = async (logGroups: LogGroup[], region: string): Promise => { + if (logGroups.length === 0) { + return []; + } + + const results = await Promise.all( + logGroups.map((logGroup) => + this.resources + .getLogGroupFields({ logGroupName: logGroup.name, arn: logGroup.arn, region }) + .then((fields) => fields.filter((f) => f).map((f) => f.value.name ?? '')) + ) + ); + // Deduplicate fields + return [...new Set(results.flat())]; + }; +} diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/completion/statementPosition.test.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/completion/statementPosition.test.ts new file mode 100644 index 00000000000..2b4deed6d77 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/completion/statementPosition.test.ts @@ -0,0 +1,264 @@ +import { monacoTypes } from '@grafana/ui'; + +import { commentOnlyQuery } from '../../../__mocks__/cloudwatch-logs-sql-test-data/commentOnlyQuery'; +import { multiLineFullQuery } from '../../../__mocks__/cloudwatch-logs-sql-test-data/multiLineFullQuery'; +import { multiLineFullQueryWithCaseClause } from '../../../__mocks__/cloudwatch-logs-sql-test-data/multiLineFullQueryWithCaseClause'; +import { partialQueryWithFunction } from '../../../__mocks__/cloudwatch-logs-sql-test-data/partialQueryWithFunction'; +import { partialQueryWithSubquery } from '../../../__mocks__/cloudwatch-logs-sql-test-data/partialQueryWithSubquery'; +import { singleLineFullQuery } from '../../../__mocks__/cloudwatch-logs-sql-test-data/singleLineFullQuery'; +import { whitespaceQuery } from '../../../__mocks__/cloudwatch-logs-sql-test-data/whitespaceQuery'; +import MonacoMock from '../../../__mocks__/monarch/Monaco'; +import TextModel from '../../../__mocks__/monarch/TextModel'; +import { linkedTokenBuilder } from '../../monarch/linkedTokenBuilder'; +import { StatementPosition } from '../../monarch/types'; +import cloudWatchLogsSqlLanguageDefinition from '../definition'; + +import { getStatementPosition } from './statementPosition'; +import { SQLTokenTypes } from './types'; + +describe('statementPosition', () => { + function assertPosition(query: string, position: monacoTypes.IPosition, expected: StatementPosition) { + const testModel = TextModel(query); + const current = linkedTokenBuilder( + MonacoMock, + cloudWatchLogsSqlLanguageDefinition, + testModel as monacoTypes.editor.ITextModel, + position, + SQLTokenTypes + ); + const statementPosition = getStatementPosition(current); + expect(StatementPosition[statementPosition]).toBe(StatementPosition[expected]); + } + + test.each([ + [commentOnlyQuery.query, { lineNumber: 1, column: 0 }], + [singleLineFullQuery.query, { lineNumber: 1, column: 202 }], + [multiLineFullQuery.query, { lineNumber: 10, column: 0 }], + [multiLineFullQuery.query, { lineNumber: 11, column: 0 }], + [multiLineFullQuery.query, { lineNumber: 12, column: 0 }], + [multiLineFullQuery.query, { lineNumber: 13, column: 0 }], + [multiLineFullQuery.query, { lineNumber: 14, column: 0 }], + [multiLineFullQuery.query, { lineNumber: 15, column: 0 }], + [multiLineFullQuery.query, { lineNumber: 16, column: 0 }], + ])('should be comment', (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.Comment); + }); + test.each([ + [singleLineFullQuery.query, { lineNumber: 1, column: 0 }], + [multiLineFullQuery.query, { lineNumber: 1, column: 0 }], + [whitespaceQuery.query, { lineNumber: 1, column: 0 }], + ])('should be select keyword', (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.SelectKeyword); + }); + test.each([ + [singleLineFullQuery.query, { lineNumber: 1, column: 7 }], + [multiLineFullQuery.query, { lineNumber: 1, column: 7 }], + ])('should be after select keyword', (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.AfterSelectKeyword); + }); + test.each([[singleLineFullQuery.query, { lineNumber: 1, column: 37 }]])( + 'should be select expression', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.SelectExpression); + } + ); + test.each([ + [singleLineFullQuery.query, { lineNumber: 1, column: 103 }], + [multiLineFullQueryWithCaseClause.query, { lineNumber: 6, column: 4 }], + ])('should be after select expression', (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.AfterSelectExpression); + }); + test.each([[partialQueryWithSubquery.query, { lineNumber: 1, column: 52 }]])( + 'should be subquery', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.Subquery); + } + ); + test.each([[partialQueryWithFunction.query, { lineNumber: 1, column: 14 }]])( + 'should be predefined function argument', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.PredefinedFunctionArgument); + } + ); + test.each([[singleLineFullQuery.query, { lineNumber: 1, column: 108 }]])( + 'should be after from keyword', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.AfterFromKeyword); + } + ); + test.each([[singleLineFullQuery.query, { lineNumber: 1, column: 125 }]])( + 'should be after from arguments', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.AfterFromArguments); + } + ); + test.each([[singleLineFullQuery.query, { lineNumber: 1, column: 182 }]])( + 'should be where key', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.WhereKey); + } + ); + test.each([[singleLineFullQuery.query, { lineNumber: 1, column: 191 }]])( + 'should be where comparison operator', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.WhereComparisonOperator); + } + ); + test.each([[singleLineFullQuery.query, { lineNumber: 1, column: 193 }]])( + 'should be where value', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.WhereValue); + } + ); + test.each([ + [singleLineFullQuery.query, { lineNumber: 1, column: 201 }], + [multiLineFullQueryWithCaseClause.query, { lineNumber: 13, column: 4 }], + ])('should be after where value', (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.AfterWhereValue); + }); + test.each([[multiLineFullQuery.query, { lineNumber: 8, column: 7 }]])( + 'should be having key', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.HavingKey); + } + ); + test.each([[multiLineFullQuery.query, { lineNumber: 8, column: 13 }]])( + 'should be having comparison operator', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.HavingComparisonOperator); + } + ); + test.each([[multiLineFullQuery.query, { lineNumber: 8, column: 15 }]])( + 'should be having value', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.HavingValue); + } + ); + test.each([[multiLineFullQuery.query, { lineNumber: 8, column: 18 }]])( + 'should be after having value', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.AfterHavingValue); + } + ); + test.each([[singleLineFullQuery.query, { lineNumber: 1, column: 156 }]])( + 'should be on key', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.OnKey); + } + ); + test.each([[singleLineFullQuery.query, { lineNumber: 1, column: 165 }]])( + 'should be on comparison operator', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.OnComparisonOperator); + } + ); + test.each([[singleLineFullQuery.query, { lineNumber: 1, column: 167 }]])( + 'should be on value', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.OnValue); + } + ); + test.each([[singleLineFullQuery.query, { lineNumber: 1, column: 176 }]])( + 'should be after on value', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.AfterOnValue); + } + ); + test.each([ + [multiLineFullQueryWithCaseClause.query, { lineNumber: 2, column: 5 }], + [multiLineFullQueryWithCaseClause.query, { lineNumber: 9, column: 5 }], + ])('should be case key', (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.CaseKey); + }); + test.each([ + [multiLineFullQueryWithCaseClause.query, { lineNumber: 2, column: 8 }], + [multiLineFullQueryWithCaseClause.query, { lineNumber: 9, column: 7 }], + ])('should be case comparison operator', (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.CaseComparisonOperator); + }); + test.each([[multiLineFullQueryWithCaseClause.query, { lineNumber: 9, column: 9 }]])( + 'should be case value', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.CaseValue); + } + ); + test.each([[multiLineFullQueryWithCaseClause.query, { lineNumber: 9, column: 11 }]])( + 'should be after case value', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.AfterCaseValue); + } + ); + test.each([ + [multiLineFullQueryWithCaseClause.query, { lineNumber: 3, column: 5 }], + [multiLineFullQueryWithCaseClause.query, { lineNumber: 4, column: 5 }], + [multiLineFullQueryWithCaseClause.query, { lineNumber: 10, column: 5 }], + ])('should be when key', (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.WhenKey); + }); + test.each([ + [multiLineFullQueryWithCaseClause.query, { lineNumber: 3, column: 9 }], + [multiLineFullQueryWithCaseClause.query, { lineNumber: 4, column: 8 }], + [multiLineFullQueryWithCaseClause.query, { lineNumber: 10, column: 9 }], + [multiLineFullQueryWithCaseClause.query, { lineNumber: 11, column: 9 }], + ])('should be when comparison operator', (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.WhenComparisonOperator); + }); + test.each([[multiLineFullQueryWithCaseClause.query, { lineNumber: 4, column: 10 }]])( + 'should be when value', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.WhenValue); + } + ); + test.each([[multiLineFullQueryWithCaseClause.query, { lineNumber: 4, column: 14 }]])( + 'should be after when value', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.AfterWhenValue); + } + ); + test.each([ + [multiLineFullQueryWithCaseClause.query, { lineNumber: 3, column: 14 }], + [multiLineFullQueryWithCaseClause.query, { lineNumber: 4, column: 19 }], + [multiLineFullQueryWithCaseClause.query, { lineNumber: 10, column: 14 }], + [multiLineFullQueryWithCaseClause.query, { lineNumber: 11, column: 14 }], + ])('should be then expression', (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.ThenExpression); + }); + test.each([ + [multiLineFullQueryWithCaseClause.query, { lineNumber: 3, column: 20 }], + [multiLineFullQueryWithCaseClause.query, { lineNumber: 4, column: 29 }], + [multiLineFullQueryWithCaseClause.query, { lineNumber: 10, column: 21 }], + [multiLineFullQueryWithCaseClause.query, { lineNumber: 11, column: 24 }], + ])('should be after then expression', (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.AfterThenExpression); + }); + test.each([ + [multiLineFullQueryWithCaseClause.query, { lineNumber: 5, column: 5 }], + [multiLineFullQueryWithCaseClause.query, { lineNumber: 12, column: 5 }], + ])('should be after else keyword', (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.AfterElseKeyword); + }); + test.each([[multiLineFullQuery.query, { lineNumber: 7, column: 9 }]])( + 'should be after group by keywords', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.AfterGroupByKeywords); + } + ); + test.each([[multiLineFullQuery.query, { lineNumber: 7, column: 27 }]])( + 'should be after group by', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.AfterGroupBy); + } + ); + test.each([[multiLineFullQuery.query, { lineNumber: 9, column: 9 }]])( + 'should be after order by keywords', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.AfterOrderByKeywords); + } + ); + test.each([[multiLineFullQuery.query, { lineNumber: 9, column: 26 }]])( + 'should be after order by direction', + (query: string, position: monacoTypes.IPosition) => { + assertPosition(query, position, StatementPosition.AfterOrderByDirection); + } + ); +}); diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/completion/statementPosition.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/completion/statementPosition.ts new file mode 100644 index 00000000000..fe91a1957c4 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/completion/statementPosition.ts @@ -0,0 +1,375 @@ +import { LinkedToken } from '../../monarch/LinkedToken'; +import { StatementPosition } from '../../monarch/types'; +import { + ALL, + DISTINCT, + AS, + ASC, + BY, + DESC, + FROM, + GROUP, + ORDER, + SELECT, + WHERE, + HAVING, + ON, + LOGICAL_OPERATORS, + PREDICATE_OPERATORS, + NULL, + TRUE, + FALSE, + IN, + CASE, + WHEN, + THEN, + ELSE, + END, +} from '../language'; + +import { SQLTokenTypes } from './types'; + +export function getStatementPosition(currentToken: LinkedToken | null): StatementPosition { + const previousNonWhiteSpace = currentToken?.getPreviousNonWhiteSpaceToken(); + const previousKeyword = currentToken?.getPreviousKeyword(); + + const normalizedPreviousNonWhiteSpaceValue = previousNonWhiteSpace?.value?.toUpperCase() || ''; + const normalizedPreviousKeywordValue = previousKeyword?.value?.toUpperCase() || ''; + + let previousNonAliasKeywordValue = previousKeyword; + let normalizedPreviousNonAliasKeywordValue = normalizedPreviousKeywordValue; + while (normalizedPreviousNonAliasKeywordValue === AS) { + previousNonAliasKeywordValue = previousNonAliasKeywordValue?.getPreviousKeyword(); + normalizedPreviousNonAliasKeywordValue = previousNonAliasKeywordValue?.value.toUpperCase() || ''; + } + + const isPreviousSelectKeywordGroup = + normalizedPreviousNonAliasKeywordValue === SELECT || + ([ALL, DISTINCT].includes(normalizedPreviousNonAliasKeywordValue) && + previousNonAliasKeywordValue?.getPreviousKeyword()?.value.toUpperCase() === SELECT); + + if (currentToken?.is(SQLTokenTypes.Comment) || currentToken?.is('comment.quote.cloudwatch-logs-sql')) { + return StatementPosition.Comment; + } + + if ( + currentToken === null || + (currentToken.previous === null && currentToken.isIdentifier()) || + (currentToken.previous === null && currentToken.isWhiteSpace()) || + (currentToken.previous === null && currentToken.isKeyword() && currentToken.value.toUpperCase() === SELECT) + ) { + return StatementPosition.SelectKeyword; + } + + if ( + (currentToken.isWhiteSpace() || currentToken.is(SQLTokenTypes.Parenthesis, ')')) && + normalizedPreviousNonWhiteSpaceValue === SELECT + ) { + return StatementPosition.AfterSelectKeyword; + } + + if ( + isPreviousSelectKeywordGroup && + (currentToken.is(SQLTokenTypes.Delimiter, ',') || + (currentToken.isWhiteSpace() && previousNonWhiteSpace?.is(SQLTokenTypes.Delimiter, ',')) || + (currentToken.isWhiteSpace() && previousNonWhiteSpace?.isKeyword()) || + (currentToken.is(SQLTokenTypes.Parenthesis, ')') && + (previousNonWhiteSpace?.isKeyword() || previousNonWhiteSpace?.is(SQLTokenTypes.Delimiter, ',')))) + ) { + return StatementPosition.SelectExpression; + } + + if ( + isPreviousSelectKeywordGroup && + (currentToken.isWhiteSpace() || currentToken.is(SQLTokenTypes.Parenthesis, ')')) && + (previousNonWhiteSpace?.isIdentifier() || + previousNonWhiteSpace?.is(SQLTokenTypes.Parenthesis, ')') || + previousNonWhiteSpace?.is(SQLTokenTypes.Parenthesis, '()') || + previousNonWhiteSpace?.is(SQLTokenTypes.Operator, '*')) + ) { + return StatementPosition.AfterSelectExpression; + } + + if ( + currentToken.is(SQLTokenTypes.Parenthesis, '()') && + normalizedPreviousNonAliasKeywordValue === WHERE && + normalizedPreviousNonWhiteSpaceValue === IN + ) { + return StatementPosition.Subquery; + } + + if ( + ((currentToken.is(SQLTokenTypes.Parenthesis, '()') || currentToken.is(SQLTokenTypes.Parenthesis, '())')) && + previousNonWhiteSpace?.isFunction()) || + (currentToken.is(SQLTokenTypes.Delimiter, ',') && + currentToken.getPreviousOfType(SQLTokenTypes.Parenthesis, '(')?.getPreviousNonWhiteSpaceToken()?.isFunction()) || + (currentToken.isWhiteSpace() && + previousNonWhiteSpace?.is(SQLTokenTypes.Delimiter, ',') && + currentToken.getPreviousOfType(SQLTokenTypes.Parenthesis, '(')?.getPreviousNonWhiteSpaceToken()?.isFunction()) || + (currentToken.is(SQLTokenTypes.Parenthesis, ')') && + previousNonWhiteSpace?.is(SQLTokenTypes.Delimiter, ',') && + currentToken.getPreviousOfType(SQLTokenTypes.Parenthesis, '(')?.getPreviousNonWhiteSpaceToken()?.isFunction()) + ) { + return StatementPosition.PredefinedFunctionArgument; + } + + if ( + (currentToken.isWhiteSpace() || currentToken.is(SQLTokenTypes.Parenthesis, ')')) && + normalizedPreviousNonWhiteSpaceValue === FROM + ) { + return StatementPosition.AfterFromKeyword; + } + + if ( + normalizedPreviousNonAliasKeywordValue === FROM && + (previousNonWhiteSpace?.isIdentifier() || + previousNonWhiteSpace?.isDoubleQuotedString() || + previousNonWhiteSpace?.isVariable() || + previousNonWhiteSpace?.is(SQLTokenTypes.Parenthesis, ')')) + ) { + return StatementPosition.AfterFromArguments; + } + + if ( + (LOGICAL_OPERATORS.includes(normalizedPreviousNonWhiteSpaceValue) && + [WHERE, HAVING, ON, CASE, WHEN].includes(normalizedPreviousKeywordValue)) || + ((currentToken.isWhiteSpace() || currentToken.is(SQLTokenTypes.Parenthesis, ')')) && + [WHERE, HAVING, ON, CASE, WHEN].includes(normalizedPreviousNonWhiteSpaceValue)) + ) { + switch (normalizedPreviousKeywordValue) { + case WHERE: + return StatementPosition.WhereKey; + case HAVING: + return StatementPosition.HavingKey; + case ON: + return StatementPosition.OnKey; + case CASE: + return StatementPosition.CaseKey; + case WHEN: + return StatementPosition.WhenKey; + } + } + + if ( + (LOGICAL_OPERATORS.includes(normalizedPreviousNonWhiteSpaceValue) && + [NULL, TRUE, FALSE].includes(normalizedPreviousKeywordValue)) || + ((currentToken.isWhiteSpace() || currentToken.is(SQLTokenTypes.Parenthesis, ')')) && + [NULL, TRUE, FALSE].includes(normalizedPreviousNonWhiteSpaceValue)) + ) { + let nearestPreviousKeyword = previousKeyword; + let normalizedNearestPreviousKeywordValue = normalizedPreviousKeywordValue; + while (![WHERE, HAVING, ON, CASE, WHEN].includes(normalizedNearestPreviousKeywordValue)) { + nearestPreviousKeyword = nearestPreviousKeyword?.getPreviousKeyword(); + normalizedNearestPreviousKeywordValue = nearestPreviousKeyword?.value.toUpperCase() || ''; + } + + switch (normalizedNearestPreviousKeywordValue) { + case WHERE: + return StatementPosition.WhereKey; + case HAVING: + return StatementPosition.HavingKey; + case ON: + return StatementPosition.OnKey; + case CASE: + return StatementPosition.CaseKey; + case WHEN: + return StatementPosition.WhenKey; + } + } + + if ( + [WHERE, HAVING, ON, CASE, WHEN].includes(normalizedPreviousKeywordValue) && + PREDICATE_OPERATORS.includes(normalizedPreviousNonWhiteSpaceValue) + ) { + switch (normalizedPreviousKeywordValue) { + case WHERE: + return StatementPosition.WhereValue; + case HAVING: + return StatementPosition.HavingValue; + case ON: + return StatementPosition.OnValue; + case CASE: + return StatementPosition.CaseValue; + case WHEN: + return StatementPosition.WhenValue; + } + } + + if ( + [NULL, TRUE, FALSE].includes(normalizedPreviousKeywordValue) && + PREDICATE_OPERATORS.includes(normalizedPreviousNonWhiteSpaceValue) + ) { + let nearestPreviousKeyword = previousKeyword; + let normalizedNearestPreviousKeywordValue = normalizedPreviousKeywordValue; + while (![WHERE, HAVING, ON, CASE, WHEN].includes(normalizedNearestPreviousKeywordValue)) { + nearestPreviousKeyword = nearestPreviousKeyword?.getPreviousKeyword(); + normalizedNearestPreviousKeywordValue = nearestPreviousKeyword?.value.toUpperCase() || ''; + } + + switch (normalizedNearestPreviousKeywordValue) { + case WHERE: + return StatementPosition.WhereValue; + case HAVING: + return StatementPosition.HavingValue; + case ON: + return StatementPosition.OnValue; + case CASE: + return StatementPosition.CaseValue; + case WHEN: + return StatementPosition.WhenValue; + } + } + + if ( + [WHERE, HAVING, ON, CASE, WHEN].includes(normalizedPreviousKeywordValue) && + (previousNonWhiteSpace?.isIdentifier() || + previousNonWhiteSpace?.isDoubleQuotedString() || + previousNonWhiteSpace?.isFunction() || + previousNonWhiteSpace?.isNumber() || + previousNonWhiteSpace?.isString() || + previousNonWhiteSpace?.is(SQLTokenTypes.Parenthesis, ')') || + previousNonWhiteSpace?.is(SQLTokenTypes.Parenthesis, '()')) + ) { + const previousTokens = currentToken.getPreviousUntil(SQLTokenTypes.Keyword, [], normalizedPreviousKeywordValue); + const numPredicateOperators = + previousTokens?.filter((token) => PREDICATE_OPERATORS.includes(token.value.toUpperCase())).length || 0; + const numLogicalOperators = + previousTokens?.filter((token) => LOGICAL_OPERATORS.includes(token.value.toUpperCase())).length || 0; + + if (numPredicateOperators - numLogicalOperators === 0) { + switch (normalizedPreviousKeywordValue) { + case WHERE: + return StatementPosition.WhereComparisonOperator; + case HAVING: + return StatementPosition.HavingComparisonOperator; + case ON: + return StatementPosition.OnComparisonOperator; + case CASE: + return StatementPosition.CaseComparisonOperator; + case WHEN: + return StatementPosition.WhenComparisonOperator; + } + } else { + switch (normalizedPreviousKeywordValue) { + case WHERE: + return StatementPosition.AfterWhereValue; + case HAVING: + return StatementPosition.AfterHavingValue; + case ON: + return StatementPosition.AfterOnValue; + case CASE: + return StatementPosition.AfterCaseValue; + case WHEN: + return StatementPosition.AfterWhenValue; + } + } + } + + if ( + [NULL, TRUE, FALSE].includes(normalizedPreviousKeywordValue) && + PREDICATE_OPERATORS.includes(previousKeyword?.getPreviousNonWhiteSpaceToken()?.value.toUpperCase() || '') + ) { + let nearestPreviousKeyword = previousKeyword?.getPreviousKeyword(); + let normalizedNearestPreviousKeywordValue = nearestPreviousKeyword?.value.toUpperCase() || ''; + while (![WHERE, HAVING, ON, CASE, WHEN].includes(normalizedNearestPreviousKeywordValue)) { + nearestPreviousKeyword = nearestPreviousKeyword?.getPreviousKeyword(); + normalizedNearestPreviousKeywordValue = nearestPreviousKeyword?.value.toUpperCase() || ''; + } + + const previousTokens = currentToken.getPreviousUntil( + SQLTokenTypes.Keyword, + [], + normalizedNearestPreviousKeywordValue + ); + const numPredicateOperators = + previousTokens?.filter((token) => PREDICATE_OPERATORS.includes(token.value.toUpperCase())).length || 0; + const numLogicalOperators = + previousTokens?.filter((token) => LOGICAL_OPERATORS.includes(token.value.toUpperCase())).length || 0; + + if (numPredicateOperators - numLogicalOperators === 0) { + switch (normalizedNearestPreviousKeywordValue) { + case WHERE: + return StatementPosition.WhereComparisonOperator; + case HAVING: + return StatementPosition.HavingComparisonOperator; + case ON: + return StatementPosition.OnComparisonOperator; + case CASE: + return StatementPosition.CaseComparisonOperator; + case WHEN: + return StatementPosition.WhenComparisonOperator; + } + } else { + switch (normalizedNearestPreviousKeywordValue) { + case WHERE: + return StatementPosition.AfterWhereValue; + case HAVING: + return StatementPosition.AfterHavingValue; + case ON: + return StatementPosition.AfterOnValue; + case CASE: + return StatementPosition.AfterCaseValue; + case WHEN: + return StatementPosition.AfterWhenValue; + } + } + } + + if (currentToken.isWhiteSpace() && normalizedPreviousNonWhiteSpaceValue === THEN) { + return StatementPosition.ThenExpression; + } + + if ( + currentToken.isWhiteSpace() && + normalizedPreviousKeywordValue === THEN && + normalizedPreviousNonWhiteSpaceValue !== THEN + ) { + return StatementPosition.AfterThenExpression; + } + + if (currentToken.isWhiteSpace() && normalizedPreviousNonWhiteSpaceValue === ELSE) { + return StatementPosition.AfterElseKeyword; + } + + if (normalizedPreviousNonWhiteSpaceValue === END && currentToken.isWhiteSpace()) { + let nearestCaseKeyword = previousKeyword; + while (CASE !== nearestCaseKeyword?.value.toUpperCase()) { + nearestCaseKeyword = nearestCaseKeyword?.getPreviousKeyword(); + } + const nearestKeywordBeforeCaseKeywordValue = nearestCaseKeyword.getPreviousKeyword()?.value.toUpperCase() || ''; + switch (nearestKeywordBeforeCaseKeywordValue) { + case SELECT: + return StatementPosition.AfterSelectExpression; + case WHERE: + return StatementPosition.AfterWhereValue; + } + } + + if ( + normalizedPreviousKeywordValue === BY && + previousKeyword?.getPreviousKeyword()?.value.toUpperCase() === GROUP && + (previousNonWhiteSpace?.value.toUpperCase() === BY || previousNonWhiteSpace?.is(SQLTokenTypes.Delimiter, ',')) + ) { + return StatementPosition.AfterGroupByKeywords; + } + + if ( + normalizedPreviousKeywordValue === BY && + previousKeyword?.getPreviousKeyword()?.value.toUpperCase() === GROUP && + (previousNonWhiteSpace?.isIdentifier() || + previousNonWhiteSpace?.is(SQLTokenTypes.Parenthesis, ')') || + previousNonWhiteSpace?.is(SQLTokenTypes.Parenthesis, '()')) + ) { + return StatementPosition.AfterGroupBy; + } + + if (normalizedPreviousKeywordValue === BY && previousKeyword?.getPreviousKeyword()?.value.toUpperCase() === ORDER) { + return StatementPosition.AfterOrderByKeywords; + } + + if ([DESC, ASC].includes(normalizedPreviousKeywordValue)) { + return StatementPosition.AfterOrderByDirection; + } + + return StatementPosition.Unknown; +} diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/completion/suggestionKind.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/completion/suggestionKind.ts new file mode 100644 index 00000000000..6b1c64014bf --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/completion/suggestionKind.ts @@ -0,0 +1,119 @@ +import { StatementPosition, SuggestionKind } from '../../monarch/types'; + +export function getSuggestionKinds(statementPosition: StatementPosition): SuggestionKind[] { + switch (statementPosition) { + case StatementPosition.SelectKeyword: + return [SuggestionKind.SelectKeyword]; + case StatementPosition.AfterSelectKeyword: + return [ + SuggestionKind.AfterSelectKeyword, + SuggestionKind.FunctionsWithArguments, + SuggestionKind.Field, + SuggestionKind.CaseKeyword, + ]; + case StatementPosition.SelectExpression: + return [SuggestionKind.FunctionsWithArguments, SuggestionKind.Field, SuggestionKind.CaseKeyword]; + case StatementPosition.AfterSelectExpression: + return [ + SuggestionKind.FromKeyword, + SuggestionKind.FunctionsWithArguments, + SuggestionKind.Field, + SuggestionKind.CaseKeyword, + ]; + + case StatementPosition.FromKeyword: + return [SuggestionKind.FromKeyword, SuggestionKind.FunctionsWithArguments, SuggestionKind.Field]; + case StatementPosition.AfterFromKeyword: + return [SuggestionKind.AfterFromKeyword]; + case StatementPosition.AfterFromArguments: + return [ + SuggestionKind.WhereKeyword, + SuggestionKind.GroupByKeywords, + SuggestionKind.OrderByKeywords, + SuggestionKind.LimitKeyword, + SuggestionKind.JoinKeywords, + SuggestionKind.HavingKeywords, + ]; + + case StatementPosition.WhereKey: + return [SuggestionKind.FunctionsWithArguments, SuggestionKind.Field, SuggestionKind.CaseKeyword]; + case StatementPosition.WhereComparisonOperator: + return [SuggestionKind.ComparisonOperators]; + case StatementPosition.WhereValue: + return [SuggestionKind.FunctionsWithArguments, SuggestionKind.Field]; + case StatementPosition.AfterWhereValue: + return [ + SuggestionKind.LogicalOperators, + SuggestionKind.GroupByKeywords, + SuggestionKind.OrderByKeywords, + SuggestionKind.LimitKeyword, + ]; + + case StatementPosition.HavingKey: + return [SuggestionKind.FunctionsWithArguments, SuggestionKind.Field]; + case StatementPosition.HavingComparisonOperator: + return [SuggestionKind.ComparisonOperators]; + case StatementPosition.HavingValue: + return [SuggestionKind.FunctionsWithArguments, SuggestionKind.Field]; + case StatementPosition.AfterHavingValue: + return [SuggestionKind.LogicalOperators, SuggestionKind.OrderByKeywords, SuggestionKind.LimitKeyword]; + + case StatementPosition.OnKey: + return [SuggestionKind.FunctionsWithArguments, SuggestionKind.Field]; + case StatementPosition.OnComparisonOperator: + return [SuggestionKind.ComparisonOperators]; + case StatementPosition.OnValue: + return [SuggestionKind.FunctionsWithArguments, SuggestionKind.Field]; + case StatementPosition.AfterOnValue: + return [ + SuggestionKind.LogicalOperators, + SuggestionKind.GroupByKeywords, + SuggestionKind.OrderByKeywords, + SuggestionKind.LimitKeyword, + ]; + + case StatementPosition.CaseKey: + return [SuggestionKind.WhenKeyword, SuggestionKind.Field, SuggestionKind.FunctionsWithArguments]; + case StatementPosition.CaseComparisonOperator: + return [SuggestionKind.ComparisonOperators, SuggestionKind.WhenKeyword]; + case StatementPosition.CaseValue: + return [SuggestionKind.FunctionsWithArguments, SuggestionKind.Field]; + case StatementPosition.AfterCaseValue: + return [SuggestionKind.WhenKeyword]; + + case StatementPosition.WhenKey: + return [SuggestionKind.Field, SuggestionKind.FunctionsWithArguments]; + case StatementPosition.WhenComparisonOperator: + return [SuggestionKind.ComparisonOperators, SuggestionKind.ThenKeyword]; + case StatementPosition.WhenValue: + return [SuggestionKind.FunctionsWithArguments, SuggestionKind.Field]; + case StatementPosition.AfterWhenValue: + return [SuggestionKind.ThenKeyword]; + + case StatementPosition.ThenExpression: + return [SuggestionKind.FunctionsWithArguments, SuggestionKind.Field]; + case StatementPosition.AfterThenExpression: + return [SuggestionKind.WhenKeyword, SuggestionKind.AfterThenExpression]; + + case StatementPosition.AfterElseKeyword: + return [SuggestionKind.FunctionsWithArguments, SuggestionKind.Field]; + + case StatementPosition.AfterGroupByKeywords: + return [SuggestionKind.Field, SuggestionKind.FunctionsWithArguments]; + case StatementPosition.AfterGroupBy: + return [SuggestionKind.OrderByKeywords, SuggestionKind.LimitKeyword, SuggestionKind.HavingKeywords]; + + case StatementPosition.AfterOrderByKeywords: + return [SuggestionKind.SortOrderDirectionKeyword, SuggestionKind.LimitKeyword, SuggestionKind.Field]; + case StatementPosition.AfterOrderByDirection: + return [SuggestionKind.LimitKeyword]; + + case StatementPosition.PredefinedFunctionArgument: + return [SuggestionKind.Field]; + + case StatementPosition.Subquery: + return [SuggestionKind.SelectKeyword, SuggestionKind.FunctionsWithArguments, SuggestionKind.Field]; + } + + return []; +} diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/completion/types.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/completion/types.ts new file mode 100644 index 00000000000..a8058fd1a19 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/completion/types.ts @@ -0,0 +1,18 @@ +import { TokenTypes } from '../../monarch/types'; +import { CLOUDWATCH_LOGS_SQL_LANGUAGE_DEFINITION_ID } from '../definition'; + +export const SQLTokenTypes: TokenTypes = { + Parenthesis: `delimiter.parenthesis.${CLOUDWATCH_LOGS_SQL_LANGUAGE_DEFINITION_ID}`, + Whitespace: `white.${CLOUDWATCH_LOGS_SQL_LANGUAGE_DEFINITION_ID}`, + Keyword: `keyword.${CLOUDWATCH_LOGS_SQL_LANGUAGE_DEFINITION_ID}`, + Delimiter: `delimiter.${CLOUDWATCH_LOGS_SQL_LANGUAGE_DEFINITION_ID}`, + Operator: `operator.${CLOUDWATCH_LOGS_SQL_LANGUAGE_DEFINITION_ID}`, + Identifier: `identifier.${CLOUDWATCH_LOGS_SQL_LANGUAGE_DEFINITION_ID}`, + Type: `type.${CLOUDWATCH_LOGS_SQL_LANGUAGE_DEFINITION_ID}`, + Function: `predefined.${CLOUDWATCH_LOGS_SQL_LANGUAGE_DEFINITION_ID}`, + Number: `number.${CLOUDWATCH_LOGS_SQL_LANGUAGE_DEFINITION_ID}`, + String: `string.${CLOUDWATCH_LOGS_SQL_LANGUAGE_DEFINITION_ID}`, + Variable: `variable.${CLOUDWATCH_LOGS_SQL_LANGUAGE_DEFINITION_ID}`, + Comment: `comment.${CLOUDWATCH_LOGS_SQL_LANGUAGE_DEFINITION_ID}`, + Regexp: `regexp.${CLOUDWATCH_LOGS_SQL_LANGUAGE_DEFINITION_ID}`, +}; diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/definition.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/definition.ts new file mode 100644 index 00000000000..5d6ce9c694c --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/definition.ts @@ -0,0 +1,12 @@ +import { LanguageDefinition } from '../monarch/register'; + +export const CLOUDWATCH_LOGS_SQL_LANGUAGE_DEFINITION_ID = 'cloudwatch-logs-sql'; + +const cloudWatchLogsSqlLanguageDefinition: LanguageDefinition = { + id: CLOUDWATCH_LOGS_SQL_LANGUAGE_DEFINITION_ID, + extensions: [], + aliases: [], + mimetypes: [], + loader: () => import('./language'), +}; +export default cloudWatchLogsSqlLanguageDefinition; diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/language.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/language.ts new file mode 100644 index 00000000000..419684e9748 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs-sql/language.ts @@ -0,0 +1,596 @@ +import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api'; + +interface CloudWatchLanguage extends monacoType.languages.IMonarchLanguage { + keywords: string[]; + operators: string[]; + builtinFunctions: string[]; +} + +/* KEYWORDS */ +export const ALL = 'ALL'; +export const AND = 'AND'; +export const ANY = 'ANY'; +export const AS = 'AS'; +export const ASC = 'ASC'; +export const BETWEEN = 'BETWEEN'; +export const BY = 'BY'; +export const CASE = 'CASE'; +export const CUBE = 'CUBE'; +export const DESC = 'DESC'; +export const DISTINCT = 'DISTINCT'; +export const ELSE = 'ELSE'; +export const END = 'END'; +export const ESCAPE = 'ESCAPE'; +export const EXISTS = 'EXISTS'; +export const FALSE = 'FALSE'; +export const FILTER = 'FILTER'; +export const FIRST = 'FIRST'; +export const FROM = 'FROM'; +export const GROUP = 'GROUP'; +export const GROUPING = 'GROUPING'; +export const HAVING = 'HAVING'; +export const ILIKE = 'ILIKE'; +export const IN = 'IN'; +export const INNER = 'INNER'; +export const IS = 'IS'; +export const JOIN = 'JOIN'; +export const LAST = 'LAST'; +export const LEFT = 'LEFT'; +export const LIKE = 'LIKE'; +export const LIMIT = 'LIMIT'; +export const NOT = 'NOT'; +export const NULL = 'NULL'; +export const ON = 'ON'; +export const OR = 'OR'; +export const ORDER = 'ORDER'; +export const OUTER = 'OUTER'; +export const ROLLUP = 'ROLLUP'; +export const SELECT = 'SELECT'; +export const SETS = 'SETS'; +export const SOME = 'SOME'; +export const THEN = 'THEN'; +export const TRUE = 'TRUE'; +export const USING = 'USING'; +export const WHEN = 'WHEN'; +export const WHERE = 'WHERE'; +export const WITH = 'WITH'; + +export const KEYWORDS = [ + ALL, + AND, + ANY, + AS, + ASC, + BETWEEN, + BY, + CASE, + CUBE, + DESC, + DISTINCT, + ELSE, + END, + ESCAPE, + EXISTS, + FALSE, + FILTER, + FIRST, + FROM, + GROUP, + GROUPING, + HAVING, + ILIKE, + IN, + INNER, + IS, + JOIN, + LAST, + LEFT, + LIKE, + LIMIT, + NOT, + NULL, + ON, + OR, + ORDER, + OUTER, + ROLLUP, + SELECT, + SETS, + SOME, + THEN, + TRUE, + USING, + WHEN, + WHERE, + WITH, +]; +export const AFTER_SELECT_KEYWORDS = [ALL, DISTINCT]; + +export const ALL_KEYWORDS = [...KEYWORDS, ...AFTER_SELECT_KEYWORDS]; + +/* FUNCTIONS */ +export const AGGREGATE_FUNCTIONS = [ + 'any', + 'any_value', + 'approx_count_distinct', + 'approx_percentile', + 'array_agg', + 'avg', + 'bit_and', + 'bit_or', + 'bit_xor', + 'bitmap_construct_agg', + 'bitmap_or_agg', + 'bool_and', + 'bool_or', + 'collect_list', + 'collect_set', + 'count', + 'count_if', + 'count_min_sketch', + 'covar_pop', + 'covar_samp', + 'every', + 'first', + 'first_value', + 'grouping', + 'grouping_id', + 'histogram_numeric', + 'hll_sketch_agg', + 'hll_union_agg', + 'kurtosis', + 'last', + 'last_value', + 'max', + 'max_by', + 'mean', + 'median', + 'min', + 'min_by', + 'mode', + 'percentile', + 'percentile_approx', + 'regr_avgx', + 'regr_avgy', + 'regr_count', + 'regr_intercept', + 'regr_r2', + 'regr_slope', + 'regr_sxx', + 'regr_sxy', + 'regr_syy', + 'skewness', + 'some', + 'std', + 'stddev', + 'stddev_pop', + 'stddev_samp', + 'sum', + 'try_avg', + 'try_sum', + 'var_pop', + 'var_samp', + 'variance', +]; +export const ARRAY_FUNCTIONS = [ + 'array', + 'array_append', + 'array_compact', + 'array_contains', + 'array_distinct', + 'array_except', + 'array_insert', + 'array_intersect', + 'array_join', + 'array_max', + 'array_min', + 'array_position', + 'array_prepend', + 'array_remove', + 'array_repeat', + 'array_union', + 'arrays_overlap', + 'arrays_zip', + 'flatten', + 'get', + 'sequence', + 'shuffle', + 'slice', + 'sort_array', +]; +export const CONDITIONAL_FUNCTIONS = ['coalesce', 'if', 'ifnull', 'nanvl', 'nullif', 'nvl', 'nvl2']; +export const CONVERSION_FUNCTIONS = [ + 'bigint', + 'binary', + 'boolean', + 'cast', + 'date', + 'decimal', + 'double', + 'float', + 'int', + 'smallint', + 'string', + 'timestamp', + 'tinyint', +]; +export const DATE_AND_TIMESTAMP_FUNCTIONS = [ + 'add_months', + 'convert_timezone', + 'curdate', + 'current_date', + 'current_timestamp', + 'current_timezone', + 'date_add', + 'date_diff', + 'date_format', + 'date_from_unix_date', + 'date_part', + 'date_sub', + 'date_trunc', + 'dateadd', + 'datediff', + 'datepart', + 'day', + 'dayofmonth', + 'dayofweek', + 'dayofyear', + 'extract', + 'from_unixtime', + 'from_utc_timestamp', + 'hour', + 'last_day', + 'localtimestamp', + 'localtimestamp', + 'make_date', + 'make_dt_interval', + 'make_interval', + 'make_timestamp', + 'make_timestamp_ltz', + 'make_timestamp_ntz', + 'make_ym_interval', + 'minute', + 'month', + 'months_between', + 'next_day', + 'now', + 'quarter', + 'second', + 'session_window', + 'timestamp_micros', + 'timestamp_millis', + 'timestamp_seconds', + 'to_date', + 'to_timestamp', + 'to_timestamp_ltz', + 'to_timestamp_ntz', + 'to_unix_timestamp', + 'to_utc_timestamp', + 'trunc', + 'try_to_timestamp', + 'unix_date', + 'unix_micros', + 'unix_millis', + 'unix_seconds', + 'unix_timestamp', + 'weekday', + 'weekofyear', + 'window', + 'window_time', + 'year', +]; +export const JSON_FUNCTIONS = [ + 'from_json', + 'get_json_object', + 'json_array_length', + 'json_object_keys', + 'json_tuple', + 'schema_of_json', + 'to_json', +]; +export const MATHEMATICAL_FUNCTIONS = [ + 'abs', + 'acos', + 'acosh', + 'asin', + 'asinh', + 'atan', + 'atan2', + 'atanh', + 'bin', + 'bround', + 'cbrt', + 'ceil', + 'ceiling', + 'conv', + 'cos', + 'cosh', + 'cot', + 'csc', + 'degrees', + 'e', + 'exp', + 'expm1', + 'factorial', + 'floor', + 'greatest', + 'hex', + 'hypot', + 'least', + 'ln', + 'log', + 'log10', + 'log1p', + 'log2', + 'negative', + 'pi', + 'pmod', + 'positive', + 'pow', + 'power', + 'radians', + 'rand', + 'randn', + 'random', + 'rint', + 'round', + 'sec', + 'shiftleft', + 'sign', + 'signum', + 'sin', + 'sinh', + 'sqrt', + 'tan', + 'tanh', + 'try_add', + 'try_divide', + 'try_multiply', + 'try_subtract', + 'unhex', + 'width_bucket', +]; +export const PREDICATE_FUNCTIONS = ['isnan', 'isnotnull', 'isnull', 'regexp', 'regexp_like', 'rlike']; +export const STRING_FUNCTIONS = [ + 'ascii', + 'base64', + 'bit_length', + 'btrim', + 'char', + 'char_length', + 'character_length', + 'chr', + 'concat_ws', + 'contains', + 'decode', + 'elt', + 'encode', + 'endswith', + 'find_in_set', + 'format_number', + 'format_string', + 'initcap', + 'instr', + 'lcase', + 'left', + 'len', + 'length', + 'levenshtein', + 'locate', + 'lower', + 'lpad', + 'ltrim', + 'luhn_check', + 'mask', + 'octet_length', + 'overlay', + 'position', + 'printf', + 'regexp_count', + 'regexp_extract', + 'regexp_extract_all', + 'regexp_instr', + 'regexp_replace', + 'regexp_substr', + 'repeat', + 'replace', + 'right', + 'rpad', + 'rtrim', + 'sentences', + 'soundex', + 'space', + 'split', + 'split_part', + 'startswith', + 'substr', + 'substring', + 'substring_index', + 'to_binary', + 'to_char', + 'to_number', + 'to_varchar', + 'translate', + 'trim', + 'try_to_binary', + 'try_to_number', + 'ucase', + 'unbase64', + 'upper', +]; +export const WINDOW_FUNCTIONS = [ + 'cume_dist', + 'dense_rank', + 'lag', + 'lead', + 'nth_value', + 'ntile', + 'percent_rank', + 'rank', + 'row_number', +]; + +export const ALL_FUNCTIONS = [ + ...AGGREGATE_FUNCTIONS, + ...ARRAY_FUNCTIONS, + ...CONDITIONAL_FUNCTIONS, + ...CONVERSION_FUNCTIONS, + ...DATE_AND_TIMESTAMP_FUNCTIONS, + ...JSON_FUNCTIONS, + ...MATHEMATICAL_FUNCTIONS, + ...PREDICATE_FUNCTIONS, + ...STRING_FUNCTIONS, + ...WINDOW_FUNCTIONS, +]; + +/* OPERATORS */ +export const EQUAL = '='; +export const DOUBLE_EQUALS = '=='; +export const NULL_SAFE_EQUAL = '<=>'; +export const NOT_EQUAL = '!='; +export const GREATER_THAN = '>'; +export const GREATER_THAN_EQUAL = '>='; +export const LESS_THAN = '<'; +export const LESS_THAN_EQUAL = '<='; + +export const LOGICAL_OPERATORS = [OR, AND]; +export const MATH_OPERATORS = ['*', '/', '+', '-', '%', 'div', 'mod']; +export const PREDICATE_OPERATORS = [ + NOT, + IS, + EQUAL, + DOUBLE_EQUALS, + NULL_SAFE_EQUAL, + NOT_EQUAL, + GREATER_THAN, + GREATER_THAN_EQUAL, + LESS_THAN, + LESS_THAN_EQUAL, + LIKE, + ILIKE, + IN, +]; + +export const ALL_OPERATORS = [...MATH_OPERATORS, ...LOGICAL_OPERATORS, ...PREDICATE_OPERATORS]; + +export const language: CloudWatchLanguage = { + defaultToken: '', + ignoreCase: true, + brackets: [ + { open: '[', close: ']', token: 'delimiter.square' }, + { open: '(', close: ')', token: 'delimiter.parenthesis' }, + { open: '{', close: '}', token: 'delimiter.curly' }, + ], + keywords: ALL_KEYWORDS, + operators: ALL_OPERATORS, + builtinFunctions: ALL_FUNCTIONS, + tokenizer: { + root: [ + { include: '@comments' }, + { include: '@whitespace' }, + { include: '@customParams' }, + { include: '@numbers' }, + { include: '@binaries' }, + { include: '@strings' }, + { include: '@strings' }, + { include: '@complexIdentifiers' }, + [/[;,.]/, 'delimiter'], + [/[\(\)\[\]\{\}]/, '@brackets'], + [ + /[\w@#$]+/, + { + cases: { + '@operators': 'operator', + '@builtinFunctions': 'predefined', + '@keywords': 'keyword', + '@default': 'identifier', + }, + }, + ], + [/[<>=!%&+\-*/|~^]/, 'operator'], + ], + whitespace: [[/[\s\t\r\n]+/, 'white']], + comments: [ + [/--+.*/, 'comment'], + [/\/\*/, { token: 'comment.quote', next: '@comment' }], + ], + comment: [ + [/[^*/]+/, 'comment'], + [/\*\//, { token: 'comment.quote', next: '@pop' }], + [/./, 'comment'], + ], + customParams: [ + [/\${[A-Za-z0-9._-]*}/, 'variable'], + [/\@\@{[A-Za-z0-9._-]*}/, 'variable'], + ], + numbers: [ + [/0[xX][0-9a-fA-F]*/, 'number'], + [/[$][+-]*\d*(\.\d*)?/, 'number'], + [/((\d+(\.\d*)?)|(\.\d+))([eE][\-+]?\d+)?/, 'number'], + ], + binaries: [ + [/X'/i, { token: 'binary', next: '@binarySingle' }], + [/X"/i, { token: 'binary', next: '@binaryDouble' }], + ], + binarySingle: [ + [/\d+/, 'binary.escape'], + [/''/, 'binary'], + [/'/, { token: 'binary', next: '@pop' }], + ], + binaryDouble: [ + [/\d+/, 'binary.escape'], + [/""/, 'binary'], + [/"/, { token: 'binary', next: '@pop' }], + ], + strings: [ + [/'/, { token: 'string', next: '@stringSingle' }], + [/R'/i, { token: 'string', next: '@stringSingle' }], + [/"/, { token: 'string', next: '@stringDouble' }], + [/R"/i, { token: 'string', next: '@stringDouble' }], + ], + stringSingle: [ + [/[^']+/, 'string.escape'], + [/''/, 'string'], + [/'/, { token: 'string', next: '@pop' }], + ], + stringDouble: [ + [/[^"]+/, 'string.escape'], + [/""/, 'string'], + [/"/, { token: 'string', next: '@pop' }], + ], + complexIdentifiers: [[/`/, { token: 'identifier', next: '@quotedIdentifier' }]], + quotedIdentifier: [ + [/[^`]+/, 'identifier'], + [/``/, 'identifier'], + [/`/, { token: 'identifier', next: '@pop' }], + ], + }, +}; + +export const conf: monacoType.languages.LanguageConfiguration = { + comments: { + lineComment: '--', + blockComment: ['/*', '*/'], + }, + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ], + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + { open: '`', close: '`' }, + ], + surroundingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + { open: '`', close: '`' }, + ], +}; diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/completion/PPLCompletionItemProvider.test.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/completion/PPLCompletionItemProvider.test.ts new file mode 100644 index 00000000000..f1403d3682b --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/completion/PPLCompletionItemProvider.test.ts @@ -0,0 +1,254 @@ +import { CustomVariableModel } from '@grafana/data'; +import { Monaco, monacoTypes } from '@grafana/ui'; + +import { logGroupNamesVariable, setupMockedTemplateService } from '../../../__mocks__/CloudWatchDataSource'; +import { newCommandQuery } from '../../../__mocks__/cloudwatch-ppl-test-data/newCommandQuery'; +import { + dedupQueryWithOptionalArgs, + emptyQuery, + evalQuery, + fieldsQuery, + headQuery, + parseQuery, + queryWithArithmeticOps, + queryWithFunctionCalls, + queryWithFieldList, + sortQuery, + statsQuery, + topQuery, + whereQuery, +} from '../../../__mocks__/cloudwatch-ppl-test-data/singleLineQueries'; +import MonacoMock from '../../../__mocks__/monarch/Monaco'; +import TextModel from '../../../__mocks__/monarch/TextModel'; +import { ResourcesAPI } from '../../../resources/ResourcesAPI'; +import { ResourceResponse } from '../../../resources/types'; +import { LogGroup, LogGroupField } from '../../../types'; +import cloudWatchLogsPPLLanguageDefinition from '../definition'; +import { + BOOLEAN_LITERALS, + CONDITION_FUNCTIONS, + DEDUP_PARAMETERS, + EVAL_FUNCTIONS, + FIELD_OPERATORS, + NOT, + PPL_COMMANDS, + PPL_FUNCTIONS, + SORT_FIELD_FUNCTIONS, + SPAN, + STATS_PARAMETERS, + STATS_FUNCTIONS, + FROM, +} from '../language'; + +import { PPLCompletionItemProvider } from './PPLCompletionItemProvider'; + +jest.mock('monaco-editor/esm/vs/editor/editor.api', () => ({ + Token: jest.fn((offset, type, language) => ({ offset, type, language })), +})); + +const logFields = [{ value: { name: '@field' } }, { value: { name: '@message' } }]; +const logFieldNames = ['@field', '@message']; +const logGroups = [{ arn: 'foo', name: 'bar' }]; + +const getSuggestions = async ( + value: string, + position: monacoTypes.IPosition, + variables: CustomVariableModel[] = [], + logGroups: LogGroup[] = [], + fields: Array> = [] +) => { + const setup = new PPLCompletionItemProvider({} as ResourcesAPI, setupMockedTemplateService(variables), { + region: 'default', + logGroups, + }); + + setup.resources.getLogGroupFields = jest.fn().mockResolvedValue(fields); + const monaco = MonacoMock as Monaco; + const provider = setup.getCompletionProvider(monaco, cloudWatchLogsPPLLanguageDefinition); + const { suggestions } = await provider.provideCompletionItems( + TextModel(value) as monacoTypes.editor.ITextModel, + position + ); + return suggestions; +}; + +describe('PPLCompletionItemProvider', () => { + describe('getSuggestions', () => { + it('should suggest commands for an empty query', async () => { + const suggestions = await getSuggestions(emptyQuery.query, { lineNumber: 1, column: 1 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining(PPL_COMMANDS)); + }); + + it('should suggest commands for a query when a new command is started', async () => { + const suggestions = await getSuggestions(newCommandQuery.query, { lineNumber: 1, column: 20 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining(PPL_COMMANDS)); + }); + + describe('SuggestionKind.ValueExpression', () => { + test.each([ + [queryWithFunctionCalls.query, { lineNumber: 1, column: 20 }], + [queryWithFunctionCalls.query, { lineNumber: 1, column: 59 }], + [queryWithFunctionCalls.query, { lineNumber: 1, column: 78 }], + [queryWithArithmeticOps.query, { lineNumber: 1, column: 14 }], + [whereQuery.query, { lineNumber: 1, column: 71 }], + ])('should suggest functions and fields as argument for value expression', async (query, position) => { + const suggestions = await getSuggestions(query, position, [], logGroups, logFields); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining([...EVAL_FUNCTIONS, ...logFieldNames])); + }); + }); + + describe('[SuggestioKind.Field]', () => { + test.each([ + [evalQuery.query, { lineNumber: 1, column: 5 }], + [fieldsQuery.query, { lineNumber: 1, column: 9 }], + [topQuery.query, { lineNumber: 1, column: 36 }], + [queryWithFieldList.query, { lineNumber: 1, column: 22 }], + [statsQuery.query, { lineNumber: 1, column: 10 }], + ])('should suggest fields for SuggestionKind.Field', async (query, position) => { + const suggestions = await getSuggestions(query, position, [], logGroups, logFields); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining(logFieldNames)); + }); + }); + + it('should suggest from clause after HEAD command', async () => { + const suggestions = await getSuggestions(headQuery.query, { lineNumber: 1, column: 5 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual([FROM]); + }); + + it('should suggest stats parameters after STATS command', async () => { + const suggestions = await getSuggestions(statsQuery.query, { lineNumber: 1, column: 6 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining([...STATS_PARAMETERS, ...STATS_FUNCTIONS])); + expect(suggestionLabels).not.toContain('@field'); + }); + + it('should suggest fields, field operators and sort functions when in a sort field position', async () => { + const suggestions = await getSuggestions(sortQuery.query, { lineNumber: 1, column: 5 }, [], logGroups, logFields); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual( + expect.arrayContaining([...FIELD_OPERATORS, ...SORT_FIELD_FUNCTIONS, ...logFieldNames]) + ); + }); + + it('should suggest field operators and fields after FIELDS command', async () => { + const suggestions = await getSuggestions( + fieldsQuery.query, + { lineNumber: 1, column: 7 }, + [], + logGroups, + logFields + ); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining([...FIELD_OPERATORS, ...logFieldNames])); + }); + + it('should suggest boolean literals after boolean argument', async () => { + const suggestions = await getSuggestions(dedupQueryWithOptionalArgs.query, { lineNumber: 1, column: 53 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual( + expect.arrayContaining(BOOLEAN_LITERALS.map((booleanLiteral) => `= ${booleanLiteral}`)) + ); + }); + + it('should suggest dedup parameters after DEDUP field names', async () => { + const suggestions = await getSuggestions(dedupQueryWithOptionalArgs.query, { lineNumber: 1, column: 43 }); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining(DEDUP_PARAMETERS)); + }); + + it('should suggest fields and span function after STATS BY', async () => { + const suggestions = await getSuggestions( + statsQuery.query, + { lineNumber: 1, column: 42 }, + [], + logGroups, + logFields + ); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining([SPAN, ...logFieldNames])); + }); + + it('should suggest fields and sort functions after SORT field operator', async () => { + const suggestions = await getSuggestions(sortQuery.query, { lineNumber: 1, column: 7 }, [], logGroups, logFields); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining([...SORT_FIELD_FUNCTIONS, ...logFieldNames])); + }); + + it('should suggest PPL functions, NOT, case and fields in Expression clauses', async () => { + const evalSuggestions = await getSuggestions( + evalQuery.query, + { lineNumber: 1, column: 21 }, + [], + logGroups, + logFields + ); + const evalSuggestionLabels = evalSuggestions.map((s) => s.label); + expect(evalSuggestionLabels).toEqual( + expect.arrayContaining([...PPL_FUNCTIONS, ...EVAL_FUNCTIONS, ...CONDITION_FUNCTIONS, NOT, '@field', '@message']) + ); + + const parseSuggestions = await getSuggestions( + parseQuery.query, + { lineNumber: 1, column: 6 }, + [], + logGroups, + logFields + ); + const parseSuggestionLabels = parseSuggestions.map((s) => s.label); + expect(parseSuggestionLabels).toEqual( + expect.arrayContaining([...PPL_FUNCTIONS, ...EVAL_FUNCTIONS, ...CONDITION_FUNCTIONS, NOT, '@field', '@message']) + ); + }); + + it('should suggest functions, fields and boolean functions in a logical expression', async () => { + const suggestions = await getSuggestions( + whereQuery.query, + { lineNumber: 1, column: 6 }, + [], + logGroups, + logFields + ); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual( + expect.arrayContaining([...CONDITION_FUNCTIONS, ...EVAL_FUNCTIONS, '@field', '@message']) + ); + }); + + it('should suggest template variables appended to list of suggestions', async () => { + const suggestions = await getSuggestions( + sortQuery.query, + { lineNumber: 1, column: 7 }, + [logGroupNamesVariable], + logGroups, + logFields + ); + const suggestionLabels = suggestions.map((s) => s.label); + const expectedTemplateVariableLabel = `$${logGroupNamesVariable.name}`; + const expectedLabels = [...SORT_FIELD_FUNCTIONS, ...logFieldNames, expectedTemplateVariableLabel]; + expect(suggestionLabels).toEqual(expect.arrayContaining(expectedLabels)); + }); + + it('fetches fields when logGroups are set', async () => { + const suggestions = await getSuggestions( + whereQuery.query, + { lineNumber: 1, column: 6 }, + [], + logGroups, + logFields + ); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining(logFieldNames)); + }); + + it('does not fetch fields when logGroups are not set', async () => { + const suggestions = await getSuggestions(whereQuery.query, { lineNumber: 1, column: 6 }, [], [], logFields); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).not.toContain('@field'); + }); + }); +}); diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/completion/PPLCompletionItemProvider.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/completion/PPLCompletionItemProvider.ts new file mode 100644 index 00000000000..82a46a0f904 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/completion/PPLCompletionItemProvider.ts @@ -0,0 +1,284 @@ +import { getTemplateSrv, type TemplateSrv } from '@grafana/runtime'; +import { Monaco, monacoTypes } from '@grafana/ui'; + +import { type ResourcesAPI } from '../../../resources/ResourcesAPI'; +import { LogGroup } from '../../../types'; +import { CompletionItemProvider } from '../../monarch/CompletionItemProvider'; +import { LinkedToken } from '../../monarch/LinkedToken'; +import { TRIGGER_SUGGEST } from '../../monarch/commands'; +import { CompletionItem, CompletionItemPriority, StatementPosition, SuggestionKind } from '../../monarch/types'; +import { + BOOLEAN_LITERALS, + CONDITION_FUNCTIONS, + DEDUP_PARAMETERS, + EVAL_FUNCTIONS, + FIELD_OPERATORS, + IN, + LOGICAL_EXPRESSION_OPERATORS, + NOT, + PPL_COMMANDS, + SORT_FIELD_FUNCTIONS, + SPAN, + STATS_PARAMETERS, + STATS_FUNCTIONS, + FROM, +} from '../language'; +import { PPLTokenTypes } from '../tokenTypes'; + +import { getStatementPosition } from './statementPosition'; +import { getSuggestionKinds } from './suggestionKinds'; + +export type queryContext = { + logGroups?: LogGroup[]; + region: string; +}; + +export function PPLCompletionItemProviderFunc(resources: ResourcesAPI, templateSrv: TemplateSrv = getTemplateSrv()) { + return (queryContext: queryContext) => { + return new PPLCompletionItemProvider(resources, templateSrv, queryContext); + }; +} + +export class PPLCompletionItemProvider extends CompletionItemProvider { + queryContext: queryContext; + constructor(resources: ResourcesAPI, templateSrv: TemplateSrv = getTemplateSrv(), queryContext: queryContext) { + super(resources, templateSrv); + this.getStatementPosition = getStatementPosition; + this.getSuggestionKinds = getSuggestionKinds; + this.tokenTypes = PPLTokenTypes; + this.queryContext = queryContext; + } + + async getSuggestions( + monaco: Monaco, + currentToken: LinkedToken | null, + suggestionKinds: SuggestionKind[], + _: StatementPosition, + position: monacoTypes.IPosition + ): Promise { + const suggestions: CompletionItem[] = []; + const invalidRangeToken = + currentToken?.isWhiteSpace() || currentToken?.isParenthesis() || currentToken?.is(PPLTokenTypes.Backtick); // PPLTokenTypes.Backtick for field wrapping + const range = + invalidRangeToken || !currentToken?.range ? monaco.Range.fromPositions(position) : currentToken?.range; + function toCompletionItem(value: string, rest: Partial = {}) { + const item: monacoTypes.languages.CompletionItem = { + label: value, + insertText: value, + kind: monaco.languages.CompletionItemKind.Field, + range, + sortText: CompletionItemPriority.Medium, + ...rest, + }; + return item; + } + + function addSuggestion(value: string, rest: Partial = {}) { + suggestions.push(toCompletionItem(value, rest)); + } + + for (const kind of suggestionKinds) { + switch (kind) { + case SuggestionKind.Command: + PPL_COMMANDS.forEach((command) => { + addSuggestion(command, { + insertText: `${command} $0`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + kind: monaco.languages.CompletionItemKind.Method, + command: TRIGGER_SUGGEST, + }); + }); + break; + + case SuggestionKind.LogicalExpression: + // booleanExpression + CONDITION_FUNCTIONS.forEach((funct) => { + addSuggestion(funct, { + insertText: `${funct}($0)`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + kind: monaco.languages.CompletionItemKind.Function, + command: TRIGGER_SUGGEST, + }); + }); + addSuggestion(NOT, { + insertText: `${NOT} $0`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + kind: monaco.languages.CompletionItemKind.Operator, + command: TRIGGER_SUGGEST, + }); + break; + + case SuggestionKind.ValueExpression: + EVAL_FUNCTIONS.forEach((funct) => { + addSuggestion(funct, { + insertText: `${funct}($0)`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + kind: monaco.languages.CompletionItemKind.Function, + command: TRIGGER_SUGGEST, + }); + }); + await this.addFieldSuggestions(addSuggestion, monaco, range, currentToken); + break; + + case SuggestionKind.FieldOperators: + FIELD_OPERATORS.forEach((operator) => { + addSuggestion(operator, { + insertText: `${operator}$0`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + kind: monaco.languages.CompletionItemKind.Operator, + command: TRIGGER_SUGGEST, + }); + }); + break; + + case SuggestionKind.BooleanLiteral: + BOOLEAN_LITERALS.forEach((literal) => + addSuggestion(`= ${literal}`, { + insertText: `= ${literal} $0`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + kind: monaco.languages.CompletionItemKind.Value, + command: TRIGGER_SUGGEST, + }) + ); + break; + + case SuggestionKind.DedupParameter: + DEDUP_PARAMETERS.forEach((keyword) => + addSuggestion(keyword, { + insertText: `${keyword} $0`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + kind: monaco.languages.CompletionItemKind.Property, + command: TRIGGER_SUGGEST, + }) + ); + break; + + case SuggestionKind.StatsParameter: + STATS_PARAMETERS.forEach((keyword) => { + addSuggestion(keyword, { + insertText: `${keyword} $0`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + kind: monaco.languages.CompletionItemKind.Property, + command: TRIGGER_SUGGEST, + }); + }); + break; + + case SuggestionKind.StatsFunctions: + STATS_FUNCTIONS.forEach((f) => { + addSuggestion(f, { + insertText: `${f}($0)`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + kind: monaco.languages.CompletionItemKind.Function, + command: TRIGGER_SUGGEST, + }); + }); + break; + + case SuggestionKind.LogicalOperators: + LOGICAL_EXPRESSION_OPERATORS.forEach((operator) => { + addSuggestion(operator, { + insertText: `${operator} $0`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + kind: monaco.languages.CompletionItemKind.Operator, + command: TRIGGER_SUGGEST, + }); + }); + break; + + case SuggestionKind.InKeyword: + addSuggestion(IN, { + insertText: `${IN} $0`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + kind: monaco.languages.CompletionItemKind.Keyword, + command: TRIGGER_SUGGEST, + }); + break; + + case SuggestionKind.SpanClause: + addSuggestion(SPAN, { + insertText: `${SPAN}($0)`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + kind: monaco.languages.CompletionItemKind.Function, + command: TRIGGER_SUGGEST, + }); + break; + + case SuggestionKind.Field: + await this.addFieldSuggestions(addSuggestion, monaco, range, currentToken); + break; + + case SuggestionKind.FromKeyword: + addSuggestion(FROM, { + insertText: `${FROM} $0`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + kind: monaco.languages.CompletionItemKind.Keyword, + command: TRIGGER_SUGGEST, + }); + break; + + case SuggestionKind.SortFunctions: + SORT_FIELD_FUNCTIONS.forEach((funct) => { + addSuggestion(funct, { + insertText: `${funct}($0)`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + kind: monaco.languages.CompletionItemKind.Function, + command: TRIGGER_SUGGEST, + }); + }); + break; + } + } + // always suggest template variables + this.templateSrv.getVariables().map((v) => { + const variable = `$${v.name}`; + addSuggestion(variable, { + range, + label: variable, + insertText: variable, + kind: monaco.languages.CompletionItemKind.Variable, + sortText: CompletionItemPriority.Low, + }); + }); + + return suggestions; + } + + private async addFieldSuggestions( + addSuggestion: (value: string, rest?: Partial) => void, + monaco: typeof monacoTypes, + range: monacoTypes.IRange | monacoTypes.languages.CompletionItemRanges, + currentToken?: LinkedToken | null + ): Promise { + if (this.queryContext.logGroups && this.queryContext.logGroups.length > 0) { + try { + let fields = await this.fetchFields(this.queryContext.logGroups, this.queryContext.region); + fields.forEach((field) => { + if (field !== '') { + addSuggestion(field, { + range, + label: field, + insertText: currentToken?.is(PPLTokenTypes.Backtick) ? field : `\`${field}\``, + kind: monaco.languages.CompletionItemKind.Field, + sortText: CompletionItemPriority.High, + }); + } + }); + } catch { + return; + } + } + } + + private async fetchFields(logGroups: LogGroup[], region: string): Promise { + const results = await Promise.all( + logGroups.map((logGroup) => + this.resources + .getLogGroupFields({ logGroupName: logGroup.name, arn: logGroup.arn, region }) + .then((fields) => fields.filter((f) => f).map((f) => f.value.name ?? '')) + ) + ); + // Deduplicate fields + return [...new Set(results.flat())]; + } +} diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/completion/statementPosition.test.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/completion/statementPosition.test.ts new file mode 100644 index 00000000000..d127553c9f3 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/completion/statementPosition.test.ts @@ -0,0 +1,553 @@ +import { monacoTypes } from '@grafana/ui'; + +import { multiLineFullQuery } from '../../../__mocks__/cloudwatch-ppl-test-data/multilineQueries'; +import { + dedupQueryWithOptionalArgs, + dedupQueryWithoutOptionalArgs, + evalQuery, + eventStatsQuery, + fieldsQuery, + headQuery, + parseQuery, + queryWithArithmeticOps, + queryWithFunctionCalls, + queryWithFieldList, + queryWithLogicalExpression, + rareQuery, + sortQuery, + sortQueryWithFunctions, + statsQuery, + topQuery, + whereQuery, +} from '../../../__mocks__/cloudwatch-ppl-test-data/singleLineQueries'; +import MonacoMock from '../../../__mocks__/monarch/Monaco'; +import TextModel from '../../../__mocks__/monarch/TextModel'; +import { linkedTokenBuilder } from '../../monarch/linkedTokenBuilder'; +import { StatementPosition } from '../../monarch/types'; +import cloudWatchLogsPPLLanguageDefinition from '../definition'; +import { PPLTokenTypes } from '../tokenTypes'; + +import { getStatementPosition } from './statementPosition'; + +function generateToken(query: string, position: monacoTypes.IPosition) { + const testModel = TextModel(query); + return linkedTokenBuilder( + MonacoMock, + cloudWatchLogsPPLLanguageDefinition, + testModel as monacoTypes.editor.ITextModel, + position, + PPLTokenTypes + ); +} + +describe('getStatementPosition', () => { + it('should return StatementPosition.AfterArithmeticOperator if the position follows an arithmetic operator and not a fields or sort command', () => { + expect(getStatementPosition(generateToken(queryWithArithmeticOps.query, { lineNumber: 1, column: 14 }))).toEqual( + StatementPosition.AfterArithmeticOperator + ); + expect( + getStatementPosition(generateToken(queryWithArithmeticOps.query, { lineNumber: 1, column: 26 })) + ).not.toEqual(StatementPosition.AfterArithmeticOperator); + expect(getStatementPosition(generateToken(fieldsQuery.query, { lineNumber: 1, column: 9 }))).not.toEqual( + StatementPosition.AfterArithmeticOperator + ); + expect(getStatementPosition(generateToken(sortQuery.query, { lineNumber: 1, column: 7 }))).not.toEqual( + StatementPosition.AfterArithmeticOperator + ); + }); + + it('should return StatementPosition.AfterBooleanAgument if the position follows a boolean argument', () => { + expect( + getStatementPosition(generateToken(dedupQueryWithOptionalArgs.query, { lineNumber: 1, column: 53 })) + ).toEqual(StatementPosition.AfterBooleanArgument); + }); + + it('should return StatementPosition.FieldList if the position follows a comma and field identifiers and is not sort or eval command', () => { + expect(getStatementPosition(generateToken(queryWithFieldList.query, { lineNumber: 1, column: 22 }))).toEqual( + StatementPosition.FieldList + ); + expect(getStatementPosition(generateToken(queryWithFieldList.query, { lineNumber: 1, column: 41 }))).toEqual( + StatementPosition.FieldList + ); + expect(getStatementPosition(generateToken(queryWithFieldList.query, { lineNumber: 1, column: 44 }))).toEqual( + StatementPosition.FieldList + ); + expect(getStatementPosition(generateToken(sortQuery.query, { lineNumber: 1, column: 53 }))).not.toEqual( + StatementPosition.FieldList + ); + expect(getStatementPosition(generateToken(evalQuery.query, { lineNumber: 1, column: 53 }))).not.toEqual( + StatementPosition.FieldList + ); + }); + + it('should return StatementPosition.AfterInKeyword if the position follows IN', () => { + expect(getStatementPosition(generateToken(whereQuery.query, { lineNumber: 1, column: 71 }))).toEqual( + StatementPosition.AfterINKeyword + ); + }); + + it('should return StatementPosition.StatementPosition.FunctionArg if the position is inside a condition function', () => { + expect(getStatementPosition(generateToken(queryWithFunctionCalls.query, { lineNumber: 1, column: 20 }))).toEqual( + StatementPosition.FunctionArg + ); + expect(getStatementPosition(generateToken(queryWithFunctionCalls.query, { lineNumber: 1, column: 11 }))).toEqual( + StatementPosition.FunctionArg + ); + }); + + it('should return StatementPosition.StatementPosition.FunctionArg if the position is inside an evalFunction', () => { + expect(getStatementPosition(generateToken(queryWithFunctionCalls.query, { lineNumber: 1, column: 59 }))).toEqual( + StatementPosition.FunctionArg + ); + expect(getStatementPosition(generateToken(queryWithFunctionCalls.query, { lineNumber: 1, column: 78 }))).toEqual( + StatementPosition.FunctionArg + ); + }); + + describe('logical expression', () => { + it('should return StatementPosition.BeforeLogicalExpression if the position follows a logical expression operator and is not an eval command', () => { + expect( + getStatementPosition(generateToken(queryWithLogicalExpression.query, { lineNumber: 1, column: 28 })) + ).toEqual(StatementPosition.BeforeLogicalExpression); + + expect(getStatementPosition(generateToken(evalQuery.query, { lineNumber: 1, column: 30 }))).not.toEqual( + StatementPosition.BeforeLogicalExpression + ); + }); + + it('should return StatementPosition.BeforeLogicalExpression after a logical expression operator', () => { + expect(getStatementPosition(generateToken(whereQuery.query, { lineNumber: 1, column: 42 }))).toEqual( + StatementPosition.BeforeLogicalExpression + ); + }); + + it('should return StatementPosition.BeforeLogicalExpression after a condition function', () => { + expect(getStatementPosition(generateToken(whereQuery.query, { lineNumber: 1, column: 38 }))).toEqual( + StatementPosition.BeforeLogicalExpression + ); + }); + + it('should return StatementPosition.BeforeLogicalExpression after a regex', () => { + expect( + getStatementPosition(generateToken(queryWithLogicalExpression.query, { lineNumber: 1, column: 43 })) + ).toEqual(StatementPosition.BeforeLogicalExpression); + }); + it('should return StatementPosition.BeforeLogicalExpression after a NOT operator', () => { + expect(getStatementPosition(generateToken(whereQuery.query, { lineNumber: 1, column: 46 }))).toEqual( + StatementPosition.BeforeLogicalExpression + ); + expect( + getStatementPosition(generateToken(queryWithLogicalExpression.query, { lineNumber: 1, column: 32 })) + ).toEqual(StatementPosition.BeforeLogicalExpression); + }); + + it('should return Statementposition.FunctionArg after a BETWEEN keyword', () => { + expect(getStatementPosition(generateToken(evalQuery.query, { lineNumber: 1, column: 106 }))).toEqual( + StatementPosition.FunctionArg + ); + }); + }); + + describe('WHERE command', () => { + it('should return StatementPosition.BeforeLogicalExpression after where command', () => { + expect(getStatementPosition(generateToken(whereQuery.query, { lineNumber: 1, column: 6 }))).toEqual( + StatementPosition.BeforeLogicalExpression + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 2, column: 8 }))).toEqual( + StatementPosition.BeforeLogicalExpression + ); + }); + }); + + describe('FIELDS command', () => { + it('should return StatementPosition.AfterFieldsCommand after fields command', () => { + expect(getStatementPosition(generateToken(fieldsQuery.query, { lineNumber: 1, column: 7 }))).toEqual( + StatementPosition.AfterFieldsCommand + ); + // multiline + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 3, column: 9 }))).toEqual( + StatementPosition.AfterFieldsCommand + ); + }); + + it('should return StatementPosition.FieldList after a + operator', () => { + expect(getStatementPosition(generateToken(fieldsQuery.query, { lineNumber: 1, column: 9 }))).toEqual( + StatementPosition.FieldList + ); + }); + + it('should return StatementPosition.FieldList after a field', () => { + expect(getStatementPosition(generateToken(fieldsQuery.query, { lineNumber: 1, column: 27 }))).toEqual( + StatementPosition.FieldList + ); + expect(getStatementPosition(generateToken(fieldsQuery.query, { lineNumber: 1, column: 38 }))).toEqual( + StatementPosition.FieldList + ); + // multiline + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 3, column: 29 }))).toEqual( + StatementPosition.FieldList + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 3, column: 40 }))).toEqual( + StatementPosition.FieldList + ); + }); + }); + + describe('STATS command', () => { + it('should return StatementPosition.AfterStatsCommand after stats command', () => { + expect(getStatementPosition(generateToken(statsQuery.query, { lineNumber: 1, column: 6 }))).toEqual( + StatementPosition.AfterStatsCommand + ); + // multiline + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 4, column: 8 }))).toEqual( + StatementPosition.AfterStatsCommand + ); + }); + + it('should return StatementPosition.AfterStatsBy after by keyword', () => { + expect(getStatementPosition(generateToken(statsQuery.query, { lineNumber: 1, column: 42 }))).toEqual( + StatementPosition.AfterStatsBy + ); + // multiline + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 4, column: 44 }))).toEqual( + StatementPosition.AfterStatsBy + ); + }); + + it('should return StatementPosition.FieldList in span function arguments', () => { + expect(getStatementPosition(generateToken(statsQuery.query, { lineNumber: 1, column: 47 }))).toEqual( + StatementPosition.FieldList + ); + // multiline + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 4, column: 49 }))).toEqual( + StatementPosition.FieldList + ); + }); + + it('should return StatementPosition.StatsFunctionArgument in span function arguments', () => { + expect(getStatementPosition(generateToken(statsQuery.query, { lineNumber: 1, column: 10 }))).toEqual( + StatementPosition.StatsFunctionArgument + ); + // multiline + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 4, column: 12 }))).toEqual( + StatementPosition.StatsFunctionArgument + ); + }); + }); + + describe('EVENTSTATS command', () => { + it('should return StatementPosition.AfterStatsCommand after eventstats command', () => { + expect(getStatementPosition(generateToken(eventStatsQuery.query, { lineNumber: 1, column: 11 }))).toEqual( + StatementPosition.AfterStatsCommand + ); + // multiline + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 5, column: 13 }))).toEqual( + StatementPosition.AfterStatsCommand + ); + }); + + it('should return StatementPosition.AfterStatsBy after by keyword', () => { + expect(getStatementPosition(generateToken(eventStatsQuery.query, { lineNumber: 1, column: 47 }))).toEqual( + StatementPosition.AfterStatsBy + ); + + // multiline + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 5, column: 49 }))).toEqual( + StatementPosition.AfterStatsBy + ); + }); + + it('should return StatementPosition.FieldList in span function arguments', () => { + expect(getStatementPosition(generateToken(eventStatsQuery.query, { lineNumber: 1, column: 52 }))).toEqual( + StatementPosition.FieldList + ); + // multiline + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 5, column: 54 }))).toEqual( + StatementPosition.FieldList + ); + }); + + it('should return StatementPosition.StatsFunctionArgument in span function arguments', () => { + expect(getStatementPosition(generateToken(eventStatsQuery.query, { lineNumber: 1, column: 15 }))).toEqual( + StatementPosition.StatsFunctionArgument + ); + // multiline + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 5, column: 17 }))).toEqual( + StatementPosition.StatsFunctionArgument + ); + }); + }); + + describe('SORT command', () => { + it('should return StatementPosition.SortField as a sort clause', () => { + expect(getStatementPosition(generateToken(sortQuery.query, { lineNumber: 1, column: 5 }))).toEqual( + StatementPosition.SortField + ); + expect(getStatementPosition(generateToken(sortQuery.query, { lineNumber: 1, column: 25 }))).toEqual( + StatementPosition.SortField + ); + expect(getStatementPosition(generateToken(sortQueryWithFunctions.query, { lineNumber: 1, column: 5 }))).toEqual( + StatementPosition.SortField + ); + expect(getStatementPosition(generateToken(sortQuery.query, { lineNumber: 1, column: 38 }))).toEqual( + StatementPosition.SortField + ); + // multiline + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 6, column: 7 }))).toEqual( + StatementPosition.SortField + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 6, column: 27 }))).toEqual( + StatementPosition.SortField + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 7, column: 7 }))).toEqual( + StatementPosition.SortField + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 6, column: 40 }))).toEqual( + StatementPosition.SortField + ); + }); + + it('should return StatementPosition.SortFieldExpression after a field operator in a sort command', () => { + expect(getStatementPosition(generateToken(sortQuery.query, { lineNumber: 1, column: 7 }))).toEqual( + StatementPosition.SortFieldExpression + ); + expect(getStatementPosition(generateToken(sortQuery.query, { lineNumber: 1, column: 27 }))).toEqual( + StatementPosition.SortFieldExpression + ); + expect(getStatementPosition(generateToken(sortQueryWithFunctions.query, { lineNumber: 1, column: 7 }))).toEqual( + StatementPosition.SortFieldExpression + ); + expect(getStatementPosition(generateToken(sortQueryWithFunctions.query, { lineNumber: 1, column: 12 }))).toEqual( + StatementPosition.SortFieldExpression + ); + // mulltiline + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 6, column: 9 }))).toEqual( + StatementPosition.SortFieldExpression + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 6, column: 29 }))).toEqual( + StatementPosition.SortFieldExpression + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 7, column: 9 }))).toEqual( + StatementPosition.SortFieldExpression + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 7, column: 14 }))).toEqual( + StatementPosition.SortFieldExpression + ); + }); + }); + + describe('DEDUP command', () => { + it('should return StatementPosition.AfterDedupFieldNames after dedup command fields', () => { + expect( + getStatementPosition(generateToken(dedupQueryWithOptionalArgs.query, { lineNumber: 1, column: 43 })) + ).toEqual(StatementPosition.AfterDedupFieldNames); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 8, column: 45 }))).toEqual( + StatementPosition.AfterDedupFieldNames + ); + }); + + it('should return StatementPosition.FieldList after dedup command', () => { + expect( + getStatementPosition(generateToken(dedupQueryWithOptionalArgs.query, { lineNumber: 1, column: 6 })) + ).toEqual(StatementPosition.FieldList); + expect( + getStatementPosition(generateToken(dedupQueryWithOptionalArgs.query, { lineNumber: 1, column: 8 })) + ).toEqual(StatementPosition.FieldList); + expect( + getStatementPosition(generateToken(dedupQueryWithOptionalArgs.query, { lineNumber: 1, column: 19 })) + ).toEqual(StatementPosition.FieldList); + expect( + getStatementPosition(generateToken(dedupQueryWithOptionalArgs.query, { lineNumber: 1, column: 34 })) + ).toEqual(StatementPosition.FieldList); + expect( + getStatementPosition(generateToken(dedupQueryWithoutOptionalArgs.query, { lineNumber: 1, column: 6 })) + ).toEqual(StatementPosition.FieldList); + expect( + getStatementPosition(generateToken(dedupQueryWithoutOptionalArgs.query, { lineNumber: 1, column: 17 })) + ).toEqual(StatementPosition.FieldList); + expect( + getStatementPosition(generateToken(dedupQueryWithoutOptionalArgs.query, { lineNumber: 1, column: 32 })) + ).toEqual(StatementPosition.FieldList); + + // multilin + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 8, column: 8 }))).toEqual( + StatementPosition.FieldList + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 8, column: 10 }))).toEqual( + StatementPosition.FieldList + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 8, column: 21 }))).toEqual( + StatementPosition.FieldList + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 8, column: 36 }))).toEqual( + StatementPosition.FieldList + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 9, column: 8 }))).toEqual( + StatementPosition.FieldList + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 9, column: 19 }))).toEqual( + StatementPosition.FieldList + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 9, column: 34 }))).toEqual( + StatementPosition.FieldList + ); + }); + }); + describe('TOP command', () => { + it('should return StatementPosition.FieldList after top by keyword', () => { + expect(getStatementPosition(generateToken(topQuery.query, { lineNumber: 1, column: 36 }))).toEqual( + StatementPosition.FieldList + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 10, column: 38 }))).toEqual( + StatementPosition.FieldList + ); + }); + + it('should return StatementPosition.FieldList after fields in top command', () => { + expect(getStatementPosition(generateToken(topQuery.query, { lineNumber: 1, column: 4 }))).toEqual( + StatementPosition.FieldList + ); + expect(getStatementPosition(generateToken(topQuery.query, { lineNumber: 1, column: 8 }))).toEqual( + StatementPosition.FieldList + ); + expect(getStatementPosition(generateToken(topQuery.query, { lineNumber: 1, column: 23 }))).toEqual( + StatementPosition.FieldList + ); + expect(getStatementPosition(generateToken(topQuery.query, { lineNumber: 1, column: 44 }))).toEqual( + StatementPosition.FieldList + ); + // multiline + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 10, column: 6 }))).toEqual( + StatementPosition.FieldList + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 10, column: 10 }))).toEqual( + StatementPosition.FieldList + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 10, column: 25 }))).toEqual( + StatementPosition.FieldList + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 10, column: 46 }))).toEqual( + StatementPosition.FieldList + ); + }); + }); + describe('HEAD command', () => { + it('should return StatementPosition.AfterHeadCommand after head', () => { + expect(getStatementPosition(generateToken(headQuery.query, { lineNumber: 1, column: 5 }))).toEqual( + StatementPosition.AfterHeadCommand + ); + expect(getStatementPosition(generateToken(headQuery.query, { lineNumber: 1, column: 8 }))).toEqual( + StatementPosition.AfterHeadCommand + ); + // multiline + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 11, column: 7 }))).toEqual( + StatementPosition.AfterHeadCommand + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 11, column: 10 }))).toEqual( + StatementPosition.AfterHeadCommand + ); + }); + }); + describe('RARE command', () => { + it('should return StatementPosition.FieldList after rare by', () => { + expect(getStatementPosition(generateToken(rareQuery.query, { lineNumber: 1, column: 30 }))).toEqual( + StatementPosition.FieldList + ); + // multiline + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 12, column: 32 }))).toEqual( + StatementPosition.FieldList + ); + }); + + it('should return StatementPosition.FieldList after rare fields', () => { + expect(getStatementPosition(generateToken(rareQuery.query, { lineNumber: 1, column: 5 }))).toEqual( + StatementPosition.FieldList + ); + expect(getStatementPosition(generateToken(rareQuery.query, { lineNumber: 1, column: 13 }))).toEqual( + StatementPosition.FieldList + ); + expect(getStatementPosition(generateToken(rareQuery.query, { lineNumber: 1, column: 38 }))).toEqual( + StatementPosition.FieldList + ); + // multiline + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 12, column: 7 }))).toEqual( + StatementPosition.FieldList + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 12, column: 15 }))).toEqual( + StatementPosition.FieldList + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 12, column: 40 }))).toEqual( + StatementPosition.FieldList + ); + }); + }); + describe('EVAL command', () => { + it('should return StatementPosition.Expression after eval = operator', () => { + expect(getStatementPosition(generateToken(evalQuery.query, { lineNumber: 1, column: 21 }))).toEqual( + StatementPosition.Expression + ); + expect(getStatementPosition(generateToken(evalQuery.query, { lineNumber: 1, column: 56 }))).toEqual( + StatementPosition.Expression + ); + expect(getStatementPosition(generateToken(evalQuery.query, { lineNumber: 1, column: 89 }))).toEqual( + StatementPosition.Expression + ); + // multiline + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 13, column: 23 }))).toEqual( + StatementPosition.Expression + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 13, column: 58 }))).toEqual( + StatementPosition.Expression + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 13, column: 91 }))).toEqual( + StatementPosition.Expression + ); + }); + + it('should return StatementPosition.EvalClause after eval commas', () => { + expect(getStatementPosition(generateToken(evalQuery.query, { lineNumber: 1, column: 5 }))).toEqual( + StatementPosition.EvalClause + ); + expect(getStatementPosition(generateToken(evalQuery.query, { lineNumber: 1, column: 39 }))).toEqual( + StatementPosition.EvalClause + ); + expect(getStatementPosition(generateToken(evalQuery.query, { lineNumber: 1, column: 70 }))).toEqual( + StatementPosition.EvalClause + ); + // multiline + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 13, column: 7 }))).toEqual( + StatementPosition.EvalClause + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 13, column: 41 }))).toEqual( + StatementPosition.EvalClause + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 13, column: 72 }))).toEqual( + StatementPosition.EvalClause + ); + }); + + it('should return StatementPosition.BeforeLogicalExpression after a logical expression operator in eval', () => { + expect(getStatementPosition(generateToken(evalQuery.query, { lineNumber: 1, column: 65 }))).toEqual( + StatementPosition.BeforeLogicalExpression + ); + // multiline + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 13, column: 67 }))).toEqual( + StatementPosition.BeforeLogicalExpression + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 13, column: 102 }))).toEqual( + StatementPosition.BeforeLogicalExpression + ); + }); + }); + + describe('PARSE command', () => { + it('should return StatementPosition.Expression after PARSE command', () => { + expect(getStatementPosition(generateToken(parseQuery.query, { lineNumber: 1, column: 6 }))).toEqual( + StatementPosition.Expression + ); + expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 14, column: 8 }))).toEqual( + StatementPosition.Expression + ); + }); + }); +}); diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/completion/statementPosition.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/completion/statementPosition.ts new file mode 100644 index 00000000000..884d8536649 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/completion/statementPosition.ts @@ -0,0 +1,220 @@ +import { LinkedToken } from '../../monarch/LinkedToken'; +import { StatementPosition } from '../../monarch/types'; +import { + ARITHMETIC_OPERATORS, + PARAMETERS_WITH_BOOLEAN_VALUES, + BY, + COMPARISON_OPERATORS, + CONDITION_FUNCTIONS, + DEDUP, + EVAL, + EVENTSTATS, + FIELD_OPERATORS, + FIELDS, + HEAD, + IN, + LOGICAL_EXPRESSION_OPERATORS, + NOT, + RARE, + SORT, + SORT_FIELD_FUNCTIONS, + SPAN, + STATS, + STATS_FUNCTIONS, + TOP, + WHERE, + PARSE, + BETWEEN, + EVAL_FUNCTIONS, +} from '../language'; +import { PPLTokenTypes } from '../tokenTypes'; + +// getStatementPosition returns the 'statement position' of the place where the cursor is currently positioned. +// Statement positions are places that are syntactically and relevant for the evaluated language and are used to determine the suggestionKinds, i.e. +// suggestions in the dropdown. +// For example, in PPL, if the cursor is currently at the whitespace after the WHERE keyword, this function returns StatementPosition.BeforeLogicalExpression. +// In getSuggestionKinds, this position will result in SuggestionKind.LogicalExpression. +// Lastly, In PPLCompletionItemProvider appropriate suggestions of logical operators are added to the dropdown based on the suggestion kind. + +export const getStatementPosition = (currentToken: LinkedToken | null): StatementPosition => { + const previousNonWhiteSpace = currentToken?.getPreviousNonWhiteSpaceToken(); + const nextNonWhiteSpace = currentToken?.getNextNonWhiteSpaceToken(); + + const normalizedPreviousNonWhiteSpace = previousNonWhiteSpace?.value?.toLowerCase(); + + if ( + currentToken === null || + (currentToken?.isWhiteSpace() && previousNonWhiteSpace === null && nextNonWhiteSpace === null) || + (previousNonWhiteSpace?.is(PPLTokenTypes.Pipe) && currentToken?.isWhiteSpace()) || + previousNonWhiteSpace?.is(PPLTokenTypes.Delimiter, '|') + ) { + return StatementPosition.NewCommand; + } + + switch (normalizedPreviousNonWhiteSpace) { + case WHERE: + return StatementPosition.BeforeLogicalExpression; + case DEDUP: + return StatementPosition.FieldList; + case FIELDS: + return StatementPosition.AfterFieldsCommand; + case EVENTSTATS: + case STATS: + return StatementPosition.AfterStatsCommand; + case SORT: + return StatementPosition.SortField; + case PARSE: + return StatementPosition.Expression; + } + + if ( + currentToken?.isWhiteSpace() || + currentToken?.is(PPLTokenTypes.Backtick) || + currentToken?.is(PPLTokenTypes.Delimiter, ',') || + currentToken?.is(PPLTokenTypes.Parenthesis) // for STATS functions + ) { + const nearestFunction = currentToken?.getPreviousOfType(PPLTokenTypes.Function)?.value.toLowerCase(); + const nearestKeyword = currentToken?.getPreviousOfType(PPLTokenTypes.Keyword)?.value.toLowerCase(); + const nearestCommand = currentToken?.getPreviousOfType(PPLTokenTypes.Command)?.value.toLowerCase(); + + if (normalizedPreviousNonWhiteSpace) { + if ( + nearestCommand !== FIELDS && // FIELDS and SORT fields can be preceeded by a + or - which are not arithmetic ops + nearestCommand !== SORT && + ARITHMETIC_OPERATORS.includes(normalizedPreviousNonWhiteSpace) + ) { + return StatementPosition.AfterArithmeticOperator; + } + if (PARAMETERS_WITH_BOOLEAN_VALUES.includes(normalizedPreviousNonWhiteSpace)) { + return StatementPosition.AfterBooleanArgument; + } + } + + const isBeforeLogicalExpression = + (normalizedPreviousNonWhiteSpace && + (COMPARISON_OPERATORS.includes(normalizedPreviousNonWhiteSpace) || + LOGICAL_EXPRESSION_OPERATORS.includes(normalizedPreviousNonWhiteSpace))) || + previousNonWhiteSpace?.is(PPLTokenTypes.Regexp) || + normalizedPreviousNonWhiteSpace === NOT || // follows a comparison operator, logical operator, NOT or a regex + (nearestFunction && CONDITION_FUNCTIONS.includes(nearestFunction) && normalizedPreviousNonWhiteSpace === ')'); // it's not a condition function argument + + if ( + nearestCommand !== SORT && // sort command fields can be followed by a field operator, which is handled lower in the block + nearestCommand !== EVAL && // eval fields can be followed by an eval clause, which is handled lower in the block + nearestCommand !== STATS && // identifiers in STATS can be followed by a stats function, which is handled lower in the block + (isListingFields(currentToken) || currentToken?.is(PPLTokenTypes.Backtick)) + ) { + return StatementPosition.FieldList; + } + + if ( + nearestCommand !== EVAL && // eval can have StatementPosition.Expression after an equal operator + isBeforeLogicalExpression + ) { + return StatementPosition.BeforeLogicalExpression; + } + + if (nearestKeyword === IN) { + return StatementPosition.AfterINKeyword; + } + if (nearestKeyword === BETWEEN) { + return StatementPosition.FunctionArg; + } + + if ( + nearestFunction && + (currentToken?.is(PPLTokenTypes.Parenthesis) || currentToken?.getNextNonWhiteSpaceToken()?.value === ')') + ) { + if ([...EVAL_FUNCTIONS, ...CONDITION_FUNCTIONS].includes(nearestFunction)) { + return StatementPosition.FunctionArg; + } + if (STATS_FUNCTIONS.includes(nearestFunction)) { + return StatementPosition.StatsFunctionArgument; + } + if (SORT_FIELD_FUNCTIONS.includes(nearestFunction)) { + return StatementPosition.SortFieldExpression; + } + } + + switch (nearestCommand) { + case SORT: { + if (previousNonWhiteSpace) { + if (previousNonWhiteSpace.is(PPLTokenTypes.Delimiter, ',')) { + return StatementPosition.SortField; + } else if (FIELD_OPERATORS.includes(previousNonWhiteSpace.value)) { + return StatementPosition.SortFieldExpression; + } + } + break; + } + case DEDUP: { + // if current active command is DEDUP and there are identifiers (fieldNames) between currentToken and the dedup command + const fieldNames = currentToken.getPreviousUntil(PPLTokenTypes.Number, [ + PPLTokenTypes.Delimiter, + PPLTokenTypes.Whitespace, + ]); + if (fieldNames?.length && !havePipe(fieldNames)) { + return StatementPosition.AfterDedupFieldNames; + } + return StatementPosition.FieldList; + } + case FIELDS: { + return StatementPosition.FieldList; + } + case STATS: + case EVENTSTATS: { + if (nearestKeyword === BY && currentToken.isWhiteSpace()) { + return StatementPosition.AfterStatsBy; + } else if (nearestFunction === SPAN && currentToken?.is(PPLTokenTypes.Parenthesis)) { + return StatementPosition.FieldList; + } + return StatementPosition.AfterStatsCommand; + } + case RARE: { + return StatementPosition.FieldList; + } + case TOP: { + return StatementPosition.FieldList; + } + case HEAD: + return StatementPosition.AfterHeadCommand; + + case EVAL: + if (previousNonWhiteSpace?.value === '=') { + return StatementPosition.Expression; + } + if ( + currentToken?.isWhiteSpace() && + (normalizedPreviousNonWhiteSpace === EVAL || previousNonWhiteSpace?.is(PPLTokenTypes.Delimiter, ',')) + ) { + return StatementPosition.EvalClause; + } + if (isBeforeLogicalExpression) { + return StatementPosition.BeforeLogicalExpression; + } + break; + } + } + + return StatementPosition.Unknown; +}; + +const havePipe = (fieldNames: LinkedToken[]) => { + return fieldNames?.some((word) => word.type === PPLTokenTypes.Pipe); +}; +const isListingFields = (currentToken: LinkedToken | null) => { + const tokensUntilFieldName = currentToken?.getPreviousUntil(PPLTokenTypes.Identifier, [PPLTokenTypes.Whitespace]); // tokens until exampleFieldName + const tokensUntilEscapedFieldName = currentToken?.getPreviousUntil(PPLTokenTypes.Backtick, [ + // tokens until `@exampleFieldName` + PPLTokenTypes.Whitespace, + ]); + const isPreceededByAFieldName = + (tokensUntilFieldName?.length && tokensUntilFieldName.every((token) => token.is(PPLTokenTypes.Delimiter, ','))) || + (tokensUntilEscapedFieldName?.length && + tokensUntilEscapedFieldName.every((token) => token.is(PPLTokenTypes.Delimiter, ','))); + const isAfterComma = + currentToken?.isWhiteSpace() && currentToken?.getPreviousNonWhiteSpaceToken()?.is(PPLTokenTypes.Delimiter, ','); + const isFunctionArgument = currentToken?.getNextNonWhiteSpaceToken()?.value === ')'; // is not e.g. span(`@timestamp`, 5m) + + return isAfterComma && isPreceededByAFieldName && !isFunctionArgument; +}; diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/completion/suggestionKinds.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/completion/suggestionKinds.ts new file mode 100644 index 00000000000..13142da0f19 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/completion/suggestionKinds.ts @@ -0,0 +1,40 @@ +import { StatementPosition, SuggestionKind } from '../../monarch/types'; + +export function getSuggestionKinds(statementPosition: StatementPosition): SuggestionKind[] { + switch (statementPosition) { + case StatementPosition.NewCommand: + return [SuggestionKind.Command]; + case StatementPosition.AfterHeadCommand: + return [SuggestionKind.FromKeyword]; + case StatementPosition.AfterStatsCommand: + return [SuggestionKind.StatsParameter, SuggestionKind.StatsFunctions]; + case StatementPosition.SortField: + return [SuggestionKind.FieldOperators, SuggestionKind.Field, SuggestionKind.SortFunctions]; + case StatementPosition.EvalClause: + case StatementPosition.StatsFunctionArgument: + return [SuggestionKind.Field]; + case StatementPosition.AfterFieldsCommand: + return [SuggestionKind.FieldOperators, SuggestionKind.Field]; + case StatementPosition.FieldList: + return [SuggestionKind.Field]; + case StatementPosition.AfterBooleanArgument: + return [SuggestionKind.BooleanLiteral]; + case StatementPosition.AfterDedupFieldNames: + return [SuggestionKind.DedupParameter]; + case StatementPosition.AfterStatsBy: + return [SuggestionKind.Field, SuggestionKind.SpanClause]; + case StatementPosition.SortFieldExpression: + return [SuggestionKind.Field, SuggestionKind.SortFunctions]; + case StatementPosition.FunctionArg: + case StatementPosition.AfterArithmeticOperator: + case StatementPosition.AfterINKeyword: + return [SuggestionKind.ValueExpression]; + // logical expression can contain comparison expression, which can start with a value expression + // so we always need to suggest valueExpression when SuggestionKind.LogicalExpression is present + case StatementPosition.Expression: + case StatementPosition.BeforeLogicalExpression: + return [SuggestionKind.LogicalExpression, SuggestionKind.ValueExpression]; + } + + return []; +} diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/definition.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/definition.ts new file mode 100644 index 00000000000..3b966756a6d --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/definition.ts @@ -0,0 +1,12 @@ +import { LanguageDefinition } from '../monarch/register'; + +import { CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID } from './language'; + +const cloudWatchPPLLanguageDefinition: LanguageDefinition = { + id: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID, + extensions: [], + aliases: [], + mimetypes: [], + loader: () => import('./language'), +}; +export default cloudWatchPPLLanguageDefinition; diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/language.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/language.ts new file mode 100644 index 00000000000..282af92f06a --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/language.ts @@ -0,0 +1,246 @@ +import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api'; + +// OpenSearch PPL syntax: https://github.com/opensearch-project/opensearch-spark/blob/0.5/ppl-spark-integration/src/main/antlr4/OpenSearchPPLParser.g4 +interface CloudWatchPPLLanguage extends monacoType.languages.IMonarchLanguage { + commands: string[]; + operators: string[]; + builtinFunctions: string[]; +} + +export const CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID = 'logs-ppl'; + +// COMMANDS +export const WHERE = 'where'; +export const FIELDS = 'fields'; +export const DEDUP = 'dedup'; +export const STATS = 'stats'; +export const EVENTSTATS = 'eventstats'; +export const SORT = 'sort'; +export const EVAL = 'eval'; +export const HEAD = 'head'; +export const TOP = 'top'; +export const RARE = 'rare'; +export const PARSE = 'parse'; + +export const PPL_COMMANDS = [WHERE, FIELDS, STATS, EVENTSTATS, DEDUP, SORT, TOP, RARE, HEAD, EVAL, PARSE]; + +// KEYWORDS +export const AS = 'as'; +export const BY = 'by'; +export const BETWEEN = 'between'; +export const FROM = 'from'; + +// PARAMETERS +const KEEP_EMPTY = 'keepempty'; +const CONSECUTIVE = 'consecutive'; +const PARTITIONS = 'partitions'; +const ALLNUM = 'allnum'; +const DELIM = 'delim'; +const DEDUP_SPLITVALUES = 'dedup_splitvalues'; + +export const STATS_PARAMETERS = [PARTITIONS, ALLNUM, DELIM, DEDUP_SPLITVALUES]; +export const DEDUP_PARAMETERS = [KEEP_EMPTY, CONSECUTIVE]; +export const PARAMETERS_WITH_BOOLEAN_VALUES = [ALLNUM, DEDUP_SPLITVALUES, KEEP_EMPTY, CONSECUTIVE]; +export const BOOLEAN_LITERALS = ['true', 'false']; +export const IN = 'in'; + +export const ALL_KEYWORDS = [...STATS_PARAMETERS, ...DEDUP_PARAMETERS, ...BOOLEAN_LITERALS, AS, BY, IN, BETWEEN, FROM]; + +// FUNCTIONS +export const MATH_FUNCTIONS = [ + 'abs', + 'acos', + 'asin', + 'atan', + 'atan2', + 'ceil', + 'ceiling', + 'conv', + 'cos', + 'cot', + 'crc32', + 'degrees', + 'e', + 'exp', + 'floor', + 'ln', + 'log', + 'log2', + 'log10', + 'mod', + 'pi', + 'pow', + 'power', + 'radians', + 'rand', + 'round', + 'sign', + 'sin', + 'sqrt', + 'cbrt', +]; +export const DATE_TIME_FUNCTIONS = [ + 'datediff', + 'day', + 'dayofmonth', + 'dayofweek', + 'dayofyear', + 'hour', + 'minute', + 'second', + 'month', + 'quarter', + 'weekday', + 'weekofyear', + 'year', + 'now', + 'curdate', + 'current_date', +]; +export const TEXT_FUNCTIONS = [ + 'concat', + 'concat_ws', + 'length', + 'lower', + 'ltrim', + 'reverse', + 'rtrim', + 'right', + 'substring', + 'substr', + 'trim', + 'upper', +]; +export const SPAN = 'span'; +export const POSITION = 'position'; +export const CONDITION_FUNCTIONS = ['like', 'isnull', 'isnotnull', 'exists', 'ifnull', 'nullif', 'if', 'ispresent']; +export const SORT_FIELD_FUNCTIONS = ['auto', 'str', 'ip', 'num']; +export const PPL_FUNCTIONS = [...MATH_FUNCTIONS, ...DATE_TIME_FUNCTIONS, ...TEXT_FUNCTIONS]; +export const EVAL_FUNCTIONS: string[] = [...PPL_FUNCTIONS, POSITION]; +export const STATS_FUNCTIONS = [ + 'avg', + 'count', + 'sum', + 'min', + 'max', + 'stddev_samp', + 'stddev_pop', + 'percentile', + 'percentile_approx', + 'distinct_count', + 'dc', +]; + +export const ALL_FUNCTIONS = [ + ...PPL_FUNCTIONS, + ...STATS_FUNCTIONS, + ...CONDITION_FUNCTIONS, + ...SORT_FIELD_FUNCTIONS, + POSITION, + SPAN, +]; + +// OPERATORS +export const PLUS = '+'; +export const MINUS = '-'; +export const NOT = 'not'; + +export const FIELD_OPERATORS = [PLUS, MINUS]; +export const ARITHMETIC_OPERATORS = [PLUS, MINUS, '*', '/', '%']; +export const COMPARISON_OPERATORS = ['>', '>=', '<', '!=', '<=', '=']; +export const LOGICAL_EXPRESSION_OPERATORS = ['and', 'or', 'xor', NOT]; +export const PPL_OPERATORS = [...ARITHMETIC_OPERATORS, ...LOGICAL_EXPRESSION_OPERATORS, ...COMPARISON_OPERATORS]; + +export const language: CloudWatchPPLLanguage = { + defaultToken: '', + id: CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID, + ignoreCase: true, + commands: PPL_COMMANDS, + operators: PPL_OPERATORS, + keywords: ALL_KEYWORDS, + builtinFunctions: ALL_FUNCTIONS, + brackets: [{ open: '(', close: ')', token: 'delimiter.parenthesis' }], + tokenizer: { + root: [ + { include: '@comments' }, + { include: '@regexes' }, + { include: '@whitespace' }, + { include: '@variables' }, + { include: '@strings' }, + { include: '@numbers' }, + + [/[,.:]/, 'delimiter'], + [/\|/, 'delimiter.pipe'], + [/[()\[\]]/, 'delimiter.parenthesis'], + + [ + /[\w@#$]+/, + { + cases: { + '@commands': 'keyword.command', + '@keywords': 'keyword', + '@builtinFunctions': 'predefined', + '@operators': 'operator', + '@default': 'identifier', + }, + }, + ], + [/[+\-*/^%=!<>]/, 'operator'], // handles the math operators + [/[,.:]/, 'operator'], + ], + // template variable syntax + variables: [ + [/\${/, { token: 'variable', next: '@variable_bracket' }], + [/\$[a-zA-Z0-9-_]+/, 'variable'], + ], + variable_bracket: [ + [/[a-zA-Z0-9-_:]+/, 'variable'], + [/}/, { token: 'variable', next: '@pop' }], + ], + whitespace: [[/\s+/, 'white']], + comments: [ + [/^#.*/, 'comment'], + [/\s+#.*/, 'comment'], + ], + numbers: [ + [/0[xX][0-9a-fA-F]*/, 'number'], + [/[$][+-]*\d*(\.\d*)?/, 'number'], + [/((\d+(\.\d*)?)|(\.\d+))([eE][\-+]?\d+)?/, 'number'], + ], + strings: [ + [/'/, { token: 'string', next: '@string' }], + [/"/, { token: 'string', next: '@string_double' }], + [/`/, { token: 'string.backtick', next: '@string_backtick' }], + ], + string: [ + [/[^']+/, 'string'], + [/''/, 'string'], + [/'/, { token: 'string', next: '@pop' }], + ], + string_double: [ + [/[^\\"]+/, 'string'], + [/"/, 'string', '@pop'], + ], + string_backtick: [ + [/[^\\`]+/, 'string.backtick'], + [/`/, 'string.backtick', '@pop'], + ], + regexes: [[/\/.*?\/(?!\s*\d)/, 'regexp']], + }, +}; + +export const conf: monacoType.languages.LanguageConfiguration = { + brackets: [['(', ')']], + autoClosingPairs: [ + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + { open: '`', close: '`' }, + ], + surroundingPairs: [ + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + { open: '`', close: '`' }, + ], +}; diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/tokenTypes.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/tokenTypes.ts new file mode 100644 index 00000000000..d7d7e844d47 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/tokenTypes.ts @@ -0,0 +1,28 @@ +import { TokenTypes } from '../monarch/types'; + +import { CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID } from './language'; + +interface IpplTokenTypes extends TokenTypes { + Pipe: string; + Backtick: string; + Command: string; +} + +export const PPLTokenTypes: IpplTokenTypes = { + Parenthesis: `delimiter.parenthesis.${CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID}`, + Whitespace: `white.${CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID}`, + Keyword: `keyword.${CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID}`, + Command: `keyword.command.${CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID}`, + Delimiter: `delimiter.${CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID}`, + Pipe: `delimiter.pipe.${CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID}`, + Operator: `operator.${CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID}`, + Identifier: `identifier.${CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID}`, + Type: `type.${CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID}`, + Function: `predefined.${CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID}`, + Number: `number.${CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID}`, + String: `string.${CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID}`, + Variable: `variable.${CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID}`, + Comment: `comment.${CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID}`, + Regexp: `regexp.${CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID}`, + Backtick: `string.backtick.${CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID}`, +}; diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/completion/statementPosition.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/completion/statementPosition.ts index 91e8c3abdc1..0f828abdfbe 100644 --- a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/completion/statementPosition.ts +++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/completion/statementPosition.ts @@ -4,6 +4,8 @@ import { AND, ASC, BY, DESC, EQUALS, FROM, GROUP, NOT_EQUALS, ORDER, SCHEMA, SEL import { SQLTokenTypes } from './types'; +// about getStatementPosition: public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/completion/statementPosition.ts + export function getStatementPosition(currentToken: LinkedToken | null): StatementPosition { const previousNonWhiteSpace = currentToken?.getPreviousNonWhiteSpaceToken(); const previousKeyword = currentToken?.getPreviousKeyword(); diff --git a/public/app/plugins/datasource/cloudwatch/language/logs/completion/statementPosition.ts b/public/app/plugins/datasource/cloudwatch/language/logs/completion/statementPosition.ts index 0c05bc57119..e0cec8ae79e 100644 --- a/public/app/plugins/datasource/cloudwatch/language/logs/completion/statementPosition.ts +++ b/public/app/plugins/datasource/cloudwatch/language/logs/completion/statementPosition.ts @@ -16,6 +16,8 @@ import { import { LogsTokenTypes } from './types'; +// about getStatementPosition: public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/completion/statementPosition.ts + export const getStatementPosition = (currentToken: LinkedToken | null): StatementPosition => { const previousNonWhiteSpace = currentToken?.getPreviousNonWhiteSpaceToken(); const nextNonWhiteSpace = currentToken?.getNextNonWhiteSpaceToken(); diff --git a/public/app/plugins/datasource/cloudwatch/language/monarch/CompletionItemProvider.ts b/public/app/plugins/datasource/cloudwatch/language/monarch/CompletionItemProvider.ts index 5d964e56664..491b7c00207 100644 --- a/public/app/plugins/datasource/cloudwatch/language/monarch/CompletionItemProvider.ts +++ b/public/app/plugins/datasource/cloudwatch/language/monarch/CompletionItemProvider.ts @@ -2,6 +2,7 @@ import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import type { Monaco, monacoTypes } from '@grafana/ui'; import { ResourcesAPI } from '../../resources/ResourcesAPI'; +import { CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID } from '../cloudwatch-ppl/language'; import { LinkedToken } from './LinkedToken'; import { linkedTokenBuilder } from './linkedTokenBuilder'; @@ -69,8 +70,11 @@ export class CompletionItemProvider implements Completeable { // called by registerLanguage and passed to monaco with registerCompletionItemProvider // returns an object that implements https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.CompletionItemProvider.html getCompletionProvider(monaco: Monaco, languageDefinition: LanguageDefinition) { + const isPPL = languageDefinition.id === CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID; // backticks for field names in PPL + const triggerCharacters = [' ', '$', ',', '(', "'"].concat(isPPL ? ['`'] : []); + return { - triggerCharacters: [' ', '$', ',', '(', "'"], // one of these characters indicates that it is time to look for a suggestion + triggerCharacters, // one of these characters indicates that it is time to look for a suggestion provideCompletionItems: async (model: monacoTypes.editor.ITextModel, position: monacoTypes.IPosition) => { const currentToken = linkedTokenBuilder(monaco, languageDefinition, model, position, this.tokenTypes); const statementPosition = this.getStatementPosition(currentToken); diff --git a/public/app/plugins/datasource/cloudwatch/language/monarch/types.ts b/public/app/plugins/datasource/cloudwatch/language/monarch/types.ts index 5d08361a1f6..475153d4a5f 100644 --- a/public/app/plugins/datasource/cloudwatch/language/monarch/types.ts +++ b/public/app/plugins/datasource/cloudwatch/language/monarch/types.ts @@ -25,21 +25,45 @@ export enum StatementPosition { // sql SelectKeyword, AfterSelectKeyword, + SelectExpression, + AfterSelectExpression, AfterSelectFuncFirstArgument, - AfterFromKeyword, - SchemaFuncFirstArgument, - SchemaFuncExtraArgument, + PredefinedFunctionArgument, FromKeyword, AfterFrom, + AfterFromKeyword, + AfterFromArguments, + SchemaFuncFirstArgument, + SchemaFuncExtraArgument, WhereKey, WhereComparisonOperator, WhereValue, AfterWhereValue, + HavingKey, + HavingComparisonOperator, + HavingValue, + AfterHavingValue, + CaseKey, + CaseComparisonOperator, + CaseValue, + AfterCaseValue, + WhenKey, + WhenComparisonOperator, + WhenValue, + AfterWhenValue, + ThenExpression, + AfterThenExpression, + AfterElseKeyword, + OnKey, + OnComparisonOperator, + OnValue, + AfterOnValue, AfterGroupByKeywords, AfterGroupBy, AfterOrderByKeywords, AfterOrderByFunction, AfterOrderByDirection, + Subquery, // metric math PredefinedFunction, SearchFuncSecondArg, @@ -90,13 +114,36 @@ export enum StatementPosition { BooleanOperatorArg, ComparisonOperator, ComparisonOperatorArg, + + //PPL + BeforeLogicalExpression, + AfterArithmeticOperator, + AfterINKeyword, + SortField, + AfterHeadCommand, + AfterFieldsCommand, + FieldList, + AfterDedupFieldNames, + AfterStatsCommand, + StatsFunctionArgument, + AfterStatsBy, + AfterBooleanArgument, + EvalClause, + Expression, + SortFieldExpression, } export enum SuggestionKind { SelectKeyword, + AfterSelectKeyword, + AfterSelectExpression, FunctionsWithArguments, Metrics, FromKeyword, + AfterFromKeyword, + AfterFromArguments, + JoinKeywords, + HavingKeywords, SchemaKeyword, Namespaces, LabelKeys, @@ -109,6 +156,10 @@ export enum SuggestionKind { ComparisonOperators, LabelValues, LogicalOperators, + CaseKeyword, + WhenKeyword, + ThenKeyword, + AfterThenExpression, // metricmath, KeywordArguments, @@ -120,6 +171,20 @@ export enum SuggestionKind { Command, Function, InKeyword, + + // PPL + BooleanFunction, + LogicalExpression, + ValueExpression, + FieldOperators, + Field, + BooleanLiteral, + DedupParameter, + StatsParameter, + BooleanArgument, + StatsFunctions, + SpanClause, + SortFunctions, } export enum CompletionItemPriority { diff --git a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts index e240b072142..715f4d62d68 100644 --- a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts +++ b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts @@ -31,6 +31,7 @@ import { rangeUtil, } from '@grafana/data'; import { TemplateSrv } from '@grafana/runtime'; +import { type CustomFormatterVariable } from '@grafana/scenes'; import { CloudWatchJsonData, @@ -40,6 +41,7 @@ import { CloudWatchQuery, GetLogEventsRequest, LogAction, + LogsQueryLanguage, QueryParam, StartQueryRequest, } from '../types'; @@ -109,12 +111,40 @@ export class CloudWatchLogsQueryRunner extends CloudWatchRequest { const logGroups = uniq(interpolatedLogGroupArns).map((arn) => ({ arn, name: arn })); const logGroupNames = uniq(interpolatedLogGroupNames); + const logsSQLCustomerFormatter = (value: unknown, model: Partial) => { + if ( + (typeof value === 'string' && value.startsWith('arn:') && value.endsWith(':*')) || + (Array.isArray(value) && + value.every((v) => typeof v === 'string' && v.startsWith('arn:') && v.endsWith(':*'))) + ) { + const varName = model.name || ''; + const variable = this.templateSrv.getVariables().find(({ name }) => name === varName); + // checks the raw query string for a log group template variable that occurs inside `logGroups(logGroupIdentifier:[ ... ])\` + // to later surround the log group names with backticks + // this assumes there's only a single template variable used inside the [ ] + const shouldSurroundInQuotes = target.expression + ?.replaceAll(/[\r\n\t\s]+/g, '') + .includes(`\`logGroups(logGroupIdentifier:[$${varName}])\``); + if (variable && 'current' in variable && 'text' in variable.current) { + if (Array.isArray(variable.current.text)) { + return variable.current.text.map((v) => (shouldSurroundInQuotes ? `'${v}'` : v)).join(','); + } + return shouldSurroundInQuotes ? `'${variable.current.text}'` : variable.current.text; + } + } + + return value; + }; + const formatter = target.queryLanguage === LogsQueryLanguage.SQL ? logsSQLCustomerFormatter : undefined; + const queryString = this.templateSrv.replace(target.expression || '', options.scopedVars, formatter); + return { refId: target.refId, region: this.templateSrv.replace(this.getActualRegion(target.region)), - queryString: this.templateSrv.replace(target.expression || '', options.scopedVars), + queryString, logGroups, logGroupNames, + queryLanguage: target.queryLanguage, }; }); @@ -406,7 +436,9 @@ export class CloudWatchLogsQueryRunner extends CloudWatchRequest { const hasMissingLogGroups = !query.logGroups?.length; const hasMissingQueryString = !query.expression?.length; - if ((hasMissingLogGroups && hasMissingLegacyLogGroupNames) || hasMissingQueryString) { + // log groups are not mandatory if language is SQL + const isInvalidCWLIQuery = query.queryLanguage !== 'SQL' && hasMissingLogGroups && hasMissingLegacyLogGroupNames; + if (isInvalidCWLIQuery || hasMissingQueryString) { return false; }