-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
/
trigger_agent.rb
172 lines (141 loc) · 6.25 KB
/
trigger_agent.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
module Agents
class TriggerAgent < Agent
cannot_be_scheduled!
can_dry_run!
VALID_COMPARISON_TYPES = %w[
regex
!regex
field<value
field<=value
field==value
field!=value
field>=value
field>value
not\ in
]
description <<~MD
The Trigger Agent will watch for a specific value in an Event payload.
The `rules` array contains a mixture of strings and hashes.
A string rule is a Liquid template and counts as a match when it expands to `true`.
A hash rule consists of the following keys: `path`, `value`, and `type`.
The `path` value is a dotted path through a hash in [JSONPaths](http://goessner.net/articles/JsonPath/) syntax. For simple events, this is usually just the name of the field you want, like 'text' for the text key of the event.
The `type` can be one of #{VALID_COMPARISON_TYPES.map { |t| "`#{t}`" }.to_sentence} and compares with the `value`. Note that regex patterns are matched case insensitively. If you want case sensitive matching, prefix your pattern with `(?-i)`.
In any `type` including regex Liquid variables can be used normally. To search for just a word matching the concatenation of `foo` and variable `bar` would use `value` of `foo{{bar}}`. Note that note that starting/ending delimiters like `/` or `|` are not required for regex.
The `value` can be a single value or an array of values. In the case of an array, all items must be strings, and if one or more values match, then the rule matches. Note: avoid using `field!=value` with arrays, you should use `not in` instead.
By default, all rules must match for the Agent to trigger. You can switch this so that only one rule must match by
setting `must_match` to `1`.
The resulting Event will have a payload message of `message`. You can use liquid templating in the `message, have a look at the [Wiki](https://github.com/huginn/huginn/wiki/Formatting-Events-using-Liquid) for details.
Set `keep_event` to `true` if you'd like to re-emit the incoming event, optionally merged with 'message' when provided.
Set `expected_receive_period_in_days` to the maximum amount of time that you'd expect to pass between Events being received by this Agent.
MD
event_description <<~MD
Events look like this:
{ "message": "Your message" }
MD
private def valid_rule?(rule)
case rule
when String
true
when Hash
VALID_COMPARISON_TYPES.include?(rule['type']) &&
/\S/.match?(rule['path']) &&
rule['value'].is_a?(String)
else
false
end
end
def validate_options
unless options['expected_receive_period_in_days'].present? &&
options['rules'].present? &&
options['rules'].all? { |rule| valid_rule?(rule) }
errors.add(:base,
"expected_receive_period_in_days, message, and rules, with a type, value, and path for every rule, are required")
end
errors.add(:base,
"message is required unless 'keep_event' is 'true'") unless options['message'].present? || keep_event?
errors.add(:base,
"keep_event, when present, must be 'true' or 'false'") unless options['keep_event'].blank? || %w[
true false
].include?(options['keep_event'])
if options['must_match'].present?
if options['must_match'].to_i < 1
errors.add(:base, "If used, the 'must_match' option must be a positive integer")
elsif options['must_match'].to_i > options['rules'].length
errors.add(:base, "If used, the 'must_match' option must be equal to or less than the number of rules")
end
end
end
def default_options
{
'expected_receive_period_in_days' => "2",
'keep_event' => 'false',
'rules' => [{
'type' => "regex",
'value' => "foo\\d+bar",
'path' => "topkey.subkey.subkey.goal",
}],
'message' => "Looks like your pattern matched in '{{value}}'!"
}
end
def working?
last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs?
end
def receive(incoming_events)
incoming_events.each do |event|
opts = interpolated(event)
match_results = opts['rules'].map do |rule|
if rule.is_a?(String)
next boolify(rule)
end
value_at_path = Utils.value_at(event['payload'], rule['path'])
rule_values = rule['value']
rule_values = [rule_values] unless rule_values.is_a?(Array)
if rule['type'] == 'not in'
!rule_values.include?(value_at_path.to_s)
elsif rule['type'] == 'field==value'
rule_values.include?(value_at_path.to_s)
else
rule_values.any? do |rule_value|
case rule['type']
when "regex"
value_at_path.to_s =~ Regexp.new(rule_value, Regexp::IGNORECASE)
when "!regex"
value_at_path.to_s !~ Regexp.new(rule_value, Regexp::IGNORECASE)
when "field>value"
value_at_path.to_f > rule_value.to_f
when "field>=value"
value_at_path.to_f >= rule_value.to_f
when "field<value"
value_at_path.to_f < rule_value.to_f
when "field<=value"
value_at_path.to_f <= rule_value.to_f
when "field!=value"
value_at_path.to_s != rule_value.to_s
else
raise "Invalid type of #{rule['type']} in TriggerAgent##{id}"
end
end
end
end
next unless matches?(match_results)
if keep_event?
payload = event.payload.dup
payload['message'] = opts['message'] if opts['message'].present?
else
payload = { 'message' => opts['message'] }
end
create_event(payload:)
end
end
def matches?(matches)
if options['must_match'].present?
matches.select { |match| match }.length >= options['must_match'].to_i
else
matches.all?
end
end
def keep_event?
boolify(interpolated['keep_event'])
end
end
end