cloudscopes 0.8.0

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/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: []