skyeye 0.0.1

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.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ Gemfile.lock
2
+ pkg
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem "aws-sdk"
4
+ gem "open4"
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Greg Fodor
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,5 @@
1
+ SkyEye
2
+ ======
3
+
4
+ SkyEye is a gem that allows the creation and management of nagios-like alerts
5
+ using CloudWatch.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/skyeye ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.dirname(__FILE__) + "/../lib"
4
+
5
+ require "aws-sdk"
6
+ require "skyeye"
7
+
8
+ @exec = SkyEye::Exec.new
9
+
10
+ trap("INT") do
11
+ @exec.shutdown!
12
+ end
13
+
14
+ trap("TERM") do
15
+ @exec.shutdown!
16
+ end
17
+
18
+ @exec.go(*ARGV)
19
+
@@ -0,0 +1,48 @@
1
+ :namespace: posto-dev
2
+
3
+ :topics:
4
+ - posto-sauron-alerts
5
+
6
+ :alarm_periods:
7
+ :warning: 5
8
+ :critical: 1
9
+
10
+ :watches:
11
+ :instance:
12
+ - :name: posto-mysql
13
+ :command: /usr/local/sbin/check_tcp -H localhost -p 3306
14
+ :interval: 5
15
+ - :name: CPUUtilization
16
+ :comparison_operator: GreaterThanThreshold
17
+ :warning: 50
18
+ :critical: 80
19
+ :elb:
20
+ - :name: HealthyHostCount
21
+ :comparison_operator: LessThanThreshold
22
+ :warning: 2
23
+ :critical: 1
24
+ :statistic: Minimum
25
+ :period: 1
26
+ :rds:
27
+ - :name: CPUUtilization
28
+ :comparison_operator: GreaterThanThreshold
29
+ :warning: 50
30
+ :critical: 80
31
+ :statistic: Average
32
+ :period: 2
33
+ - :name: DatabaseConnections
34
+ :comparison_operator: GreaterThanThreshold
35
+ :critical: 128
36
+ :statistic: Maximum
37
+ :period: 1
38
+ :elasticache:
39
+ - :name: CmdGet
40
+ :comparison_operator: GreaterThanThreshold
41
+ :critical: 6000
42
+ :period: 1
43
+ - :name: SwapUsage
44
+ :comparison_operator: GreaterThanThreshold
45
+ :critical: 0
46
+ :statistic: Minimum
47
+ :period: 5
48
+
@@ -0,0 +1,300 @@
1
+ require "logger"
2
+ require "pp"
3
+ require "json"
4
+ require "open4"
5
+
6
+ module SkyEye
7
+ class Exec
8
+ attr_accessor :config
9
+
10
+ def instance_id
11
+ "i-d3b842a1"
12
+ end
13
+
14
+ def go(*args)
15
+ config_file = ENV["SKYEYE_CONFIG_FILE"] || "/etc/skyeye.yml"
16
+
17
+ unless File.exists?(config_file)
18
+ puts "Missing #{config_file}. You can set the path to this file by exporting SKYEYE_CONFIG_FILE."
19
+ return
20
+ end
21
+
22
+ @config = YAML.load(config_file)
23
+ @logger = Logger.new(STDOUT)
24
+ @mutex = Mutex.new
25
+
26
+ if args.size == 0
27
+ start!
28
+ elsif args[0] == "register:instance"
29
+ load_or_register_topics!
30
+ register_instance_alarms!
31
+ elsif args[0] == "deregister:instance"
32
+ deregister_instance_alarms!
33
+ elsif args[0] == "deregister:aws"
34
+ deregister_aws_alarms!
35
+ elsif args[0] == "register:aws"
36
+ load_or_register_topics!
37
+ register_aws_alarms!
38
+ else
39
+ puts "unknown command #{args[0]}"
40
+ end
41
+ end
42
+
43
+ def deregister_instance_alarms!
44
+ deregister_alarms_matching!(/^skyeye::instance::#{instance_id}::/)
45
+ end
46
+
47
+ def deregister_aws_alarms!
48
+ @config[:watches].keys.each do |resource_type|
49
+ unless resource_type == :instance
50
+ deregister_alarms_matching!(/^skyeye::#{resource_type}::/)
51
+ end
52
+ end
53
+ end
54
+
55
+ def deregister_alarms_matching!(pattern)
56
+ cw = AWS::CloudWatch.new
57
+ alarms = cw.client.describe_alarms
58
+
59
+ to_delete = []
60
+
61
+ alarms.data[:metric_alarms].each do |alarm|
62
+ if alarm[:alarm_name] =~ pattern
63
+ to_delete << alarm[:alarm_name]
64
+ end
65
+ end
66
+
67
+ if to_delete.size > 0
68
+ @logger.info "De-Register Alarms #{to_delete.inspect}"
69
+ cw.alarms.delete(*to_delete)
70
+ end
71
+ end
72
+
73
+ def register_aws_alarms!
74
+ @config[:watches].each do |resource_type, watches|
75
+ dimension_values = dimension_values_for_resource_type(resource_type)
76
+ register_alarms_for_watches!(resource_type, dimension_values) if dimension_values
77
+ end
78
+ end
79
+
80
+ def dimension_values_for_resource_type(resource_type)
81
+ values = []
82
+
83
+ case resource_type
84
+ when :elb
85
+ AWS::ELB.new.load_balancers.each do |load_balancer|
86
+ values << [{ name: "LoadBalancerName", value: load_balancer.name }]
87
+ end
88
+ when :rds
89
+ AWS::RDS.new.db_instances.each do |db_instance|
90
+ values << [{ name: "DBInstanceIdentifier", value: db_instance.db_instance_identifier }]
91
+ end
92
+ when :elasticache
93
+ AWS::ElastiCache.new.client.describe_cache_clusters(show_cache_node_info: true).data[:cache_clusters].each do |cluster|
94
+ cluster[:cache_nodes].each do |node|
95
+ values << [{ name: "CacheClusterId", value: cluster[:cache_cluster_id] },
96
+ { name: "CacheNodeId", value: node[:cache_node_id] }]
97
+ end
98
+ end
99
+ when :sqs
100
+ AWS::SQS.new.queues.each do |queue|
101
+ values << [{ name: "QueueName", value: queue.arn.split(/:/)[-1] }]
102
+ end
103
+ end
104
+
105
+ values
106
+ end
107
+
108
+ def namespace_for_resource_type(resource_type)
109
+ case resource_type
110
+ when :instance
111
+ return "AWS/EC2"
112
+ when :elb
113
+ return "AWS/ELB"
114
+ when :rds
115
+ return "AWS/RDS"
116
+ when :elasticache
117
+ return "AWS/ElastiCache"
118
+ when :sqs
119
+ return "AWS/SQS"
120
+ end
121
+ end
122
+
123
+ def register_instance_alarms!
124
+ cw = AWS::CloudWatch.new
125
+
126
+ watches = @config[:watches][:instance]
127
+ dimension_values = [[{ name: "InstanceId", value: instance_id }]]
128
+
129
+ register_alarms_for_watches!(:instance, dimension_values)
130
+ end
131
+
132
+ def register_alarms_for_watches!(resource_type, dimension_values)
133
+ cw = AWS::CloudWatch.new
134
+
135
+ dimension_values.each do |dimension_value|
136
+ @config[:watches][resource_type].each do |watch|
137
+ [:warning, :critical].each do |threshold|
138
+ target = dimension_value.map { |v| v[:value] }.join("_")
139
+ alarm_name = "skyeye::#{resource_type}::#{target}::#{watch[:name]}-#{threshold}"
140
+
141
+ alarm = cw.alarms[alarm_name]
142
+
143
+ unless alarm.exists?
144
+ is_command = !!watch[:command]
145
+ raise "Cannot specify command for non-instance watch" if is_command && resource_type != :instance
146
+
147
+ if is_command || watch[threshold]
148
+ @logger.info "Register Alarm #{alarm_name}"
149
+
150
+ cw.alarms.create(alarm_name, {
151
+ namespace: is_command ? @config[:namespace] : namespace_for_resource_type(resource_type),
152
+ metric_name: watch[:name],
153
+ dimensions: dimension_value,
154
+ comparison_operator: watch[:comparison_operator] || "GreaterThanThreshold",
155
+ evaluation_periods: 1,
156
+ period: (watch[:period] || @config[:alarm_periods][threshold] || 5) * 60,
157
+ statistic: watch[:statistic] || "Maximum",
158
+ threshold: is_command ? (threshold == :warning ? 0 : 1) : watch[threshold],
159
+ insufficient_data_actions: (threshold == :critical && is_command ? @config[:arns] : []),
160
+ actions_enabled: true,
161
+ alarm_actions: @config[:arns],
162
+ alarm_description: "skyeye: #{watch.to_json}",
163
+ })
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
170
+
171
+ def start!
172
+ @running = true
173
+ @logger.info "SkyEye starting."
174
+
175
+ namespace = @config[:namespace] || "skyeye"
176
+
177
+ @threads = @config[:watches][:instance].map do |w|
178
+ thread_for_watch(namespace, w)
179
+ end.compact
180
+
181
+ @threads.each(&:join)
182
+
183
+ if @killed_by
184
+ @logger.error(@killed_by.to_s + " " + @killed_by.backtrace.join("\n"))
185
+ raise @killed_by
186
+ end
187
+ end
188
+
189
+ def kill!(e)
190
+ @mutex.synchronize do
191
+ @killed_by = e
192
+ end
193
+ end
194
+
195
+ def thread_for_watch(namespace, watch)
196
+ return nil if watch[:type] == "aws"
197
+ raise "missing name" unless watch[:name]
198
+
199
+ Thread.new do
200
+ last_check = nil
201
+
202
+ interval = watch[:interval] || 5
203
+ cw = AWS::CloudWatch.new.client
204
+
205
+ do_check = lambda do |&block|
206
+ if !last_check || (Time.now - last_check > interval)
207
+ last_check = Time.now
208
+ block.call
209
+ end
210
+ end
211
+
212
+ begin
213
+ loop do
214
+ running = true
215
+
216
+ @mutex.synchronize do
217
+ running = @running && !@killed_by
218
+ end
219
+
220
+ break unless running
221
+
222
+ do_check.call do
223
+ @logger.info "[CHECK] [#{instance_id}] #{watch[:name]}::#{watch[:type]} #{watch[:command] || ""}"
224
+
225
+ if watch[:command]
226
+ command = watch[:command]
227
+ message = nil
228
+
229
+ status = Open4::popen4(command) do |pid, stdin, stdout, stderr|
230
+ message = "#{watch[:name]} :: #{stdout.readlines.join(" ").gsub(/\n/, " ").chomp}"
231
+ end
232
+
233
+ case status
234
+ when 0
235
+ @logger.info message
236
+ when 1
237
+ @logger.warn message
238
+ when 2
239
+ @logger.error message
240
+ else
241
+ @logger.warn message
242
+ end
243
+
244
+ cw.put_metric_data({
245
+ namespace: namespace,
246
+ metric_data: [{
247
+ metric_name: watch[:name],
248
+ dimensions: [{
249
+ :name => "InstanceId",
250
+ :value => instance_id
251
+ }],
252
+ value: status.exitstatus,
253
+ unit: "None"
254
+ }]
255
+ })
256
+ else
257
+ raise "Missing command or aws id for watch #{watch[:name]}"
258
+ end
259
+ end
260
+
261
+ sleep 1
262
+ end
263
+ rescue Exception => e
264
+ kill!(e)
265
+ end
266
+ end
267
+ end
268
+
269
+ def shutdown!
270
+ @mutex.synchronize do
271
+ @running = false
272
+ end
273
+ end
274
+
275
+ def load_or_register_topics!
276
+ sns = AWS::SNS.new
277
+
278
+ current_topics = sns.client.list_topics
279
+
280
+ arns = []
281
+
282
+ (@config[:topics] || ["skyeye-alerts"]).each do |topic_name|
283
+ current_topic = current_topics[:topics].find do |topic|
284
+ topic[:topic_arn].split(/:/)[-1] == topic_name
285
+ end
286
+
287
+ if current_topic
288
+ arns << current_topic[:topic_arn]
289
+ else
290
+ arn = sns.topics.create(topic_name).arn
291
+ arns << arn
292
+
293
+ @logger.info "Created Topic #{arn}"
294
+ end
295
+ end
296
+
297
+ @config[:arns] = arns
298
+ end
299
+ end
300
+ end
@@ -0,0 +1,3 @@
1
+ module Skyeye
2
+ VERSION = "0.0.1"
3
+ end
data/lib/skyeye.rb ADDED
@@ -0,0 +1,4 @@
1
+ module SkyEye
2
+ end
3
+
4
+ require 'skyeye/exec'
data/skyeye.gemspec ADDED
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'skyeye/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "skyeye"
8
+ gem.version = Skyeye::VERSION
9
+ gem.authors = ["Greg Fodor"]
10
+ gem.email = ["gfodor@gmail.com"]
11
+ gem.description = %q{SkyEye is a daemon that lets you have nagios-like alerts on EC2 using CloudWatch.}
12
+ gem.summary = %q{SkyEye is a daemon that lets you have nagios-like alerts on EC2 using CloudWatch.}
13
+ gem.homepage = "http://github.com/gfodor/skyeye"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: skyeye
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Greg Fodor
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-03-14 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: SkyEye is a daemon that lets you have nagios-like alerts on EC2 using
15
+ CloudWatch.
16
+ email:
17
+ - gfodor@gmail.com
18
+ executables:
19
+ - skyeye
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - .gitignore
24
+ - Gemfile
25
+ - LICENSE.txt
26
+ - README.md
27
+ - Rakefile
28
+ - bin/skyeye
29
+ - config/skyeye.yml.example
30
+ - lib/skyeye.rb
31
+ - lib/skyeye/exec.rb
32
+ - lib/skyeye/version.rb
33
+ - skyeye.gemspec
34
+ homepage: http://github.com/gfodor/skyeye
35
+ licenses: []
36
+ post_install_message:
37
+ rdoc_options: []
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ none: false
48
+ requirements:
49
+ - - ! '>='
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubyforge_project:
54
+ rubygems_version: 1.8.24
55
+ signing_key:
56
+ specification_version: 3
57
+ summary: SkyEye is a daemon that lets you have nagios-like alerts on EC2 using CloudWatch.
58
+ test_files: []