|
module Agents
class EventFormattingAgent < Agent
cannot_be_scheduled!
can_dry_run!
description <<-MD
The Event Formatting Agent allows you to format incoming Events, adding new fields as needed.
For example, here is a possible Event:
{
"high": {
"celsius": "18",
"fahreinheit": "64"
},
"date": {
"epoch": "1357959600",
"pretty": "10:00 PM EST on January 11, 2013"
},
"conditions": "Rain showers",
"data": "This is some data"
}
You may want to send this event to another Agent, for example a Twilio Agent, which expects a `message` key.
You can use an Event Formatting Agent's `instructions` setting to do this in the following way:
"instructions": {
"message": "Today's conditions look like {{conditions}} with a high temperature of {{high.celsius}} degrees Celsius.",
"subject": "{{data}}",
"created_at": "{{created_at}}"
}
Names here like `conditions`, `high` and `data` refer to the corresponding values in the Event hash.
The special key `created_at` refers to the timestamp of the Event, which can be reformatted by the `date` filter, like `{{created_at | date:"at %I:%M %p" }}`.
The upstream agent of each received event is accessible via the key `agent`, which has the following attributes: #{''.tap { |s| s << AgentDrop.instance_methods(false).map { |m| "`#{m}`" }.join(', ') }}.
Have a look at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to learn more about liquid templating.
Events generated by this possible Event Formatting Agent will look like:
{
"message": "Today's conditions look like Rain showers with a high temperature of 18 degrees Celsius.",
"subject": "This is some data"
}
In `matchers` setting you can perform regular expression matching against contents of events and expand the match data for use in `instructions` setting. Here is an example:
{
"matchers": [
{
"path": "{{date.pretty}}",
"regexp": "\\A(?<time>\\d\\d:\\d\\d [AP]M [A-Z]+)",
"to": "pretty_date"
}
]
}
This virtually merges the following hash into the original event hash:
"pretty_date": {
"time": "10:00 PM EST",
"0": "10:00 PM EST on January 11, 2013"
"1": "10:00 PM EST"
}
So you can use it in `instructions` like this:
"instructions": {
"message": "Today's conditions look like {{conditions}} with a high temperature of {{high.celsius}} degrees Celsius according to the forecast at {{pretty_date.time}}.",
"subject": "{{data}}"
}
If you want to retain original contents of events and only add new keys, then set `mode` to `merge`, otherwise set it to `clean`.
To CGI escape output (for example when creating a link), use the Liquid `uri_escape` filter, like so:
{
"message": "A peak was on Twitter in {{group_by}}. Search: https://twitter.com/search?q={{group_by | uri_escape}}"
}
MD
event_description do
"Events will have the following fields%s:\n\n %s" % [
case options['mode'].to_s
when 'merged'
', merged with the original contents'
when /\{/
', conditionally merged with the original contents'
end,
Utils.pretty_print(Hash[options['instructions'].keys.map { |key|
[key, "..."]
}])
]
end
def validate_options
errors.add(:base, "instructions and mode need to be present.") unless options['instructions'].present? && options['mode'].present?
validate_matchers
end
def default_options
{
'instructions' => {
'message' => "You received a text {{text}} from {{fields.from}}",
'agent' => "{{agent.type}}",
'some_other_field' => "Looks like the weather is going to be {{fields.weather}}"
},
'matchers' => [],
'mode' => "clean",
}
end
def working?
!recent_error_logs?
end
def receive(incoming_events)
matchers = compiled_matchers
incoming_events.each do |event|
interpolate_with(event) do
apply_compiled_matchers(matchers, event) do
formatted_event = interpolated['mode'].to_s == "merge" ? event.payload.dup : {}
formatted_event.merge! interpolated['instructions']
create_event payload: formatted_event
end
end
end
end
private
def validate_matchers
matchers = options['matchers'] or return
unless matchers.is_a?(Array)
errors.add(:base, "matchers must be an array if present")
return
end
matchers.each do |matcher|
unless matcher.is_a?(Hash)
errors.add(:base, "each matcher must be a hash")
next
end
regexp, path, to = matcher.values_at(*%w[regexp path to])
if regexp.present?
begin
Regexp.new(regexp)
rescue
errors.add(:base, "bad regexp found in matchers: #{regexp}")
end
else
errors.add(:base, "regexp is mandatory for a matcher and must be a string")
end
errors.add(:base, "path is mandatory for a matcher and must be a string") if !path.present?
errors.add(:base, "to must be a string if present in a matcher") if to.present? && !to.is_a?(String)
end
end
def compiled_matchers
if matchers = options['matchers']
matchers.map { |matcher|
regexp, path, to = matcher.values_at(*%w[regexp path to])
[Regexp.new(regexp), path, to]
}
end
end
def apply_compiled_matchers(matchers, event, &block)
return yield if matchers.nil?
# event.payload.dup does not work; HashWithIndifferentAccess is
# a source of trouble here.
hash = {}.update(event.payload)
matchers.each do |re, path, to|
m = re.match(interpolate_string(path, hash)) or next
mhash =
if to
case value = hash[to]
when Hash
value
else
hash[to] = {}
end
else
hash
end
m.size.times do |i|
mhash[i.to_s] = m[i]
end
m.names.each do |name|
mhash[name] = m[name]
end
end
interpolate_with(hash, &block)
end
end
end
|