flapjack 1.0.0rc3 → 1.0.0rc5

Sign up to get free protection for your applications and to get access to all the features.
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) -%>)