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