johnf-fnordmetric 1.2.7
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +6 -0
- data/Rakefile +9 -0
- data/fnordmetric.gemspec +41 -0
- data/lib/fnordmetric/acceptors/acceptor.rb +42 -0
- data/lib/fnordmetric/acceptors/amqp_acceptor.rb +56 -0
- data/lib/fnordmetric/acceptors/fyrehose_acceptor.rb +43 -0
- data/lib/fnordmetric/acceptors/stomp_acceptor.rb +71 -0
- data/lib/fnordmetric/acceptors/tcp_acceptor.rb +58 -0
- data/lib/fnordmetric/acceptors/udp_acceptor.rb +37 -0
- data/lib/fnordmetric/api.rb +46 -0
- data/lib/fnordmetric/cache.rb +20 -0
- data/lib/fnordmetric/context.rb +96 -0
- data/lib/fnordmetric/defaults.rb +22 -0
- data/lib/fnordmetric/enterprise/compatibility_handler.rb +42 -0
- data/lib/fnordmetric/ext.rb +75 -0
- data/lib/fnordmetric/gauge.rb +98 -0
- data/lib/fnordmetric/gauge_calculations.rb +106 -0
- data/lib/fnordmetric/gauge_modifiers.rb +144 -0
- data/lib/fnordmetric/gauge_rendering.rb +40 -0
- data/lib/fnordmetric/gauge_validations.rb +15 -0
- data/lib/fnordmetric/gauges/distribution_gauge.rb +87 -0
- data/lib/fnordmetric/gauges/server_health_gauge.rb +13 -0
- data/lib/fnordmetric/gauges/timeseries_gauge.rb +138 -0
- data/lib/fnordmetric/gauges/toplist_gauge.rb +44 -0
- data/lib/fnordmetric/histogram.rb +64 -0
- data/lib/fnordmetric/logger.rb +63 -0
- data/lib/fnordmetric/namespace.rb +208 -0
- data/lib/fnordmetric/session.rb +139 -0
- data/lib/fnordmetric/standalone.rb +20 -0
- data/lib/fnordmetric/timeseries.rb +79 -0
- data/lib/fnordmetric/toplist.rb +61 -0
- data/lib/fnordmetric/udp_client.rb +22 -0
- data/lib/fnordmetric/util.rb +25 -0
- data/lib/fnordmetric/version.rb +3 -0
- data/lib/fnordmetric/web/app.rb +63 -0
- data/lib/fnordmetric/web/app_helpers.rb +42 -0
- data/lib/fnordmetric/web/dashboard.rb +40 -0
- data/lib/fnordmetric/web/event.rb +99 -0
- data/lib/fnordmetric/web/reactor.rb +127 -0
- data/lib/fnordmetric/web/web.rb +59 -0
- data/lib/fnordmetric/web/websocket.rb +41 -0
- data/lib/fnordmetric/widget.rb +82 -0
- data/lib/fnordmetric/widgets/bars_widget.rb +44 -0
- data/lib/fnordmetric/widgets/html_widget.rb +28 -0
- data/lib/fnordmetric/widgets/numbers_widget.rb +80 -0
- data/lib/fnordmetric/widgets/pie_widget.rb +23 -0
- data/lib/fnordmetric/widgets/timeseries_widget.rb +65 -0
- data/lib/fnordmetric/widgets/toplist_widget.rb +68 -0
- data/lib/fnordmetric/worker.rb +89 -0
- data/lib/fnordmetric/zero_config_gauge.rb +138 -0
- data/lib/fnordmetric.rb +149 -0
- data/run_specs.sh +11 -0
- data/spec/api_spec.rb +49 -0
- data/spec/context_spec.rb +42 -0
- data/spec/dashboard_spec.rb +38 -0
- data/spec/event_spec.rb +170 -0
- data/spec/ext_spec.rb +14 -0
- data/spec/fnordmetric_spec.rb +56 -0
- data/spec/gauge_like_shared.rb +56 -0
- data/spec/gauge_modifiers_spec.rb +583 -0
- data/spec/gauge_spec.rb +230 -0
- data/spec/namespace_spec.rb +114 -0
- data/spec/session_spec.rb +231 -0
- data/spec/spec_helper.rb +49 -0
- data/spec/tcp_acceptor_spec.rb +35 -0
- data/spec/timeseries_gauge_spec.rb +56 -0
- data/spec/udp_acceptor_spec.rb +35 -0
- data/spec/util_spec.rb +46 -0
- data/spec/widget_spec.rb +113 -0
- data/spec/worker_spec.rb +40 -0
- data/web/.gitignore +4 -0
- data/web/build.sh +34 -0
- data/web/css/fnordmetric.core.css +868 -0
- data/web/fnordmetric-core.css +1409 -0
- data/web/fnordmetric-core.js +3420 -0
- data/web/fnordmetric-ui.css +282 -0
- data/web/fnordmetric-ui.js +12032 -0
- data/web/haml/app.haml +20 -0
- data/web/haml/distribution_gauge.haml +118 -0
- data/web/haml/timeseries_gauge.haml +80 -0
- data/web/haml/toplist_gauge.haml +194 -0
- data/web/img/head.png +0 -0
- data/web/img/list.png +0 -0
- data/web/img/list_active.png +0 -0
- data/web/img/list_hover.png +0 -0
- data/web/img/loader.gif +0 -0
- data/web/img/loader_white.gif +0 -0
- data/web/img/navbar.png +0 -0
- data/web/img/navbar_btn.png +0 -0
- data/web/img/picto_gauge.png +0 -0
- data/web/js/fnordmetric.bars_widget.js +178 -0
- data/web/js/fnordmetric.dashboard_view.js +99 -0
- data/web/js/fnordmetric.gauge_explorer.js +173 -0
- data/web/js/fnordmetric.gauge_view.js +260 -0
- data/web/js/fnordmetric.html_widget.js +21 -0
- data/web/js/fnordmetric.js +315 -0
- data/web/js/fnordmetric.numbers_widget.js +122 -0
- data/web/js/fnordmetric.overview_view.js +35 -0
- data/web/js/fnordmetric.pie_widget.js +118 -0
- data/web/js/fnordmetric.realtime_timeline_widget.js +175 -0
- data/web/js/fnordmetric.session_view.js +342 -0
- data/web/js/fnordmetric.timeline_widget.js +333 -0
- data/web/js/fnordmetric.timeseries_widget.js +405 -0
- data/web/js/fnordmetric.toplist_widget.js +119 -0
- data/web/js/fnordmetric.ui.js +91 -0
- data/web/js/fnordmetric.util.js +248 -0
- data/web/vendor/font-awesome/css/font-awesome-ie7.min.css +22 -0
- data/web/vendor/font-awesome/css/font-awesome.css +540 -0
- data/web/vendor/font-awesome/css/font-awesome.min.css +33 -0
- data/web/vendor/font-awesome/font/FontAwesome.otf +0 -0
- data/web/vendor/font-awesome/font/fontawesome-webfont.eot +0 -0
- data/web/vendor/font-awesome/font/fontawesome-webfont.svg +284 -0
- data/web/vendor/font-awesome/font/fontawesome-webfont.ttf +0 -0
- data/web/vendor/font-awesome/font/fontawesome-webfont.woff +0 -0
- data/web/vendor/jquery-1.6.2.min.js +18 -0
- data/web/vendor/jquery-ui.min.js +6 -0
- data/web/vendor/jquery.combobox.js +129 -0
- data/web/vendor/jquery.maskedinput.js +252 -0
- metadata +444 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class FnordMetric::App < Sinatra::Base
|
4
|
+
|
5
|
+
include FnordMetric::AppHelpers
|
6
|
+
|
7
|
+
if RUBY_VERSION =~ /1.9.\d/
|
8
|
+
Encoding.default_external = Encoding::UTF_8
|
9
|
+
end
|
10
|
+
|
11
|
+
if ENV['RACK_ENV'] == "test"
|
12
|
+
set :raise_errors, true
|
13
|
+
end
|
14
|
+
|
15
|
+
enable :session
|
16
|
+
set :haml, :format => :html5
|
17
|
+
set :views, ::File.expand_path('../../../../web/haml', __FILE__)
|
18
|
+
set :public_folder, ::File.expand_path('../../../../web', __FILE__)
|
19
|
+
|
20
|
+
helpers do
|
21
|
+
include Rack::Utils
|
22
|
+
include FnordMetric::AppHelpers
|
23
|
+
end
|
24
|
+
|
25
|
+
%w(fnordmetric-ui.js fnordmetric-ui.css fnordmetric-core.css fnordmetric-core.js).each do |f|
|
26
|
+
next if ::File.exists?(::File.expand_path("../../../../web/#{f}", __FILE__))
|
27
|
+
raise "error: file 'web/#{f}' does not exist, please run build.sh in web/"
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(opts = {})
|
31
|
+
@opts = FnordMetric.options(opts)
|
32
|
+
|
33
|
+
@namespaces = FnordMetric.namespaces
|
34
|
+
@redis = Redis.connect(:url => @opts[:redis_url])
|
35
|
+
|
36
|
+
super(nil)
|
37
|
+
end
|
38
|
+
|
39
|
+
get '/' do
|
40
|
+
redirect "#{path_prefix}/#{@namespaces.keys.first}"
|
41
|
+
end
|
42
|
+
|
43
|
+
get '/:namespace' do
|
44
|
+
pass unless current_namespace
|
45
|
+
current_namespace.ready!(@redis)
|
46
|
+
haml :app
|
47
|
+
end
|
48
|
+
|
49
|
+
post '/events' do
|
50
|
+
params = JSON.parse(request.body.read) unless params
|
51
|
+
halt 400, 'please specify the event_type (_type)' unless params["_type"]
|
52
|
+
track_event((8**32).to_s(36), parse_params(params))
|
53
|
+
end
|
54
|
+
|
55
|
+
# FIXPAUL move to websockets
|
56
|
+
get '/:namespace/dashboard/:dashboard' do
|
57
|
+
dashboard = current_namespace.dashboards.fetch(params[:dashboard])
|
58
|
+
|
59
|
+
dashboard.to_json
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module FnordMetric::AppHelpers
|
2
|
+
|
3
|
+
def h(*args)
|
4
|
+
escape_html(*args)
|
5
|
+
end
|
6
|
+
|
7
|
+
def path_prefix
|
8
|
+
request.env["SCRIPT_NAME"]
|
9
|
+
end
|
10
|
+
|
11
|
+
def namespaces
|
12
|
+
@namespaces
|
13
|
+
end
|
14
|
+
|
15
|
+
def current_namespace
|
16
|
+
@namespaces[@namespaces.keys.detect{ |k|
|
17
|
+
k.to_s == params[:namespace]
|
18
|
+
}.try(:intern)]
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse_params(hash)
|
22
|
+
hash.tap do |h|
|
23
|
+
h.keys.each{ |k| h[k] = parse_param(h[k]) }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def parse_param(object)
|
28
|
+
return object unless object.is_a?(String)
|
29
|
+
return object.to_f if object.match(/^[0-9]+[,\.][0-9]+$/)
|
30
|
+
return object.to_i if object.match(/^[0-9]+$/)
|
31
|
+
object
|
32
|
+
end
|
33
|
+
|
34
|
+
def track_event(event_id, event_data)
|
35
|
+
# FIXPAUL: use api
|
36
|
+
@redis.hincrby "#{@opts[:redis_prefix]}-stats", "events_received", 1
|
37
|
+
@redis.set "#{@opts[:redis_prefix]}-event-#{event_id}", event_data.to_json
|
38
|
+
@redis.lpush "#{@opts[:redis_prefix]}-queue", event_id
|
39
|
+
@redis.expire "#{@opts[:redis_prefix]}-event-#{event_id}", @opts[:event_queue_ttl]
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class FnordMetric::Dashboard
|
2
|
+
|
3
|
+
attr_accessor :widgets
|
4
|
+
|
5
|
+
def initialize(options={})
|
6
|
+
raise "please provide a :title" unless options[:title]
|
7
|
+
@widgets = Array.new
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def add_widget(w)
|
12
|
+
@widgets << w
|
13
|
+
end
|
14
|
+
|
15
|
+
def title
|
16
|
+
@options[:title]
|
17
|
+
end
|
18
|
+
|
19
|
+
def group
|
20
|
+
@options[:group] || "Dashboards"
|
21
|
+
end
|
22
|
+
|
23
|
+
def token
|
24
|
+
token = title.to_s.gsub(/[\W]/, '')
|
25
|
+
token = Digest::SHA1.hexdigest(title.to_s) if token.empty?
|
26
|
+
token
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_json
|
30
|
+
{
|
31
|
+
:title => title,
|
32
|
+
:widgets => {}.tap { |wids|
|
33
|
+
@widgets.each do |w|
|
34
|
+
wids[w.token] = w.render
|
35
|
+
end
|
36
|
+
}
|
37
|
+
}.to_json
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module FnordMetric
|
2
|
+
class Event
|
3
|
+
|
4
|
+
attr_accessor :time, :type, :event_id
|
5
|
+
|
6
|
+
#def self.track!(event_type, event_data)
|
7
|
+
#end
|
8
|
+
|
9
|
+
def self.all(opts)
|
10
|
+
opts[:limit] ||= 100
|
11
|
+
|
12
|
+
range_opts = { :withscores => true }
|
13
|
+
range_opts.merge!(:limit => [0,opts[:limit]]) if opts[:limit]
|
14
|
+
|
15
|
+
events = opts[:redis].zrevrangebyscore(
|
16
|
+
"#{opts[:namespace_prefix]}-timeline",
|
17
|
+
'+inf', opts[:since]||'0',
|
18
|
+
range_opts
|
19
|
+
)
|
20
|
+
|
21
|
+
events = events[0..opts[:limit] - 1]
|
22
|
+
|
23
|
+
unless events.first.is_a?(Array)
|
24
|
+
events = events.in_groups_of(2).map do |event_id, ts|
|
25
|
+
[event_id, Float(ts)]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
events.map do |event_id, ts|
|
30
|
+
next if event_id.blank?
|
31
|
+
find(event_id, opts).tap{ |e| e.time = "%.f" % ts }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.by_type(_type, opts)
|
36
|
+
opts[:redis].lrange(
|
37
|
+
"#{opts[:namespace_prefix]}-type-#{_type}",
|
38
|
+
0, 200).map do |event_id|
|
39
|
+
next if event_id.blank?
|
40
|
+
find(event_id, opts).tap{ |e| }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.by_session_key(_session_key, opts)
|
45
|
+
session = Session.find(_session_key, opts)
|
46
|
+
session.fetch_event_ids!(200).reject(&:blank?).map do |event_id|
|
47
|
+
find(event_id, opts)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.find(event_id, opts)
|
52
|
+
self.new(event_id, opts).tap do |event|
|
53
|
+
event.fetch!
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def initialize(event_id, opts)
|
58
|
+
@opts = opts
|
59
|
+
@event_id = event_id
|
60
|
+
end
|
61
|
+
|
62
|
+
def fetch!
|
63
|
+
@data = JSON.parse(fetch_json).tap do |event|
|
64
|
+
@type = event.delete("_type")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def fetch_json
|
69
|
+
@opts[:redis].get(redis_key) || "{}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def redis_key
|
73
|
+
[@opts[:redis_prefix], :event, @event_id].join("-")
|
74
|
+
end
|
75
|
+
|
76
|
+
def session_key
|
77
|
+
@data["_session"] ? Digest::MD5.hexdigest(@data["_session"]) : nil
|
78
|
+
end
|
79
|
+
|
80
|
+
def id
|
81
|
+
@event_id
|
82
|
+
end
|
83
|
+
|
84
|
+
def data(key=nil)
|
85
|
+
key ? @data[key.to_s] : @data
|
86
|
+
end
|
87
|
+
|
88
|
+
alias :[] :data
|
89
|
+
|
90
|
+
def to_json
|
91
|
+
@data.merge!(
|
92
|
+
:_type => @type,
|
93
|
+
:_session_key => session_key,
|
94
|
+
:_eid => @event_id,
|
95
|
+
:_time => @time
|
96
|
+
)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
class FnordMetric::Reactor
|
2
|
+
|
3
|
+
def initialize
|
4
|
+
@redis = FnordMetric.mk_redis
|
5
|
+
@namespaces = FnordMetric.namespaces.dup
|
6
|
+
end
|
7
|
+
|
8
|
+
def ready!
|
9
|
+
@namespaces.each do |key, ns|
|
10
|
+
ns.ready!(@redis)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(*args)
|
15
|
+
execute_unsafe(*args)
|
16
|
+
rescue Exception => e
|
17
|
+
FnordMetric.error("reactor crashed: " + e.to_s)
|
18
|
+
puts (e.backtrace * "\n") if ENV["FNORDMETRIC_ENV"] == "dev"
|
19
|
+
[]
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def execute_unsafe(socket, event, messages = [])
|
25
|
+
return [] unless event["namespace"]
|
26
|
+
|
27
|
+
unless ns = @namespaces[event["namespace"].to_sym]
|
28
|
+
return([{ "error" => "invalid namespace: #{event["namespace"]}" }])
|
29
|
+
end
|
30
|
+
|
31
|
+
messages << discover(ns) if event["type"] == "discover_request"
|
32
|
+
messages << widget(ns, event) if event["type"] == "widget_request"
|
33
|
+
messages << gauge(ns, event) if event["type"] == "render_request"
|
34
|
+
messages << active_users(ns, event) if event["type"] == "active_users_request"
|
35
|
+
messages << gauge_list(ns, event) if event["type"] == "gauge_list_request"
|
36
|
+
|
37
|
+
messages.flatten.compact.map do |m|
|
38
|
+
m["namespace"] = event["namespace"]; m
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def widget(namespace, event)
|
43
|
+
klass = if event["klass"] == "generic" && event["cmd"] == "values_for"
|
44
|
+
FnordMetric::NumbersWidget
|
45
|
+
elsif event["klass"] == "generic" && event["cmd"] == "values_at"
|
46
|
+
FnordMetric::TimeseriesWidget
|
47
|
+
else
|
48
|
+
"FnordMetric::#{event["klass"]}".constantize
|
49
|
+
end
|
50
|
+
|
51
|
+
klass.execute(namespace, event)
|
52
|
+
end
|
53
|
+
|
54
|
+
def gauge(namespace, event)
|
55
|
+
return false unless gauge = namespace.gauges[event["gauge"].to_sym]
|
56
|
+
|
57
|
+
{
|
58
|
+
:type => "render_response",
|
59
|
+
:gauge => gauge.name,
|
60
|
+
:payload => gauge.render_to_event(namespace, event)
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
def discover(namespace)
|
65
|
+
namespace.ready!(@redis)
|
66
|
+
|
67
|
+
[namespace.dashboards.map do |dash_key, dash|
|
68
|
+
{ "type" => "discover_response", "gauge_key" => dash_key, "view" => "dashboard",
|
69
|
+
"group" => dash.group }
|
70
|
+
end,
|
71
|
+
namespace.gauges.map do |gauge_key, gauge|
|
72
|
+
next unless gauge.renderable?
|
73
|
+
{ "type" => "discover_response", "gauge_key" => gauge_key, "view" => "gauge",
|
74
|
+
"title" => gauge.title, "group" => gauge.group, "tick" => gauge.tick }
|
75
|
+
end.compact]
|
76
|
+
end
|
77
|
+
|
78
|
+
def active_users(namespace, event)
|
79
|
+
namespace.ready!(@redis)
|
80
|
+
|
81
|
+
events = if event["filter_by_type"]
|
82
|
+
namespace.events(:by_type, :type => event["type"])
|
83
|
+
elsif event["filter_by_session_key"]
|
84
|
+
namespace.events(:by_session_key, :session_key => params["session_key"])
|
85
|
+
else
|
86
|
+
find_opts = { :limit => 100 }
|
87
|
+
find_opts.merge!(:since => event["since"].to_i+1) if event["since"]
|
88
|
+
namespace.events(:all, find_opts)
|
89
|
+
end
|
90
|
+
|
91
|
+
sessions = namespace.sessions(:all, :limit => 100).map do |session|
|
92
|
+
session.fetch_data!
|
93
|
+
session.to_json
|
94
|
+
end
|
95
|
+
|
96
|
+
types_key = namespace.key_prefix("type-")
|
97
|
+
types = if event["first_poll"]
|
98
|
+
@redis.keys("#{types_key}*").map{ |k| k.gsub(types_key,'') }
|
99
|
+
else
|
100
|
+
[]
|
101
|
+
end
|
102
|
+
|
103
|
+
{
|
104
|
+
:type => "active_users_response",
|
105
|
+
:sessions => sessions,
|
106
|
+
:events => events.map(&:to_json),
|
107
|
+
:types => types
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
def gauge_list(namespace, event)
|
112
|
+
namespace.ready!(@redis)
|
113
|
+
|
114
|
+
gauges = namespace.gauges.map do |name, gauge|
|
115
|
+
{
|
116
|
+
"key" => gauge.name,
|
117
|
+
"title" => gauge.title
|
118
|
+
}
|
119
|
+
end
|
120
|
+
|
121
|
+
{
|
122
|
+
:type => "gauge_list_response",
|
123
|
+
:gauges => gauges
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
class FnordMetric::Web
|
2
|
+
|
3
|
+
def initialize(opts)
|
4
|
+
@opts = opts
|
5
|
+
|
6
|
+
@opts[:server] ||= "thin"
|
7
|
+
@opts[:host] ||= "0.0.0.0"
|
8
|
+
@opts[:port] ||= "4242"
|
9
|
+
|
10
|
+
FnordMetric.register(self)
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialized
|
14
|
+
server = @opts[:server].downcase
|
15
|
+
|
16
|
+
middleware_stack = @opts[:use] || []
|
17
|
+
|
18
|
+
websocket = FnordMetric::WebSocket.new
|
19
|
+
|
20
|
+
webapp = if FnordMetric.options[:http_websocket_only]
|
21
|
+
lambda { |env| [204, {}, [""]] }
|
22
|
+
else
|
23
|
+
FnordMetric::App.new(@opts)
|
24
|
+
end
|
25
|
+
|
26
|
+
dispatch = Rack::Builder.app do
|
27
|
+
use Rack::CommonLogger
|
28
|
+
use Rack::ShowExceptions
|
29
|
+
|
30
|
+
map "/stream" do
|
31
|
+
run websocket
|
32
|
+
end
|
33
|
+
|
34
|
+
map "/" do
|
35
|
+
middleware_stack.each do |middleware|
|
36
|
+
use(*middleware[0..1], &middleware[2])
|
37
|
+
end
|
38
|
+
|
39
|
+
run webapp
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
unless ["thin", "hatetepe"].include? server
|
45
|
+
raise "Need an EventMachine webserver, but #{server} isn't"
|
46
|
+
end
|
47
|
+
|
48
|
+
host = @opts[:host]
|
49
|
+
port = @opts[:port]
|
50
|
+
|
51
|
+
Rack::Server.start(
|
52
|
+
:app => dispatch,
|
53
|
+
:server => server,
|
54
|
+
:Host => host,
|
55
|
+
:Port => port
|
56
|
+
) && FnordMetric.log("listening on http://#{host}:#{port}")
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "eventmachine"
|
2
|
+
require 'rack/websocket'
|
3
|
+
require "em-hiredis"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
class FnordMetric::WebSocket < Rack::WebSocket::Application
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
super
|
10
|
+
|
11
|
+
@reactor = FnordMetric::Reactor.new
|
12
|
+
@uuid = "websocket-#{get_uuid}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def on_open(env)
|
16
|
+
@reactor.ready!
|
17
|
+
end
|
18
|
+
|
19
|
+
def on_message(env, message)
|
20
|
+
begin
|
21
|
+
message = JSON.parse(message)
|
22
|
+
rescue
|
23
|
+
puts "websocket: invalid json"
|
24
|
+
else
|
25
|
+
message["_eid"] ||= get_uuid
|
26
|
+
message["_sender"] = @uuid
|
27
|
+
|
28
|
+
@reactor.execute(self, message).each do |m|
|
29
|
+
send_data m.to_json
|
30
|
+
end
|
31
|
+
end
|
32
|
+
rescue Exception => e
|
33
|
+
FnordMetric.error("[WebSocket] #{e.to_s}")
|
34
|
+
puts e.backtrace.join("\n")
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_uuid
|
38
|
+
rand(8**64).to_s(36)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
class FnordMetric::Widget
|
2
|
+
|
3
|
+
attr_accessor :gauges, :tick
|
4
|
+
|
5
|
+
def initialize(opts={})
|
6
|
+
@opts = opts
|
7
|
+
|
8
|
+
unless opts.has_key?(:title)
|
9
|
+
error! "widget can't be initialized without a title"
|
10
|
+
end
|
11
|
+
|
12
|
+
add_gauges(opts.delete(:gauges))
|
13
|
+
end
|
14
|
+
|
15
|
+
def title
|
16
|
+
@opts[:title]
|
17
|
+
end
|
18
|
+
|
19
|
+
def token
|
20
|
+
token = title.to_s.gsub(/[\W]/, '').downcase
|
21
|
+
token = Digest::SHA1.hexdigest(title.to_s) if token.empty?
|
22
|
+
token
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_gauges(gauges)
|
26
|
+
if gauges.blank? && has_tick?
|
27
|
+
error! "initializing a widget without gauges is void"
|
28
|
+
else
|
29
|
+
@gauges = gauges
|
30
|
+
end
|
31
|
+
|
32
|
+
if (ticks = gauges.map{ |g| g.tick }).uniq.length == 1
|
33
|
+
@tick = ticks.first
|
34
|
+
elsif !!self.try(:has_tick?)
|
35
|
+
error! "you can't add gauges with different ticks to the same widget"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def error!(msg)
|
40
|
+
FnordMetric.error!(msg)
|
41
|
+
end
|
42
|
+
|
43
|
+
def range
|
44
|
+
ensure_has_tick!
|
45
|
+
#@opts[:range] || default_range # FIXME: allow custom ranges, but assure that the range-start is 'on a tick'
|
46
|
+
default_range
|
47
|
+
end
|
48
|
+
|
49
|
+
def ticks
|
50
|
+
ensure_has_tick!
|
51
|
+
range.step(@tick)
|
52
|
+
end
|
53
|
+
|
54
|
+
def default_range(now=Time.now)
|
55
|
+
ensure_has_tick!
|
56
|
+
te = gauges.first.tick_at(now.to_i)
|
57
|
+
te -= @tick unless include_current?
|
58
|
+
rs = (@opts[:ticks] || (@tick == 1.hour.to_i ? 24 : 30)).to_i
|
59
|
+
(te-(@tick*rs)..te)
|
60
|
+
end
|
61
|
+
|
62
|
+
def include_current?
|
63
|
+
!(@opts[:include_current] == false)
|
64
|
+
end
|
65
|
+
|
66
|
+
def data
|
67
|
+
{
|
68
|
+
:title => @opts[:title],
|
69
|
+
:width => @opts[:width] || 100,
|
70
|
+
:klass => self.class.name.split("::").last
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
def render
|
75
|
+
data
|
76
|
+
end
|
77
|
+
|
78
|
+
def ensure_has_tick!
|
79
|
+
error! "widget does not have_tick" unless has_tick?
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
class FnordMetric::BarsWidget < FnordMetric::Widget
|
2
|
+
|
3
|
+
def self.execute(namespace, event)
|
4
|
+
resp = if event["cmd"] == "values_for"
|
5
|
+
{
|
6
|
+
:cmd => :values_for,
|
7
|
+
:values => execute_values_for(namespace.gauges[event["gauge"].to_sym], event["until"])
|
8
|
+
}
|
9
|
+
end
|
10
|
+
|
11
|
+
return false unless resp
|
12
|
+
|
13
|
+
resp.merge(
|
14
|
+
:type => "widget_response",
|
15
|
+
:widget_key => event["widget_key"]
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.execute_values_for(gauge, time)
|
20
|
+
gauge.field_values_at(time).sort do |a,b|
|
21
|
+
a.first.to_i <=> b.first.to_i
|
22
|
+
end.map do |a|
|
23
|
+
[a.first, a.second.to_i]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def data
|
28
|
+
super.merge(
|
29
|
+
:gauge => gauges.first.name,
|
30
|
+
:title => gauges.first.title,
|
31
|
+
:autoupdate => (@opts[:autoupdate] || 60),
|
32
|
+
:order_by => (@opts[:order_by] || 'value'),
|
33
|
+
:plot_style => (@opts[:plot_style] || 'vertical'),
|
34
|
+
:async_chart => true,
|
35
|
+
:color => FnordMetric::COLORS.last,
|
36
|
+
:tick => tick
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
def has_tick?
|
41
|
+
false
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class FnordMetric::HtmlWidget < FnordMetric::Widget
|
2
|
+
def data
|
3
|
+
super.merge(
|
4
|
+
:html => @opts[:html]
|
5
|
+
)
|
6
|
+
end
|
7
|
+
|
8
|
+
def add_gauges(gauges)
|
9
|
+
@gauges = []
|
10
|
+
@tick = 0
|
11
|
+
|
12
|
+
if !gauges.blank?
|
13
|
+
error! "initializing a html widget with gauges is void"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def data_gauges
|
18
|
+
{}
|
19
|
+
end
|
20
|
+
|
21
|
+
def default_range(now=Time.now)
|
22
|
+
0..0
|
23
|
+
end
|
24
|
+
|
25
|
+
def has_tick?
|
26
|
+
false
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
class FnordMetric::NumbersWidget < FnordMetric::Widget
|
2
|
+
|
3
|
+
def self.execute(namespace, event)
|
4
|
+
resp = if event["cmd"] == "values_for"
|
5
|
+
execute_values_for(namespace.gauges[event["gauge"].to_sym], event)
|
6
|
+
end
|
7
|
+
|
8
|
+
return false unless resp
|
9
|
+
|
10
|
+
resp.merge(
|
11
|
+
:type => "widget_response",
|
12
|
+
:widget_key => event["widget_key"]
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.execute_values_for(gauge, event)
|
17
|
+
unless gauge
|
18
|
+
return { "error" => "gauge not found..." }
|
19
|
+
end
|
20
|
+
|
21
|
+
_t = Time.now.to_i
|
22
|
+
|
23
|
+
if at = event["at"]
|
24
|
+
value = if at =~ /sum\((.+)\)/
|
25
|
+
vals = gauge.values_in(FnordMetric::Util.parse_time($1).._t+gauge.tick)
|
26
|
+
vals.values.compact.map(&:to_f).inject(&:+)
|
27
|
+
elsif at =~ /avg\((.+)\)/
|
28
|
+
vals = gauge.values_in(FnordMetric::Util.parse_time($1).._t+gauge.tick)
|
29
|
+
(vals.values.compact.map(&:to_f).inject(&:+) || 0) / vals.size.to_f
|
30
|
+
else
|
31
|
+
gauge.value_at(FnordMetric::Util.parse_time(at)).to_i
|
32
|
+
end
|
33
|
+
|
34
|
+
return({
|
35
|
+
"cmd" => "values_for",
|
36
|
+
"at" => event["at"],
|
37
|
+
"gauge" => gauge.name,
|
38
|
+
"value" => value })
|
39
|
+
end
|
40
|
+
|
41
|
+
values = {}.tap do |out|
|
42
|
+
event["offsets"].each do |off|
|
43
|
+
if off.to_s.starts_with?("s")
|
44
|
+
offset = 0
|
45
|
+
span = (gauge.tick * off.to_s[1..-1].to_i)
|
46
|
+
values = gauge.values_in((_t-span).._t+gauge.tick)
|
47
|
+
value = values.values.compact.map(&:to_i).sum
|
48
|
+
else
|
49
|
+
offset = off.to_i * gauge.tick
|
50
|
+
span = gauge.tick
|
51
|
+
value = gauge.value_at(_t-offset)
|
52
|
+
end
|
53
|
+
|
54
|
+
out["#{gauge.name}-#{offset}-#{span}"] = {
|
55
|
+
:value => value,
|
56
|
+
:desc => "$formatOffset(#{offset}, #{span})"
|
57
|
+
}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
{ "cmd" => "values_for",
|
62
|
+
"gauge" => gauge.name,
|
63
|
+
"values" => values }
|
64
|
+
end
|
65
|
+
|
66
|
+
def data
|
67
|
+
super.merge(
|
68
|
+
:series => gauges.map(&:name),
|
69
|
+
:series_titles => gauges.map(&:title),
|
70
|
+
:series_units => Hash[gauges.map{ |g| [g.name, g.unit] }],
|
71
|
+
:offsets => (@opts[:offsets] || [0, 1, "s30"]),
|
72
|
+
:autoupdate => (@opts[:autoupdate] || 60)
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
def has_tick?
|
77
|
+
false
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|