flapjack 1.0.0rc3 → 1.0.0rc5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -2
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +20 -0
  5. data/CONTRIBUTING.md +2 -2
  6. data/Gemfile +1 -1
  7. data/README.md +6 -16
  8. data/build.sh +13 -1
  9. data/etc/flapjack_config.yaml.example +98 -12
  10. data/features/cli.feature +8 -8
  11. data/features/cli_flapjack-nagios-receiver.feature +29 -37
  12. data/features/cli_flapper.feature +24 -12
  13. data/features/cli_simulate-failed-check.feature +2 -2
  14. data/features/notifications.feature +18 -1
  15. data/features/steps/cli_steps.rb +2 -2
  16. data/features/steps/notifications_steps.rb +71 -0
  17. data/features/support/env.rb +7 -6
  18. data/flapjack.gemspec +3 -1
  19. data/lib/flapjack/cli/flapper.rb +74 -25
  20. data/lib/flapjack/cli/import.rb +3 -4
  21. data/lib/flapjack/cli/maintenance.rb +182 -0
  22. data/lib/flapjack/cli/receiver.rb +110 -121
  23. data/lib/flapjack/cli/server.rb +30 -26
  24. data/lib/flapjack/cli/simulate.rb +2 -3
  25. data/lib/flapjack/data/contact.rb +1 -1
  26. data/lib/flapjack/data/entity.rb +425 -32
  27. data/lib/flapjack/data/entity_check.rb +212 -14
  28. data/lib/flapjack/data/event.rb +1 -1
  29. data/lib/flapjack/gateways/aws_sns.rb +134 -0
  30. data/lib/flapjack/gateways/aws_sns/alert.text.erb +5 -0
  31. data/lib/flapjack/gateways/aws_sns/rollup.text.erb +2 -0
  32. data/lib/flapjack/gateways/jabber.rb +2 -2
  33. data/lib/flapjack/gateways/jsonapi/check_methods.rb +1 -1
  34. data/lib/flapjack/gateways/jsonapi/contact_methods.rb +1 -1
  35. data/lib/flapjack/gateways/jsonapi/entity_methods.rb +15 -1
  36. data/lib/flapjack/gateways/jsonapi/metrics_methods.rb +4 -3
  37. data/lib/flapjack/gateways/jsonapi/report_methods.rb +1 -1
  38. data/lib/flapjack/gateways/web.rb +35 -16
  39. data/lib/flapjack/gateways/web/public/css/tablesort.css +0 -16
  40. data/lib/flapjack/gateways/web/public/js/backbone.jsonapi.js +1 -1
  41. data/lib/flapjack/gateways/web/public/js/jquery.tablesorter.widgets.js +0 -45
  42. data/lib/flapjack/gateways/web/public/js/modules/contact.js +2 -2
  43. data/lib/flapjack/gateways/web/public/js/modules/entity.js +2 -2
  44. data/lib/flapjack/gateways/web/public/js/modules/medium.js +4 -4
  45. data/lib/flapjack/gateways/web/public/js/self_stats.js +1 -1
  46. data/lib/flapjack/gateways/web/views/check.html.erb +10 -10
  47. data/lib/flapjack/gateways/web/views/checks.html.erb +1 -1
  48. data/lib/flapjack/gateways/web/views/contact.html.erb +5 -1
  49. data/lib/flapjack/gateways/web/views/edit_contacts.html.erb +3 -4
  50. data/lib/flapjack/gateways/web/views/entities.html.erb +1 -1
  51. data/lib/flapjack/gateways/web/views/index.html.erb +2 -2
  52. data/lib/flapjack/gateways/web/views/layout.erb +3 -3
  53. data/lib/flapjack/gateways/web/views/self_stats.html.erb +5 -6
  54. data/lib/flapjack/notifier.rb +4 -1
  55. data/lib/flapjack/patches.rb +8 -2
  56. data/lib/flapjack/pikelet.rb +3 -1
  57. data/lib/flapjack/version.rb +1 -1
  58. data/libexec/httpbroker.go +1 -1
  59. data/spec/lib/flapjack/coordinator_spec.rb +3 -3
  60. data/spec/lib/flapjack/data/contact_spec.rb +2 -2
  61. data/spec/lib/flapjack/data/entity_check_spec.rb +805 -53
  62. data/spec/lib/flapjack/data/entity_spec.rb +661 -0
  63. data/spec/lib/flapjack/gateways/aws_sns_spec.rb +123 -0
  64. data/spec/lib/flapjack/gateways/jabber_spec.rb +1 -1
  65. data/spec/lib/flapjack/gateways/jsonapi/check_methods_spec.rb +1 -1
  66. data/spec/lib/flapjack/gateways/jsonapi/entity_methods_spec.rb +2 -2
  67. data/spec/lib/flapjack/gateways/pagerduty_spec.rb +1 -1
  68. data/spec/lib/flapjack/gateways/web_spec.rb +11 -11
  69. data/spec/support/profile_all_formatter.rb +10 -10
  70. data/spec/support/uncolored_doc_formatter.rb +66 -4
  71. data/src/flapjack/event.go +1 -1
  72. data/tasks/benchmarks.rake +24 -20
  73. data/tasks/entities.rake +148 -0
  74. data/tmp/dummy_contacts.json +43 -0
  75. data/tmp/dummy_entities.json +37 -1
  76. metadata +43 -7
  77. data/tmp/test_entities.json +0 -1
