lib/hawkular/alerts/alerts_api.rb
require 'hawkular/base_client'
require 'ostruct'
# Alerts module provides access to Hawkular-Alerts.
# There are three main parts here:
# Triggers, that define alertable conditions
# Alerts, that represent a fired Alert trigger
# Events, that represent a fired Event trigger or an externally injected event (hawkular, not miq event)
# @see http://www.hawkular.org/docs/rest/rest-alerts.html
module Hawkular::Alerts
# Interface to use to talk to the Hawkular-Alerts component.
# @param entrypoint [String] base url of Hawkular-Alerts - e.g
# http://localhost:8080/hawkular/alerts
# @param credentials [Hash{String=>String}] Hash of username, password, token(optional)
# @param options [Hash{String=>String}] Additional rest client options
class Client < Hawkular::BaseClient
def initialize(entrypoint, credentials = {}, options = {})
entrypoint = normalize_entrypoint_url entrypoint, 'hawkular/alerts'
@entrypoint = entrypoint
super(entrypoint, credentials, options)
end
# Return version and status information for the used version of Hawkular-Alerting
# @return [Hash{String=>String}]
# ('Implementation-Version', 'Built-From-Git-SHA1', 'status')
def fetch_version_and_status
http_get('/status')
end
# Lists defined triggers in the system
# @param [Array] ids List of trigger ids. If provided, limits to the given triggers
# @param [Array] tags List of tags. If provided, limits to the given tags. Individual
# tags are of the format # key|value. Tags are OR'd together. If a tag-key shows up
# more than once, only the last one is accepted
# @return [Array<Trigger>] Triggers found
def list_triggers(ids = [], tags = [])
query = generate_query_params 'triggerIds' => ids, 'tags' => tags
sub_url = '/triggers' + query
ret = http_get(sub_url)
val = []
ret.each { |t| val.push(Trigger.new(t)) }
val
end
# Obtains one Trigger definition from the server.
# @param [String] trigger_id Id of the trigger to fetch
# @param full If true then conditions and dampenings for the trigger are also fetched
# @return [Trigger] the selected trigger
def get_single_trigger(trigger_id, full = false)
the_trigger = '/triggers/' + trigger_id
ret = http_get(the_trigger)
trigger = Trigger.new(ret)
if full
ret = http_get(the_trigger + '/conditions')
ret.each { |c| trigger.conditions.push(Trigger::Condition.new(c)) }
ret = http_get(the_trigger + '/dampenings')
ret.each { |c| trigger.dampenings.push(Trigger::Dampening.new(c)) }
end
trigger
end
# Import multiple trigger or action definitions specified as a hash to the server.
# @param [Hash] hash The hash with the trigger and action definitions.
# see the https://git.io/va5UO for more details about the structure
# @return [Hash] The newly entities as hash
def bulk_import_triggers(hash)
http_post 'import/all', hash
end
# Creates the trigger definition.
# @param [Trigger] trigger The trigger to be created
# @param [Array<Condition>] conditions Array of associated conditions
# @param [Array<Dampening>] dampenings Array of associated dampenings
# @return [Trigger] The newly created trigger
def create_trigger(trigger, conditions = [], dampenings = [], _actions = [])
full_trigger = {}
full_trigger[:trigger] = trigger.to_h
conds = []
conditions.each { |c| conds.push(c.to_h) }
full_trigger[:conditions] = conds
damps = []
dampenings.each { |d| damps.push(d.to_h) } unless dampenings.nil?
full_trigger[:dampenings] = damps
http_post 'triggers/trigger', full_trigger
end
# Creates the group trigger definition.
# @param [Trigger] trigger The group trigger to be created
# @return [Trigger] The newly created group trigger
def create_group_trigger(trigger)
ret = http_post 'triggers/groups', trigger.to_h
Trigger.new(ret)
end
# Updates a given group trigger definition
# @param [Trigger] trigger the group trigger to be updated
# @return [Trigger] The updated group trigger
def update_group_trigger(trigger)
http_put "triggers/groups/#{trigger.id}/", trigger.to_h
get_single_trigger trigger.id, false
end
# Creates the group conditions definitions.
# @param [String] trigger_id ID of the group trigger to set conditions
# @param [String] trigger_mode Mode of the trigger where conditions are attached (:FIRING, :AUTORESOLVE)
# @param [GroupConditionsInfo] group_conditions_info the conditions to set into the group trigger with the mapping
# with the data_id members map
# @return [Array<Condition>] conditions Array of associated conditions
def set_group_conditions(trigger_id, trigger_mode, group_conditions_info)
ret = http_put "triggers/groups/#{trigger_id}/conditions/#{trigger_mode}", group_conditions_info.to_h
conditions = []
ret.each { |c| conditions.push(Trigger::Condition.new(c)) }
conditions
end
# Creates a member trigger
# @param [GroupMemberInfo] group_member_info the group member to be added
# @return [Trigger] the newly created member trigger
def create_member_trigger(group_member_info)
ret = http_post 'triggers/groups/members', group_member_info.to_h
Trigger.new(ret)
end
# Detaches a member trigger from its group trigger
# @param [String] trigger_id ID of the member trigger to detach
# @return [Trigger] the orphan trigger
def orphan_member(trigger_id)
http_post "triggers/groups/members/#{trigger_id}/orphan", {}
get_single_trigger trigger_id, false
end
# Lists members of a group trigger
# @param [String] trigger_id ID of the group trigger to list members
# @param [boolean] orphans flag to include orphans
# @return [Array<Trigger>] Members found
def list_members(trigger_id, orphans = false)
ret = http_get "triggers/groups/#{trigger_id}/members?includeOrphans=#{orphans}"
ret.collect { |t| Trigger.new(t) }
end
# Creates a dampening for a group trigger
# @param [Dampening] dampening the dampening to create
# @return [Dampening] the newly created dampening
def create_group_dampening(dampening)
ret = http_post "triggers/groups/#{dampening.trigger_id}/dampenings", dampening.to_h
Trigger::Dampening.new(ret)
end
# Updates a dampening for a group trigger
# @param [Dampening] dampening the dampening to update
# @return [Dampening] the updated dampening
def update_group_dampening(dampening)
ret = http_put "triggers/groups/#{dampening.trigger_id}/dampenings/#{dampening.dampening_id}", dampening.to_h
Trigger::Dampening.new(ret)
end
# Deletes the dampening of a group trigger
# @param [String] trigger_id ID of the group trigger
# @param [String] dampening_id ID
def delete_group_dampening(trigger_id, dampening_id)
http_delete "/triggers/groups/#{trigger_id}/dampenings/#{dampening_id}"
end
# Deletes the trigger definition.
# @param [String] trigger_id ID of the trigger to delete
def delete_trigger(trigger_id)
http_delete "/triggers/#{trigger_id}"
end
# Deletes the group trigger definition.
# @param [String] trigger_id ID of the group trigger to delete
def delete_group_trigger(trigger_id)
http_delete "/triggers/groups/#{trigger_id}"
end
# Obtains action definition/plugin from the server.
# @param [String] action_plugin Id of the action plugin to fetch. If nil, all the plugins are fetched
def get_action_definition(action_plugin = nil)
plugins = action_plugin.nil? ? http_get('plugins') : [action_plugin]
ret = {}
plugins.each do |p|
ret[p] = http_get("/plugins/#{p}")
end
ret
end
# Creates the action.
# @param [String] plugin The id of action definition/plugin
# @param [String] action_id The id of action
# @param [Hash] properties Troperties of action
# @return [Action] The newly created action
def create_action(plugin, action_id, properties = {})
the_plugin = hawk_escape plugin
# Check if plugin exists
http_get("/plugins/#{the_plugin}")
payload = { actionId: action_id, actionPlugin: plugin, properties: properties }
ret = http_post('/actions', payload)
Trigger::Action.new(ret)
end
# Obtains one action of given action plugin from the server.
# @param [String] plugin Id of the action plugin
# @param [String] action_id Id of the action
# @return [Action] the selected trigger
def get_action(plugin, action_id)
the_plugin = hawk_escape plugin
the_action_id = hawk_escape action_id
ret = http_get "/actions/#{the_plugin}/#{the_action_id}"
Trigger::Action.new(ret)
end
# Deletes the action of given action plugin.
# @param [String] plugin Id of the action plugin
# @param [String] action_id Id of the action
def delete_action(plugin, action_id)
the_plugin = hawk_escape plugin
the_action_id = hawk_escape action_id
http_delete "/actions/#{the_plugin}/#{the_action_id}"
end
# Obtain the alerts for the Trigger with the passed id
# @param [String] trigger_id Id of the trigger that has fired the alerts
# @return [Array<Alert>] List of alerts for the trigger. Can be empty
def get_alerts_for_trigger(trigger_id)
# TODO: add additional filters
return [] unless trigger_id
url = '/?triggerIds=' + trigger_id
ret = http_get(url)
val = []
ret.each { |a| val.push(Alert.new(a)) }
val
end
# List fired alerts
# @param [Hash]criteria optional query criteria
# @return [Array<Alert>] List of alerts in the system. Can be empty
def list_alerts(criteria = {})
alerts(criteria: criteria)
end
# List fired alerts
# @param [Hash] criteria optional query criteria
# @param [Array] tenants optional list of tenants. The elements of the array can be any object
# convertible to a string
# @return [Array<Alert>] List of alerts in the system. Can be empty
def alerts(criteria: {}, tenants: nil)
query = generate_query_params(criteria)
uri = tenants ? '/admin/alerts/' : '/'
ret = http_get(uri + query, multi_tenants_header(tenants))
val = []
ret.each { |a| val.push(Alert.new(a)) }
val
end
# Retrieve a single Alert by its id
# @param [String] alert_id id of the alert to fetch
# @return [Alert] Alert object retrieved
def get_single_alert(alert_id)
ret = http_get('/alert/' + alert_id)
val = Alert.new(ret)
val
end
# Mark one alert as resolved
# @param [String] alert_id Id of the alert to resolve
# @param [String] by name of the user resolving the alert
# @param [String] comment A comment on the resolution
def resolve_alert(alert_id, by = nil, comment = nil)
sub_url = "/resolve/#{alert_id}"
query = generate_query_params 'resolvedBy' => by, 'resolvedNotes' => comment
sub_url += query
http_put(sub_url, {})
true
end
# Mark one alert as acknowledged
# @param [String] alert_id Id of the alert to ack
# @param [String] by name of the user acknowledging the alert
# @param [String] comment A comment on the acknowledge
def acknowledge_alert(alert_id, by = nil, comment = nil)
sub_url = "/ack/#{alert_id}"
query = generate_query_params 'ackBy' => by, 'ackNotes' => comment
sub_url += query
http_put(sub_url, {})
true
end
# List Events given optional criteria. Criteria keys are strings (not symbols):
# startTime numeric, milliseconds from epoch
# endTime numeric, milliseconds from epoch
# eventIds array of strings
# triggerIds array of strings
# categories array of strings
# tags array of strings, each tag of format 'name|value'. Specify '*' for value to match all values
# thin boolean, return lighter events (omits triggering data for trigger-generated events)
# @param [Hash] criteria optional query criteria
# @return [Array<Event>] List of events. Can be empty
def list_events(criteria = {})
events(criteria: criteria)
end
# List Events given optional criteria. Criteria keys are strings (not symbols):
# startTime numeric, milliseconds from epoch
# endTime numeric, milliseconds from epoch
# eventIds array of strings
# triggerIds array of strings
# categories array of strings
# tags array of strings, each tag of format 'name|value'. Specify '*' for value to match all values
# thin boolean, return lighter events (omits triggering data for trigger-generated events)
# @param [Hash] criteria optional query criteria
# @param [Array] tenants optional list of tenants. The elements of the array can be any object
# convertible to a string
# @return [Array<Event>] List of events. Can be empty
def events(criteria: {}, tenants: nil)
query = generate_query_params(criteria)
uri = tenants ? '/admin/events' : '/events'
http_get(uri + query, multi_tenants_header(tenants)).map { |e| Event.new(e) }
end
# Inject an event into Hawkular-alerts
# @param [String] id Id of the event must be unique
# @param [String] category Event category for further distinction
# @param [String] text Some text to the user
# @param [Hash<String,Object>] extras additional parameters
def create_event(id, category, text, extras)
event = {}
event['id'] = id
event['ctime'] = Time.now.to_i * 1000
event['category'] = category
event['text'] = text
event.merge!(extras) { |_key, v1, _v2| v1 }
http_post('/events', event)
end
def delete_event(id)
http_delete "/events/#{id}"
end
# Add tags to existing Alerts.
# @param [Array<numeric>] alert_ids List of alert IDs
# @param [Array<String>] tags List of tags. Each tag of format 'name|value'.
def add_tags(alert_ids, tags)
query = generate_query_params(alertIds: alert_ids, tags: tags)
http_put('/tags' + query, {})
end
# Remove tags from existing Alerts.
# @param [Array<numeric>] alert_ids List of alert IDs
# @param [Array<String>] tag_names List of tag names.
def remove_tags(alert_ids, tag_names)
query = generate_query_params(alertIds: alert_ids, tagNames: tag_names)
http_delete('/tags' + query)
end
private
# Builds the tenant HTTP header for multi-tenant operations
# @param [Array] tenants an array of tenant names. The elements of the array can
# be any object convertible to a string. Can be nil
# @return [Hash] The HTTP header for multi-tenant operations. An empty hash is returned
# if tenants parameter is nil
def multi_tenants_header(tenants)
tenants = tenants.join(',') if tenants.respond_to?(:join)
tenants ? { 'Hawkular-Tenant': tenants } : {}
end
end
# Representation of one Trigger
## (22 known properties: "enabled", "autoResolveMatch", "name", "memberOf", "autoEnable",
# "firingMatch", "tags", "id", "source", "tenantId", "eventText", "context", "eventType",
# "autoResolveAlerts", "dataIdMap", "eventCategory", "autoDisable", "type", "description",
# "severity", "autoResolve", "actions"])
class Trigger
attr_accessor :id, :name, :context, :actions, :auto_disable, :auto_enable
attr_accessor :auto_resolve, :auto_resolve_alerts, :tags, :type
attr_accessor :tenant, :description, :group, :severity, :event_type, :event_category, :member_of, :data_id_map
attr_reader :conditions, :dampenings
attr_accessor :enabled, :firing_match, :auto_resolve_match
def initialize(trigger_hash)
return if trigger_hash.nil?
@_hash = trigger_hash
@conditions = []
@dampenings = []
@actions = []
@id = trigger_hash['id']
@name = trigger_hash['name']
@enabled = trigger_hash['enabled']
@severity = trigger_hash['severity']
@auto_resolve = trigger_hash['autoResolve']
@auto_resolve_alerts = trigger_hash['autoResolveAlerts']
@event_type = trigger_hash['eventType']
@event_category = trigger_hash['eventCategory']
@member_of = trigger_hash['memberOf']
@data_id_map = trigger_hash['dataIdMap']
@tenant = trigger_hash['tenantId']
@description = trigger_hash['description']
@auto_enable = trigger_hash['autoEnable']
@auto_disable = trigger_hash['autoDisable']
@context = trigger_hash['context']
@type = trigger_hash['type']
@tags = trigger_hash['tags']
@firing_match = trigger_hash['firingMatch']
@auto_resolve_match = trigger_hash['autoResolveMatch']
# acts = trigger_hash['actions']
# acts.each { |a| @actions.push(Action.new(a)) } unless acts.nil?
end
def to_h
trigger_hash = {}
to_camel = lambda do |x|
ret = x.to_s.split('_').collect(&:capitalize).join
ret[0, 1].downcase + ret[1..-1]
end
fields = %i[id name enabled severity auto_resolve auto_resolve_alerts
event_type event_category description auto_enable auto_disable
context type tags member_of data_id_map firing_match
auto_resolve_match]
fields.each do |field|
camelized_field = to_camel.call(field)
field_value = __send__ field
trigger_hash[camelized_field] = field_value unless field_value.nil?
end
trigger_hash['tenantId'] = @tenant unless @tenant.nil?
trigger_hash['actions'] = []
@actions.each { |d| trigger_hash['actions'].push d.to_h }
trigger_hash
end
# Representing of one Condition
class Condition
attr_accessor :condition_id, :type, :operator, :threshold
attr_accessor :trigger_mode, :data_id, :data2_id, :data2_multiplier
attr_accessor :alerter_id, :expression
attr_reader :condition_set_size, :condition_set_index, :trigger_id
def initialize(cond_hash)
@condition_id = cond_hash['conditionId']
@type = cond_hash['type']
@operator = cond_hash['operator']
@threshold = cond_hash['threshold']
@type = cond_hash['type']
@trigger_mode = cond_hash['triggerMode']
@data_id = cond_hash['dataId']
@data2_id = cond_hash['data2Id']
@data2_multiplier = cond_hash['data2Multiplier']
@trigger_id = cond_hash['triggerId']
@alerter_id = cond_hash['alerterId']
@expression = cond_hash['expression']
end
def to_h
cond_hash = {}
cond_hash['conditionId'] = @condition_id
cond_hash['type'] = @type
cond_hash['operator'] = @operator
cond_hash['threshold'] = @threshold
cond_hash['type'] = @type
cond_hash['triggerMode'] = @trigger_mode
cond_hash['dataId'] = @data_id
cond_hash['data2Id'] = @data2_id
cond_hash['data2Multiplier'] = @data2_multiplier
cond_hash['triggerId'] = @trigger_id
cond_hash['alerterId'] = @alerter_id
cond_hash['expression'] = @expression
cond_hash
end
end
# Representing of one GroupConditionsInfo
# - The data_id_member_map should be null if the group has no members.
# - The data_id_member_map should be null if this is a [data-driven] group trigger.
# In this case the member triggers are removed and will be re-populated as incoming data demands.
# - For [non-data-driven] group triggers with existing members the data_id_member_map is handled as follows.
# For members not included in the dataIdMemberMap their most recently supplied dataIdMap will be used.
# This means that it is not necessary to supply mappings if the new condition set uses only dataIds found
# in the old condition set. If the new conditions introduce new dataIds a full dataIdMemberMap must be supplied.
class GroupConditionsInfo
attr_accessor :conditions, :data_id_member_map
def initialize(conditions)
@conditions = conditions
@data_id_member_map = {}
end
def to_h
cond_hash = {}
cond_hash['conditions'] = []
@conditions.each { |c| cond_hash['conditions'].push(c.to_h) }
cond_hash['dataIdMemberMap'] = @data_id_member_map
cond_hash
end
end
# Representing of one GroupMemberInfo
class GroupMemberInfo
attr_accessor :group_id, :member_id, :member_name, :member_description
attr_accessor :member_context, :member_tags, :data_id_map
def to_h
cond_hash = {}
cond_hash['groupId'] = @group_id
cond_hash['memberId'] = @member_id
cond_hash['memberName'] = @member_name
cond_hash['memberDescription'] = @member_description
cond_hash['memberContext'] = @member_context
cond_hash['memberTags'] = @member_tags
cond_hash['dataIdMap'] = @data_id_map
cond_hash
end
end
# Representation of one Dampening setting
class Dampening
attr_accessor :dampening_id, :trigger_id, :type, :eval_true_setting, :eval_total_setting,
:eval_time_setting
attr_accessor :current_evals
def initialize(damp_hash)
@current_evals = {}
@dampening_id = damp_hash['dampeningId']
@trigger_id = damp_hash['triggerId']
@type = damp_hash['type']
@eval_true_setting = damp_hash['evalTrueSetting']
@eval_total_setting = damp_hash['evalTotalSetting']
@eval_time_setting = damp_hash['evalTimeSetting']
end
def to_h
cond_hash = {}
cond_hash['dampeningId'] = @dampening_id
cond_hash['triggerId'] = @trigger_id
cond_hash['type'] = @type
cond_hash['evalTrueSetting'] = @eval_true_setting
cond_hash['evalTotalSetting'] = @eval_total_setting
cond_hash['evalTimeSetting'] = @eval_time_setting
cond_hash
end
end
class Action
attr_accessor :action_plugin, :action_id, :states, :tenant_id
def initialize(action_hash)
return if action_hash.nil?
@action_plugin = action_hash['actionPlugin']
@action_id = action_hash['actionId']
@tenant_id = action_hash['tenantId']
@states = action_hash['states']
end
def to_h
action_hash = {}
action_hash['actionPlugin'] = @action_plugin
action_hash['actionId'] = @action_id
action_hash['tenantId'] = @tenant_id
action_hash['states'] = @states
action_hash
end
end
end
# Representation of one alert.
# The name of the members are literally what they are in the JSON sent from the
# server and not 'rubyfied'. So 'alertId' and not 'alert_id'
# Check http://www.hawkular.org/docs/rest/rest-alerts.html#Alert for details
class Alert < OpenStruct
attr_accessor :lifecycle
def initialize(alert_hash)
super(alert_hash)
@lifecycle = alert_hash['lifecycle']
end
def ack_by
status_by('ACKNOWLEDGED')
end
def resolved_by
status_by('RESOLVED')
end
def status_by(status)
a = @lifecycle.nil? ? [] : @lifecycle.select { |l| l['status'].eql? status }
a.empty? ? nil : a.last['user']
end
# for some API back compatibility
alias ackBy ack_by
alias resolvedBy resolved_by
end
# Representation of one event.
# The name of the members are literally what they are in the JSON sent from the
# server and not 'rubyfied'. So 'eventId' and not 'event_id'
# Check http://www.hawkular.org/docs/rest/rest-alerts.html#Event for details
class Event < OpenStruct
def initialize(event_hash)
super(event_hash)
end
end
end