Datasource/CloudWatch: Better handling of stats grouping (#24789)

* Datasource/CloudWatch: Better handling of stats grouping
This commit is contained in:
kay delaney 2020-05-21 15:18:09 +01:00 committed by GitHub
parent 44ca05272a
commit e1f4287f70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 10840 additions and 112 deletions

View File

@ -208,6 +208,7 @@
"@grafana/slate-react": "0.22.9-grafana",
"@reduxjs/toolkit": "1.3.4",
"@torkelo/react-select": "3.0.8",
"@types/antlr4": "^4.7.1",
"@types/braintree__sanitize-url": "4.0.0",
"@types/common-tags": "^1.8.0",
"@types/jsurl": "^1.2.28",
@ -221,6 +222,7 @@
"angular-native-dragdrop": "1.2.2",
"angular-route": "1.6.6",
"angular-sanitize": "1.6.6",
"antlr4": "^4.8.0",
"baron": "3.0.3",
"brace": "0.11.1",
"calculate-size": "1.1.1",

View File

@ -4,14 +4,12 @@ import (
"context"
"fmt"
"regexp"
"strconv"
"sync"
"time"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
@ -137,7 +135,7 @@ func (e *CloudWatchExecutor) Query(ctx context.Context, dsInfo *models.DataSourc
*/
queryParams := queryContext.Queries[0].Model
_, fromAlert := queryContext.Headers["FromAlert"]
isLogAlertQuery := fromAlert && queryParams.Get("mode").MustString("") == "Logs"
isLogAlertQuery := fromAlert && queryParams.Get("queryMode").MustString("") == "Logs"
if isLogAlertQuery {
return e.executeLogAlertQuery(ctx, queryContext)
@ -192,11 +190,39 @@ func (e *CloudWatchExecutor) executeLogAlertQuery(ctx context.Context, queryCont
return nil, err
}
dataframe, err := queryResultsToDataframe(getQueryResultsOutput)
dataframe, err := logsResultsToDataframes(getQueryResultsOutput)
if err != nil {
return nil, err
}
statsGroups := queryParams.Get("statsGroups").MustStringArray()
if len(statsGroups) > 0 && len(dataframe.Fields) > 0 {
groupedFrames, err := groupResults(dataframe, statsGroups)
if err != nil {
return nil, err
}
encodedFrames := make([][]byte, 0)
for _, frame := range groupedFrames {
dataframeEnc, err := frame.MarshalArrow()
if err != nil {
return nil, err
}
encodedFrames = append(encodedFrames, dataframeEnc)
}
response := &tsdb.Response{
Results: make(map[string]*tsdb.QueryResult),
}
response.Results["A"] = &tsdb.QueryResult{
RefId: "A",
Dataframes: encodedFrames,
}
return response, nil
}
dataframeEnc, err := dataframe.MarshalArrow()
if err != nil {
return nil, err
@ -213,56 +239,6 @@ func (e *CloudWatchExecutor) executeLogAlertQuery(ctx context.Context, queryCont
return response, nil
}
func queryResultsToDataframe(results *cloudwatchlogs.GetQueryResultsOutput) (*data.Frame, error) {
rowCount := len(results.Results)
fieldValues := make(map[string]interface{})
for i, row := range results.Results {
for _, resultField := range row {
// Strip @ptr field from results as it's not needed
if *resultField.Field == "@ptr" {
continue
}
if _, exists := fieldValues[*resultField.Field]; !exists {
if _, err := time.Parse(cloudWatchTSFormat, *resultField.Value); err == nil {
fieldValues[*resultField.Field] = make([]*time.Time, rowCount)
} else if _, err := strconv.ParseFloat(*resultField.Value, 64); err == nil {
fieldValues[*resultField.Field] = make([]*float64, rowCount)
} else {
continue
}
}
if timeField, ok := fieldValues[*resultField.Field].([]*time.Time); ok {
parsedTime, err := time.Parse(cloudWatchTSFormat, *resultField.Value)
if err != nil {
return nil, err
}
timeField[i] = &parsedTime
} else if numericField, ok := fieldValues[*resultField.Field].([]*float64); ok {
parsedFloat, err := strconv.ParseFloat(*resultField.Value, 64)
if err != nil {
return nil, err
}
numericField[i] = &parsedFloat
}
}
}
newFields := make([]*data.Field, 0)
for fieldName, vals := range fieldValues {
newFields = append(newFields, data.NewField(fieldName, nil, vals))
if fieldName == "@timestamp" {
newFields[len(newFields)-1].SetConfig(&data.FieldConfig{Title: "Time"})
}
}
frame := data.NewFrame("CloudWatchLogsResponse", newFields...)
return frame, nil
}
func isTerminated(queryStatus string) bool {
return queryStatus == "Complete" || queryStatus == "Cancelled" || queryStatus == "Failed" || queryStatus == "Timeout"
}

View File

@ -4,7 +4,6 @@ import (
"context"
"fmt"
"sort"
"strconv"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
@ -33,12 +32,11 @@ func (e *CloudWatchExecutor) executeLogActions(ctx context.Context, queryContext
// the query response is in, there does not seem to be a way to tell
// by the response alone if/how the results should be grouped.
// Because of this, if the frontend sees that a "stats ... by ..." query is being made
// the "groupResults" parameter is sent along with the query to the backend so that we
// the "statsGroups" parameter is sent along with the query to the backend so that we
// can correctly group the CloudWatch logs response.
if query.Model.Get("groupResults").MustBool() && len(dataframe.Fields) > 0 {
groupingFields := findGroupingFields(dataframe.Fields)
groupedFrames, err := groupResults(dataframe, groupingFields)
statsGroups := query.Model.Get("statsGroups").MustStringArray()
if len(statsGroups) > 0 && len(dataframe.Fields) > 0 {
groupedFrames, err := groupResults(dataframe, statsGroups)
if err != nil {
return err
}
@ -81,23 +79,6 @@ func (e *CloudWatchExecutor) executeLogActions(ctx context.Context, queryContext
return response, nil
}
func findGroupingFields(fields []*data.Field) []string {
groupingFields := make([]string, 0)
for _, field := range fields {
if field.Type().Numeric() || field.Type() == data.FieldTypeNullableTime || field.Type() == data.FieldTypeTime {
continue
}
if _, err := strconv.ParseFloat(*field.At(0).(*string), 64); err == nil {
continue
}
groupingFields = append(groupingFields, field.Name)
}
return groupingFields
}
func (e *CloudWatchExecutor) executeLogAction(ctx context.Context, queryContext *tsdb.TsdbQuery, query *tsdb.Query) (*data.Frame, error) {
parameters := query.Model
subType := query.Model.Get("subtype").MustString()

View File

@ -1,6 +1,8 @@
package cloudwatch
import (
"sort"
"strconv"
"time"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
@ -47,6 +49,8 @@ func logsResultsToDataframes(response *cloudwatchlogs.GetQueryResultsOutput) (*d
// Check if field is time field
if _, err := time.Parse(cloudWatchTSFormat, *resultField.Value); err == nil {
fieldValues[*resultField.Field] = make([]*time.Time, rowCount)
} else if _, err := strconv.ParseFloat(*resultField.Value, 64); err == nil {
fieldValues[*resultField.Field] = make([]*float64, rowCount)
} else {
fieldValues[*resultField.Field] = make([]*string, rowCount)
}
@ -59,6 +63,12 @@ func logsResultsToDataframes(response *cloudwatchlogs.GetQueryResultsOutput) (*d
}
timeField[i] = &parsedTime
} else if numericField, ok := fieldValues[*resultField.Field].([]*float64); ok {
parsedFloat, err := strconv.ParseFloat(*resultField.Value, 64)
if err != nil {
return nil, err
}
numericField[i] = &parsedFloat
} else {
fieldValues[*resultField.Field].([]*string)[i] = resultField.Value
}
@ -90,6 +100,8 @@ func logsResultsToDataframes(response *cloudwatchlogs.GetQueryResultsOutput) (*d
},
}
// Results aren't guaranteed to come ordered by time (ascending), so we need to sort
sort.Sort(ByTime(*frame))
return frame, nil
}

View File

@ -0,0 +1,41 @@
package cloudwatch
import (
"time"
"github.com/grafana/grafana-plugin-sdk-go/data"
)
// ByTime implements sort.Interface for data.Frame based on the frame's time field
type ByTime data.Frame
func (a ByTime) Len() int {
if len(a.Fields) > 0 {
return a.Fields[0].Len()
}
return 0
}
func (a ByTime) Swap(i, j int) {
for _, field := range a.Fields {
temp := field.At(i)
field.Set(i, field.At(j))
field.Set(j, temp)
}
}
func (a ByTime) Less(i, j int) bool {
var timeField *data.Field = nil
for _, field := range a.Fields {
if field.Type() == data.FieldTypeNullableTime {
timeField = field
break
}
}
if timeField == nil {
return false
}
return (timeField.At(i).(*time.Time)).Before(*timeField.At(j).(*time.Time))
}

View File

@ -0,0 +1,57 @@
package cloudwatch
import (
"sort"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/stretchr/testify/assert"
)
func TestFrameSort(t *testing.T) {
timeA, _ := time.Parse("2006-01-02 15:04:05.000", "2020-03-02 17:04:05.000")
timeB, _ := time.Parse("2006-01-02 15:04:05.000", "2020-03-02 16:04:05.000")
timeC, _ := time.Parse("2006-01-02 15:04:05.000", "2020-03-02 15:04:05.000")
timeVals := []*time.Time{
&timeA, &timeB, &timeC,
}
timeField := data.NewField("@timestamp", nil, timeVals)
stringField := data.NewField("line", nil, []*string{
aws.String("test message 1"),
aws.String("test message 2"),
aws.String("test message 3"),
})
numberField := data.NewField("nums", nil, []*float64{
aws.Float64(20.0),
aws.Float64(50.0),
aws.Float64(17.0),
})
expectedDataframe := &data.Frame{
Name: "CloudWatchLogsResponse",
Fields: []*data.Field{
timeField,
stringField,
numberField,
},
}
sort.Sort(ByTime(*expectedDataframe))
for i := 1; i < timeField.Len(); i++ {
assert.True(t, timeField.At(i).(*time.Time).After(*(timeField.At(i - 1).(*time.Time))))
}
assert.Equal(t, *stringField.At(0).(*string), "test message 3")
assert.Equal(t, *stringField.At(1).(*string), "test message 2")
assert.Equal(t, *stringField.At(2).(*string), "test message 1")
assert.Equal(t, *numberField.At(0).(*float64), 17.0)
assert.Equal(t, *numberField.At(1).(*float64), 50.0)
assert.Equal(t, *numberField.At(2).(*float64), 20.0)
}

View File

@ -33,6 +33,7 @@ import { dispatch } from 'app/store/store';
import { changeModeAction } from 'app/features/explore/state/actionTypes';
import { appEvents } from 'app/core/core';
import { InputActionMeta } from '@grafana/ui/src/components/Select/types';
import { getStatsGroups } from '../utils/query/getStatsGroups';
export interface CloudWatchLogsQueryFieldProps extends ExploreQueryFieldProps<CloudWatchDatasource, CloudWatchQuery> {
absoluteRange: AbsoluteTimeRange;
@ -193,6 +194,7 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
expression: value,
logGroupNames: selectedLogGroups?.map(logGroupName => logGroupName.value!) ?? [],
region: selectedRegion.value ?? 'default',
statsGroups: getStatsGroups(value),
};
onChange(nextQuery);
}

View File

@ -17,8 +17,6 @@ import {
DataQueryResponse,
LoadingState,
toDataFrame,
guessFieldTypes,
FieldType,
LogRowModel,
} from '@grafana/data';
import { getBackendSrv, toDataQueryResponse } from '@grafana/runtime';
@ -137,9 +135,8 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa
queryId: dataFrame.fields[0].values.get(0),
region: dataFrame.meta?.custom?.['Region'] ?? 'default',
refId: dataFrame.refId!,
groupResults: this.languageProvider.isStatsQuery(
options.targets.find(target => target.refId === dataFrame.refId)!.expression
),
statsGroups: (options.targets.find(target => target.refId === dataFrame.refId)! as CloudWatchLogsQuery)
.statsGroups,
}))
)
),
@ -205,7 +202,7 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa
}
logsQuery(
queryParams: Array<{ queryId: string; refId: string; limit?: number; region: string; groupResults?: boolean }>
queryParams: Array<{ queryId: string; refId: string; limit?: number; region: string; statsGroups?: string[] }>
): Observable<DataQueryResponse> {
this.logQueries = {};
queryParams.forEach(param => {
@ -257,19 +254,15 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa
}
});
}),
map(dataFrames => {
const correctedFrames = dataFrames.map(frame => correctFrameTypes(frame));
return {
data: correctedFrames,
key: 'test-key',
state: correctedFrames.every(
dataFrame => dataFrame.meta?.custom?.['Status'] === CloudWatchLogsQueryStatus.Complete
)
? LoadingState.Done
: LoadingState.Loading,
};
})
map(dataFrames => ({
data: dataFrames,
key: 'test-key',
state: dataFrames.every(
dataFrame => dataFrame.meta?.custom?.['Status'] === CloudWatchLogsQueryStatus.Complete
)
? LoadingState.Done
: LoadingState.Loading,
}))
),
() => this.stopQueries()
);
@ -925,22 +918,6 @@ function withTeardown<T = any>(observable: Observable<T>, onUnsubscribe: () => v
});
}
function correctFrameTypes(frame: DataFrame): DataFrame {
frame.fields.forEach(field => {
if (field.type === FieldType.string) {
field.type = FieldType.other;
}
});
const correctedFrame = guessFieldTypes(frame);
// const timeField = correctedFrame.fields.find(field => field.name === '@timestamp');
// if (timeField) {
// timeField.type = FieldType.time;
// }
return correctedFrame;
}
function parseLogGroupName(logIdentifier: string): string {
const colonIndex = logIdentifier.lastIndexOf(':');
return logIdentifier.substr(colonIndex + 1);

View File

@ -43,6 +43,7 @@ export interface CloudWatchLogsQuery extends DataQuery {
namespace: string;
expression: string;
logGroupNames: string[];
statsGroups?: string[];
}
export type CloudWatchQuery = CloudWatchMetricsQuery | CloudWatchLogsQuery;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,566 @@
// Generated from ScrollQLParser.g4 by ANTLR 4.8
// jshint ignore: start
var antlr4 = require('antlr4/index');
// This class defines a complete listener for a parse tree produced by ScrollQLParser.
function ScrollQLParserListener() {
antlr4.tree.ParseTreeListener.call(this);
return this;
}
ScrollQLParserListener.prototype = Object.create(antlr4.tree.ParseTreeListener.prototype);
ScrollQLParserListener.prototype.constructor = ScrollQLParserListener;
// Enter a parse tree produced by ScrollQLParser#query.
ScrollQLParserListener.prototype.enterQuery = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#query.
ScrollQLParserListener.prototype.exitQuery = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#logQuery.
ScrollQLParserListener.prototype.enterLogQuery = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#logQuery.
ScrollQLParserListener.prototype.exitLogQuery = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#logAesthetic.
ScrollQLParserListener.prototype.enterLogAesthetic = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#logAesthetic.
ScrollQLParserListener.prototype.exitLogAesthetic = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#logSourceStage.
ScrollQLParserListener.prototype.enterLogSourceStage = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#logSourceStage.
ScrollQLParserListener.prototype.exitLogSourceStage = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#logStatsStage.
ScrollQLParserListener.prototype.enterLogStatsStage = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#logStatsStage.
ScrollQLParserListener.prototype.exitLogStatsStage = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#logOp.
ScrollQLParserListener.prototype.enterLogOp = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#logOp.
ScrollQLParserListener.prototype.exitLogOp = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#logAestheticOp.
ScrollQLParserListener.prototype.enterLogAestheticOp = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#logAestheticOp.
ScrollQLParserListener.prototype.exitLogAestheticOp = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#logSource.
ScrollQLParserListener.prototype.enterLogSource = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#logSource.
ScrollQLParserListener.prototype.exitLogSource = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#timeExpr.
ScrollQLParserListener.prototype.enterTimeExpr = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#timeExpr.
ScrollQLParserListener.prototype.exitTimeExpr = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#nowTimeExpr.
ScrollQLParserListener.prototype.enterNowTimeExpr = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#nowTimeExpr.
ScrollQLParserListener.prototype.exitNowTimeExpr = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#negRelativeTimeExpr.
ScrollQLParserListener.prototype.enterNegRelativeTimeExpr = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#negRelativeTimeExpr.
ScrollQLParserListener.prototype.exitNegRelativeTimeExpr = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#posRelativeTimeExpr.
ScrollQLParserListener.prototype.enterPosRelativeTimeExpr = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#posRelativeTimeExpr.
ScrollQLParserListener.prototype.exitPosRelativeTimeExpr = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#iso8601TimeExpr.
ScrollQLParserListener.prototype.enterIso8601TimeExpr = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#iso8601TimeExpr.
ScrollQLParserListener.prototype.exitIso8601TimeExpr = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#epochTimeExpr.
ScrollQLParserListener.prototype.enterEpochTimeExpr = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#epochTimeExpr.
ScrollQLParserListener.prototype.exitEpochTimeExpr = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#bareSpaceDelimited.
ScrollQLParserListener.prototype.enterBareSpaceDelimited = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#bareSpaceDelimited.
ScrollQLParserListener.prototype.exitBareSpaceDelimited = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#logStats.
ScrollQLParserListener.prototype.enterLogStats = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#logStats.
ScrollQLParserListener.prototype.exitLogStats = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#statsExpr.
ScrollQLParserListener.prototype.enterStatsExpr = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#statsExpr.
ScrollQLParserListener.prototype.exitStatsExpr = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#statsGroupFieldId.
ScrollQLParserListener.prototype.enterStatsGroupFieldId = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#statsGroupFieldId.
ScrollQLParserListener.prototype.exitStatsGroupFieldId = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#statsGroupFieldProjection.
ScrollQLParserListener.prototype.enterStatsGroupFieldProjection = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#statsGroupFieldProjection.
ScrollQLParserListener.prototype.exitStatsGroupFieldProjection = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#logOpFieldsFields.
ScrollQLParserListener.prototype.enterLogOpFieldsFields = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#logOpFieldsFields.
ScrollQLParserListener.prototype.exitLogOpFieldsFields = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#logOpFieldsDisplay.
ScrollQLParserListener.prototype.enterLogOpFieldsDisplay = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#logOpFieldsDisplay.
ScrollQLParserListener.prototype.exitLogOpFieldsDisplay = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#fieldSpec.
ScrollQLParserListener.prototype.enterFieldSpec = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#fieldSpec.
ScrollQLParserListener.prototype.exitFieldSpec = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#logOpParse.
ScrollQLParserListener.prototype.enterLogOpParse = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#logOpParse.
ScrollQLParserListener.prototype.exitLogOpParse = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#logOpSearch.
ScrollQLParserListener.prototype.enterLogOpSearch = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#logOpSearch.
ScrollQLParserListener.prototype.exitLogOpSearch = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#implicitLogOpSearch.
ScrollQLParserListener.prototype.enterImplicitLogOpSearch = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#implicitLogOpSearch.
ScrollQLParserListener.prototype.exitImplicitLogOpSearch = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#searchExprTerm.
ScrollQLParserListener.prototype.enterSearchExprTerm = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#searchExprTerm.
ScrollQLParserListener.prototype.exitSearchExprTerm = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#searchExprNot.
ScrollQLParserListener.prototype.enterSearchExprNot = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#searchExprNot.
ScrollQLParserListener.prototype.exitSearchExprNot = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#searchExprAnd.
ScrollQLParserListener.prototype.enterSearchExprAnd = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#searchExprAnd.
ScrollQLParserListener.prototype.exitSearchExprAnd = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#searchExprNested.
ScrollQLParserListener.prototype.enterSearchExprNested = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#searchExprNested.
ScrollQLParserListener.prototype.exitSearchExprNested = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#searchExprOr.
ScrollQLParserListener.prototype.enterSearchExprOr = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#searchExprOr.
ScrollQLParserListener.prototype.exitSearchExprOr = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#searchTerm.
ScrollQLParserListener.prototype.enterSearchTerm = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#searchTerm.
ScrollQLParserListener.prototype.exitSearchTerm = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#logOpFilter.
ScrollQLParserListener.prototype.enterLogOpFilter = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#logOpFilter.
ScrollQLParserListener.prototype.exitLogOpFilter = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#logOpSort.
ScrollQLParserListener.prototype.enterLogOpSort = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#logOpSort.
ScrollQLParserListener.prototype.exitLogOpSort = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#sortExprDesc.
ScrollQLParserListener.prototype.enterSortExprDesc = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#sortExprDesc.
ScrollQLParserListener.prototype.exitSortExprDesc = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#sortExprAsc.
ScrollQLParserListener.prototype.enterSortExprAsc = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#sortExprAsc.
ScrollQLParserListener.prototype.exitSortExprAsc = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#logOpLimitHead.
ScrollQLParserListener.prototype.enterLogOpLimitHead = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#logOpLimitHead.
ScrollQLParserListener.prototype.exitLogOpLimitHead = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#logOpLimitTail.
ScrollQLParserListener.prototype.enterLogOpLimitTail = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#logOpLimitTail.
ScrollQLParserListener.prototype.exitLogOpLimitTail = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#expressionRoot.
ScrollQLParserListener.prototype.enterExpressionRoot = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#expressionRoot.
ScrollQLParserListener.prototype.exitExpressionRoot = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#expressionAddSub.
ScrollQLParserListener.prototype.enterExpressionAddSub = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#expressionAddSub.
ScrollQLParserListener.prototype.exitExpressionAddSub = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#expressionEq.
ScrollQLParserListener.prototype.enterExpressionEq = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#expressionEq.
ScrollQLParserListener.prototype.exitExpressionEq = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#expressionComp.
ScrollQLParserListener.prototype.enterExpressionComp = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#expressionComp.
ScrollQLParserListener.prototype.exitExpressionComp = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#expressionExpo.
ScrollQLParserListener.prototype.enterExpressionExpo = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#expressionExpo.
ScrollQLParserListener.prototype.exitExpressionExpo = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#expressionLike.
ScrollQLParserListener.prototype.enterExpressionLike = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#expressionLike.
ScrollQLParserListener.prototype.exitExpressionLike = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#expressionTerm.
ScrollQLParserListener.prototype.enterExpressionTerm = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#expressionTerm.
ScrollQLParserListener.prototype.exitExpressionTerm = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#expressionNeg.
ScrollQLParserListener.prototype.enterExpressionNeg = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#expressionNeg.
ScrollQLParserListener.prototype.exitExpressionNeg = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#expressionNot.
ScrollQLParserListener.prototype.enterExpressionNot = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#expressionNot.
ScrollQLParserListener.prototype.exitExpressionNot = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#expressionPos.
ScrollQLParserListener.prototype.enterExpressionPos = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#expressionPos.
ScrollQLParserListener.prototype.exitExpressionPos = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#expressionMulDivMod.
ScrollQLParserListener.prototype.enterExpressionMulDivMod = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#expressionMulDivMod.
ScrollQLParserListener.prototype.exitExpressionMulDivMod = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#expressionAnd.
ScrollQLParserListener.prototype.enterExpressionAnd = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#expressionAnd.
ScrollQLParserListener.prototype.exitExpressionAnd = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#expressionNested.
ScrollQLParserListener.prototype.enterExpressionNested = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#expressionNested.
ScrollQLParserListener.prototype.exitExpressionNested = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#expressionOr.
ScrollQLParserListener.prototype.enterExpressionOr = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#expressionOr.
ScrollQLParserListener.prototype.exitExpressionOr = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#expressionIn.
ScrollQLParserListener.prototype.enterExpressionIn = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#expressionIn.
ScrollQLParserListener.prototype.exitExpressionIn = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#termId.
ScrollQLParserListener.prototype.enterTermId = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#termId.
ScrollQLParserListener.prototype.exitTermId = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#termNum.
ScrollQLParserListener.prototype.enterTermNum = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#termNum.
ScrollQLParserListener.prototype.exitTermNum = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#termStr.
ScrollQLParserListener.prototype.enterTermStr = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#termStr.
ScrollQLParserListener.prototype.exitTermStr = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#termFn.
ScrollQLParserListener.prototype.enterTermFn = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#termFn.
ScrollQLParserListener.prototype.exitTermFn = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#likeTerm.
ScrollQLParserListener.prototype.enterLikeTerm = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#likeTerm.
ScrollQLParserListener.prototype.exitLikeTerm = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#functionWithArgs.
ScrollQLParserListener.prototype.enterFunctionWithArgs = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#functionWithArgs.
ScrollQLParserListener.prototype.exitFunctionWithArgs = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#functionWithNoArgs.
ScrollQLParserListener.prototype.enterFunctionWithNoArgs = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#functionWithNoArgs.
ScrollQLParserListener.prototype.exitFunctionWithNoArgs = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#functionArgTimePeriod.
ScrollQLParserListener.prototype.enterFunctionArgTimePeriod = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#functionArgTimePeriod.
ScrollQLParserListener.prototype.exitFunctionArgTimePeriod = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#functionArgFieldClause.
ScrollQLParserListener.prototype.enterFunctionArgFieldClause = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#functionArgFieldClause.
ScrollQLParserListener.prototype.exitFunctionArgFieldClause = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#array.
ScrollQLParserListener.prototype.enterArray = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#array.
ScrollQLParserListener.prototype.exitArray = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#arrayElem.
ScrollQLParserListener.prototype.enterArrayElem = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#arrayElem.
ScrollQLParserListener.prototype.exitArrayElem = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#number.
ScrollQLParserListener.prototype.enterNumber = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#number.
ScrollQLParserListener.prototype.exitNumber = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#string.
ScrollQLParserListener.prototype.enterString = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#string.
ScrollQLParserListener.prototype.exitString = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#stringOrBareString.
ScrollQLParserListener.prototype.enterStringOrBareString = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#stringOrBareString.
ScrollQLParserListener.prototype.exitStringOrBareString = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#regex.
ScrollQLParserListener.prototype.enterRegex = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#regex.
ScrollQLParserListener.prototype.exitRegex = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#regexString.
ScrollQLParserListener.prototype.enterRegexString = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#regexString.
ScrollQLParserListener.prototype.exitRegexString = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#logId.
ScrollQLParserListener.prototype.enterLogId = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#logId.
ScrollQLParserListener.prototype.exitLogId = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#fieldId.
ScrollQLParserListener.prototype.enterFieldId = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#fieldId.
ScrollQLParserListener.prototype.exitFieldId = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#aliasId.
ScrollQLParserListener.prototype.enterAliasId = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#aliasId.
ScrollQLParserListener.prototype.exitAliasId = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#userId.
ScrollQLParserListener.prototype.enterUserId = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#userId.
ScrollQLParserListener.prototype.exitUserId = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#unquotedUserId.
ScrollQLParserListener.prototype.enterUnquotedUserId = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#unquotedUserId.
ScrollQLParserListener.prototype.exitUnquotedUserId = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#unquotedUserAtId.
ScrollQLParserListener.prototype.enterUnquotedUserAtId = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#unquotedUserAtId.
ScrollQLParserListener.prototype.exitUnquotedUserAtId = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#unquotedUserBareId.
ScrollQLParserListener.prototype.enterUnquotedUserBareId = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#unquotedUserBareId.
ScrollQLParserListener.prototype.exitUnquotedUserBareId = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#quotedUserId.
ScrollQLParserListener.prototype.enterQuotedUserId = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#quotedUserId.
ScrollQLParserListener.prototype.exitQuotedUserId = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#systemId.
ScrollQLParserListener.prototype.enterSystemId = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#systemId.
ScrollQLParserListener.prototype.exitSystemId = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#unquotedSystemId.
ScrollQLParserListener.prototype.enterUnquotedSystemId = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#unquotedSystemId.
ScrollQLParserListener.prototype.exitUnquotedSystemId = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#quotedSystemId.
ScrollQLParserListener.prototype.enterQuotedSystemId = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#quotedSystemId.
ScrollQLParserListener.prototype.exitQuotedSystemId = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#resultId.
ScrollQLParserListener.prototype.enterResultId = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#resultId.
ScrollQLParserListener.prototype.exitResultId = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#functionId.
ScrollQLParserListener.prototype.enterFunctionId = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#functionId.
ScrollQLParserListener.prototype.exitFunctionId = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#rawId.
ScrollQLParserListener.prototype.enterRawId = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#rawId.
ScrollQLParserListener.prototype.exitRawId = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#keywords.
ScrollQLParserListener.prototype.enterKeywords = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#keywords.
ScrollQLParserListener.prototype.exitKeywords = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#timeUnitMilliSeconds.
ScrollQLParserListener.prototype.enterTimeUnitMilliSeconds = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#timeUnitMilliSeconds.
ScrollQLParserListener.prototype.exitTimeUnitMilliSeconds = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#timeUnitSeconds.
ScrollQLParserListener.prototype.enterTimeUnitSeconds = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#timeUnitSeconds.
ScrollQLParserListener.prototype.exitTimeUnitSeconds = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#timeUnitMinutes.
ScrollQLParserListener.prototype.enterTimeUnitMinutes = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#timeUnitMinutes.
ScrollQLParserListener.prototype.exitTimeUnitMinutes = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#timeUnitHours.
ScrollQLParserListener.prototype.enterTimeUnitHours = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#timeUnitHours.
ScrollQLParserListener.prototype.exitTimeUnitHours = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#timeUnitDays.
ScrollQLParserListener.prototype.enterTimeUnitDays = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#timeUnitDays.
ScrollQLParserListener.prototype.exitTimeUnitDays = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#timeUnitWeeks.
ScrollQLParserListener.prototype.enterTimeUnitWeeks = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#timeUnitWeeks.
ScrollQLParserListener.prototype.exitTimeUnitWeeks = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#timeUnitMonths.
ScrollQLParserListener.prototype.enterTimeUnitMonths = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#timeUnitMonths.
ScrollQLParserListener.prototype.exitTimeUnitMonths = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#timeUnitQuarters.
ScrollQLParserListener.prototype.enterTimeUnitQuarters = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#timeUnitQuarters.
ScrollQLParserListener.prototype.exitTimeUnitQuarters = function(ctx) {};
// Enter a parse tree produced by ScrollQLParser#timeUnitYears.
ScrollQLParserListener.prototype.enterTimeUnitYears = function(ctx) {};
// Exit a parse tree produced by ScrollQLParser#timeUnitYears.
ScrollQLParserListener.prototype.exitTimeUnitYears = function(ctx) {};
exports.ScrollQLParserListener = ScrollQLParserListener;

View File

@ -0,0 +1,69 @@
import { getStatsGroups } from './getStatsGroups';
describe('GroupListener', () => {
it('should correctly parse groups in stats query', () => {
const testQueries = [
{
query:
'filter @message like /Exception/ | stats count(*) as exceptionCount by bin(1h) | sort exceptionCount desc',
expected: ['bin(1h)'],
},
{
query: `filter @type = "REPORT"
| stats max(@memorySize / 1024 / 1024) as provisonedMemoryMB,
min(@maxMemoryUsed / 1024 / 1024) as smallestMemoryRequestMB,
avg(@maxMemoryUsed / 1024 / 1024) as avgMemoryUsedMB,
max(@maxMemoryUsed / 1024 / 1024) as maxMemoryUsedMB,
provisonedMemoryMB - maxMemoryUsedMB as overProvisionedMB`,
expected: [],
},
{
query: `stats count(@message) by bin(1h), @log, @logStream as fieldAlias`,
expected: ['bin(1h)', '@log', 'fieldAlias'],
},
{
query: `stats sum(packets) as packetsTransferred by srcAddr, dstAddr
| sort packetsTransferred desc
| limit 15`,
expected: ['srcAddr', 'dstAddr'],
},
{
query: `filter isIpv4InSubnet(srcAddr, "192.0.2.0/24")
| stats sum(bytes) as bytesTransferred by dstAddr
| sort bytesTransferred desc
| limit 15`,
expected: ['dstAddr'],
},
{
query: `filter logStatus="SKIPDATA"
| stats count(*) by bin(1h) as t
| sort t
`,
expected: ['t'],
},
{
query: `stats count(*) by queryType, bin(1h)`,
expected: ['queryType', 'bin(1h)'],
},
{
query: `parse @message "user=*, method:*, latency := *" as @user,
@method, @latency | stats avg(@latency) by @method,
@user`,
expected: ['@method', '@user'],
},
{
query: 'fields @timestamp, @message | sort @timestamp desc | limit 25',
expected: [],
},
{
query: `stats count(*)`,
expected: [],
},
];
for (const { query, expected } of testQueries) {
expect(getStatsGroups(query)).toStrictEqual(expected);
}
});
});

View File

@ -0,0 +1,41 @@
const antlr4 = require('antlr4');
const ScrollQLLexer = require('./ScrollQLLexer').ScrollQLLexer;
const ScrollQLParser = require('./ScrollQLParser').ScrollQLParser;
const ScrollQLParserListener = require('./ScrollQLParserListener').ScrollQLParserListener;
class GroupListener extends ScrollQLParserListener {
groupNames: string[] = [];
enterLogStats(ctx: any) {
this.groupNames = [];
if (ctx.groups && ctx.groups.length > 0) {
const groups = ctx.groups;
groups.forEach((group: any) => {
// This code is for handling the case where a field specifier is aliased, with the alias available via
// the proj property. Otherwise we can just take the group text as it is.
const proj = group.fieldSpec?.().proj;
if (proj) {
this.groupNames.push(proj.getText());
} else {
this.groupNames.push(group.getText());
}
});
}
}
}
export function getStatsGroups(text: string): string[] {
// Dummy prefix needed here for parser to function correctly
const dummyPrefix = 'source test start=0 end=1|';
const queryText = dummyPrefix + text;
const chars = new antlr4.InputStream(queryText);
const lexer = new ScrollQLLexer(chars);
const tokens = new antlr4.CommonTokenStream(lexer);
const parser = new ScrollQLParser(tokens);
parser.buildParseTrees = true;
const tree = parser.query();
const groupListener = new GroupListener();
antlr4.tree.ParseTreeWalker.DEFAULT.walk(groupListener, tree);
return groupListener.groupNames;
}

View File

@ -5056,6 +5056,11 @@
resolved "https://registry.yarnpkg.com/@types/angular/-/angular-1.6.56.tgz#20124077bd44061e018c7283c0bb83f4b00322dd"
integrity sha512-HxtqilvklZ7i6XOaiP7uIJIrFXEVEhfbSY45nfv2DeBRngncI58Y4ZOUMiUkcT8sqgLL1ablmbfylChUg7A3GA==
"@types/antlr4@^4.7.1":
version "4.7.1"
resolved "https://registry.yarnpkg.com/@types/antlr4/-/antlr4-4.7.1.tgz#09a8f985e29149c73e92b161d08691a1fd8425ef"
integrity sha512-mjQv+WtdJnwI5qhNh5yJkZ9rVFdRClUyaO5KebaLSJFHT6uSyDLAK9jUke4zLKZXk6vQQ/QJN2j7QV2q7l5Slw==
"@types/anymatch@*":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
@ -7050,6 +7055,11 @@ ansi-to-html@^0.6.11:
dependencies:
entities "^1.1.2"
antlr4@^4.8.0:
version "4.8.0"
resolved "https://registry.yarnpkg.com/antlr4/-/antlr4-4.8.0.tgz#f938ec171be7fc2855cd3a533e87647185b32b6a"
integrity sha512-en/MxQ4OkPgGJQ3wD/muzj1uDnFSzdFIhc2+c6bHZokWkuBb6RRvFjpWhPxWLbgQvaEzldJZ0GSQpfSAaE3hqg==
any-observable@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b"