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