Skip to content

Commit 2de7697

Browse files
committed
Support rules with a script node interior type.
1 parent 186c4bb commit 2de7697

8 files changed

Lines changed: 446 additions & 111 deletions

File tree

pkg/ast/ast.go

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,9 @@ func (b *builderT) descendTree(fn func() error) error {
106106
}
107107

108108
func Build(data []byte) (*AstT, error) {
109-
var (
110-
parseTree *parser.TreeT
111-
err error
112-
)
113109

114-
if parseTree, err = parser.Parse(data); err != nil {
110+
parseTree, err := parser.Parse(data)
111+
if err != nil {
115112
log.Error().Any("err", err).Msg("Parser failed")
116113
return nil, err
117114
}
@@ -157,33 +154,17 @@ func BuildTree(tree *parser.TreeT) (*AstT, error) {
157154
func (b *builderT) buildTree(parserNode *parser.NodeT, parentMachineAddress *AstNodeAddressT, termIdx *uint32) (*AstNodeT, error) {
158155

159156
var (
160-
machineMatchNode *AstNodeT
161-
matchNode *AstNodeT
162-
children = make([]*AstNodeT, 0)
163-
machineAddress = b.newAstNodeAddress(parserNode.Metadata.RuleHash, parserNode.Metadata.Type.String(), termIdx)
164-
err error
157+
machineAddress = b.newAstNodeAddress(parserNode.Metadata.RuleHash, parserNode.Metadata.Type.String(), termIdx)
165158
)
166159

167-
// Build children (either matcher children or nested machines)
168-
if parserNode.IsMatcherNode() {
169-
if matchNode, err = b.buildMatcherChildren(parserNode, machineAddress, termIdx); err != nil {
170-
return nil, err
171-
}
172-
children = append(children, matchNode)
173-
} else if parserNode.IsPromNode() {
174-
if matchNode, err = b.buildPromQLNode(parserNode, machineAddress, termIdx); err != nil {
175-
return nil, err
176-
}
177-
children = append(children, matchNode)
178-
179-
} else {
180-
if children, err = b.buildMachineChildren(parserNode, machineAddress); err != nil {
181-
return nil, err
182-
}
160+
children, err := b.buildChildrenNodes(parserNode, machineAddress, termIdx)
161+
if err != nil {
162+
return nil, err
183163
}
184164

185165
// Build state machine after recursively building children
186-
if machineMatchNode, err = b.buildStateMachine(parserNode, parentMachineAddress, machineAddress, children); err != nil {
166+
machineMatchNode, err := b.buildStateMachine(parserNode, parentMachineAddress, machineAddress, children)
167+
if err != nil {
187168
return nil, err
188169
}
189170

@@ -192,6 +173,35 @@ func (b *builderT) buildTree(parserNode *parser.NodeT, parentMachineAddress *Ast
192173
return machineMatchNode, nil
193174
}
194175

176+
func (b *builderT) buildChildrenNodes(parserNode *parser.NodeT, machineAddress *AstNodeAddressT, termIdx *uint32) (children []*AstNodeT, err error) {
177+
178+
leaf, err := b.buildLeafChild(parserNode, machineAddress, termIdx)
179+
180+
switch {
181+
case err != nil:
182+
return nil, err
183+
case leaf != nil:
184+
return []*AstNodeT{leaf}, nil
185+
case parserNode.IsScriptNode():
186+
children, err = b.buildScriptChildren(parserNode, machineAddress)
187+
default:
188+
children, err = b.buildMachineChildren(parserNode, machineAddress)
189+
}
190+
191+
return children, err
192+
}
193+
194+
func (b *builderT) buildLeafChild(parserNode *parser.NodeT, machineAddress *AstNodeAddressT, termIdx *uint32) (leaf *AstNodeT, err error) {
195+
196+
switch {
197+
case parserNode.IsMatcherNode():
198+
leaf, err = b.buildMatcherChild(parserNode, machineAddress, termIdx)
199+
case parserNode.IsPromNode():
200+
leaf, err = b.buildPromQLNode(parserNode, machineAddress, termIdx)
201+
}
202+
return
203+
}
204+
195205
func (b *builderT) newAstNodeAddress(ruleHash, name string, termIdx *uint32) *AstNodeAddressT {
196206
var address = &AstNodeAddressT{
197207
Version: "v" + strconv.FormatInt(int64(AstVersion), 10),
@@ -220,7 +230,7 @@ func newAstNode(parserNode *parser.NodeT, typ schema.NodeTypeT, scope string, pa
220230
}
221231
}
222232

223-
func (b *builderT) buildMatcherChildren(parserNode *parser.NodeT, machineAddress *AstNodeAddressT, termIdx *uint32) (*AstNodeT, error) {
233+
func (b *builderT) buildMatcherChild(parserNode *parser.NodeT, machineAddress *AstNodeAddressT, termIdx *uint32) (*AstNodeT, error) {
224234

225235
var (
226236
matchNode *AstNodeT
@@ -360,7 +370,7 @@ func addNegateOpts(assert *AstNodeT, negateOpts *parser.NegateOptsT) {
360370
}
361371
}
362372

363-
func (b *builderT) buildStateMachine(parserNode *parser.NodeT, parentMachineAddress *AstNodeAddressT, machineAddress *AstNodeAddressT, children []*AstNodeT) (*AstNodeT, error) {
373+
func (b *builderT) buildStateMachine(parserNode *parser.NodeT, parentMachineAddress, machineAddress *AstNodeAddressT, children []*AstNodeT) (*AstNodeT, error) {
364374

365375
switch parserNode.Metadata.Type {
366376
case schema.NodeTypeSeq, schema.NodeTypeLogSeq:
@@ -371,6 +381,9 @@ func (b *builderT) buildStateMachine(parserNode *parser.NodeT, parentMachineAddr
371381
return nil, parserNode.WrapError(ErrInvalidWindow)
372382
}
373383
case schema.NodeTypeSet, schema.NodeTypeLogSet, schema.NodeTypePromQL:
384+
case schema.NodeTypeScript:
385+
return b.buildScriptNode(parserNode, parentMachineAddress, machineAddress)
386+
374387
default:
375388
log.Error().
376389
Any("address", machineAddress).

pkg/ast/ast_script.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package ast
2+
3+
import (
4+
"time"
5+
6+
"github.com/prequel-dev/prequel-compiler/pkg/parser"
7+
"github.com/prequel-dev/prequel-compiler/pkg/schema"
8+
"github.com/rs/zerolog/log"
9+
)
10+
11+
type AstScriptT struct {
12+
Code string
13+
Language string
14+
Timeout time.Duration
15+
}
16+
17+
// Build the child Ast nodes for the script.
18+
//
19+
// Script nodes are internal nodes with one input node.
20+
// The parser node for a script contains a ScriptT struct as its first child, followed by one input node.
21+
// Build the the child nodes for the script node by building each of the parser node's children;
22+
// the first child is skipped since it is the script definition, and the remaining child is built as the input to the script node.
23+
24+
func (b *builderT) buildScriptChildren(parserNode *parser.NodeT, machineAddress *AstNodeAddressT) ([]*AstNodeT, error) {
25+
26+
if len(parserNode.Children) != 2 {
27+
log.Error().Int("child_count", len(parserNode.Children)).Msg("Script node must have two children")
28+
return nil, parserNode.WrapError(ErrInvalidNodeType)
29+
}
30+
31+
termIdx := uint32(1)
32+
33+
child := parserNode.Children[1]
34+
parserChildNode, ok := child.(*parser.NodeT)
35+
if !ok {
36+
log.Error().Any("child", child).Msg("Failed to build Script child node")
37+
return nil, parserNode.WrapError(ErrInvalidNodeType)
38+
}
39+
40+
leaf, err := b.buildLeafChild(parserChildNode, machineAddress, &termIdx)
41+
42+
switch {
43+
case err != nil:
44+
return nil, err
45+
case leaf != nil:
46+
return []*AstNodeT{leaf}, nil
47+
default:
48+
node, err := b.buildTree(parserChildNode, machineAddress, &termIdx)
49+
if err != nil {
50+
return nil, err
51+
}
52+
53+
return []*AstNodeT{node}, nil
54+
}
55+
}
56+
57+
// Validate script definitions and build the script node.
58+
59+
func (b *builderT) buildScriptNode(parserNode *parser.NodeT, parentMachineAddress, machineAddress *AstNodeAddressT) (*AstNodeT, error) {
60+
61+
// Expects exactly two children, the first should be parser.ScriptT, the following is the script input node.
62+
63+
if len(parserNode.Children) != 2 {
64+
log.Error().Int("child_count", len(parserNode.Children)).Msg("Script node must have exactly two children")
65+
return nil, parserNode.WrapError(ErrInvalidNodeType)
66+
}
67+
68+
scriptNode, ok := parserNode.Children[0].(*parser.ScriptT)
69+
70+
if !ok {
71+
log.Error().Any("script", parserNode.Children[0]).Msg("Failed to build Script node")
72+
return nil, parserNode.WrapError(ErrMissingScalar)
73+
}
74+
75+
if scriptNode.Code == "" {
76+
log.Error().Msg("Script code string is empty")
77+
return nil, parserNode.WrapError(ErrMissingScalar)
78+
}
79+
80+
pn := &AstScriptT{
81+
Code: scriptNode.Code,
82+
Language: scriptNode.Language,
83+
}
84+
85+
if scriptNode.Timeout != nil {
86+
pn.Timeout = *scriptNode.Timeout
87+
}
88+
89+
var (
90+
node = newAstNode(parserNode, parserNode.Metadata.Type, schema.ScopeCluster, parentMachineAddress, machineAddress)
91+
)
92+
93+
node.Object = pn
94+
return node, nil
95+
}

pkg/ast/ast_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ func gatherNodeAddresses(node *AstNodeT, out *[]string) {
3333
}
3434

3535
*out = append(*out, node.Metadata.Address.String())
36+
for _, child := range node.Children {
37+
gatherNodeAddresses(child, out)
38+
}
3639
}
3740

3841
func TestAstSuccess(t *testing.T) {
@@ -73,6 +76,14 @@ func TestAstSuccess(t *testing.T) {
7376
rule: testdata.TestSuccessSimplePromQL,
7477
expectedNodeTypes: []string{"machine_set", "promql", "log_set"},
7578
},
79+
"Success_ChildScript": {
80+
rule: testdata.TestSuccessChildScript,
81+
expectedNodeTypes: []string{"machine_seq", "script", "log_seq", "log_set"},
82+
},
83+
"Success_ChildScriptMultipleInputs": {
84+
rule: testdata.TestSuccessChildScriptMultipleInputs,
85+
expectedNodeTypes: []string{"machine_set", "script", "machine_seq", "log_seq", "log_set"},
86+
},
7687
}
7788

7889
for name, test := range tests {

pkg/parser/parse.go

Lines changed: 59 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
)
66

77
// Note that we prefer lower camel case like Kubernetes
8+
// Also, have to keep the JSON tags although we are using YAML.
9+
// The hash function uses JSON serialization, so the JSON tags are required to ensure consistent field names for hashing.
810

911
const (
1012
docRules = "rules"
@@ -92,19 +94,6 @@ type ParseNegateOptsT struct {
9294
Absolute bool `yaml:"absolute,omitempty"`
9395
}
9496

95-
type ParseTermT struct {
96-
Field string `yaml:"field,omitempty"`
97-
StrValue string `yaml:"value,omitempty"`
98-
JqValue string `yaml:"jq,omitempty"`
99-
RegexValue string `yaml:"regex,omitempty"`
100-
Count int `yaml:"count,omitempty"`
101-
Set *ParseSetT `yaml:"set,omitempty"`
102-
Sequence *ParseSequenceT `yaml:"sequence,omitempty"`
103-
NegateOpts *ParseNegateOptsT `yaml:",inline,omitempty"`
104-
PromQL *ParsePromQL `yaml:"promql,omitempty"`
105-
Extract []ParseExtractT `yaml:"extract,omitempty"`
106-
}
107-
10897
type ParseSetT struct {
10998
Window string `yaml:"window,omitempty"`
11099
Correlations []string `yaml:"correlations,omitempty"`
@@ -126,23 +115,55 @@ type ParsePromQL struct {
126115
Event *ParseEventT `yaml:"event,omitempty"`
127116
}
128117

118+
type ParseScriptT struct {
119+
Code string `yaml:"code"`
120+
Language string `yaml:"language,omitempty"` // Assumes 'lua' if empty
121+
Timeout string `yaml:"timeout,omitempty"` // Uses default if empty; expects duration string
122+
Input *ParseTermT `yaml:"input"` // Required input
123+
}
124+
125+
type ParseEventT struct {
126+
Source string `yaml:"source"`
127+
Origin bool `yaml:"origin,omitempty" json:"origin,omitempty"`
128+
}
129+
130+
type ParseTermT struct {
131+
Field string `yaml:"field,omitempty"`
132+
StrValue string `yaml:"value,omitempty"`
133+
JqValue string `yaml:"jq,omitempty"`
134+
RegexValue string `yaml:"regex,omitempty"`
135+
Count int `yaml:"count,omitempty"`
136+
Set *ParseSetT `yaml:"set,omitempty"`
137+
Sequence *ParseSequenceT `yaml:"sequence,omitempty"`
138+
NegateOpts *ParseNegateOptsT `yaml:",inline,omitempty"`
139+
PromQL *ParsePromQL `yaml:"promql,omitempty"`
140+
Script *ParseScriptT `yaml:"script,omitempty"`
141+
Extract []ParseExtractT `yaml:"extract,omitempty"`
142+
}
143+
129144
func (o *ParseTermT) UnmarshalYAML(unmarshal func(any) error) error {
145+
146+
// Try to unmarshal as a raw string first.
147+
// If that fails, unmarshal as a struct.
148+
// This allows for a shorthand syntax for simple match terms.
130149
var str string
131150
if err := unmarshal(&str); err == nil {
132151
o.StrValue = str
133152
return nil
134153
}
154+
135155
var temp struct {
136-
Field string `yaml:"field,omitempty"`
137-
StrValue string `yaml:"value,omitempty"`
138-
JqValue string `yaml:"jq,omitempty"`
139-
RegexValue string `yaml:"regex,omitempty"`
140-
Count int `yaml:"count,omitempty"`
141-
Set *ParseSetT `yaml:"set,omitempty"`
142-
Sequence *ParseSequenceT `yaml:"sequence,omitempty"`
143-
NegateOpts *ParseNegateOptsT `yaml:",inline,omitempty"`
144-
ParsePromQL *ParsePromQL `yaml:"promql,omitempty"`
145-
Extract []ParseExtractT `yaml:"extract,omitempty"`
156+
Field string `yaml:"field"`
157+
StrValue string `yaml:"value"`
158+
JqValue string `yaml:"jq"`
159+
RegexValue string `yaml:"regex"`
160+
Count int `yaml:"count"`
161+
Set *ParseSetT `yaml:"set"`
162+
Sequence *ParseSequenceT `yaml:"sequence"`
163+
NegateOpts *ParseNegateOptsT `yaml:",inline"`
164+
ParsePromQL *ParsePromQL `yaml:"promql"`
165+
Script *ParseScriptT `yaml:"script"`
166+
Extract []ParseExtractT `yaml:"extract"`
146167
}
147168
if err := unmarshal(&temp); err != nil {
148169
return err
@@ -156,22 +177,11 @@ func (o *ParseTermT) UnmarshalYAML(unmarshal func(any) error) error {
156177
o.Sequence = temp.Sequence
157178
o.NegateOpts = temp.NegateOpts
158179
o.PromQL = temp.ParsePromQL
180+
o.Script = temp.Script
159181
o.Extract = temp.Extract
160182
return nil
161183
}
162184

163-
type ParseEventT struct {
164-
Source string `yaml:"source"`
165-
Origin bool `yaml:"origin,omitempty" json:"origin,omitempty"`
166-
}
167-
168-
type RulesT struct {
169-
Rules []ParseRuleT `yaml:"rules"`
170-
Root *yaml.Node `yaml:"-"`
171-
TermsT map[string]ParseTermT `yaml:"terms,omitempty"`
172-
TermsY map[string]*yaml.Node `yaml:"-"`
173-
}
174-
175185
func RootNode(data []byte) (*yaml.Node, error) {
176186
var root yaml.Node
177187
if err := yaml.Unmarshal(data, &root); err != nil {
@@ -180,21 +190,25 @@ func RootNode(data []byte) (*yaml.Node, error) {
180190
return &root, nil
181191
}
182192

183-
func _parse(data []byte) (RulesT, *yaml.Node, error) {
193+
type RulesT struct {
194+
Rules []ParseRuleT `yaml:"rules"`
195+
Root *yaml.Node `yaml:"-"`
196+
TermsT map[string]ParseTermT `yaml:"terms,omitempty"`
197+
TermsY map[string]*yaml.Node `yaml:"-"`
198+
}
184199

185-
var (
186-
root yaml.Node
187-
rules RulesT
188-
err error
189-
)
200+
func _parse(data []byte) (*RulesT, *yaml.Node, error) {
190201

191-
if err = yaml.Unmarshal(data, &root); err != nil {
192-
return RulesT{}, nil, err
202+
root, err := RootNode(data)
203+
if err != nil {
204+
return nil, nil, err
193205
}
194206

207+
var rules RulesT
195208
if err := root.Decode(&rules); err != nil {
196-
return RulesT{}, nil, err
209+
return nil, nil, err
210+
197211
}
198212

199-
return rules, &root, nil
213+
return &rules, root, nil
200214
}

0 commit comments

Comments
 (0)