cloudscopes 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,53 @@
1
+ Cloudscopes
2
+ ===========
3
+
4
+ Ruby gem to report system statistics to web based monitoring services such as CloudWatch
5
+
6
+ Features:
7
+
8
+ * Easy to extend with additional metrics, using system classes or ruby experssions
9
+ * Easily readable configuration using YAML
10
+ * Metric conditions (only evaluate and publish metric if condition is met) allowing single simple configuration for hetrogenous environments
11
+ * Conditions and value calculation done using Ruby expressions, allowing infinite customizability
12
+
13
+ Supported monitoring providers:
14
+
15
+ * Amazon Cloudwatch
16
+
17
+ Supported metrics to monitor:
18
+
19
+ * Linux /proc/meminfo data
20
+ * Linux /proc/cpuinfo data
21
+ * Linux /proc/loadavg data
22
+ * Linux /proc/diskstats data
23
+ * Linux SysV service status
24
+ * Bluepill service status
25
+ * Redis queue size (for Resque, but may possible work with other protocols)
26
+ * Network port listen status
27
+
28
+ ## Installation
29
+
30
+ 1. Install the gem into your system's Ruby:
31
+ `$ sudo gem install cloudscopes`
32
+ 1. Setup the system with the default configuration and cron job:
33
+ `$ sudo cloudscopes-setup`
34
+ 1. Edit the configuration file according to your requirements:
35
+ `$ sudo editor /etc/cloudscopes-monitoring.yaml`
36
+
37
+ ## Usage
38
+
39
+ Cloudscopes monitoring script will be run every minute using the cron system, and will publish all specified metrics to the monitoring
40
+ service. Alternatively, the cloudscopes-monitor command can be called directly, by providing the path to the cloudscopes configuration file and one of the following options:
41
+
42
+ * -t : instead of publishing the calculated metrics, dump them to the console. Useful for testing
43
+
44
+ If no additional options are specified, the default behavior of the monitor command is to publish the metrics using the configured provider.
45
+
46
+ ## Contributing
47
+
48
+ 1. Fork it ( https://github.com/guss77/cloudscopes/fork )
49
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
50
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
51
+ 4. Push to the branch (`git push origin my-new-feature`)
52
+ 5. Create a new Pull Request
53
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'cloudscopes'
4
+
5
+ def handle(metrics)
6
+ return publish metrics if Cloudscopes.should_publish
7
+ metrics.each do |category, samples|
8
+ valid_data = samples.select(&:valid)
9
+ next if valid_data.empty?
10
+ puts "#{category.rjust(10,' ')}: "
11
+ valid_data.each { |s| puts "#{' '*12}#{s.name} - #{s.value} #{s.unit} (#{s.to_cloudwatch_metric_data})" }
12
+ end
13
+ end
14
+
15
+ metrics = Cloudscopes.init
16
+ handle metrics.collect { |metric| sample metric }
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'fileutils'
4
+
5
+ basedir = File.expand_path(File.dirname(__FILE__) + "/..")
6
+ FileUtils.cp "#{basedir}/config/cron.config", '/etc/cron.d/cloudscopes-monitoring'
7
+ FileUtils.cp "#{basedir}/config/monitoring.yaml", '/etc/cloudscopes-monitoring.yaml'
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'cloudscopes/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "cloudscopes"
8
+ spec.version = Cloudscopes::VERSION
9
+ spec.authors = ["Oded Arbel"]
10
+ spec.email = ["oded@geek.co.il"]
11
+ spec.summary = %q{Ruby gem to report system statistics to web based monitoring services such as CloudWatch}
12
+ spec.description = %q{Ruby gem to report system statistics to web based monitoring services such as CloudWatch}
13
+ spec.homepage = "http://github.com/guss77/cloudscopes"
14
+ spec.license = "GPLv3"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+ spec.extra_rdoc_files = ["README.md"]
21
+
22
+ spec.add_dependency 'aws-sdk', '~> 1.40'
23
+ spec.add_dependency 'redis', '~> 3.0'
24
+ spec.add_dependency 'ffi', '~> 1.9', '>= 1.9.3'
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.6"
27
+ spec.add_development_dependency "rake", '~> 0'
28
+ end
@@ -0,0 +1 @@
1
+ */1 * * * * root cloudscopes-monitor /etc/cloudscopes-monitoring.yaml
@@ -0,0 +1,39 @@
1
+ settings:
2
+ provider: cloudwatch
3
+ key: AWS_KEY_ID
4
+ secret: AWS_SECRET_KEY
5
+ region: us-west-2
6
+
7
+ metrics:
8
+ Systems:
9
+ - name: Load Per CPU
10
+ unit: Percent
11
+ value: 100 * system.loadavg5 / system.cpucount
12
+ - name: Pending IO
13
+ unit: Count
14
+ value: system.iostat[8]
15
+ - name: Memory Utilization
16
+ unit: Percent
17
+ value: 100 * memory.Active / memory.MemTotal
18
+
19
+ - name: Available Space on System
20
+ unit: Gigabytes
21
+ value: filesystem.df("/").avail / 1024 / 1024 / 1024
22
+
23
+ Queues:
24
+ - name: Pending Resque Items
25
+ unit: Count
26
+ value: redis.resque_size("some_queue")
27
+ requires: system.service('redis-server')
28
+
29
+ Services:
30
+ - name: My service status
31
+ value: if system.service('my-service') then 1 else 0 end
32
+ requires: File.exists?('/usr/sbin/my-service')
33
+ - name: Nginx Running
34
+ value: if network.port_open?(80) then 1 else 0 end
35
+ requires: File.exists?(Dir.glob("/etc/nginx/sites-enabled/*").first || "")
36
+
37
+ - name: Bluepill service Running
38
+ value: if system.bluepill_ok?('my-service') then 1 else 0 end
39
+ requires: system.service('my-service')
@@ -0,0 +1,39 @@
1
+ module Cloudscopes
2
+
3
+ attr_reader :should_publish, :usage_requested
4
+
5
+ def self.init
6
+ @opts = Monitoring::Options.new
7
+ usage if usage_requested
8
+ configuration = {}
9
+ (@opts.files.empty?? [ STDIN ] : @opts.files.collect { |fn| File.new(fn) }).each do |configfile|
10
+ configuration.merge! YAML.load(configfile.read)
11
+ end
12
+ @settings = configuration['settings']
13
+ configuration['metrics']
14
+ end
15
+
16
+ def self.usage_requested
17
+ @opts.usage
18
+ end
19
+
20
+ def self.should_publish
21
+ @opts.publish != false
22
+ end
23
+
24
+ def self.usage
25
+ puts "#{@opts}"
26
+ exit 5
27
+ end
28
+
29
+ def self.client
30
+ @client ||= initClient
31
+ end
32
+
33
+ def self.initClient
34
+ AWS::CloudWatch.new access_key_id: @settings['aws-key'],
35
+ secret_access_key: @settings['aws-secret'],
36
+ region: @settings['region']
37
+ end
38
+
39
+ end
@@ -0,0 +1,48 @@
1
+ module Cloudscopes
2
+
3
+ module StatFs
4
+ # The result of a statfs operation, see "man statfs" for more information
5
+ # on each field. We add some helper methods that deal in bytes.
6
+ class Result < Struct.new(:type, :bsize, :blocks, :bfree, :bavail, :files, :ffree)
7
+ def total; blocks * bsize; end
8
+ def free; bfree * bsize; end
9
+ def avail; bavail * bsize; end
10
+ end
11
+
12
+ module Lib
13
+ extend FFI::Library
14
+ ffi_lib FFI::Library::LIBC
15
+ attach_function 'statfs64', [:string, :pointer], :int
16
+ end
17
+
18
+ # Drives the interface to the C library, returns a StatFs::Result object
19
+ # to show filesystem information for the given path.
20
+ #
21
+ def self.statfs(path)
22
+ output = FFI::MemoryPointer.new(128)
23
+ begin
24
+ error_code = Lib::statfs64(path, output)
25
+ raise "statfs raised error #{error_code}" if error_code != 0
26
+ return Result.new(*output[0].read_array_of_long(7))
27
+ ensure
28
+ output.free
29
+ end
30
+ end
31
+ end
32
+
33
+ class Filesystem
34
+
35
+ @@mountpoints = File.read("/proc/mounts").split("\n").grep(/(?:xv|s)d/).collect { |l| l.split(/\s+/)[1] }
36
+
37
+ def mountpoints
38
+ @@mountpoints
39
+ end
40
+
41
+ def df(path)
42
+ StatFs.statfs(path)
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+
@@ -0,0 +1,25 @@
1
+ ##
2
+ # public API expressed through kernel (global) methods, for simplicity
3
+ #
4
+
5
+ def publish(samples)
6
+ raise "Not running in EC2, so won't publish!" unless File.executable?("/usr/bin/ec2metadata")
7
+ samples.each do |type,metric_samples|
8
+ begin
9
+ valid_data = metric_samples.select(&:valid)
10
+ next if valid_data.empty?
11
+ valid_data.each_slice(4) do |slice| # slice metrics to chunks - the actual limit is 20, but CloudWatch starts misbehaving if I put too much data
12
+ Monitoring.client.put_metric_data namespace: type,
13
+ metric_data: slice.collect(&:to_cloudwatch_metric_data)
14
+ end
15
+ rescue Exception => e
16
+ puts "Error publishing metrics for #{type}: #{e}"
17
+ end
18
+ end
19
+ end
20
+
21
+ def sample(category, *metrics)
22
+ category, metrics = category if category.is_a? Array # sample may be passed the single yield variable of Hash#each
23
+ metrics = [ metrics ] unless metrics.is_a? Array
24
+ [ category, metrics.collect { |m| Monitoring::Sample.new(category, m) } ]
25
+ end
@@ -0,0 +1,29 @@
1
+ require 'socket'
2
+ require 'timeout'
3
+
4
+ module Cloudscopes
5
+
6
+ class Network
7
+
8
+ def local_ip
9
+ Socket.ip_address_list.detect {|intf| intf.ipv4? and !intf.ipv4_loopback? and !intf.ipv4_multicast? }.ip_address
10
+ end
11
+
12
+ def port_open?(port)
13
+ begin
14
+ Timeout::timeout(1) do
15
+ begin
16
+ TCPSocket.new(local_ip, port).close
17
+ return true
18
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
19
+ return false
20
+ end
21
+ end
22
+ rescue Timeout::Error
23
+ return false
24
+ end
25
+ end
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,17 @@
1
+ module Cloudscopes
2
+
3
+ class Options
4
+
5
+ attr_reader :publish, :usage, :files
6
+
7
+ def initialize
8
+ @publish = true
9
+ @files = OptionParser.new do |opts|
10
+ opts.banner = "Usage: #{$0} [options] [<config.yaml>]\n\nOptions:"
11
+ opts.on("-t", "dump samples to the console instead of publishing, for testing") { @publish = false }
12
+ opts.on_tail("-?", "-h", "--help", "Show this message") { @usage = true }
13
+ end.parse!
14
+ end
15
+ end
16
+
17
+ end
@@ -0,0 +1,34 @@
1
+ module Cloudscopes
2
+
3
+ class RedisClient
4
+
5
+ def initialize(host,port)
6
+ @redis = Redis.new(host: host, port: port)
7
+ end
8
+
9
+ def resque_size(*keys)
10
+ keys = [ keys ] unless keys.is_a? Array
11
+ keys.collect do |key|
12
+ @redis.llen("resque:queue:#{key}")
13
+ end.reduce(0,:+)
14
+ end
15
+
16
+ def list_size(*keys)
17
+ keys = [ keys ] unless keys.is_a? Array
18
+ keys.collect do |key|
19
+ @redis.llen("#{key}")
20
+ end.reduce(0,:+)
21
+ end
22
+
23
+ def resques(pattern)
24
+ @redis.smembers("resque:queues").grep(pattern)
25
+ end
26
+
27
+ end
28
+
29
+ def self.redis(host = 'localhost', port = 6379)
30
+ RedisClient.new(host,port)
31
+ end
32
+
33
+ end
34
+
@@ -0,0 +1,38 @@
1
+ module Cloudscopes
2
+
3
+ class Sample
4
+
5
+ attr_reader :name, :value, :unit
6
+
7
+ ec2_instanceid_file = "/var/lib/cloud/data/previous-instance-id"
8
+ @@instanceid = File.exists?(ec2_instanceid_file) ? File.read(ec2_instanceid_file).chomp : nil
9
+
10
+ def initialize(namespace, metric)
11
+ @name = metric['name']
12
+ @unit = metric['unit']
13
+ @value = nil
14
+
15
+ begin
16
+ return if metric['requires'] and ! Monitoring.get_binding.eval(metric['requires'])
17
+ @value = Monitoring.get_binding.eval(metric['value'])
18
+ rescue => e
19
+ STDERR.puts("Error evaluating #{@name}: #{e}")
20
+ puts e.backtrace
21
+ end
22
+ end
23
+
24
+ def valid
25
+ ! @value.nil?
26
+ end
27
+
28
+ def to_cloudwatch_metric_data
29
+ return nil if @value.nil?
30
+ data = { metric_name: @name, value: @value }
31
+ data[:unit] = @unit if @unit
32
+ data[:dimensions] = [ { name: "InstanceId", value: @@instanceid } ] unless @@instanceid.nil?
33
+ data
34
+ end
35
+
36
+ end
37
+
38
+ end
@@ -0,0 +1,52 @@
1
+ module Cloudscopes
2
+
3
+ class Memory
4
+
5
+ File.read('/proc/meminfo').split("\n").collect do |line|
6
+ line =~ /(\w+):\s+(\d+)/ and { name: $1, value: $2.to_i }
7
+ end.each do |data|
8
+ next if data.nil?
9
+ define_method(data[:name]) { data[:value] }
10
+ end
11
+
12
+ end
13
+
14
+ class System
15
+
16
+ def loadavg5
17
+ File.read("/proc/loadavg").split(/\s+/).first.to_f
18
+ end
19
+
20
+ def cpucount
21
+ File.read("/proc/cpuinfo").split("\n").grep(/^processor\s+/).count
22
+ end
23
+
24
+ # Read https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats for the field meanings
25
+ # this method returns fields 4~14 as sum for all devices (as array indexes 0..10)
26
+ def iostat
27
+ File.read("/proc/diskstats").split("\n").collect do |dev|
28
+ dev.gsub(/^\s+/,"").split(/\s+/)[3..13].collect(&:to_i)
29
+ end.inject() do |sums,vals|
30
+ sums ||= vals.collect { 0 } # init with zeros
31
+ sums.zip(vals).map {|a| a.reduce(:+) } # sum array values
32
+ end
33
+ end
34
+
35
+ def service(name)
36
+ %x(PATH=/usr/sbin:/usr/bin:/sbin:/bin /usr/sbin/service #{name} status 2>/dev/null)
37
+ $?.exitstatus == 0
38
+ end
39
+
40
+ def bluepill_ok?(name)
41
+ %x(/usr/local/bin/bluepill wfs status | grep -v up | grep pid)
42
+ $?.exitstatus == 1 # grep pid should not match because all the pids are "up"
43
+ end
44
+
45
+ end
46
+
47
+ def self.system # must define, otherwise kernel.system matches
48
+ System.new
49
+ end
50
+
51
+ end
52
+
@@ -0,0 +1,3 @@
1
+ module Cloudscopes
2
+ VERSION = "0.8.0"
3
+ end
@@ -0,0 +1,30 @@
1
+ require 'rubygems'
2
+
3
+ #require 'bundler/setup' #if ENV['BUNDLE_GEMFILE'] && File.exists?(ENV['BUNDLE_GEMFILE'])
4
+
5
+ require 'aws-sdk'
6
+ require 'redis'
7
+ require 'optparse'
8
+ require 'ffi'
9
+
10
+ require 'cloudscopes/version'
11
+ require 'cloudscopes/configuration'
12
+ require 'cloudscopes/options'
13
+ require 'cloudscopes/globals'
14
+ require 'cloudscopes/sample'
15
+ require 'cloudscopes/system'
16
+ require 'cloudscopes/filesystem'
17
+ require 'cloudscopes/redis'
18
+ require 'cloudscopes/network'
19
+
20
+ module Cloudscopes
21
+
22
+ def self.get_binding
23
+ return binding()
24
+ end
25
+
26
+ def self.method_missing(*args)
27
+ Monitoring.const_get(args.shift.to_s.capitalize).new(*args)
28
+ end
29
+
30
+ end
metadata ADDED
@@ -0,0 +1,147 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cloudscopes
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.0
5
+ platform: ruby
6
+ authors:
7
+ - Oded Arbel
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-08-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.40'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.40'
27
+ - !ruby/object:Gem::Dependency
28
+ name: redis
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: ffi
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '1.9'
48
+ - - '>='
49
+ - !ruby/object:Gem::Version
50
+ version: 1.9.3
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ~>
56
+ - !ruby/object:Gem::Version
57
+ version: '1.9'
58
+ - - '>='
59
+ - !ruby/object:Gem::Version
60
+ version: 1.9.3
61
+ - !ruby/object:Gem::Dependency
62
+ name: bundler
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ~>
66
+ - !ruby/object:Gem::Version
67
+ version: '1.6'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ~>
73
+ - !ruby/object:Gem::Version
74
+ version: '1.6'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rake
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ~>
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ~>
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ description: Ruby gem to report system statistics to web based monitoring services
90
+ such as CloudWatch
91
+ email:
92
+ - oded@geek.co.il
93
+ executables:
94
+ - cloudscopes-monitor
95
+ - cloudscopes-setup
96
+ extensions: []
97
+ extra_rdoc_files:
98
+ - README.md
99
+ files:
100
+ - .buildpath
101
+ - .gitignore
102
+ - .project
103
+ - Gemfile
104
+ - LICENSE
105
+ - README.md
106
+ - Rakefile
107
+ - bin/cloudscopes-monitor
108
+ - bin/cloudscopes-setup
109
+ - cloudscopes.gemspec
110
+ - config/cron.config
111
+ - config/monitoring.yaml
112
+ - lib/cloudscopes.rb
113
+ - lib/cloudscopes/configuration.rb
114
+ - lib/cloudscopes/filesystem.rb
115
+ - lib/cloudscopes/globals.rb
116
+ - lib/cloudscopes/network.rb
117
+ - lib/cloudscopes/options.rb
118
+ - lib/cloudscopes/redis.rb
119
+ - lib/cloudscopes/sample.rb
120
+ - lib/cloudscopes/system.rb
121
+ - lib/cloudscopes/version.rb
122
+ homepage: http://github.com/guss77/cloudscopes
123
+ licenses:
124
+ - GPLv3
125
+ metadata: {}
126
+ post_install_message:
127
+ rdoc_options: []
128
+ require_paths:
129
+ - lib
130
+ required_ruby_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - '>='
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ required_rubygems_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - '>='
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ requirements: []
141
+ rubyforge_project:
142
+ rubygems_version: 2.2.2
143
+ signing_key:
144
+ specification_version: 4
145
+ summary: Ruby gem to report system statistics to web based monitoring services such
146
+ as CloudWatch
147
+ test_files: []