diff --git a/app/models/pager_tree/integrations/hetrix_tools/v3.rb b/app/models/pager_tree/integrations/hetrix_tools/v3.rb
new file mode 100644
index 0000000..d39c2f1
--- /dev/null
+++ b/app/models/pager_tree/integrations/hetrix_tools/v3.rb
@@ -0,0 +1,160 @@
+module PagerTree::Integrations
+ class HetrixTools::V3 < Integration
+ OPTIONS = [
+ {key: :authentication_token, type: :string, default: nil}
+ ]
+ store_accessor :options, *OPTIONS.map { |x| x[:key] }.map(&:to_s), prefix: "option"
+
+ after_initialize do
+ self.option_authentication_token ||= nil
+ end
+
+ def adapter_should_block_incoming?(request)
+ self.option_authentication_token.present? && (request.headers["Authorization"] != "Bearer #{self.option_authentication_token}")
+ end
+
+ def adapter_supports_incoming?
+ true
+ end
+
+ def adapter_supports_outgoing?
+ false
+ end
+
+ def adapter_incoming_can_defer?
+ true
+ end
+
+ def adapter_thirdparty_id
+ try("_adapter_thirdparty_id_#{_webhook_type}") || SecureRandom.hex(16)
+ end
+
+ def adapter_action
+ try("_adapter_action_#{_webhook_type}") || :other
+ end
+
+ def _adapter_action_uptime
+ case adapter_incoming_request_params.dig("monitor_status")
+ when "online" then :resolve
+ when "offline" then :create
+ else
+ :other
+ end
+ end
+
+ def _adapter_action_blacklist
+ :create
+ end
+
+ def _adapter_action_resource_usage
+ :create
+ end
+
+ def adapter_process_create
+ Alert.new(
+ title: _title,
+ description: _description,
+ thirdparty_id: adapter_thirdparty_id,
+ dedup_keys: [],
+ additional_data: _additional_datums
+ )
+ end
+
+ def _adapter_thirdparty_id_uptime
+ adapter_incoming_request_params.dig("monitor_id")
+ end
+
+ def _adapter_thirdparty_id_blacklist
+ SecureRandom.hex(16)
+ end
+
+ def _adapter_thirdparty_id_resource_usage
+ adapter_incoming_request_params.dig("monitor_id")
+ end
+
+ def _webhook_type
+ @_webhook_type ||= if _webhook_type_uptime?
+ :uptime
+ elsif _webhook_type_resource_usage?
+ :resource_usage
+ elsif _webhook_type_blacklist?
+ :blacklist
+ else
+ :unknown
+ end
+ end
+
+ def _webhook_type_uptime?
+ adapter_incoming_request_params.dig("monitor_errors").present?
+ end
+
+ def _webhook_type_blacklist?
+ json = adapter_incoming_request_params.dig("_json")
+ json.present? && json.is_a?(Array)
+ end
+
+ def _webhook_type_resource_usage?
+ adapter_incoming_request_params.dig("resource_usage").present?
+ end
+
+ def _title
+ try("_title_#{_webhook_type}") || "HetrixTools Alert"
+ end
+
+ def _title_uptime
+ "#{adapter_incoming_request_params.dig("monitor_name")} is #{adapter_incoming_request_params.dig("monitor_status")}"
+ end
+
+ def _title_blacklist
+ "Blacklist Alert"
+ end
+
+ def _title_resource_usage
+ "#{adapter_incoming_request_params.dig("monitor_name")} usage alert"
+ end
+
+ def _description
+ try("_description_#{_webhook_type}") || "No description provided"
+ end
+
+ def _description_uptime
+ "
#{adapter_incoming_request_params.dig("monitor_target")} is #{adapter_incoming_request_params.dig("monitor_status")}
" +
+ adapter_incoming_request_params.dig("monitor_errors").map { |k, v| "#{k}: #{v}
" }.join("")
+ end
+
+ def _description_blacklist
+ adapter_incoming_request_params.dig("_json").map { |x| "#{x["monitor"]} (#{x["blacklisted_now"]})" }.join("
")
+ end
+
+ def _description_resource_usage
+ [
+ "Resource Type: #{adapter_incoming_request_params.dig("resource_usage", "resource_type")}
",
+ "Current Usage: #{adapter_incoming_request_params.dig("resource_usage", "current_usage")}
",
+ "Average Usage: #{adapter_incoming_request_params.dig("resource_usage", "average_usage")} / #{adapter_incoming_request_params.dig("resource_usage", "average_minutes")}m
"
+ ].join("")
+ end
+
+ def _additional_datums
+ try("_additional_datums_#{_webhook_type}") || []
+ end
+
+ def _additional_datums_uptime
+ [
+ AdditionalDatum.new(format: "datetime", label: "Timestamp", value: Time.at(adapter_incoming_request_params.dig("timestamp"))),
+ AdditionalDatum.new(format: "text", label: "Monitor Type", value: adapter_incoming_request_params.dig("monitor_type")),
+ AdditionalDatum.new(format: "link", label: "Monitor Target", value: adapter_incoming_request_params.dig("monitor_target"))
+ ]
+ end
+
+ def _additional_datums_blacklist
+ []
+ end
+
+ def _additional_datums_resource_usage
+ [
+ AdditionalDatum.new(format: "text", label: "Resource Type", value: adapter_incoming_request_params.dig("resource_usage", "resource_type")),
+ AdditionalDatum.new(format: "text", label: "Current Usage", value: adapter_incoming_request_params.dig("resource_usage", "current_usage"))
+ ]
+ end
+ end
+end
diff --git a/app/views/pager_tree/integrations/hetrix_tools/v3/_form_options.html.erb b/app/views/pager_tree/integrations/hetrix_tools/v3/_form_options.html.erb
new file mode 100644
index 0000000..8176cd0
--- /dev/null
+++ b/app/views/pager_tree/integrations/hetrix_tools/v3/_form_options.html.erb
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/app/views/pager_tree/integrations/hetrix_tools/v3/_show_options.html.erb b/app/views/pager_tree/integrations/hetrix_tools/v3/_show_options.html.erb
new file mode 100644
index 0000000..054fe34
--- /dev/null
+++ b/app/views/pager_tree/integrations/hetrix_tools/v3/_show_options.html.erb
@@ -0,0 +1,12 @@
+
+
+ <%= t("activerecord.attributes.pager_tree/integrations/hetrix_tools/v3.option_authentication_token") %>
+
+
+
+
+ <%= mask integration.option_authentication_token %>
+
+
+
+
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 8f4e1bf..8e1bc66 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -46,6 +46,10 @@ en:
option_form_phone_required_hint_html: "Should the phone field be required to submit?"
option_form_description_required_hint_html: "Should the description field be required to submit?"
option_form_urgency_required_hint_html: "Should the urgency field be required to submit?"
+ hetrix_tools:
+ v3:
+ form_options:
+ option_authentication_token_hint_html: "Authentication Token to be used to authenticate requests from Hetrix Tools servers (optional, see docs)"
honeybadger:
v3:
form_options:
@@ -161,6 +165,8 @@ en:
option_form_phone_required: "Phone Required?"
option_form_description_required: "Description Required?"
option_form_urgency_required: "Urgency Required?"
+ "pager_tree/integrations/hetrix_tools/v3":
+ option_authentication_token: "Authentication Token"
"pager_tree/integrations/honeybadger/v3":
option_token: "Honeybadger Token"
"pager_tree/integrations/jira_server/v3":
diff --git a/test/fixtures/pager_tree/integrations/integrations.yml b/test/fixtures/pager_tree/integrations/integrations.yml
index dacf80d..b67e93f 100644
--- a/test/fixtures/pager_tree/integrations/integrations.yml
+++ b/test/fixtures/pager_tree/integrations/integrations.yml
@@ -70,6 +70,10 @@ healthchecks_v3:
type: "PagerTree::Integrations::Healthchecks::V3"
# options: no_options
+hetrix_tools_v3:
+ type: "PagerTree::Integrations::HetrixTools::V3"
+ # options: no_options
+
honeybadger_v3:
type: "PagerTree::Integrations::Honeybadger::V3"
options:
diff --git a/test/models/pager_tree/integrations/hetrix_tools/v3_test.rb b/test/models/pager_tree/integrations/hetrix_tools/v3_test.rb
new file mode 100644
index 0000000..cc2e910
--- /dev/null
+++ b/test/models/pager_tree/integrations/hetrix_tools/v3_test.rb
@@ -0,0 +1,215 @@
+require "test_helper"
+
+module PagerTree::Integrations
+ class HetrixTools::V3Test < ActiveSupport::TestCase
+ include Integrateable
+
+ setup do
+ @integration = pager_tree_integrations_integrations(:hetrix_tools_v3)
+
+ # https://docs.hetrixtools.com/uptime-monitoring-webhook-notifications/
+ @uptime_request = {
+ monitor_id: "ThisWillBeTheMonitorID32CharLong",
+ monitor_name: "Test Monitor Label",
+ monitor_target: "http:\/\/this-is-a-test.com\/",
+ monitor_type: "website",
+ monitor_category: "Test Category",
+ monitor_status: "offline",
+ timestamp: 1499666192,
+ monitor_errors: {
+ "New York": "http code 403",
+ "San Francisco": "http code 403",
+ Dallas: "timeout",
+ Amsterdam: "http code 403",
+ London: "http code 403",
+ Frankfurt: "http code 403",
+ Singapore: "timeout",
+ Sydney: "http code 403",
+ "Sao Paulo": "http code 403",
+ Tokyo: "keyword not found",
+ Mumbai: "http code 403",
+ Moscow: "keyword not found"
+ }
+ }.with_indifferent_access
+
+ # https://docs.hetrixtools.com/blacklist-monitoring-webhook-notifications/
+ @blacklist_request = {_json: [
+ {
+ monitor: "98.88.89.102",
+ label: "some label",
+ blacklisted_before: "7",
+ blacklisted_now: "6",
+ blacklisted_on: [
+ {
+ rbl: "bl.nszones.com",
+ delist: "http://www.nszones.com/contact.shtml"
+ },
+ {
+ rbl: "bl.score.senderscore.com",
+ delist: "https://www.senderscore.org/blacklistlookup/"
+ },
+ {
+ rbl: "cidr.bl.mcafee.com",
+ delist: "https://kc.mcafee.com/corporate/index?page=content&id=KB53783"
+ },
+ {
+ rbl: "dyn.nszones.com",
+ delist: "http://db.nszones.com/dyn.ip?98.88.89.102"
+ },
+ {
+ rbl: "pbl.spamhaus.org",
+ delist: "https://www.spamhaus.org/query/ip/98.88.89.102"
+ },
+ {
+ rbl: "zen.spamhaus.org",
+ delist: "https://www.spamhaus.org/query/ip/98.88.89.102"
+ }
+ ],
+ links: {
+ report_link: "https://hetrixtools.com/report/blacklist/c855b5712bd63a3c8153690b56d5385e/",
+ whitelabel_report_link: "http://status.hetrixtools.com/report/blacklist/c855b5712bd63a3c8153690b56d5385e/"
+ }
+ },
+ {
+ monitor: "190.129.206.24",
+ label: "another label",
+ blacklisted_before: "2",
+ blacklisted_now: "0",
+ blacklisted_on: nil,
+ links: {
+ report_link: "https://hetrixtools.com/report/blacklist/4255d1931a5c5547a0fce88e6cdff008/",
+ whitelabel_report_link: "http://status.hetrixtools.com/report/blacklist/4255d1931a5c5547a0fce88e6cdff008/"
+ }
+ }
+ ]}.with_indifferent_access
+
+ @resource_usage_request = {
+ monitor_id: "ThisWillBeTheMonitorID32CharLong",
+ monitor_name: "Test Monitor Label",
+ timestamp: 1499666613,
+ resource_usage: {
+ resource_type: "cpu",
+ current_usage: "24.60",
+ average_usage: "25.29",
+ average_minutes: "3"
+ }
+ }.with_indifferent_access
+
+ @resolve_request = @uptime_request.deep_dup
+ @resolve_request["monitor_status"] = "online"
+ end
+
+ test "sanity" do
+ assert @integration.adapter_supports_incoming?
+ assert @integration.adapter_incoming_can_defer?
+ assert_not @integration.adapter_supports_outgoing?
+ assert @integration.adapter_show_alerts?
+ assert @integration.adapter_show_logs?
+ assert_not @integration.adapter_show_outgoing_webhook_delivery?
+ end
+
+ test "adapter_action_uptime_create" do
+ @integration.adapter_incoming_request_params = @uptime_request
+ assert_equal :create, @integration.adapter_action
+ end
+
+ test "adapter_action_uptime_resolve" do
+ @integration.adapter_incoming_request_params = @resolve_request
+ assert_equal :resolve, @integration.adapter_action
+ end
+
+ test "adapter_action_blacklist_create" do
+ @integration.adapter_incoming_request_params = @blacklist_request
+ assert_equal :create, @integration.adapter_action
+ end
+
+ test "adapter_action_resource_usage_create" do
+ @integration.adapter_incoming_request_params = @resource_usage_request
+ assert_equal :create, @integration.adapter_action
+ end
+
+ test "adapter_thirdparty_id_uptime" do
+ @integration.adapter_incoming_request_params = @uptime_request
+ assert_equal @uptime_request.dig("monitor_id"), @integration.adapter_thirdparty_id
+ end
+
+ test "adapter_thirdparty_id_blacklist" do
+ @integration.adapter_incoming_request_params = @blacklist_request
+ assert_equal 32, @integration.adapter_thirdparty_id.length # each hex is 2 chars
+ end
+
+ test "adapter_thirdparty_id_resource_usage" do
+ @integration.adapter_incoming_request_params = @resource_usage_request
+ assert_equal 32, @integration.adapter_thirdparty_id.length # each hex is 2 chars
+ end
+
+ test "adapter_process_create_uptime" do
+ @integration.adapter_incoming_request_params = @uptime_request
+
+ true_alert = Alert.new(
+ title: "#{@uptime_request.dig("monitor_name")} is #{@uptime_request.dig("monitor_status")}",
+ description: "#{@uptime_request.dig("monitor_target")} is #{@uptime_request.dig("monitor_status")}
" + @uptime_request.dig("monitor_errors").map { |k, v| "#{k}: #{v}
" }.join(""),
+ urgency: nil,
+ thirdparty_id: @uptime_request.dig("monitor_id"),
+ dedup_keys: [],
+ additional_data: [
+ AdditionalDatum.new(format: "datetime", label: "Timestamp", value: Time.at(@uptime_request.dig("timestamp"))),
+ AdditionalDatum.new(format: "text", label: "Monitor Type", value: @uptime_request.dig("monitor_type")),
+ AdditionalDatum.new(format: "link", label: "Monitor Target", value: @uptime_request.dig("monitor_target"))
+ ]
+ )
+
+ assert_equal true_alert.to_json, @integration.adapter_process_create.to_json
+ end
+
+ test "adapter_process_create_blacklist" do
+ @integration.adapter_incoming_request_params = @blacklist_request
+
+ true_alert = Alert.new(
+ title: "Blacklist Alert",
+ description: @blacklist_request.dig("_json").map { |x| "#{x["monitor"]} (#{x["blacklisted_now"]})" }.join("
"),
+ urgency: nil,
+ thirdparty_id: nil,
+ dedup_keys: [],
+ additional_data: []
+ )
+
+ actual = @integration.adapter_process_create
+ actual.thirdparty_id = nil
+ assert_equal true_alert.to_json, actual.to_json
+ end
+
+ test "adapter_process_create_resource_usage" do
+ @integration.adapter_incoming_request_params = @resource_usage_request
+
+ true_alert = Alert.new(
+ title: "#{@resource_usage_request.dig("monitor_name")} usage alert",
+ description: [
+ "Resource Type: #{@resource_usage_request.dig("resource_usage", "resource_type")}
",
+ "Current Usage: #{@resource_usage_request.dig("resource_usage", "current_usage")}
",
+ "Average Usage: #{@resource_usage_request.dig("resource_usage", "average_usage")} / #{@resource_usage_request.dig("resource_usage", "average_minutes")}m
"
+ ].join(""),
+ urgency: nil,
+ thirdparty_id: nil,
+ dedup_keys: [],
+ additional_data: [
+ AdditionalDatum.new(format: "text", label: "Resource Type", value: @resource_usage_request.dig("resource_usage", "resource_type")),
+ AdditionalDatum.new(format: "text", label: "Current Usage", value: @resource_usage_request.dig("resource_usage", "current_usage"))
+ ]
+ )
+
+ actual = @integration.adapter_process_create
+ actual.thirdparty_id = nil
+ assert_equal true_alert.to_json, actual.to_json
+ end
+
+ test "blocking_incoming" do
+ assert @integration.option_authentication_token.blank?
+ assert_not @integration.adapter_should_block_incoming?(OpenStruct.new({headers: {"Authorization" => ""}}))
+
+ @integration.option_authentication_token = "abc123456"
+ assert @integration.adapter_should_block_incoming?(OpenStruct.new({headers: {"Authorization" => ""}}))
+ assert_not @integration.adapter_should_block_incoming?(OpenStruct.new({headers: {"Authorization" => "Bearer abc123456"}}))
+ end
+ end
+end