trigger_agent.rb 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. module Agents
  2. class TriggerAgent < Agent
  3. cannot_be_scheduled!
  4. VALID_COMPARISON_TYPES = %w[regex !regex field<value field<=value field==value field!=value field>=value field>value not\ in]
  5. description <<-MD
  6. The Trigger Agent will watch for a specific value in an Event payload.
  7. The `rules` array contains hashes of `path`, `value`, and `type`. The `path` value is a dotted path through a hash in [JSONPaths](http://goessner.net/articles/JsonPath/) syntax.
  8. 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)`.
  9. 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.
  10. By default, all rules must match for the Agent to trigger. You can switch this so that only one rule must match by
  11. setting `must_match` to `1`.
  12. 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/cantino/huginn/wiki/Formatting-Events-using-Liquid) for details.
  13. Set `keep_event` to `true` if you'd like to re-emit the incoming event, optionally merged with 'message' when provided.
  14. 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.
  15. MD
  16. event_description <<-MD
  17. Events look like this:
  18. { "message": "Your message" }
  19. MD
  20. def validate_options
  21. unless options['expected_receive_period_in_days'].present? && options['rules'].present? &&
  22. options['rules'].all? { |rule| rule['type'].present? && VALID_COMPARISON_TYPES.include?(rule['type']) && rule['value'].present? && rule['path'].present? }
  23. errors.add(:base, "expected_receive_period_in_days, message, and rules, with a type, value, and path for every rule, are required")
  24. end
  25. errors.add(:base, "message is required unless 'keep_event' is 'true'") unless options['message'].present? || keep_event?
  26. errors.add(:base, "keep_event, when present, must be 'true' or 'false'") unless options['keep_event'].blank? || %w[true false].include?(options['keep_event'])
  27. if options['must_match'].present?
  28. if options['must_match'].to_i < 1
  29. errors.add(:base, "If used, the 'must_match' option must be a positive integer")
  30. elsif options['must_match'].to_i > options['rules'].length
  31. errors.add(:base, "If used, the 'must_match' option must be equal to or less than the number of rules")
  32. end
  33. end
  34. end
  35. def default_options
  36. {
  37. 'expected_receive_period_in_days' => "2",
  38. 'keep_event' => 'false',
  39. 'rules' => [{
  40. 'type' => "regex",
  41. 'value' => "foo\\d+bar",
  42. 'path' => "topkey.subkey.subkey.goal",
  43. }],
  44. 'message' => "Looks like your pattern matched in '{{value}}'!"
  45. }
  46. end
  47. def working?
  48. last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs?
  49. end
  50. def receive(incoming_events)
  51. incoming_events.each do |event|
  52. opts = interpolated(event)
  53. match_results = opts['rules'].map do |rule|
  54. value_at_path = Utils.value_at(event['payload'], rule['path'])
  55. rule_values = rule['value']
  56. rule_values = [rule_values] unless rule_values.is_a?(Array)
  57. if rule['type'] == 'not in'
  58. !rule_values.include?(value_at_path.to_s)
  59. elsif rule['type'] == 'field==value'
  60. rule_values.include?(value_at_path.to_s)
  61. else
  62. rule_values.any? do |rule_value|
  63. case rule['type']
  64. when "regex"
  65. value_at_path.to_s =~ Regexp.new(rule_value, Regexp::IGNORECASE)
  66. when "!regex"
  67. value_at_path.to_s !~ Regexp.new(rule_value, Regexp::IGNORECASE)
  68. when "field>value"
  69. value_at_path.to_f > rule_value.to_f
  70. when "field>=value"
  71. value_at_path.to_f >= rule_value.to_f
  72. when "field<value"
  73. value_at_path.to_f < rule_value.to_f
  74. when "field<=value"
  75. value_at_path.to_f <= rule_value.to_f
  76. when "field!=value"
  77. value_at_path.to_s != rule_value.to_s
  78. else
  79. raise "Invalid type of #{rule['type']} in TriggerAgent##{id}"
  80. end
  81. end
  82. end
  83. end
  84. if matches?(match_results)
  85. if keep_event?
  86. payload = event.payload.dup
  87. payload['message'] = opts['message'] if opts['message'].present?
  88. else
  89. payload = { 'message' => opts['message'] }
  90. end
  91. create_event :payload => payload
  92. end
  93. end
  94. end
  95. def matches?(matches)
  96. if options['must_match'].present?
  97. matches.select { |match| match }.length >= options['must_match'].to_i
  98. else
  99. matches.all?
  100. end
  101. end
  102. def keep_event?
  103. boolify(interpolated['keep_event'])
  104. end
  105. end
  106. end