casseo 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +77 -0
- data/Rakefile +2 -0
- data/bin/casseo +54 -0
- data/lib/casseo.rb +4 -0
- data/lib/casseo/config.rb +49 -0
- data/lib/casseo/dashboard.rb +166 -0
- data/lib/casseo/index.rb +27 -0
- data/lib/casseo/version.rb +3 -0
- metadata +55 -0
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
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,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
|
data/lib/casseo/index.rb
ADDED
@@ -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
|
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: []
|