@@ -7,6 +7,8 @@ require 'flapjack/patches'
7
7
  require 'flapjack/data/contact'
8
8
  require 'flapjack/data/event'
9
9
  require 'flapjack/data/entity'
10
+ #FIXME: Require chronic_duration in the correct place
11
+ require 'chronic_duration'
10
12
 
11
13
  # TODO might want to split the class methods out to a separate class, DAO pattern
12
14
  # ( http://en.wikipedia.org/wiki/Data_access_object ).
@@ -61,17 +63,17 @@ module Flapjack
61
63
  self.new(entity, check, :logger => logger, :redis => redis)
62
64
  end
63
65
 
64
- def self.find_all_for_entity_name(entity_name, options = {})
66
+ def self.find_current_for_entity_name(entity_name, options = {})
65
67
  raise "Redis connection not set" unless redis = options[:redis]
66
68
  redis.zrange("current_checks:#{entity_name}", 0, -1)
67
69
  end
68
70
 
69
- def self.find_all(options = {})
71
+ def self.find_current(options = {})
70
72
  raise "Redis connection not set" unless redis = options[:redis]
71
- self.conflate_to_keys(self.find_all_by_entity(:redis => redis))
73
+ self.conflate_to_keys(self.find_current_by_entity(:redis => redis))
72
74
  end
73
75
 
74
- def self.find_all_by_entity(options = {})
76
+ def self.find_current_by_entity(options = {})
75
77
  raise "Redis connection not set" unless redis = options[:redis]
76
78
  d = {}
