class Kennel::Models::Monitor

Constants

ALLOWED_PRIORITY_CLASSES
DEFAULT_ESCALATION_MESSAGE
MONITOR_DEFAULTS
MONITOR_OPTION_DEFAULTS

defaults that datadog uses when options are not sent, so safe to leave out if our values match their defaults

OPTIONAL_SERVICE_CHECK_THRESHOLDS
READONLY_ATTRIBUTES
RENOTIFY_INTERVALS
TRACKING_FIELD

Public Class Methods

api_resource() click to toggle source
# File lib/kennel/models/monitor.rb, line 124
def self.api_resource
  "monitor"
end
normalize(expected, actual) click to toggle source
Calls superclass method
# File lib/kennel/models/monitor.rb, line 144
def self.normalize(expected, actual)
  super

  ignore_default(expected, actual, MONITOR_DEFAULTS)

  options = actual.fetch(:options)
  options.delete(:silenced) # we do not manage silenced, so ignore it when diffing

  # fields are not returned when set to true
  if ["service check", "event alert"].include?(actual[:type])
    options[:include_tags] = true unless options.key?(:include_tags)
    options[:require_full_window] = true unless options.key?(:require_full_window)
  end

  case actual[:type]
  when "event alert"
    # setting nothing results in thresholds not getting returned from the api
    options[:thresholds] ||= { critical: 0 }

  when "service check"
    # fields are not returned when created with default values via UI
    OPTIONAL_SERVICE_CHECK_THRESHOLDS.each do |t|
      options[:thresholds][t] ||= 1
    end
  end

  # nil / "" / 0 are not returned from the api when set via the UI
  options[:evaluation_delay] ||= nil

  expected_options = expected[:options] || {}
  ignore_default(expected_options, options, MONITOR_OPTION_DEFAULTS)
  if DEFAULT_ESCALATION_MESSAGE.include?(options[:escalation_message])
    options.delete(:escalation_message)
    expected_options.delete(:escalation_message)
  end
end
parse_url(url) click to toggle source
# File lib/kennel/models/monitor.rb, line 132
def self.parse_url(url)
  # datadog uses / for show and # for edit as separator in it's links
  id = url[/\/monitors[\/#](\d+)/, 1]

  # slo alert url
  id ||= url[/\/slo\/edit\/[a-z\d]{10,}\/alerts\/(\d+)/, 1]

  return unless id

  Integer(id)
end
url(id) click to toggle source
# File lib/kennel/models/monitor.rb, line 128
def self.url(id)
  Utils.path_to_url "/monitors##{id}/edit"
end

Public Instance Methods

as_json() click to toggle source
# File lib/kennel/models/monitor.rb, line 57
def as_json
  return @as_json if @as_json
  data = {
    name: "#{name}#{LOCK}",
    type: type,
    query: query.strip,
    message: message.strip,
    tags: tags.uniq,
    priority: priority,
    options: {
      timeout_h: timeout_h,
      notify_no_data: notify_no_data,
      no_data_timeframe: notify_no_data ? no_data_timeframe : nil,
      notify_audit: notify_audit,
      require_full_window: require_full_window,
      new_host_delay: new_host_delay,
      include_tags: true,
      escalation_message: Utils.presence(escalation_message.strip),
      evaluation_delay: evaluation_delay,
      locked: false, # setting this to true prevents any edit and breaks updates when using replace workflow
      renotify_interval: renotify_interval || 0
    }
  }

  data[:id] = id if id

  options = data[:options]
  if data.fetch(:type) != "composite"
    thresholds = (options[:thresholds] = { critical: critical })

    # warning, ok, critical_recovery, and warning_recovery are optional
    [:warning, :ok, :critical_recovery, :warning_recovery].each do |key|
      if value = send(key)
        thresholds[key] = value
      end
    end

    thresholds[:critical] = critical unless
    case data.fetch(:type)
    when "service check"
      # avoid diff for default values of 1
      OPTIONAL_SERVICE_CHECK_THRESHOLDS.each { |t| thresholds[t] ||= 1 }
    when "query alert"
      # metric and query values are stored as float by datadog
      thresholds.each { |k, v| thresholds[k] = Float(v) }
    end
  end

  if windows = threshold_windows
    options[:threshold_windows] = windows
  end

  validate_json(data) if validate

  @as_json = data
end
resolve_linked_tracking_ids!(id_map, **args) click to toggle source
# File lib/kennel/models/monitor.rb, line 114
def resolve_linked_tracking_ids!(id_map, **args)
  case as_json[:type]
  when "composite", "slo alert"
    type = (as_json[:type] == "composite" ? :monitor : :slo)
    as_json[:query] = as_json[:query].gsub(/%{(.*?)}/) do
      resolve_link($1, type, id_map, **args) || $&
    end
  end
end

Private Instance Methods

require_full_window() click to toggle source
# File lib/kennel/models/monitor.rb, line 183
def require_full_window
  # default 'on_average', 'at_all_times', 'in_total' aggregations to true, otherwise false
  # https://docs.datadoghq.com/ap/#create-a-monitor
  type != "query alert" || query.start_with?("avg", "min", "sum")
end
validate_json(data) click to toggle source
# File lib/kennel/models/monitor.rb, line 189
def validate_json(data)
  super

  type = data.fetch(:type)

  # do not allow deprecated type that will be coverted by datadog and then produce a diff
  if type == "metric alert"
    invalid! "type 'metric alert' is deprecated, please set to a different type (e.g. 'query alert')"
  end

  # verify query includes critical value
  if query_value = data.fetch(:query)[/\s*[<>]=?\s*(\d+(\.\d+)?)\s*$/, 1]
    if Float(query_value) != Float(data.dig(:options, :thresholds, :critical))
      invalid! "critical and value used in query must match"
    end
  end

  # verify renotify interval is valid
  unless RENOTIFY_INTERVALS.include? data.dig(:options, :renotify_interval)
    invalid! "renotify_interval must be one of #{RENOTIFY_INTERVALS.join(", ")}"
  end

  if ["query alert", "service check"].include?(type) # TODO: most likely more types need this
    # verify is_match/is_exact_match uses available variables
    message = data.fetch(:message)
    used = message.scan(/{{\s*([#^]is(?:_exact)?_match)\s*([^\s}]+)/)
    if used.any?
      allowed = data.fetch(:query)[/by\s*[({]([^})]+)[})]/, 1]
        .to_s.gsub(/["']/, "").split(/\s*,\s*/)
        .map! { |w| %("#{w}.name") }
      used.uniq.each do |match, group|
        next if allowed.include?(group)
        invalid!(
          "#{match} used with #{group}, but can only be used with #{allowed.join(", ")}. " \
          "Group the query by #{group.sub(".name", "").tr('"', "")} or change the #{match}"
        )
      end
    end
  end

  unless ALLOWED_PRIORITY_CLASSES.include?(priority.class)
    invalid! "priority needs to be an Integer"
  end
end