CloudWatch: Add OpenSearch PPL and SQL support in Logs Insights (#97508)

Cloudwatch: OpenSearch PPL and SQL support in Logs Insights

Co-authored-by: Kevin Yu <kevinwcyu@users.noreply.github.com>
Co-authored-by: Nathan Vērzemnieks <njvrzm@users.noreply.github.com>
This commit is contained in:
Ida Štambuk 2024-12-06 15:27:19 +01:00 committed by GitHub
parent 3d856dcb33
commit 2e342e5b1b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 7182 additions and 630 deletions

View File

@ -5765,14 +5765,6 @@ exports[`no gf-form usage`] = {
"public/app/plugins/datasource/cloudwatch/components/ConfigEditor/XrayLinkConfig.tsx:5381": [ "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"] [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": [ "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"],
[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"]

View File

@ -226,17 +226,40 @@ The label field allows you to override the default name of the metric legend usi
## Query CloudWatch Logs ## Query CloudWatch Logs
The logs query editor helps you write CloudWatch Logs Query Language queries across defined regions and log groups. 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 ### 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. 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). 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. 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 ## 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. 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.

View File

@ -218,6 +218,12 @@ export interface QueryEditorArrayExpression {
export type QueryEditorExpression = (QueryEditorArrayExpression | QueryEditorPropertyExpression | QueryEditorGroupByExpression | QueryEditorFunctionExpression | QueryEditorFunctionParameterExpression | QueryEditorOperatorExpression); export type QueryEditorExpression = (QueryEditorArrayExpression | QueryEditorPropertyExpression | QueryEditorGroupByExpression | QueryEditorFunctionExpression | QueryEditorFunctionParameterExpression | QueryEditorOperatorExpression);
export enum LogsQueryLanguage {
CWLI = 'CWLI',
PPL = 'PPL',
SQL = 'SQL',
}
/** /**
* Shape of a CloudWatch Logs query * Shape of a CloudWatch Logs query
*/ */
@ -235,6 +241,10 @@ export interface CloudWatchLogsQuery extends common.DataQuery {
* Log groups to query * Log groups to query
*/ */
logGroups?: Array<LogGroup>; logGroups?: Array<LogGroup>;
/**
* 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 * Whether a query is a Metrics, Logs, or Annotations query
*/ */

View File

@ -16,6 +16,13 @@ const (
CloudWatchQueryModeMetrics CloudWatchQueryMode = "Metrics" CloudWatchQueryModeMetrics CloudWatchQueryMode = "Metrics"
) )
// Defines values for LogsQueryLanguage.
const (
LogsQueryLanguageCWLI LogsQueryLanguage = "CWLI"
LogsQueryLanguagePPL LogsQueryLanguage = "PPL"
LogsQueryLanguageSQL LogsQueryLanguage = "SQL"
)
// Defines values for MetricEditorMode. // Defines values for MetricEditorMode.
const ( const (
MetricEditorModeN0 MetricEditorMode = 0 MetricEditorModeN0 MetricEditorMode = 0
@ -194,8 +201,9 @@ type CloudWatchLogsQuery struct {
LogGroupNames []string `json:"logGroupNames,omitempty"` LogGroupNames []string `json:"logGroupNames,omitempty"`
// Log groups to query // Log groups to query
LogGroups []LogGroup `json:"logGroups,omitempty"` LogGroups []LogGroup `json:"logGroups,omitempty"`
QueryMode *CloudWatchQueryMode `json:"queryMode,omitempty"` QueryLanguage *LogsQueryLanguage `json:"queryLanguage,omitempty"`
QueryMode *CloudWatchQueryMode `json:"queryMode,omitempty"`
// Specify the query flavor // Specify the query flavor
// TODO make this required and give it a default // TODO make this required and give it a default
@ -325,6 +333,9 @@ type LogGroup struct {
Name string `json:"name"` Name string `json:"name"`
} }
// LogsQueryLanguage defines model for LogsQueryLanguage.
type LogsQueryLanguage string
// MetricEditorMode defines model for MetricEditorMode. // MetricEditorMode defines model for MetricEditorMode.
type MetricEditorMode int type MetricEditorMode int

View File

@ -12,6 +12,7 @@ import (
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr" "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"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface" "github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
"github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend"
@ -20,6 +21,7 @@ import (
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/features" "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" "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) 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) { func (e *cloudWatchExecutor) executeLogActions(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
resp := backend.NewQueryDataResponse() resp := backend.NewQueryDataResponse()
@ -191,13 +232,21 @@ func (e *cloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient c
if !startTime.Before(endTime) { if !startTime.Before(endTime) {
return nil, errorsource.DownstreamError(fmt.Errorf("invalid time range: start time must be before end time"), false) 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 // 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. // 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, // 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. // CloudWatch wouldn't consider a query using a non-alised @log/@logStream valid.
modifiedQueryString := "fields @timestamp,ltrim(@log) as " + logIdentifierInternal + ",ltrim(@logStream) as " + if *logsQuery.QueryLanguage == dataquery.LogsQueryLanguageCWLI {
logStreamIdentifierInternal + "|" + logsQuery.QueryString finalQueryString = "fields @timestamp,ltrim(@log) as " + logIdentifierInternal + ",ltrim(@logStream) as " +
logStreamIdentifierInternal + "|" + logsQuery.QueryString
}
startQueryInput := &cloudwatchlogs.StartQueryInput{ startQueryInput := &cloudwatchlogs.StartQueryInput{
StartTime: aws.Int64(startTime.Unix()), 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 // and also a little bit more but as CW logs accept only seconds as integers there is not much to do about
// that. // that.
EndTime: aws.Int64(int64(math.Ceil(float64(endTime.UnixNano()) / 1e9))), 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) { // log group identifiers can be left out if the query is an SQL query
var logGroupIdentifiers []string if *logsQuery.QueryLanguage != dataquery.LogsQueryLanguageSQL {
for _, lg := range logsQuery.LogGroups { if len(logsQuery.LogGroups) > 0 && features.IsEnabled(ctx, features.FlagCloudWatchCrossAccountQuerying) {
arn := lg.Arn var logGroupIdentifiers []string
// due to a bug in the startQuery api, we remove * from the arn, otherwise it throws an error for _, lg := range logsQuery.LogGroups {
logGroupIdentifiers = append(logGroupIdentifiers, strings.TrimSuffix(arn, "*")) 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 { 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) 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 { if err != nil {
var awsErr awserr.Error var awsErr awserr.Error
if errors.As(err, &awsErr) && awsErr.Code() == "LimitExceededException" { if errors.As(err, &awsErr) && awsErr.Code() == "LimitExceededException" {

View File

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/aws/aws-sdk-go/aws" "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/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs" "github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface" "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/backend/log"
"github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/features" "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/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models" "github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils" "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) { func Test_executeStartQuery(t *testing.T) {
origNewCWLogsClient := NewCWLogsClient origNewCWLogsClient := NewCWLogsClient
t.Cleanup(func() { t.Cleanup(func() {
@ -321,40 +343,135 @@ func Test_executeStartQuery(t *testing.T) {
return &cli return &cli
} }
t.Run("successfully parses information from JSON to StartQueryWithContext", func(t *testing.T) { t.Run("successfully parses information from JSON to StartQueryWithContext for language", func(t *testing.T) {
cli = fakeCWLogsClient{} testCases := map[string]struct {
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { queries []backend.DataQuery
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil expectedOutput []*cloudwatchlogs.StartQueryInput
}) queryLanguage dataquery.LogsQueryLanguage
executor := newExecutor(im, log.NewNullLogger()) }{
"not defined": {
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ queries: []backend.DataQuery{
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, {
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", RefID: "A",
TimeRange: backend.TimeRange{From: time.Unix(0, 0), To: time.Unix(1, 0)}, TimeRange: backend.TimeRange{From: time.Unix(0, 0), To: time.Unix(1, 0)},
JSON: json.RawMessage(`{ JSON: json.RawMessage(`{
"type": "logAction", "type": "logAction",
"subtype": "StartQuery", "subtype": "StartQuery",
"limit": 12, "limit": 12,
"queryLanguage": "CWLI",
"queryString":"fields @message", "queryString":"fields @message",
"logGroupNames":["some name","another name"] "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) languageMock := newWithQueryLanguageMock()
assert.Equal(t, []*cloudwatchlogs.StartQueryInput{ originalWithQueryLanguage := WithQueryLanguage
{ WithQueryLanguage = languageMock.mockWithQueryLanguage
StartTime: aws.Int64(0), defer func() {
EndTime: aws.Int64(1), WithQueryLanguage = originalWithQueryLanguage
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")}, _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
}, PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
}, cli.calls.startQueryWithContext) 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) { 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", "type": "logAction",
"subtype": "StartQuery", "subtype": "StartQuery",
"limit": 12, "limit": 12,
"queryLanguage": "CWLI",
"queryString":"fields @message", "queryString":"fields @message",
"logGroups":[{"arn": "fakeARN"}] "logGroups":[{"arn": "fakeARN"}]
}`), }`),

View File

@ -54,8 +54,10 @@ func logsResultsToDataframes(response *cloudwatchlogs.GetQueryResultsOutput) (*d
if _, exists := fieldValues[*resultField.Field]; !exists { if _, exists := fieldValues[*resultField.Field]; !exists {
fieldNames = append(fieldNames, *resultField.Field) fieldNames = append(fieldNames, *resultField.Field)
// Check if it's a time field // Check if it's a cloudWatchTSFormat field or one of the known timestamp fields:
if _, err := time.Parse(cloudWatchTSFormat, *resultField.Value); err == nil { // 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) fieldValues[*resultField.Field] = make([]*time.Time, rowCount)
} else if _, err := strconv.ParseFloat(*resultField.Value, 64); err == nil { } else if _, err := strconv.ParseFloat(*resultField.Value, 64); err == nil {
fieldValues[*resultField.Field] = make([]*float64, rowCount) 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 { if timeField, ok := fieldValues[*resultField.Field].([]*time.Time); ok {
parsedTime, err := time.Parse(cloudWatchTSFormat, *resultField.Value) parsedTime, err := time.Parse(cloudWatchTSFormat, *resultField.Value)
if err != nil { 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 timeField[i] = &parsedTime
} else if numericField, ok := fieldValues[*resultField.Field].([]*float64); ok { } else if numericField, ok := fieldValues[*resultField.Field].([]*float64); ok {
parsedFloat, err := strconv.ParseFloat(*resultField.Value, 64) parsedFloat, err := strconv.ParseFloat(*resultField.Value, 64)
@ -313,3 +319,7 @@ func numericFieldToStringField(field *data.Field) (*data.Field, error) {
return newField, nil return newField, nil
} }
func isTimestampField(fieldName string) bool {
return fieldName == "@timestamp" || fieldName == "@ingestionTime"
}

View File

@ -1,6 +1,7 @@
package cloudwatch package cloudwatch
import ( import (
"fmt"
"testing" "testing"
"time" "time"
@ -277,6 +278,72 @@ func TestLogsResultsToDataframes_MixedTypes_NumericValuesMixedWithStringFallBack
assert.ElementsMatch(t, expectedDataframe.Fields, dataframes.Fields) 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) { func TestGroupKeyGeneration(t *testing.T) {
logField := data.NewField("@log", data.Labels{}, []*string{ logField := data.NewField("@log", data.Labels{}, []*string{
aws.String("fakelog-a"), aws.String("fakelog-a"),

View File

@ -127,6 +127,7 @@ export function setupMockedDataSource({
datasource.resources.getMetrics = jest.fn().mockResolvedValue([]); datasource.resources.getMetrics = jest.fn().mockResolvedValue([]);
datasource.resources.getAccounts = jest.fn().mockResolvedValue([]); datasource.resources.getAccounts = jest.fn().mockResolvedValue([]);
datasource.resources.getLogGroups = jest.fn().mockResolvedValue([]); datasource.resources.getLogGroups = jest.fn().mockResolvedValue([]);
datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(false);
const fetchMock = jest.fn().mockReturnValue(of({})); const fetchMock = jest.fn().mockReturnValue(of({}));
setBackendSrv({ setBackendSrv({
...getBackendSrv(), ...getBackendSrv(),

View File

@ -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[][],
};

View File

@ -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[][],
};

View File

@ -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[][],
};

View File

@ -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[][],
};

View File

@ -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[][],
};

View File

@ -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[][],
};

View File

@ -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[][],
};

View File

@ -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 ".+@(?<host>.+)"'`,
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 }, // '".+@(?<host>.+)"
],
] as monacoTypes.Token[][],
};

View File

@ -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[][],
};

View File

@ -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 ".+@(?<host>.+)"',
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 }, // '".+@(?<host>.+)"
],
] 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[][],
};

View File

@ -1,7 +1,20 @@
import { monacoTypes } from '@grafana/ui'; 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 { 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 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 SQLTestData from '../cloudwatch-sql-test-data';
import * as DynamicLabelTestData from '../dynamic-label-test-data'; import * as DynamicLabelTestData from '../dynamic-label-test-data';
import * as MetricMathTestData from '../metric-math-test-data'; import * as MetricMathTestData from '../metric-math-test-data';
@ -40,7 +53,7 @@ const MonacoMock: Monaco = {
}; };
return TestData[value]; return TestData[value];
} }
if (languageId === 'cloudwatch-logs') { if (languageId === CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID) {
const TestData = { const TestData = {
[CloudwatchLogsTestData.emptyQuery.query]: CloudwatchLogsTestData.emptyQuery.tokens, [CloudwatchLogsTestData.emptyQuery.query]: CloudwatchLogsTestData.emptyQuery.tokens,
[CloudwatchLogsTestData.whitespaceOnlyQuery.query]: CloudwatchLogsTestData.whitespaceOnlyQuery.tokens, [CloudwatchLogsTestData.whitespaceOnlyQuery.query]: CloudwatchLogsTestData.whitespaceOnlyQuery.tokens,
@ -53,6 +66,49 @@ const MonacoMock: Monaco = {
}; };
return TestData[value]; 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 []; return [];
}, },
}, },

View File

@ -84,7 +84,10 @@ export const validMetricQueryCodeQuery: CloudWatchMetricsQuery = {
export const validLogsQuery: CloudWatchLogsQuery = { export const validLogsQuery: CloudWatchLogsQuery = {
queryMode: 'Logs', queryMode: 'Logs',
logGroupNames: ['group-A', 'group-B'], logGroups: [
{ arn: 'group-A', name: 'A' },
{ arn: 'group-B', name: 'B' },
],
hide: false, hide: false,
id: '', id: '',
region: 'us-east-2', region: 'us-east-2',

View File

@ -1,339 +1,58 @@
import { css, cx } from '@emotion/css'; import { css } from '@emotion/css';
import { stripIndent, stripIndents } from 'common-tags';
import Prism from 'prismjs'; import Prism from 'prismjs';
import { useState } from 'react'; import { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; 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 { flattenTokens } from '@grafana/ui/src/slate-plugins/slate-prism';
import tokenizer from '../../language/cloudwatch-logs/syntax'; import { CloudWatchLogsQuery, CloudWatchQuery, LogsQueryLanguage } from '../../types';
import { CloudWatchQuery } from '../../types';
import * as sampleQueries from './sampleQueries';
import { cwliTokenizer, pplTokenizer, sqlTokenizer } from './tokenizer';
interface QueryExample { interface QueryExample {
category: string; category: string;
examples: Array<{ examples: sampleQueries.SampleQuery[];
title?: string;
description?: string;
expr: string;
}>;
} }
const QUERIES: QueryExample[] = [ const QUERIES: QueryExample[] = [
{
category: 'General queries',
examples: sampleQueries.generalQueries,
},
{ {
category: 'Lambda', category: 'Lambda',
examples: [ examples: sampleQueries.lambdaSamples,
{
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`,
},
],
}, },
{ {
category: 'VPC Flow Logs', category: 'VPC Flow Logs',
examples: [ examples: sampleQueries.vpcSamples,
{
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`,
},
],
}, },
{ {
category: 'CloudTrail', category: 'CloudTrail Logs',
examples: [ examples: sampleQueries.cloudtrailSamples,
{
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: 'Common Queries', category: 'NAT Gateway',
examples: [ examples: sampleQueries.natSamples,
{
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 = <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=(?<user2>.*?), method:(?<method2>.*?), latency := (?<latency2>.*?)/
| 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: 'Route 53', category: 'AWS App Sync',
examples: [ examples: sampleQueries.appSyncSamples,
{
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 AppSync', category: 'IOT queries',
examples: [ examples: sampleQueries.iotSamples,
{
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`,
},
],
}, },
]; ];
const COMMANDS: QueryExample[] = [ function renderHighlightedMarkup(
{ code: string,
category: 'fields', keyPrefix: string,
examples: [ queryLanugage: LogsQueryLanguage = LogsQueryLanguage.CWLI
{ ) {
description: const grammar = getGrammarForLanguage(queryLanugage);
'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;
const tokens = flattenTokens(Prism.tokenize(code, grammar)); const tokens = flattenTokens(Prism.tokenize(code, grammar));
const spans = tokens const spans = tokens
.filter((token) => typeof token !== 'string') .filter((token) => typeof token !== 'string')
@ -351,91 +70,73 @@ function renderHighlightedMarkup(code: string, keyPrefix: string) {
return <div className="slate-query-field">{spans}</div>; return <div className="slate-query-field">{spans}</div>;
} }
interface CollapseProps {
key?: string;
label: string;
children: React.ReactNode;
}
const CheatSheetCollapse = (props: CollapseProps) => {
const [isOpen, setIsOpen] = useState(false);
return (
<Collapse label={props.label} isOpen={isOpen} onToggle={setIsOpen} key={props.key} collapsible>
{props.children}
</Collapse>
);
};
type Props = { type Props = {
onClickExample: (query: CloudWatchQuery) => void; onClickExample: (query: CloudWatchQuery) => void;
query: CloudWatchQuery; query: CloudWatchQuery;
}; };
const isLogsQuery = (query: CloudWatchQuery): query is CloudWatchLogsQuery => query.queryMode === 'Logs';
const LogsCheatSheet = (props: Props) => { const LogsCheatSheet = (props: Props) => {
const [isCommandsOpen, setIsCommandsOpen] = useState(false);
const [isQueriesOpen, setIsQueriesOpen] = useState(false);
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const queryLanugage: LogsQueryLanguage =
(isLogsQuery(props.query) && props.query.queryLanguage) || LogsQueryLanguage.CWLI;
return ( return (
<div> <div>
<h3>CloudWatch Logs cheat sheet</h3> <div className={styles.heading}>
<Collapse <Text variant="h3" weight="bold">
label="Commands" CloudWatch Logs cheat sheet
collapsible={true} </Text>
isOpen={isCommandsOpen} </div>
onToggle={(isOpen) => setIsCommandsOpen(isOpen)} {QUERIES.map((query, i) => (
> <CheatSheetCollapse key={query.category} label={query.category}>
<>
{COMMANDS.map((cat, i) => (
<div key={`cat-${i}`}>
<h5>{cat.category}</h5>
{cat.examples.map((item, j) => (
<div key={`item-${j}`}>
<p>{item.description}</p>
<button
type="button"
className={styles.cheatSheetExample}
key={item.expr}
onClick={() =>
props.onClickExample({
refId: props.query.refId ?? 'A',
expression: item.expr,
queryMode: 'Logs',
region: props.query.region,
id: props.query.refId ?? 'A',
logGroupNames: 'logGroupNames' in props.query ? props.query.logGroupNames : [],
logGroups: 'logGroups' in props.query ? props.query.logGroups : [],
})
}
>
<pre>{renderHighlightedMarkup(item.expr, `item-${j}`)}</pre>
</button>
</div>
))}
</div>
))}
</>
</Collapse>
<Collapse
label="Queries"
collapsible={true}
isOpen={isQueriesOpen}
onToggle={(isOpen) => setIsQueriesOpen(isOpen)}
>
{QUERIES.map((cat, i) => (
<div key={`cat-${i}`}> <div key={`cat-${i}`}>
<div className={cx(styles.cheatSheetItemTitle, styles.exampleCategory)}>{cat.category}</div> {query.examples.map((item, j) => (
{cat.examples.map((item, j) => ( <>
<div className={styles.cheatSheetItem} key={`item-${j}`}> {item.expr[queryLanugage] && (
<h4>{item.title}</h4> <>
<button <Text variant="h6" weight="bold">
type="button" {item.title}
className={styles.cheatSheetExample} </Text>
key={item.expr} <button
onClick={() => type="button"
props.onClickExample({ className={styles.cheatSheetExample}
refId: props.query.refId ?? 'A', key={item.expr[queryLanugage]}
expression: item.expr, onClick={() =>
queryMode: 'Logs', props.onClickExample({
region: props.query.region, ...props.query,
id: props.query.refId ?? 'A', refId: props.query.refId ?? 'A',
logGroupNames: 'logGroupNames' in props.query ? props.query.logGroupNames : [], expression: item.expr[queryLanugage],
logGroups: 'logGroups' in props.query ? props.query.logGroups : [], queryMode: 'Logs',
}) region: props.query.region,
} id: props.query.refId ?? 'A',
> logGroupNames: 'logGroupNames' in props.query ? props.query.logGroupNames : [],
<pre>{renderHighlightedMarkup(item.expr, `item-${j}`)}</pre> logGroups: 'logGroups' in props.query ? props.query.logGroups : [],
</button> })
</div> }
>
<pre>{renderHighlightedMarkup(item.expr[queryLanugage], `item-${j}`, queryLanugage)}</pre>
</button>
</>
)}
</>
))} ))}
</div> </div>
))} </CheatSheetCollapse>
</Collapse> ))}
<div> <div>
Note: If you are seeing masked data, you may have CloudWatch logs data protection enabled.{' '} Note: If you are seeing masked data, you may have CloudWatch logs data protection enabled.{' '}
<a <a
@ -455,18 +156,12 @@ const LogsCheatSheet = (props: Props) => {
export default LogsCheatSheet; export default LogsCheatSheet;
const getStyles = (theme: GrafanaTheme2) => ({ const getStyles = (theme: GrafanaTheme2) => ({
exampleCategory: css({ heading: css({
marginTop: '5px', marginBottom: theme.spacing(2),
}), }),
link: css({ link: css({
textDecoration: 'underline', textDecoration: 'underline',
}), }),
cheatSheetItem: css({
margin: theme.spacing(3, 0),
}),
cheatSheetItemTitle: css({
fontSize: theme.typography.h3.fontSize,
}),
cheatSheetExample: css({ cheatSheetExample: css({
margin: theme.spacing(0.5, 0), margin: theme.spacing(0.5, 0),
// element is interactive, clear button styles // element is interactive, clear button styles
@ -476,3 +171,14 @@ const getStyles = (theme: GrafanaTheme2) => ({
display: 'block', display: 'block',
}), }),
}); });
const getGrammarForLanguage = (queryLanugage: LogsQueryLanguage) => {
switch (queryLanugage) {
case LogsQueryLanguage.CWLI:
return cwliTokenizer;
case LogsQueryLanguage.PPL:
return pplTokenizer;
case LogsQueryLanguage.SQL:
return sqlTokenizer;
}
};

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { usePrevious } from 'react-use'; import { usePrevious } from 'react-use';
import { PanelData } from '@grafana/data'; import { PanelData } from '@grafana/data';
import { Icon } from '@grafana/ui'; import { LinkButton } from '@grafana/ui';
import { AwsUrl, encodeUrl } from '../../../aws_url'; import { AwsUrl, encodeUrl } from '../../../aws_url';
import { CloudWatchDatasource } from '../../../datasource'; import { CloudWatchDatasource } from '../../../datasource';
@ -45,8 +45,8 @@ export function CloudWatchLink({ panelData, query, datasource }: Props) {
}, [panelData, prevPanelData, datasource, query]); }, [panelData, prevPanelData, datasource, query]);
return ( return (
<a href={href} target="_blank" rel="noopener noreferrer"> <LinkButton variant="secondary" icon="share-alt" href={href} target="_blank" rel="noopener noreferrer">
<Icon name="share-alt" /> CloudWatch Logs Insights CloudWatch Logs Insights
</a> </LinkButton>
); );
} }

View File

@ -1,37 +1,102 @@
import { css } from '@emotion/css'; import { memo, useCallback, useEffect, useState } from 'react';
import { memo } from 'react'; import { useEffectOnce } from 'react-use';
import { QueryEditorProps } from '@grafana/data'; import { QueryEditorProps, SelectableValue } from '@grafana/data';
import { InlineFormLabel } from '@grafana/ui'; import { InlineSelect } from '@grafana/experimental';
import { CloudWatchDatasource } from '../../../datasource'; 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 { CloudWatchLink } from './CloudWatchLink';
import CloudWatchLogsQueryField from './LogsQueryField'; import { CloudWatchLogsQueryField } from './LogsQueryField';
type Props = QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData> & { type Props = QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData> & {
query: CloudWatchLogsQuery; query: CloudWatchLogsQuery;
extraHeaderElementLeft?: React.Dispatch<JSX.Element | undefined>;
}; };
const labelClass = css({ const logsQueryLanguageOptions: Array<SelectableValue<LogsQueryLanguage>> = [
marginLeft: '3px', { label: 'Logs Insights QL', value: LogsQueryLanguage.CWLI },
flexGrow: 0, { label: 'OpenSearch SQL', value: LogsQueryLanguage.SQL },
}); { label: 'OpenSearch PPL', value: LogsQueryLanguage.PPL },
];
export const CloudWatchLogsQueryEditor = memo(function CloudWatchLogsQueryEditor(props: Props) { 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?.(
<InlineSelect
label="Query language"
value={query.queryLanguage || LogsQueryLanguage.CWLI}
options={logsQueryLanguageOptions}
onChange={({ value }) => {
onQueryLanguageChange(value);
}}
/>
);
return () => {
extraHeaderElementLeft?.(undefined);
};
}, [extraHeaderElementLeft, onChange, onQueryLanguageChange, query]);
const onQueryStringChange = (query: CloudWatchQuery) => {
onChange(query);
setIsQueryNew(false);
};
return ( return (
<CloudWatchLogsQueryField <CloudWatchLogsQueryField
{...props} {...props}
ExtraFieldElement={ onChange={onQueryStringChange}
<InlineFormLabel className={`gf-form-label--btn ${labelClass}`} width="auto" tooltip="Link to Graph in AWS"> ExtraFieldElement={<CloudWatchLink query={query} panelData={data} datasource={datasource} />}
<CloudWatchLink query={query} panelData={data} datasource={datasource} />
</InlineFormLabel>
}
/> />
); );
}); });
export default CloudWatchLogsQueryEditor; 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;
}
};

View File

@ -1,28 +1,26 @@
import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api'; import { css } from '@emotion/css';
import { ReactNode, useCallback, useRef } from 'react'; import { ReactNode, useCallback } from 'react';
import { QueryEditorProps } from '@grafana/data'; import { GrafanaTheme2, QueryEditorProps } from '@grafana/data';
import { CodeEditor, Monaco, Themeable2, withTheme2 } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
import { CloudWatchDatasource } from '../../../datasource'; import { CloudWatchDatasource } from '../../../datasource';
import language from '../../../language/logs/definition'; import { CloudWatchJsonData, CloudWatchLogsQuery, CloudWatchQuery, LogsQueryLanguage } from '../../../types';
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 { LogGroupsFieldWrapper } from '../../shared/LogGroups/LogGroupsField'; 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 export interface CloudWatchLogsQueryFieldProps
extends QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData>, extends QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData> {
Themeable2 {
ExtraFieldElement?: ReactNode; ExtraFieldElement?: ReactNode;
query: CloudWatchLogsQuery; query: CloudWatchLogsQuery;
} }
export const CloudWatchLogsQueryField = (props: CloudWatchLogsQueryFieldProps) => { export const CloudWatchLogsQueryField = (props: CloudWatchLogsQueryFieldProps) => {
const { query, datasource, onChange, ExtraFieldElement } = props; const { query, datasource, onChange, ExtraFieldElement } = props;
const monacoRef = useRef<Monaco>(); const styles = useStyles2(getStyles);
const disposalRef = useRef<monacoType.IDisposable>();
const onChangeLogs = useCallback( const onChangeLogs = useCallback(
async (query: CloudWatchLogsQuery) => { async (query: CloudWatchLogsQuery) => {
@ -31,51 +29,6 @@ export const CloudWatchLogsQueryField = (props: CloudWatchLogsQueryFieldProps) =
[onChange] [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 ( return (
<> <>
<LogGroupsFieldWrapper <LogGroupsFieldWrapper
@ -91,48 +44,31 @@ export const CloudWatchLogsQueryField = (props: CloudWatchLogsQueryFieldProps) =
onChangeLogs({ ...query, logGroupNames }); onChangeLogs({ ...query, logGroupNames });
}} }}
/> />
<div className="gf-form-inline gf-form-inline--nowrap flex-grow-1"> <div>
<div className="gf-form--grow flex-shrink-1"> {getCodeEditor(query, datasource, onChange)}
<CodeEditor <div className={styles.editor}>{ExtraFieldElement}</div>
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,
},
}}
language={language.id}
value={query.expression ?? ''}
onBlur={(value) => {
if (value !== query.expression) {
onChangeQuery(value);
}
disposalRef.current?.dispose();
}}
onFocus={onFocus}
onBeforeEditorMount={onBeforeEditorMount}
onEditorDidMount={onEditorMount}
onEditorWillUnmount={() => disposalRef.current?.dispose()}
/>
</div>
{ExtraFieldElement}
</div> </div>
</> </>
); );
}; };
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 <PPLQueryEditor query={query} datasource={datasource} onChange={onChange} />;
case LogsQueryLanguage.SQL:
return <SQLQueryEditor query={query} datasource={datasource} onChange={onChange} />;
default:
return <LogsQLCodeEditor query={query} datasource={datasource} onChange={onChange} />;
}
};

View File

@ -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<Monaco>();
const disposalRef = useRef<monacoType.IDisposable>();
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 (
<CodeEditor
{...codeEditorCommonProps}
language={language.id}
value={query.expression ?? ''}
onBlur={(value: string) => {
if (value !== query.expression) {
onChangeQuery(value);
}
disposalRef.current?.dispose();
}}
onFocus={onFocus}
onBeforeEditorMount={onBeforeEditorMount}
onEditorDidMount={onEditorMount}
onEditorWillUnmount={() => disposalRef.current?.dispose()}
/>
);
};

View File

@ -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<CodeEditorProps> = {
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<Monaco>();
const disposalRef = useRef<monacoType.IDisposable>();
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 (
<CodeEditor
{...codeEditorCommonProps}
language={language.id}
value={query.expression ?? ''}
onBlur={(value: string) => {
if (value !== query.expression) {
onChangeQuery(value);
}
disposalRef.current?.dispose();
}}
onFocus={onFocus}
onBeforeEditorMount={onBeforeEditorMount}
onEditorDidMount={onEditorMount}
onEditorWillUnmount={() => disposalRef.current?.dispose()}
/>
);
};

View File

@ -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<Monaco>();
const disposalRef = useRef<monacoType.IDisposable>();
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 (
<CodeEditor
{...codeEditorCommonProps}
language={language.id}
value={query.expression ?? ''}
onBlur={(value: string) => {
if (value !== query.expression) {
onChangeQuery(value);
}
disposalRef.current?.dispose();
}}
onFocus={onFocus}
onBeforeEditorMount={onBeforeEditorMount}
onEditorDidMount={onEditorMount}
onEditorWillUnmount={() => disposalRef.current?.dispose()}
/>
);
};

View File

@ -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 userEvent from '@testing-library/user-event';
import { selectOptionInTest } from 'test/helpers/selectOptionInTest';
import { QueryEditorProps } from '@grafana/data'; import { QueryEditorProps } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
@ -13,7 +14,8 @@ import {
validMetricSearchCodeQuery, validMetricSearchCodeQuery,
} from '../../__mocks__/queries'; } from '../../__mocks__/queries';
import { CloudWatchDatasource } from '../../datasource'; 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'; import { QueryEditor } from './QueryEditor';
@ -23,11 +25,11 @@ const migratedFields = {
metricEditorMode: MetricEditorMode.Builder, metricEditorMode: MetricEditorMode.Builder,
metricQueryType: MetricQueryType.Insights, metricQueryType: MetricQueryType.Insights,
}; };
const mockOnChange = jest.fn();
const props: QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData> = { const props: QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData> = {
datasource: setupMockedDataSource().datasource, datasource: setupMockedDataSource().datasource,
onRunQuery: jest.fn(), onRunQuery: jest.fn(),
onChange: jest.fn(), onChange: mockOnChange,
query: {} as CloudWatchQuery, query: {} as CloudWatchQuery,
}; };
@ -54,6 +56,13 @@ jest.mock('@grafana/runtime', () => ({
}, },
})); }));
jest.mock('@grafana/ui', () => ({
...jest.requireActual('@grafana/ui'),
CodeEditor: jest.fn().mockImplementation(() => {
return <input id="cloudwatch-fake-editor"></input>;
}),
}));
export { SQLCodeEditor } from './MetricsQueryEditor/SQLCodeEditor'; export { SQLCodeEditor } from './MetricsQueryEditor/SQLCodeEditor';
describe('QueryEditor should render right editor', () => { 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(<QueryEditor {...logsProps} query={emptyQuery} />);
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(<QueryEditor {...logsProps} query={nonEmptyQuery} />);
await waitFor(() => {
expect(mockOnChange).not.toHaveBeenCalled();
});
});
it('should set the correct default expression if query is new', async () => {
const emptyQuery = { ...logsProps.query, expression: '' };
render(<QueryEditor {...logsProps} query={emptyQuery} />);
await selectOptionInTest(screen.getByLabelText(/Query language/), 'OpenSearch SQL');
expect(mockOnChange).toHaveBeenCalledWith({
...logsProps.query,
queryLanguage: LogsQueryLanguage.SQL,
expression: DEFAULT_SQL_QUERY_STRING,
});
});
});
});

View File

@ -49,7 +49,14 @@ export const QueryEditor = (props: Props) => {
extraHeaderElementRight={setExtraHeaderElementRight} extraHeaderElementRight={setExtraHeaderElementRight}
/> />
)} )}
{isCloudWatchLogsQuery(query) && <LogsQueryEditor {...props} query={query} onChange={onChangeInternal} />} {isCloudWatchLogsQuery(query) && (
<LogsQueryEditor
{...props}
query={query}
onChange={onChangeInternal}
extraHeaderElementLeft={setExtraHeaderElementLeft}
/>
)}
</> </>
); );
}; };

View File

@ -5,7 +5,8 @@ import { config } from '@grafana/runtime';
import { setupMockedDataSource } from '../../__mocks__/CloudWatchDataSource'; import { setupMockedDataSource } from '../../__mocks__/CloudWatchDataSource';
import { validLogsQuery, validMetricSearchBuilderQuery } from '../../__mocks__/queries'; 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'; import QueryHeader from './QueryHeader';
@ -111,32 +112,6 @@ describe('QueryHeader', () => {
describe('when changing query mode', () => { describe('when changing query mode', () => {
const { datasource } = setupMockedDataSource(); 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(
<QueryHeader
datasource={datasource}
query={{ ...validMetricSearchBuilderQuery, expression: 'foo' }}
onChange={onChange}
onRunQuery={jest.fn()}
dataIsStale={false}
/>
);
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 () => { it('should set expression to empty when switching to metrics mode', async () => {
const onChange = jest.fn(); const onChange = jest.fn();
datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(false); datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(false);
@ -155,6 +130,7 @@ describe('QueryHeader', () => {
}); });
expect(onChange).toHaveBeenCalledWith({ expect(onChange).toHaveBeenCalledWith({
...validMetricSearchBuilderQuery, ...validMetricSearchBuilderQuery,
queryLanguage: LogsQueryLanguage.CWLI,
logGroupNames: undefined, logGroupNames: undefined,
logGroups: [], logGroups: [],
sqlExpression: '', sqlExpression: '',
@ -185,7 +161,7 @@ describe('QueryHeader', () => {
render( render(
<QueryHeader <QueryHeader
datasource={datasource} datasource={datasource}
query={{ ...validMetricSearchBuilderQuery, queryMode: 'Logs', expression: DEFAULT_LOGS_QUERY_STRING }} query={{ ...validMetricSearchBuilderQuery, queryMode: 'Logs', expression: DEFAULT_CWLI_QUERY_STRING }}
onChange={onChange} onChange={onChange}
onRunQuery={jest.fn()} onRunQuery={jest.fn()}
dataIsStale={false} dataIsStale={false}

View File

@ -4,7 +4,6 @@ import { config } from '@grafana/runtime';
import { Badge, Button } from '@grafana/ui'; import { Badge, Button } from '@grafana/ui';
import { CloudWatchDatasource } from '../../datasource'; import { CloudWatchDatasource } from '../../datasource';
import { DEFAULT_LOGS_QUERY_STRING } from '../../defaultQueries';
import { isCloudWatchLogsQuery, isCloudWatchMetricsQuery } from '../../guards'; import { isCloudWatchLogsQuery, isCloudWatchMetricsQuery } from '../../guards';
import { useIsMonitoringAccount, useRegions } from '../../hooks'; import { useIsMonitoringAccount, useRegions } from '../../hooks';
import { CloudWatchJsonData, CloudWatchQuery, CloudWatchQueryMode, MetricQueryType } from '../../types'; import { CloudWatchJsonData, CloudWatchQuery, CloudWatchQueryMode, MetricQueryType } from '../../types';
@ -37,16 +36,11 @@ const QueryHeader = ({
const onQueryModeChange = ({ value }: SelectableValue<CloudWatchQueryMode>) => { const onQueryModeChange = ({ value }: SelectableValue<CloudWatchQueryMode>) => {
if (value && value !== queryMode) { 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({ onChange({
...datasource.getDefaultQuery(CoreApp.Unknown), ...datasource.getDefaultQuery(CoreApp.Unknown),
...query, ...query,
expression,
queryMode: value, queryMode: value,
expression: '',
}); });
} }
}; };

View File

@ -147,6 +147,8 @@ composableKinds: DataQuery: {
#QueryEditorExpression: #QueryEditorArrayExpression | #QueryEditorPropertyExpression | #QueryEditorGroupByExpression | #QueryEditorFunctionExpression | #QueryEditorFunctionParameterExpression | #QueryEditorOperatorExpression @cuetsy(kind="type") #QueryEditorExpression: #QueryEditorArrayExpression | #QueryEditorPropertyExpression | #QueryEditorGroupByExpression | #QueryEditorFunctionExpression | #QueryEditorFunctionParameterExpression | #QueryEditorOperatorExpression @cuetsy(kind="type")
#LogsQueryLanguage: "CWLI" | "SQL" | "PPL" @cuetsy(kind="enum")
// Shape of a CloudWatch Logs query // Shape of a CloudWatch Logs query
#CloudWatchLogsQuery: { #CloudWatchLogsQuery: {
common.DataQuery common.DataQuery
@ -164,6 +166,8 @@ composableKinds: DataQuery: {
logGroups?: [...#LogGroup] logGroups?: [...#LogGroup]
// @deprecated use logGroups // @deprecated use logGroups
logGroupNames?: [...string] 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") } @cuetsy(kind="interface")
#LogGroup: { #LogGroup: {
// ARN of the log group // ARN of the log group

View File

@ -216,6 +216,12 @@ export interface QueryEditorArrayExpression {
export type QueryEditorExpression = (QueryEditorArrayExpression | QueryEditorPropertyExpression | QueryEditorGroupByExpression | QueryEditorFunctionExpression | QueryEditorFunctionParameterExpression | QueryEditorOperatorExpression); export type QueryEditorExpression = (QueryEditorArrayExpression | QueryEditorPropertyExpression | QueryEditorGroupByExpression | QueryEditorFunctionExpression | QueryEditorFunctionParameterExpression | QueryEditorOperatorExpression);
export enum LogsQueryLanguage {
CWLI = 'CWLI',
PPL = 'PPL',
SQL = 'SQL',
}
/** /**
* Shape of a CloudWatch Logs query * Shape of a CloudWatch Logs query
*/ */
@ -233,6 +239,10 @@ export interface CloudWatchLogsQuery extends common.DataQuery {
* Log groups to query * Log groups to query
*/ */
logGroups?: Array<LogGroup>; logGroups?: Array<LogGroup>;
/**
* 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 * Whether a query is a Metrics, Logs, or Annotations query
*/ */

View File

@ -16,8 +16,10 @@ import { TimeRangeMock } from './__mocks__/timeRange';
import { import {
CloudWatchDefaultQuery, CloudWatchDefaultQuery,
CloudWatchLogsQuery, CloudWatchLogsQuery,
CloudWatchLogsRequest,
CloudWatchMetricsQuery, CloudWatchMetricsQuery,
CloudWatchQuery, CloudWatchQuery,
LogsQueryLanguage,
MetricEditorMode, MetricEditorMode,
MetricQueryType, MetricQueryType,
} from './types'; } from './types';
@ -28,10 +30,16 @@ describe('datasource', () => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
describe('query', () => { describe('query', () => {
it('should not run a query if log groups is not specified', async () => { beforeEach(() => {
const { datasource, queryMock } = setupMockedDataSource(); jest.clearAllMocks();
await lastValueFrom( });
datasource.query({ describe('query filtering', () => {
const testCases: Array<{
targets: CloudWatchQuery[];
queryLanguage: string | LogsQueryLanguage;
expectedOutput: Partial<CloudWatchLogsRequest>;
}> = [
{
targets: [ targets: [
{ {
queryMode: 'Logs', queryMode: 'Logs',
@ -49,22 +57,104 @@ describe('datasource', () => {
expression: 'some query string', expression: 'some query string',
}, },
], ],
requestId: '', expectedOutput: {
interval: '', queryString: 'some query string',
intervalMs: 0, logGroupNames: ['/some/group'],
range: TimeRangeMock, region: 'us-west-1',
scopedVars: {}, },
timezone: '', queryLanguage: 'undefined',
app: '', },
startTime: 0, {
}) 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).toHaveLength(1);
expect(queryMock.mock.calls[0][0].targets[0]).toMatchObject({ expect(queryMock.mock.calls[0][0].targets[0]).toMatchObject(testCase.expectedOutput);
queryString: 'some query string', });
logGroupNames: ['/some/group'],
region: 'us-west-1',
}); });
}); });
@ -365,5 +455,19 @@ describe('datasource', () => {
); );
expect((datasource.getDefaultQuery(CoreApp.PanelEditor) as CloudWatchDefaultQuery).matchExact).toEqual(true); 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
);
});
}); });
}); });

View File

@ -18,6 +18,14 @@ import { CloudWatchAnnotationSupport } from './annotationSupport';
import { DEFAULT_METRICS_QUERY, getDefaultLogsQuery } from './defaultQueries'; import { DEFAULT_METRICS_QUERY, getDefaultLogsQuery } from './defaultQueries';
import { isCloudWatchAnnotationQuery, isCloudWatchLogsQuery, isCloudWatchMetricsQuery } from './guards'; import { isCloudWatchAnnotationQuery, isCloudWatchLogsQuery, isCloudWatchMetricsQuery } from './guards';
import { CloudWatchLogsLanguageProvider } from './language/cloudwatch-logs/CloudWatchLogsLanguageProvider'; 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 { SQLCompletionItemProvider } from './language/cloudwatch-sql/completion/CompletionItemProvider';
import { import {
LogsCompletionItemProvider, LogsCompletionItemProvider,
@ -46,8 +54,10 @@ export class CloudWatchDatasource
languageProvider: CloudWatchLogsLanguageProvider; languageProvider: CloudWatchLogsLanguageProvider;
sqlCompletionItemProvider: SQLCompletionItemProvider; sqlCompletionItemProvider: SQLCompletionItemProvider;
metricMathCompletionItemProvider: MetricMathCompletionItemProvider; metricMathCompletionItemProvider: MetricMathCompletionItemProvider;
logsCompletionItemProviderFunc: (queryContext: queryContext) => LogsCompletionItemProvider;
defaultLogGroups?: string[]; defaultLogGroups?: string[];
logsSqlCompletionItemProviderFunc: (queryContext: queryContext) => LogsSQLCompletionItemProvider;
logsCompletionItemProviderFunc: (queryContext: queryContext) => LogsCompletionItemProvider;
pplCompletionItemProviderFunc: (queryContext: queryContext) => PPLCompletionItemProvider;
type = 'cloudwatch'; type = 'cloudwatch';
@ -65,14 +75,17 @@ export class CloudWatchDatasource
this.resources = new ResourcesAPI(instanceSettings, templateSrv); this.resources = new ResourcesAPI(instanceSettings, templateSrv);
this.languageProvider = new CloudWatchLogsLanguageProvider(this); this.languageProvider = new CloudWatchLogsLanguageProvider(this);
this.sqlCompletionItemProvider = new SQLCompletionItemProvider(this.resources, this.templateSrv); this.sqlCompletionItemProvider = new SQLCompletionItemProvider(this.resources, this.templateSrv);
this.metricMathCompletionItemProvider = new MetricMathCompletionItemProvider(this.resources, this.templateSrv);
this.metricsQueryRunner = new CloudWatchMetricsQueryRunner(instanceSettings, templateSrv); this.metricsQueryRunner = new CloudWatchMetricsQueryRunner(instanceSettings, templateSrv);
this.logsCompletionItemProviderFunc = LogsCompletionItemProviderFunc(this.resources, this.templateSrv);
this.logsQueryRunner = new CloudWatchLogsQueryRunner(instanceSettings, templateSrv); this.logsQueryRunner = new CloudWatchLogsQueryRunner(instanceSettings, templateSrv);
this.annotationQueryRunner = new CloudWatchAnnotationQueryRunner(instanceSettings, templateSrv); this.annotationQueryRunner = new CloudWatchAnnotationQueryRunner(instanceSettings, templateSrv);
this.variables = new CloudWatchVariableSupport(this.resources); this.variables = new CloudWatchVariableSupport(this.resources);
this.annotations = CloudWatchAnnotationSupport; this.annotations = CloudWatchAnnotationSupport;
this.defaultLogGroups = instanceSettings.jsonData.defaultLogGroups; 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) { filterQuery(query: CloudWatchQuery) {

View File

@ -3,6 +3,7 @@ import {
CloudWatchLogsQuery, CloudWatchLogsQuery,
CloudWatchMetricsQuery, CloudWatchMetricsQuery,
LogGroup, LogGroup,
LogsQueryLanguage,
MetricEditorMode, MetricEditorMode,
MetricQueryType, MetricQueryType,
VariableQuery, VariableQuery,
@ -33,7 +34,10 @@ export const DEFAULT_ANNOTATIONS_QUERY: Omit<CloudWatchAnnotationQuery, 'refId'>
statistic: 'Average', 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 = ( export const getDefaultLogsQuery = (
defaultLogGroups?: LogGroup[], 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. // the migration requires async backend calls, so we don't want to do it here as it would block the UI.
logGroupNames: legacyDefaultLogGroups, logGroupNames: legacyDefaultLogGroups,
logGroups: defaultLogGroups ?? [], logGroups: defaultLogGroups ?? [],
queryLanguage: LogsQueryLanguage.CWLI,
}); });
export const DEFAULT_VARIABLE_QUERY: Partial<VariableQuery> = { export const DEFAULT_VARIABLE_QUERY: Partial<VariableQuery> = {

View File

@ -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<ResourceResponse<LogGroupField>> = []
) => {
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} <log group> ${ON} <field>`,
`${LEFT} ${OUTER} ${JOIN} <log group> ${ON} <field>`,
])
);
});
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']));
});
});
});

View File

@ -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<CompletionItem[]> {
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<CompletionItem> = {}) => {
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<CompletionItem> = {}) {
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} <log group> ${ON} <field>`, {
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} <log group> ${ON} <field>`, {
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<string[]> => {
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())];
};
}

View File

@ -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);
}
);
});

View File

@ -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;
}

View File

@ -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 [];
}

View File

@ -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}`,
};

View File

@ -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;

View File

@ -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: '`' },
],
};

View File

@ -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<ResourceResponse<LogGroupField>> = []
) => {
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');
});
});
});

View File

@ -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<CompletionItem[]> {
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<CompletionItem> = {}) {
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<CompletionItem> = {}) {
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<CompletionItem>) => void,
monaco: typeof monacoTypes,
range: monacoTypes.IRange | monacoTypes.languages.CompletionItemRanges,
currentToken?: LinkedToken | null
): Promise<void> {
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<string[]> {
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())];
}
}

View File

@ -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
);
});
});
});

View File

@ -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;
};

View File

@ -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 [];
}

View File

@ -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;

View File

@ -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: '`' },
],
};

View File

@ -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}`,
};

View File

@ -4,6 +4,8 @@ import { AND, ASC, BY, DESC, EQUALS, FROM, GROUP, NOT_EQUALS, ORDER, SCHEMA, SEL
import { SQLTokenTypes } from './types'; import { SQLTokenTypes } from './types';
// about getStatementPosition: public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/completion/statementPosition.ts
export function getStatementPosition(currentToken: LinkedToken | null): StatementPosition { export function getStatementPosition(currentToken: LinkedToken | null): StatementPosition {
const previousNonWhiteSpace = currentToken?.getPreviousNonWhiteSpaceToken(); const previousNonWhiteSpace = currentToken?.getPreviousNonWhiteSpaceToken();
const previousKeyword = currentToken?.getPreviousKeyword(); const previousKeyword = currentToken?.getPreviousKeyword();

View File

@ -16,6 +16,8 @@ import {
import { LogsTokenTypes } from './types'; import { LogsTokenTypes } from './types';
// about getStatementPosition: public/app/plugins/datasource/cloudwatch/language/cloudwatch-ppl/completion/statementPosition.ts
export const getStatementPosition = (currentToken: LinkedToken | null): StatementPosition => { export const getStatementPosition = (currentToken: LinkedToken | null): StatementPosition => {
const previousNonWhiteSpace = currentToken?.getPreviousNonWhiteSpaceToken(); const previousNonWhiteSpace = currentToken?.getPreviousNonWhiteSpaceToken();
const nextNonWhiteSpace = currentToken?.getNextNonWhiteSpaceToken(); const nextNonWhiteSpace = currentToken?.getNextNonWhiteSpaceToken();

View File

@ -2,6 +2,7 @@ import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
import type { Monaco, monacoTypes } from '@grafana/ui'; import type { Monaco, monacoTypes } from '@grafana/ui';
import { ResourcesAPI } from '../../resources/ResourcesAPI'; import { ResourcesAPI } from '../../resources/ResourcesAPI';
import { CLOUDWATCH_PPL_LANGUAGE_DEFINITION_ID } from '../cloudwatch-ppl/language';
import { LinkedToken } from './LinkedToken'; import { LinkedToken } from './LinkedToken';
import { linkedTokenBuilder } from './linkedTokenBuilder'; import { linkedTokenBuilder } from './linkedTokenBuilder';
@ -69,8 +70,11 @@ export class CompletionItemProvider implements Completeable {
// called by registerLanguage and passed to monaco with registerCompletionItemProvider // 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 // returns an object that implements https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.CompletionItemProvider.html
getCompletionProvider(monaco: Monaco, languageDefinition: LanguageDefinition) { 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 { 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) => { provideCompletionItems: async (model: monacoTypes.editor.ITextModel, position: monacoTypes.IPosition) => {
const currentToken = linkedTokenBuilder(monaco, languageDefinition, model, position, this.tokenTypes); const currentToken = linkedTokenBuilder(monaco, languageDefinition, model, position, this.tokenTypes);
const statementPosition = this.getStatementPosition(currentToken); const statementPosition = this.getStatementPosition(currentToken);

View File

@ -25,21 +25,45 @@ export enum StatementPosition {
// sql // sql
SelectKeyword, SelectKeyword,
AfterSelectKeyword, AfterSelectKeyword,
SelectExpression,
AfterSelectExpression,
AfterSelectFuncFirstArgument, AfterSelectFuncFirstArgument,
AfterFromKeyword, PredefinedFunctionArgument,
SchemaFuncFirstArgument,
SchemaFuncExtraArgument,
FromKeyword, FromKeyword,
AfterFrom, AfterFrom,
AfterFromKeyword,
AfterFromArguments,
SchemaFuncFirstArgument,
SchemaFuncExtraArgument,
WhereKey, WhereKey,
WhereComparisonOperator, WhereComparisonOperator,
WhereValue, WhereValue,
AfterWhereValue, AfterWhereValue,
HavingKey,
HavingComparisonOperator,
HavingValue,
AfterHavingValue,
CaseKey,
CaseComparisonOperator,
CaseValue,
AfterCaseValue,
WhenKey,
WhenComparisonOperator,
WhenValue,
AfterWhenValue,
ThenExpression,
AfterThenExpression,
AfterElseKeyword,
OnKey,
OnComparisonOperator,
OnValue,
AfterOnValue,
AfterGroupByKeywords, AfterGroupByKeywords,
AfterGroupBy, AfterGroupBy,
AfterOrderByKeywords, AfterOrderByKeywords,
AfterOrderByFunction, AfterOrderByFunction,
AfterOrderByDirection, AfterOrderByDirection,
Subquery,
// metric math // metric math
PredefinedFunction, PredefinedFunction,
SearchFuncSecondArg, SearchFuncSecondArg,
@ -90,13 +114,36 @@ export enum StatementPosition {
BooleanOperatorArg, BooleanOperatorArg,
ComparisonOperator, ComparisonOperator,
ComparisonOperatorArg, ComparisonOperatorArg,
//PPL
BeforeLogicalExpression,
AfterArithmeticOperator,
AfterINKeyword,
SortField,
AfterHeadCommand,
AfterFieldsCommand,
FieldList,
AfterDedupFieldNames,
AfterStatsCommand,
StatsFunctionArgument,
AfterStatsBy,
AfterBooleanArgument,
EvalClause,
Expression,
SortFieldExpression,
} }
export enum SuggestionKind { export enum SuggestionKind {
SelectKeyword, SelectKeyword,
AfterSelectKeyword,
AfterSelectExpression,
FunctionsWithArguments, FunctionsWithArguments,
Metrics, Metrics,
FromKeyword, FromKeyword,
AfterFromKeyword,
AfterFromArguments,
JoinKeywords,
HavingKeywords,
SchemaKeyword, SchemaKeyword,
Namespaces, Namespaces,
LabelKeys, LabelKeys,
@ -109,6 +156,10 @@ export enum SuggestionKind {
ComparisonOperators, ComparisonOperators,
LabelValues, LabelValues,
LogicalOperators, LogicalOperators,
CaseKeyword,
WhenKeyword,
ThenKeyword,
AfterThenExpression,
// metricmath, // metricmath,
KeywordArguments, KeywordArguments,
@ -120,6 +171,20 @@ export enum SuggestionKind {
Command, Command,
Function, Function,
InKeyword, InKeyword,
// PPL
BooleanFunction,
LogicalExpression,
ValueExpression,
FieldOperators,
Field,
BooleanLiteral,
DedupParameter,
StatsParameter,
BooleanArgument,
StatsFunctions,
SpanClause,
SortFunctions,
} }
export enum CompletionItemPriority { export enum CompletionItemPriority {

View File

@ -31,6 +31,7 @@ import {
rangeUtil, rangeUtil,
} from '@grafana/data'; } from '@grafana/data';
import { TemplateSrv } from '@grafana/runtime'; import { TemplateSrv } from '@grafana/runtime';
import { type CustomFormatterVariable } from '@grafana/scenes';
import { import {
CloudWatchJsonData, CloudWatchJsonData,
@ -40,6 +41,7 @@ import {
CloudWatchQuery, CloudWatchQuery,
GetLogEventsRequest, GetLogEventsRequest,
LogAction, LogAction,
LogsQueryLanguage,
QueryParam, QueryParam,
StartQueryRequest, StartQueryRequest,
} from '../types'; } from '../types';
@ -109,12 +111,40 @@ export class CloudWatchLogsQueryRunner extends CloudWatchRequest {
const logGroups = uniq(interpolatedLogGroupArns).map((arn) => ({ arn, name: arn })); const logGroups = uniq(interpolatedLogGroupArns).map((arn) => ({ arn, name: arn }));
const logGroupNames = uniq(interpolatedLogGroupNames); const logGroupNames = uniq(interpolatedLogGroupNames);
const logsSQLCustomerFormatter = (value: unknown, model: Partial<CustomFormatterVariable>) => {
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 { return {
refId: target.refId, refId: target.refId,
region: this.templateSrv.replace(this.getActualRegion(target.region)), region: this.templateSrv.replace(this.getActualRegion(target.region)),
queryString: this.templateSrv.replace(target.expression || '', options.scopedVars), queryString,
logGroups, logGroups,
logGroupNames, logGroupNames,
queryLanguage: target.queryLanguage,
}; };
}); });
@ -406,7 +436,9 @@ export class CloudWatchLogsQueryRunner extends CloudWatchRequest {
const hasMissingLogGroups = !query.logGroups?.length; const hasMissingLogGroups = !query.logGroups?.length;
const hasMissingQueryString = !query.expression?.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; return false;
} }