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 +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: []
|