instrumental_tools 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ Gemfile.lock
2
+ /pkg
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # Instrumental Tools
2
+
3
+ Collection of tools for use with the Instrumental (http://www.instrumentalapp.com/) app
4
+
5
+ ## instrument_server
6
+
7
+ Use to collect various monitoring statistics of a server. Execute with:
8
+
9
+ ```sh
10
+ instrument_server [INSTRUMENTAL_API_KEY]
11
+ ```
12
+
13
+ Linux note: Install iostat (part of the sysstat package) in order to collect disk I/O metrics.
14
+
15
+ ## instrumental
16
+
17
+ Output text graphs of the different metrics in your project.
18
+
19
+ See all options with: `instrumental --help`
20
+
21
+ ## gitstrumental
22
+
23
+ Collect statistics on commit counts in a given git repo. Execute in the repo directory with:
24
+
25
+ ```sh
26
+ gitstrumental [INSTRUMENTAL_API_KEY]
27
+ ```
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
data/bin/gitstrumental ADDED
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ gem 'instrumental_agent'
4
+ require 'instrumental_agent'
5
+
6
+ token = ENV["INSTRUMENTAL_TOKEN"] || ARGV[0]
7
+
8
+ if !token
9
+ puts 'Usage: gitstrumental API_TOKEN'
10
+ exit 1
11
+ end
12
+
13
+ lines = `git log --format="REC %at %h '%cn'" --numstat`.chomp.split(/\n+/).reject(&:empty?)
14
+ branch = File.basename(`git symbolic-ref HEAD`.chomp).gsub(/[^a-z0-9]/i, "_")
15
+ if fetch_url = `git remote show origin`.chomp.split(/\n+/).grep(/Fetch URL:/).first
16
+ repo = File.basename(fetch_url.split("/").last, ".git")
17
+ else
18
+ repo = File.basename(Dir.pwd, ".git")
19
+ end
20
+ prolog = [repo, branch].join(".")
21
+ I = Instrumental::Agent.new(token)
22
+ curstat = {}
23
+ lines.each do |line|
24
+ if line =~ /^REC/
25
+ if !curstat.empty?
26
+ name, ts, changes = curstat[:name], curstat[:timestamp], curstat[:changes]
27
+ I.increment("git.#{prolog}.commits.#{name}", 1, ts)
28
+
29
+ adds, deletes = changes.reduce do |prev, pair|
30
+ [prev[0] + pair[0], prev[1] + pair[1]]
31
+ end
32
+ I.increment("git.#{prolog}.commits.#{name}.add", adds, ts)
33
+ I.increment("git.#{prolog}.commits.#{name}.delete", deletes, ts)
34
+ I.increment("git.#{prolog}.num_commits", 1, ts)
35
+ end
36
+ rec, ts, hash, *name_parts = line.split(" ")
37
+ name = name_parts.join(" ").gsub(/^'([^']+)'$/, "\\1").gsub(/[^a-z0-9]/i, "_")
38
+ curstat = {
39
+ :name => name,
40
+ :timestamp => ts.to_i,
41
+ :hash => hash,
42
+ :changes => []
43
+ }
44
+ else
45
+ adds, deletes, file = line.split(/\s+/)
46
+ curstat[:changes] << [
47
+ adds.to_i, deletes.to_i
48
+ ]
49
+ end
50
+ end
@@ -0,0 +1,267 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ gem 'instrumental_agent'
5
+ require 'instrumental_agent'
6
+
7
+
8
+ class SystemInspector
9
+ TYPES = [:gauges, :incrementors]
10
+ attr_accessor *TYPES
11
+
12
+ def initialize
13
+ @gauges = {}
14
+ @incrementors = {}
15
+ @platform =
16
+ case RUBY_PLATFORM
17
+ when /linux/
18
+ Linux
19
+ when /darwin/
20
+ OSX
21
+ else
22
+ raise "unsupported OS"
23
+ end
24
+ end
25
+
26
+ def load_all
27
+ load @platform.load_cpu
28
+ load @platform.load_memory
29
+ load @platform.load_disks
30
+ load @platform.load_filesystem
31
+ end
32
+
33
+ def load(stats)
34
+ @gauges.merge!(stats[:gauges] || {})
35
+ end
36
+
37
+ module OSX
38
+ def self.load_cpu
39
+ { :gauges => top }
40
+ end
41
+
42
+ def self.top
43
+ lines = []
44
+ processes = date = load = cpu = nil
45
+ IO.popen('top -l 1 -n 0') do |top|
46
+ processes = top.gets.split(': ')[1]
47
+ date = top.gets
48
+ load = top.gets.split(': ')[1]
49
+ cpu = top.gets.split(': ')[1]
50
+ end
51
+
52
+ user, system, idle = cpu.split(", ").map { |v| v.to_f }
53
+ load1, load5, load15 = load.split(", ").map { |v| v.to_f }
54
+ total, running, stuck, sleeping, threads = processes.split(", ").map { |v| v.to_i }
55
+
56
+ {
57
+ 'cpu.user' => user,
58
+ 'cpu.system' => system,
59
+ 'cpu.idle' => idle,
60
+ 'load.1min' => load1,
61
+ 'load.5min' => load5,
62
+ 'load.15min' => load15,
63
+ 'processes.total' => total,
64
+ 'processes.running' => running,
65
+ 'processes.stuck' => stuck,
66
+ 'processes.sleeping' => sleeping,
67
+ 'threads' => threads,
68
+ }
69
+ end
70
+
71
+ def self.load_memory
72
+ # TODO: swap
73
+ { :gauges => vm_stat }
74
+ end
75
+
76
+ def self.vm_stat
77
+ header, *rows = `vm_stat`.split("\n")
78
+ page_size = header.match(/page size of (\d+) bytes/)[1].to_i
79
+ sections = ["free", "active", "inactive", "wired", "speculative", "wired down"]
80
+ output = {}
81
+ total = 0.0
82
+ rows.each do |row|
83
+ if match = row.match(/Pages (.*):\s+(\d+)\./)
84
+ section, value = match[1, 2]
85
+ if sections.include?(section)
86
+ value = value.to_f * page_size / 1024 / 1024
87
+ output["memory.#{section.gsub(' ', '_')}_mb"] = value
88
+ total += value
89
+ end
90
+ end
91
+ end
92
+ output["memory.free_percent"] = output["memory.free_mb"] / total * 100 # TODO: verify
93
+ output
94
+ end
95
+
96
+ def self.load_disks
97
+ { :gauges => df }
98
+ end
99
+
100
+ def self.df
101
+ output = {}
102
+ `df -k`.split("\n").grep(%r{^/dev/}).each do |line|
103
+ device, total, used, available, capacity, mount = line.split(/\s+/)
104
+ names = [File.basename(device)]
105
+ names << 'root' if mount == '/'
106
+ names.each do |name|
107
+ output["disk.#{name}.total_mb"] = total.to_f / 1024
108
+ output["disk.#{name}.used_mb"] = used.to_f / 1024
109
+ output["disk.#{name}.available_mb"] = available.to_f / 1024
110
+ output["disk.#{name}.available_percent"] = available.to_f / total.to_f * 100
111
+ end
112
+ end
113
+ output
114
+ end
115
+
116
+ def self.netstat(interface = 'en1')
117
+ # mostly functional network io stats
118
+ headers, *lines = `netstat -ibI #{interface}`.split("\n").map { |l| l.split(/\s+/) } # FIXME: vulnerability?
119
+ headers = headers.map { |h| h.downcase }
120
+ lines.each do |line|
121
+ if !line[3].include?(':')
122
+ return Hash[headers.zip(line)]
123
+ end
124
+ end
125
+ end
126
+
127
+ def self.load_filesystem
128
+ {}
129
+ end
130
+
131
+ end
132
+
133
+ module Linux
134
+ def self.load_cpu
135
+ output = { :gauges => {} }
136
+ output[:gauges].merge!(cpu)
137
+ output[:gauges].merge!(loadavg)
138
+ output
139
+ end
140
+
141
+ def self.cpu
142
+ cpu, user, nice, system, idle, iowait = `cat /proc/stat | grep cpu[^0-9]`.chomp.split(/\s+/)
143
+ total = user.to_i + system.to_i + idle.to_i + iowait.to_i
144
+ {
145
+ 'cpu.user' => (user.to_f / total) * 100,
146
+ 'cpu.system' => (system.to_f / total) * 100,
147
+ 'cpu.idle' => (idle.to_f / total) * 100,
148
+ 'cpu.iowait' => (iowait.to_f / total) * 100,
149
+ }
150
+ end
151
+
152
+ def self.loadavg
153
+ min_1, min_5, min_15 = `cat /proc/loadavg`.split(/\s+/)
154
+ {
155
+ 'load.1min' => min_1.to_f,
156
+ 'load.5min' => min_5.to_f,
157
+ 'load.15min' => min_15.to_f,
158
+ }
159
+ end
160
+
161
+ def self.load_memory
162
+ output = { :gauges => {} }
163
+ output[:gauges].merge!(memory)
164
+ output[:gauges].merge!(swap)
165
+ output
166
+ end
167
+
168
+ def self.memory
169
+ _, total, used, free, shared, buffers, cached = `free -k -o | grep Mem`.chomp.split(/\s+/)
170
+ {
171
+ 'memory.used_mb' => used.to_f / 1024,
172
+ 'memory.free_mb' => free.to_f / 1024,
173
+ 'memory.buffers_mb' => buffers.to_f / 1024,
174
+ 'memory.cached_mb' => cached.to_f / 1024,
175
+ 'memory.free_percent' => (free.to_f / total.to_f) * 100,
176
+ }
177
+ end
178
+
179
+ def self.swap
180
+ _, total, used, free = `free -k -o | grep Swap`.chomp.split(/\s+/)
181
+ {
182
+ 'swap.used_mb' => used.to_f / 1024,
183
+ 'swap.free_mb' => free.to_f / 1024,
184
+ 'swap.free_percent' => (free.to_f / total.to_f) * 100,
185
+ }
186
+ end
187
+
188
+ def self.load_disks
189
+ { :gauges => disks.merge(iostat) }
190
+ end
191
+
192
+ def self.disks
193
+ output = {}
194
+ `df -Pk`.split("\n").grep(%r{^/dev/}).each do |line|
195
+ device, total, used, available, capacity, mount = line.split(/\s+/)
196
+ names = [File.basename(device)]
197
+ names << 'root' if mount == '/'
198
+ names.each do |name|
199
+ output["disk.#{name}.total_mb"] = total.to_f / 1024
200
+ output["disk.#{name}.used_mb"] = used.to_f / 1024
201
+ output["disk.#{name}.available_mb"] = available.to_f / 1024
202
+ output["disk.#{name}.available_percent"] = available.to_f / total.to_f * 100
203
+ end
204
+ end
205
+ output
206
+ end
207
+
208
+ def self.iostat
209
+ output = {}
210
+ if system('iostat > /dev/null 2>&1')
211
+ lines = `iostat -dx`.split("\n")
212
+ header = lines.grep(/^Device/i).first
213
+ dev_lines = lines[(lines.index(header) + 1)..-1]
214
+ util_index = header.split.index('%util')
215
+ dev_lines.each do |dev_line|
216
+ values = dev_line.split
217
+ output["disk.#{values[0]}.percent_utilization"] = values[util_index].to_f
218
+ end
219
+ end
220
+ output
221
+ end
222
+
223
+ def self.load_filesystem
224
+ { :gauges => filesystem }
225
+ end
226
+
227
+ def self.filesystem
228
+ allocated, unused, max = `sysctl fs.file-nr`.split[-3..-1].map(&:to_i)
229
+ open_files = allocated - unused
230
+ { 'filesystem.open_files' => open_files,
231
+ 'filesystem.max_open_files' => max,
232
+ 'filesystem.open_files_pct_max' => (open_files.to_f / max.to_f) * 100,
233
+ }
234
+ end
235
+ end
236
+
237
+ end
238
+
239
+ token = ENV["INSTRUMENTAL_TOKEN"]
240
+ if !token
241
+ token, collector = *ARGV
242
+ else
243
+ collector = *ARGV
244
+ end
245
+ unless token
246
+ puts "Usage: #{$0} <token> [collector]"
247
+ exit 1
248
+ end
249
+ I = Instrumental::Agent.new(token, :collector => collector)
250
+
251
+ host = `hostname`.chomp
252
+
253
+ puts "Collecting stats under the hostname: #{host}"
254
+
255
+ unless system('iostat > /dev/null 2>&1')
256
+ puts 'Install iostat (sysstat package) to collect disk I/O metrics.'
257
+ end
258
+
259
+ loop do
260
+ inspector = SystemInspector.new
261
+ inspector.load_all
262
+ inspector.gauges.each do |stat, value|
263
+ I.gauge("#{host}.#{stat}", value)
264
+ end
265
+ # I.increment("#{host}.#{stat}", delta)
266
+ sleep 10
267
+ end
data/bin/instrumental ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env ruby
2
+ require 'cgi'
3
+ require 'net/http'
4
+ require 'optparse'
5
+ require 'rubygems'
6
+ gem 'json'
7
+ require 'json'
8
+
9
+
10
+ metrics = "*"
11
+ token = ENV["INSTRUMENTAL_TOKEN"]
12
+ allowed_durations = [1800, 3600, 10800, 21600, 43200, 86400, 259200, 604800, 1209600]
13
+ duration = nil
14
+
15
+ opts = OptionParser.new do |opts|
16
+ opts.banner = "Usage: instrumental [options]"
17
+
18
+ opts.on("-m", "--metrics METRICS", "Comma separated list of metrics you're interested in. Wildcard (*) allowed") do |m|
19
+ metrics = m
20
+ end
21
+
22
+ opts.on("-t", "--token TOKEN", "Your API token. Also can be set as an environment variable, INSTRUMENTAL_TOKEN") do |t|
23
+ token = t
24
+ end
25
+
26
+ opts.on("-d", "--duration DURATION", "Duration ago in seconds to view. You can specify #{allowed_durations.join(" ")}") do |d|
27
+ duration = allowed_durations.index(d.to_i) && d.to_i
28
+ end
29
+ end.parse!
30
+
31
+ search_line = CGI.escape(metrics)
32
+ duration &&= "&duration=#{duration}"
33
+ http = Net::HTTP.new("instrumentalapp.com", 80)
34
+ request = Net::HTTP::Get.new("/metrics/#{search_line}?token=#{token}&format=json#{duration}")
35
+ response = http.request(request)
36
+ raise "Server error: #{response.message} #{response.code}" if response.code.to_i != 200
37
+ response = JSON.parse(response.body)
38
+ bars = ["\342\226\201", "\342\226\202", "\342\226\203", "\342\226\204", "\342\226\205", "\342\226\206", "\342\226\207"]
39
+ max_key_size = response.collect { |k, v| k.size }.max
40
+ scale = bars.size - 1
41
+ response.each do |metric, data|
42
+ data = data.collect { |h| h["value"].to_f }
43
+ min = data.min
44
+ range = data.max - min
45
+ graph = data.collect do |v|
46
+ if (idx = (((v - min) / range) * scale)).nan?
47
+ idx = 0
48
+ elsif idx.infinite?
49
+ idx = scale
50
+ end
51
+ bars[idx.round]
52
+ end
53
+ puts "%#{max_key_size}s | %s" % [metric, graph.join]
54
+ end
@@ -0,0 +1,20 @@
1
+ $: << "./lib"
2
+ require 'instrumental_tools/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "instrumental_tools"
6
+ s.version = Instrumental::Tools::VERSION
7
+ s.authors = ["Elijah Miller", "Christopher Zelenak", "Kristopher Chambers", "Matthew Hassfurder"]
8
+ s.email = ["support@instrumentalapp.com"]
9
+ s.homepage = "http://github.com/fastestforward/instrumental_tools"
10
+ s.summary = %q{Command line tools for Instrumental}
11
+ s.description = %q{Tools for displaying information from and reporting to Instrumental (instrumentalapp.com)}
12
+
13
+ s.files = `git ls-files`.split("\n")
14
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
+ s.require_paths = ["lib"]
17
+ s.add_runtime_dependency(%q<json>, [">=0"])
18
+ s.add_runtime_dependency(%q<instrumental_agent>, [">=0.5"])
19
+ s.add_development_dependency(%q<rake>, [">=0"])
20
+ end
@@ -0,0 +1,5 @@
1
+ module Instrumental
2
+ module Tools
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: instrumental_tools
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Elijah Miller
9
+ - Christopher Zelenak
10
+ - Kristopher Chambers
11
+ - Matthew Hassfurder
12
+ autorequire:
13
+ bindir: bin
14
+ cert_chain: []
15
+ date: 2011-12-12 00:00:00.000000000 Z
16
+ dependencies:
17
+ - !ruby/object:Gem::Dependency
18
+ name: json
19
+ requirement: &70344909872300 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ! '>='
23
+ - !ruby/object:Gem::Version
24
+ version: '0'
25
+ type: :runtime
26
+ prerelease: false
27
+ version_requirements: *70344909872300
28
+ - !ruby/object:Gem::Dependency
29
+ name: instrumental_agent
30
+ requirement: &70344909871780 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ! '>='
34
+ - !ruby/object:Gem::Version
35
+ version: '0.5'
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: *70344909871780
39
+ - !ruby/object:Gem::Dependency
40
+ name: rake
41
+ requirement: &70344909871300 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: *70344909871300
50
+ description: Tools for displaying information from and reporting to Instrumental (instrumentalapp.com)
51
+ email:
52
+ - support@instrumentalapp.com
53
+ executables:
54
+ - gitstrumental
55
+ - instrument_server
56
+ - instrumental
57
+ extensions: []
58
+ extra_rdoc_files: []
59
+ files:
60
+ - .gitignore
61
+ - Gemfile
62
+ - README.md
63
+ - Rakefile
64
+ - bin/gitstrumental
65
+ - bin/instrument_server
66
+ - bin/instrumental
67
+ - instrumental_tools.gemspec
68
+ - lib/instrumental_tools/version.rb
69
+ homepage: http://github.com/fastestforward/instrumental_tools
70
+ licenses: []
71
+ post_install_message:
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ segments:
82
+ - 0
83
+ hash: 266192678575170194
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ segments:
91
+ - 0
92
+ hash: 266192678575170194
93
+ requirements: []
94
+ rubyforge_project:
95
+ rubygems_version: 1.8.11
96
+ signing_key:
97
+ specification_version: 3
98
+ summary: Command line tools for Instrumental
99
+ test_files: []