77
79
  redis.zrange("current_entities", 0, -1).each {|entity|
@@ -80,19 +82,19 @@ module Flapjack
80
82
  d
81
83
  end
82
84
 
83
- def self.count_all(options = {})
85
+ def self.count_current(options = {})
84
86
  raise "Redis connection not set" unless redis = options[:redis]
85
87
  redis.zrange("current_entities", 0, -1).inject(0) {|memo, entity|
86
88
  memo + redis.zcount("current_checks:#{entity}", '-inf', '+inf')
87
89
  }
88
90
  end
89
91
 
90
- def self.find_all_failing(options = {})
92
+ def self.find_current_failing(options = {})
91
93
  raise "Redis connection not set" unless redis = options[:redis]
92
- self.conflate_to_keys(self.find_all_failing_by_entity(:redis => redis))
94
+ self.conflate_to_keys(self.find_current_failing_by_entity(:redis => redis))
93
95
  end
94
96
 
95
- def self.find_all_failing_by_entity(options = {})
97
+ def self.find_current_failing_by_entity(options = {})
96
98
  raise "Redis connection not set" unless redis = options[:redis]
97
99
  redis.zrange("failed_checks", 0, -1).inject({}) do |memo, key|
98
100
  entity, check = key.split(':', 2)
@@ -104,7 +106,7 @@ module Flapjack
104
106
  end
105
107
  end
106
108
 
107
- def self.count_all_failing(options = {})
109
+ def self.count_current_failing(options = {})
108
110
  raise "Redis connection not set" unless redis = options[:redis]
109
111
  redis.zrange("failed_checks", 0, -1).count do |key|
110
112
  entity, check = key.split(':', 2)
@@ -132,6 +134,201 @@ module Flapjack
132
134
  result
133
135
  end
134
136
 
137
+ def self.find_maintenance(options = {})
138
+ raise "Redis connection not set" unless redis = options[:redis]
139
+ type = options[:type]
140
+ keys = redis.keys("*:#{type}_maintenances")
141
+ keys.flat_map { |k|
142
+ entity = k.split(':')[0]
143
+ check = k.split(':')[1]
144
+ ec = Flapjack::Data::EntityCheck.for_entity_name(entity, check, :redis => redis)
145
+
146
+ # Only return entries which match what was passed in
147
+ case
148
+ when options[:state] && options[:state] != ec.state
149
+ nil
150
+ when options[:entity] && !Regexp.new(options[:entity]).match(entity)
151
+ nil
152
+ when options[:check] && !Regexp.new(options[:check]).match(check)
153
+ nil
154
+ else
155
+ windows = ec.maintenances(nil, nil, type.to_sym => true)
156
+ windows.map { |window|
157
+ entry = { :entity => entity,
158
+ :check => check,
159
+ :state => ec.state
160
+ }
161
+ if (options[:reason].nil? || Regexp.new(options[:reason]).match(window[:summary])) &&
162
+ check_maintenance_timestamp(options[:started], window[:start_time]) &&
163
+ check_maintenance_timestamp(options[:finishing], window[:end_time]) &&
164
+ check_maintenance_interval(options[:duration], window[:duration])
165
+ entry.merge!(window)
166
+ end
167
+ }.compact
168
+ end
169
+ }.compact
170
+ end
171
+
172
+ def self.delete_maintenance(options = {})
173
+ raise "Redis connection not set" unless redis = options[:redis]
174
+ entries = find_maintenance(options)
175
+ # Try to delete all entries passed in, but return false if any entries failed
176
+ errors = {}
177
+ entries.each do |entry|
178
+ identifier = "#{entry[:entity]}:#{entry[:check]}:#{entry[:start_time]}"
179
+ if entry[:end_time] < Time.now.to_i
180
+ errors[identifier] = "Maintenance can't be deleted as it finished in the past"
181
+ else
182
+ entity = entry[:entity]
183
+ check = entry[:check]
184
+
185
+ ec = Flapjack::Data::EntityCheck.for_entity_name(entity, check, :redis => redis)
186
+ success = case options[:type]
187
+ when 'scheduled'
188
+ ec.end_scheduled_maintenance(entry[:start_time])
189
+ when 'unscheduled'
190
+ ec.end_unscheduled_maintenance(entry[:end_time])
191
+ end
192
+ errors[identifier] = "The following entry failed to delete: #{entry}" unless success
193
+ end
194
+ end
195
+ errors
196
+ end
197
+
198
+ def self.create_maintenance(options = {})
199
+ raise "Redis connection not set" unless redis = options[:redis]
200
+ errors = {}
201
+ entities = options[:entity].is_a?(String) ? options[:entity].split(',') : options[:entity]
202
+ checks = options[:check].is_a?(String) ? options[:check].split(',') : options[:check]
203
+ entities.each do |entity|
204
+ # Create the entity if it doesn't exist, so we can schedule maintenance against it
205
+ Flapjack::Data::Entity.find_by_name(entity, :redis => redis, :create => true)
206
+ checks.each do |check|
207
+ ec = Flapjack::Data::EntityCheck.for_entity_name(entity, check, :redis => redis)
208
+ started = Chronic.parse(options[:started]).to_i
209
+ duration = ChronicDuration.parse(options[:duration]).to_i
210
+ raise "Failed to parse start time #{options[:started]}" if started == 0
211
+ raise"Failed to parse duration #{options[:duration]}" if duration == 0
212
+
213
+ success = case options[:type]
214
+ when 'scheduled'
215
+ ec.create_scheduled_maintenance(started, duration, :summary => options[:reason])
216
+ when 'unscheduled'
217
+ ec.create_unscheduled_maintenance(started, duration, :summary => options[:reason])
218
+ end
219
+ identifier = "#{entity}:#{check}:#{started}"
220
+ errors[identifier] = "The following check failed to create: #{identifier}" unless success
221
+ end
222
+ end
223
+ errors
224
+ end
225
+
226
+
227
+ def self.check_maintenance_interval(input, maintenance_duration)
228
+ # If no duration was specified, give back all results
229
+ return true unless input
230
+ inp = input.downcase
231
+
232
+ if inp.start_with?('between')
233
+ # Between 3 hours and 4 hours translates to more than 3 hours, less than 4 hours
234
+ first, last = inp.match(/between (.*) and (.*)/).captures
235
+ suffix = last.match(/\w (.*)/) ? last.match(/\w (.*)/).captures.first : ''
236
+
237
+ # If the first duration only contains only a single word, the unit is
238
+ # most likely directly after the first word of the the second duration
239
+ # eg between 3 and 4 hours
240
+ first = "#{first} #{suffix}" unless / /.match(first)
241
+ raise "Failed to parse #{first}" unless ChronicDuration.parse(first)
242
+ raise "Failed to parse #{last}" unless ChronicDuration.parse(last)
243
+
244
+ (first, last = last, first) if ChronicDuration.parse(first) > ChronicDuration.parse(last)
245
+ return check_maintenance_interval("more than #{first}", maintenance_duration) && check_maintenance_interval("less than #{last}", maintenance_duration)
246
+ end
247
+
248
+ # ChronicDuration can't parse timestamps for strings starting with before or after.
249
+ # Strip the before or after for the conversion only, but use it for the comparison later
250
+ ctime = inp.gsub(/^(more than|less than|before|after)/, '')
251
+ input_duration = ChronicDuration.parse(ctime, :keep_zero => true)
252
+
253
+ raise "Failed to parse time: #{input}" if input_duration.nil?
254
+
255
+ case inp
256
+ when /^(less than|before)/
257
+ maintenance_duration < input_duration
258
+ when /^(more than|after)/
259
+ maintenance_duration > input_duration
260
+ else
261
+ maintenance_duration == input_duration
262
+ end
263
+ end
264
+
265
+ def self.check_maintenance_timestamp(input, maintenance_timestamp)
266
+ # If no time was specified, give back all results
267
+ return true unless input
268
+ inp = input.downcase
269
+
270
+ # Chronic can't parse timestamps for strings starting with before, after or in some cases, on.
271
+ # Strip the before or after for the conversion only, but use it for the comparison later
272
+ ctime = inp.gsub(/^(on|before|after)/, '')
273
+
274
+ base_time = Time.now
275
+
276
+ case inp
277
+ # Between 3 and 4 hours ago translates to more than 3 hours ago, less than 4 hours ago
278
+ when /^between/
279
+ first, last = inp.match(/between (.*) and (.*)/).captures
280
+
281
+ # If the first time only contains only a single word, the unit (and past/future) is
282
+ # most likely directly after the first word of the the second time
283
+ # eg between 3 and 4 hours ago
284
+ suffix = last.match(/\w (.*)/) ? last.match(/\w (.*)/).captures.first : ''
285
+ first = "#{first} #{suffix}" unless / /.match(first)
286
+
287
+ first += ' from now' unless Chronic.parse(first, :now => base_time)
288
+ last += ' from now' unless Chronic.parse(last, :now => base_time)
289
+ raise "Failed to parse #{first}" unless ChronicDuration.parse(first)
290
+ raise "Failed to parse #{last}" unless ChronicDuration.parse(last)
291
+
292
+ (first, last = last, first) if Chronic.parse(first, :now => base_time) > Chronic.parse(last, :now => base_time)
293
+ (check_maintenance_timestamp("after #{first}", maintenance_timestamp) &&
294
+ check_maintenance_timestamp("before #{last}", maintenance_timestamp))
295
+ # On 1/1/15. We use Chronic to work out the minimum and maximum timestamp, and use the same behaviour as between.
296
+ when /^on/
297
+ first = Chronic.parse(ctime, :guess => false, :now => base_time).first
298
+ last = Chronic.parse(ctime, :guess => false, :now => base_time).last
299
+ (check_maintenance_timestamp("after #{first}", maintenance_timestamp) &&
300
+ check_maintenance_timestamp("before #{last}", maintenance_timestamp))
301
+ else
302
+ # We assume timestamps are rooted against the current time.
303
+ # Chronic doesn't always handle this correctly, so we need to handhold it a little
304
+ input_timestamp = Chronic.parse(ctime, :keep_zero => true, :now => base_time).to_i
305
+ input_timestamp = Chronic.parse(ctime + ' from now', :keep_zero => true, :now => base_time).to_i if input_timestamp == 0
306
+
307
+ raise "Failed to parse time: #{input}" if input_timestamp == 0
308
+
309
+ case inp
310
+ when /^less than/
311
+ if input_timestamp < base_time.to_i
312
+ maintenance_timestamp > input_timestamp
313
+ else
314
+ maintenance_timestamp < input_timestamp
315
+ end
316
+ when /^more than/
317
+ # FIXME: and here is the race condition. input timestamp could be in the previous second
318
+ # to Time.now due to code execution time:
319
+ if input_timestamp < base_time.to_i
320
+ maintenance_timestamp < input_timestamp
321
+ else
322
+ maintenance_timestamp > input_timestamp
323
+ end
324
+ when /^before/
325
+ maintenance_timestamp < input_timestamp
326
+ when /^after/
327
+ maintenance_timestamp > input_timestamp
328
+ end
329
+ end
330
+ end
331
+
135
332
  def self.in_unscheduled_maintenance_for_event_id?(event_id, options)
136
333
  raise "Redis connection not set" unless redis = options[:redis]
137
334
  redis.exists("#{event_id}:unscheduled_maintenance")
@@ -184,10 +381,9 @@ module Flapjack
184
381
 
185
382
  checks = []
186
383
  # get all the current checks, with last update time
187
- Flapjack::Data::Entity.find_all_current(:redis => redis).each do |entity|
188
- redis.zrange("current_checks:#{entity}", 0, -1, {:withscores => true}).each do |check|
189
- check[0] = "#{entity}:#{check[0]}"
190
- checks << check
384
+ Flapjack::Data::Entity.all(:enabled => true, :redis => redis).each do |entity|
385
+ redis.zrange("current_checks:#{entity}", 0, -1, :withscores => true).each do |check, score|
386
+ checks << ["#{entity}:#{check}", score]
191
387
  end
192
388
  end
193
389
 
@@ -270,10 +466,11 @@ module Flapjack
270
466
  if (um_start = @redis.get("#{@key}:unscheduled_maintenance"))
271
467
  duration = end_time - um_start.to_i
272
468
  @logger.debug("ending unscheduled downtime for #{@key} at #{Time.at(end_time).to_s}") if @logger
273
- @redis.del("#{@key}:unscheduled_maintenance")
274
469
  @redis.zadd("#{@key}:unscheduled_maintenances", duration, um_start) # updates existing UM 'score'
470
+ @redis.del("#{@key}:unscheduled_maintenance") == 1
275
471
  else
276
472
  @logger.debug("end_unscheduled_maintenance called for #{@key} but none found") if @logger
473
+ true
277
474
  end
278
475
  end
279
476
 
@@ -392,6 +589,7 @@ module Flapjack
392
589
  end
393
590
 
394
591
  # Retain event data for entity:check pair
592
+ # NB (appending to tail as far as Redis is concerned)
395
593
  @redis.rpush("#{@key}:states", timestamp)
396
594
  @redis.set("#{@key}:#{timestamp}:state", new_state)
397
595
  @redis.set("#{@key}:#{timestamp}:summary", summary) if summary
@@ -124,7 +124,7 @@ module Flapjack
124
124
  if parsed.is_a?(Hash)
125
125
  errors = validation_errors_for_hash(parsed, opts)
126
126
  else
127
- errors << "Event must be a JSON hash, see https://github.com/flapjack/flapjack/wiki/DATA_STRUCTURES#event-queue"
127
+ errors << "Event must be a JSON hash, see http://flapjack.io/docs/1.0/development/DATA_STRUCTURES#event-queue"
128
128
  end
129
129
  return parsed if errors.empty?
130
130
  end
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'em-synchrony'
4
+ require 'em-synchrony/em-http'
5
+ require 'active_support/inflector'
6
+
7
+ require 'flapjack/data/alert'
8
+ require 'flapjack/utility'
9
+
10
+ module Flapjack
11
+ module Gateways
12
+ class AwsSns
13
+
14
+ SNS_DEFAULT_REGION_NAME = 'us-east-1'
15
+
16
+ class << self
17
+
18
+ include Flapjack::Utility
19
+
20
+ def start
21
+ @sent = 0
22
+ end
23
+
24
+ def perform(contents)
25
+ @logger.debug "Received a notification: #{contents.inspect}"
26
+ alert = Flapjack::Data::Alert.new(contents, :logger => @logger)
27
+
28
+ region_name = @config["region_name"] || SNS_DEFAULT_REGION_NAME
29
+ hostname = "sns.#{region_name}.amazonaws.com"
30
+ endpoint = "http://#{hostname}/"
31
+ access_key = @config["access_key"]
32
+ secret_key = @config["secret_key"]
33
+ timestamp = @config["timestamp"] || DateTime.now.iso8601
34
+
35
+ address = alert.address
36
+ notification_id = alert.notification_id
37
+ message_type = alert.rollup ? 'rollup' : 'alert'
38
+
39
+ my_dir = File.dirname(__FILE__)
40
+ sms_template_path = case
41
+ when @config.has_key?('templates') && @config['templates']["#{message_type}.text"]
42
+ @config['templates']["#{message_type}.text"]
43
+ else
44
+ my_dir + "/aws_sns/#{message_type}.text.erb"
45
+ end
46
+ sms_template = ERB.new(File.read(sms_template_path), nil, '-')
47
+
48
+ @alert = alert
49
+ bnd = binding
50
+
51
+ begin
52
+ message = sms_template.result(bnd).chomp
53
+ rescue => e
54
+ @logger.error "Error while excuting the ERB for an sms: " +
55
+ "ERB being executed: #{sms_template_path}"
56
+ raise
57
+ end
58
+
59
+ if @config.nil? || (@config.respond_to?(:empty?) && @config.empty?)
60
+ @logger.error "AWS SNS config is missing"
61
+ return
62
+ end
63
+
64
+ errors = []
65
+
66
+ [[address, "AWS SNS topic ARN is missing"],
67
+ [access_key, "AWS SNS access key is missing"],
68
+ [secret_key, "AWS SNS secret key is missing"],
69
+ [notification_id, "Notification id is missing"]].each do |val_err|
70
+
71
+ next unless val_err.first.nil? || (val_err.first.respond_to?(:empty?) && val_err.first.empty?)
72
+ errors << val_err.last
73
+ end
74
+
75
+ unless errors.empty?
76
+ errors.each {|err| @logger.error err }
77
+ return
78
+ end
79
+
80
+
81
+ query = {'Subject' => message,
82
+ 'TopicArn' => address,
83
+ 'Message' => message,
84
+ 'Action' => 'Publish',
85
+ 'SignatureVersion' => 2,
86
+ 'SignatureMethod' => 'HmacSHA256',
87
+ 'Timestamp' => timestamp,
88
+ 'AWSAccessKeyId' => access_key}
89
+
90
+ string_to_sign = string_to_sign('POST', hostname, "/", query)
91
+
92
+ query['Signature'] = get_signature(secret_key, string_to_sign)
93
+
94
+ http = EM::HttpRequest.new(endpoint).post(:query => query)
95
+
96
+ @logger.debug "server response: #{http.response}"
97
+
98
+ status = (http.nil? || http.response_header.nil?) ? nil : http.response_header.status
99
+ if (status >= 200) && (status <= 206)
100
+ @sent += 1
101
+ alert.record_send_success!
102
+ @logger.debug "Sent notification via SNS, response status is #{status}, " +
103
+ "notification_id: #{notification_id}"
104
+ else
105
+ @logger.error "Failed to send notification via SNS, response status is #{status}, " +
106
+ "notification_id: #{notification_id}"
107
+ end
108
+ rescue => e
109
+ @logger.error "Error generating or delivering notification to #{address}: #{e.class}: #{e.message}"
110
+ @logger.error e.backtrace.join("\n")
111
+ raise
112
+ end
113
+
114
+ def get_signature(secret_key, string_to_sign)
115
+ signature = OpenSSL::HMAC.digest('sha256', secret_key, string_to_sign)
116
+
117
+ Base64.encode64(signature).strip
118
+ end
119
+
120
+ def string_to_sign(method, host, uri, query)
121
+ query = query.sort_by { |key, value| key }
122
+
123
+ [method.upcase,
124
+ host.downcase,
125
+ uri,
126
+ URI.encode_www_form(query)
127
+ ].join("\n")
128
+ end
129
+
130
+ end
131
+ end
132
+ end
133
+ end
134
+
@@ -0,0 +1,5 @@
1
+ <%= @alert.type_sentence_case %>: '<%= @alert.check %>' on <%= @alert.entity -%>
2
+ <% unless ['acknowledgement', 'test'].include?(@alert.notification_type) -%>
3
+ is <%= @alert.state_title_case -%>
4
+ <% end -%>
5
+ at <%= Time.at(@alert.time).strftime('%-d %b %H:%M') %>, <%= @alert.summary -%>
@@ -0,0 +1,2 @@
1
+ <%= @alert.type_sentence_case %>: <%= @alert.rollup_states_summary -%>
2
+ (<%= @alert.rollup_states_detail_text(:max_checks_per_state => 3) -%>)