casseo 0.1.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,77 @@
1
+ Casseo
2
+ ======
3
+
4
+ A Graphite dashboard viewable without ever leaving the command line. Configuration and concept very similar to [Tasseo](tasseo).
5
+
6
+ Install via Rubygems:
7
+
8
+ gem install casseo
9
+
10
+ Or if you're really concerned about Rubygems' speed, clone the reposistory and create a standalone version (Ruby 1.9 satisfies all of Casseo's dependencies):
11
+
12
+ git clone https://github.com/brandur/casseo.git
13
+ cd casseo
14
+ rake standalone
15
+ mv casseo ~/bin/casseo
16
+
17
+ Configuration
18
+ -------------
19
+
20
+ Casseo expects to be able to find your Graphite credentials at `~/.casseorc`:
21
+
22
+ echo '{ graphite_auth: "graphite:my_secret_api_key", graphite_url: "https://graphite.example.com:8080" }' > ~/.casseorc
23
+ chmod 600 ~/.casseorc
24
+
25
+ Other allowed configuration options are:
26
+
27
+ * `compressed_chart:` whether to include a space between chart symbols
28
+ * `dashboard_default:` name of the dashboard to load if none is specified
29
+ * `interval:` Graphite update interval in seconds
30
+
31
+ Dashboards
32
+ ----------
33
+
34
+ Dashboards are configured via simple Ruby in a manner reminiscent of Tasseo. All `*.rb` files in `~/.casseo/dashboards` or in any of its subdirectories are loaded automatically at startup. Dashboards are assigned names so that they can be referenced and opened like so:
35
+
36
+ casseo home
37
+
38
+ An example dashboard that can be saved to `~/.casseo/dashboards/home.rb`:
39
+
40
+ ``` ruby
41
+ Casseo::Dashboard.define(:api) do |d|
42
+ d.metric "custom.api.production.requests.per-sec", display: "req/sec"
43
+ d.blank
44
+ d.metric "custom.api.production.requests.500.per-min", display: "req 500/min"
45
+ d.metric "custom.api.production.requests.502.per-min", display: "req 502/min"
46
+ d.metric "custom.api.production.requests.503.per-min", display: "req 503/min"
47
+ d.metric "custom.api.production.requests.504.per-min", display: "req 504/min"
48
+ d.blank
49
+ d.metric "custom.api.production.requests.user-errors.per-min", display: "req user err/min"
50
+ d.blank
51
+ d.metric "custom.api.production.requests.latency.avg", display: "req latency"
52
+ end
53
+ ```
54
+
55
+ Get a list of all known dashboards:
56
+
57
+ casseo --list
58
+
59
+ Casseo also takes a file as its first parameter:
60
+
61
+ casseo ~/.casseo/dashboards/home.rb
62
+
63
+ Key Bindings
64
+ ------------
65
+
66
+ For now, there are no options on key bindings. Here's what you get:
67
+
68
+ * `j` page down
69
+ * `k` page up
70
+ * `q` quit
71
+ * `1` 5 minute range
72
+ * `2` 60 minute range
73
+ * `3` 3 hour range
74
+ * `4` 24 hour range
75
+ * `5` 7 day range
76
+
77
+ [tasseo]: https://github.com/obfuscurity/tasseo
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ task :standalone
2
+ end
data/bin/casseo ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require "curses"
5
+ require "json"
6
+ require "net/https"
7
+ require "timeout"
8
+ require "uri"
9
+
10
+ require_relative "../lib/casseo"
11
+
12
+ # load all user dashboards
13
+ dir = "#{File.expand_path("~")}/.casseo/dashboards"
14
+ if File.exists?(dir)
15
+ # clever hack to get symlinks working properly
16
+ Dir["#{dir}/**{,/*/**}/*.rb"].each do |d|
17
+ require d
18
+ end
19
+ end
20
+
21
+ $exit_message = nil
22
+
23
+ ["TERM", "INT"].each do |s|
24
+ Signal.trap(s) do
25
+ $exit_message = "Caught deadly signal"
26
+ Kernel.exit(0)
27
+ end
28
+ end
29
+
30
+ begin
31
+ Curses.cbreak # no need for a newline to get type chars
32
+ Curses.curs_set(1) # invisible
33
+ Curses.noecho # don't echo character on a getch
34
+
35
+ Curses.init_screen
36
+
37
+ # fail fast on missing required config
38
+ Casseo::Config.required
39
+
40
+ if ARGV.count > 0 && File.exists?(ARGV.first)
41
+ require ARGV.first
42
+ elsif ARGV.count > 0 && ["-l", "--list"].include?(ARGV.first)
43
+ $exit_message = Casseo::Dashboard.index.sort.join("\n")
44
+ elsif ARGV.first
45
+ Casseo::Dashboard.run(ARGV.first.to_sym)
46
+ else
47
+ Casseo::Dashboard.run(Casseo::Config.dashboard_default)
48
+ end
49
+ rescue Casseo::ConfigError, Casseo::DashboardNotFound => e
50
+ $exit_message = e.message
51
+ ensure
52
+ Curses.close_screen
53
+ puts $exit_message if $exit_message
54
+ end
data/lib/casseo.rb ADDED
@@ -0,0 +1,4 @@
1
+ require_relative "casseo/config"
2
+ require_relative "casseo/index"
3
+ require_relative "casseo/dashboard"
4
+ require_relative "casseo/version"
@@ -0,0 +1,49 @@
1
+ module Casseo
2
+ module Config
3
+ extend self
4
+
5
+ # whether an extra space is inserted between chart characters
6
+ def compressed_chart
7
+ config(:compressed_chart)
8
+ end
9
+
10
+ def dashboard_default
11
+ config(:dashboard_default) || :home
12
+ end
13
+
14
+ def graphite_auth
15
+ config!(:graphite_auth)
16
+ end
17
+
18
+ def graphite_url
19
+ config!(:graphite_url)
20
+ end
21
+
22
+ # seconds
23
+ def interval
24
+ config(:interval) || 2.0
25
+ end
26
+
27
+ def required
28
+ [ Casseo::Config.graphite_auth,
29
+ Casseo::Config.graphite_url ]
30
+ end
31
+
32
+ private
33
+
34
+ def casseorc
35
+ @@casseorc ||= eval(File.read(File.expand_path("~/.casseorc")))
36
+ end
37
+
38
+ def config(sym)
39
+ casseorc[sym]
40
+ end
41
+
42
+ def config!(sym)
43
+ casseorc[sym] or raise ConfigError.new(":#{sym} not found in ~/.casseorc")
44
+ end
45
+ end
46
+
47
+ class ConfigError < StandardError
48
+ end
49
+ end
@@ -0,0 +1,166 @@
1
+ # encoding: utf-8
2
+
3
+ module Casseo
4
+ class Dashboard
5
+ CHART_CHARS = [" "] + %w(▁ ▂ ▃ ▄ ▅ ▆ ▇)
6
+
7
+ extend Index
8
+
9
+ def initialize
10
+ @confs = []
11
+ @data = nil
12
+ @page = 0
13
+ @period = 5 # minutes
14
+ end
15
+
16
+ def blank
17
+ @confs << nil
18
+ end
19
+
20
+ def metric(metric, conf = {})
21
+ @confs << conf.merge(metric: metric)
22
+ end
23
+
24
+ def run
25
+ @longest_display = @confs.compact.
26
+ map { |c| c[:display] || c[:metric] }.map { |c| c.length }.max
27
+
28
+ # no data yet, but force drawing of stats to the screen
29
+ show(true)
30
+
31
+ Thread.new do
32
+ # one initial fetch where we don't suppress errors so that the user can
33
+ # verify that their credentials are right
34
+ fetch(false)
35
+ sleep(Config.interval)
36
+
37
+ loop do
38
+ fetch
39
+ sleep(Config.interval)
40
+ end
41
+ end
42
+
43
+ loop do
44
+ show
45
+ begin
46
+ Timeout::timeout(Config.interval) do
47
+ handle_key_presses
48
+ end
49
+ rescue Timeout::Error
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def clamp(n, min, max)
57
+ if n < min
58
+ min
59
+ elsif n > max
60
+ max
61
+ else
62
+ n
63
+ end
64
+ end
65
+
66
+ def fetch(suppress_errors=true)
67
+ metrics = @confs.compact.map { |c| c[:metric] }
68
+ targets = metrics.map { |m| "target=#{URI.encode(m)}" }.join("&")
69
+ uri = URI.parse("#{Config.graphite_url}/render/?#{targets}&from=-#{@period}minutes&format=json")
70
+
71
+ http = Net::HTTP.new(uri.host, uri.port)
72
+ http.use_ssl = true
73
+ request = Net::HTTP::Get.new(uri.request_uri)
74
+ request.basic_auth(*Config.graphite_auth.split(":"))
75
+
76
+ @data = begin
77
+ response = http.request(request)
78
+ JSON.parse(response.body)
79
+ rescue
80
+ raise unless suppress_errors
81
+ nil
82
+ end
83
+ end
84
+
85
+ def handle_key_presses
86
+ loop do
87
+ new_page = nil
88
+ new_period = nil
89
+
90
+ case Curses.getch
91
+ when Curses::KEY_RESIZE then show
92
+ when ?j then new_page = clamp(@page + 1, 0, num_pages)
93
+ when ?k then new_page = clamp(@page - 1, 0, num_pages)
94
+ when ?q then Kernel.exit(0)
95
+ when ?1 then new_period = 5
96
+ when ?2 then new_period = 60
97
+ when ?3 then new_period = 60 * 3
98
+ when ?4 then new_period = 60 * 24
99
+ when ?5 then new_period = 60 * 24 * 7
100
+ end
101
+
102
+ if new_page && new_page != @page
103
+ @page = new_page
104
+ Curses.clear
105
+ show
106
+ end
107
+
108
+ if new_period && new_period != @period
109
+ @period = new_period
110
+ # will update the next time the fetch loop runs
111
+ end
112
+ end
113
+ end
114
+
115
+ def num_pages
116
+ (@confs.count / Curses.lines).ceil
117
+ end
118
+
119
+ def show(force_draw=false)
120
+ # force us through the method
121
+ @data = @data || [] if force_draw
122
+
123
+ # failed to fetch on this cycle
124
+ return unless @data
125
+
126
+ @confs.each_with_index do |conf, i|
127
+ next unless conf
128
+ next unless i >= @page * Curses.lines && i < (@page + 1) * Curses.lines
129
+
130
+ data_points = @data.detect { |d| d["target"] == conf[:metric] }
131
+ data_points = data_points ? data_points["datapoints"] : []
132
+
133
+ # show left to right latest to oldest
134
+ data_points.reverse!
135
+
136
+ max = data_points.
137
+ select { |p| p[0] != nil }.
138
+ max { |p1, p2| p1[0] <=> p2[0] }
139
+ max = max ? max[0] : nil
140
+
141
+ latest = data_points.detect { |p| p[0] != nil }
142
+ latest = latest ? latest[0] : 0.0
143
+
144
+ current = nil
145
+ chart = ""
146
+ if max && max > 0
147
+ data_points.each do |p|
148
+ current = p[0] || current
149
+ next unless current
150
+ index = (current.to_f / max * CHART_CHARS.count).to_i - 1
151
+ chart += CHART_CHARS[index]
152
+ chart += " " unless Config.compressed_chart
153
+ end
154
+ end
155
+
156
+ unit = conf[:unit] || " "
157
+ str = "%-#{@longest_display}s %8.1f%s %s" %
158
+ [conf[:display] || conf[:metric], latest, unit, chart]
159
+ str = str[0...Curses.cols]
160
+ Curses.setpos(i % Curses.lines, 0)
161
+ Curses.addstr(str)
162
+ end
163
+ Curses.refresh
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,27 @@
1
+ module Casseo
2
+ module Index
3
+ @@index = {}
4
+
5
+ def define(name)
6
+ dashboard = Dashboard.new
7
+ yield(dashboard)
8
+ @@index[name] = dashboard
9
+ dashboard
10
+ end
11
+
12
+ def index
13
+ @@index.keys
14
+ end
15
+
16
+ def run(name)
17
+ if @@index.key?(name)
18
+ @@index[name].run
19
+ else
20
+ raise DashboardNotFound.new("#{name} is not a known dashboard")
21
+ end
22
+ end
23
+ end
24
+
25
+ class DashboardNotFound < StandardError
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ module Casseo
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: casseo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Brandur
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-01 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description:
15
+ email: brandur@mutelight.org
16
+ executables:
17
+ - casseo
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - README.md
22
+ - Rakefile
23
+ - !binary |-
24
+ YmluL2Nhc3Nlbw==
25
+ - lib/casseo/config.rb
26
+ - lib/casseo/dashboard.rb
27
+ - lib/casseo/index.rb
28
+ - lib/casseo/version.rb
29
+ - lib/casseo.rb
30
+ homepage: https://github.com/brandur/casseo
31
+ licenses:
32
+ - MIT
33
+ post_install_message:
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ! '>='
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ! '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubyforge_project:
51
+ rubygems_version: 1.8.24
52
+ signing_key:
53
+ specification_version: 3
54
+ summary: A Graphite dashboard for the command line.
55
+ test_files: []