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,64 @@
|
|
1
|
+
class FnordMetric::Histogram < Hash
|
2
|
+
|
3
|
+
def initialize
|
4
|
+
super{ |h,k| h[k]=0 }
|
5
|
+
end
|
6
|
+
|
7
|
+
def set_opts(opts = {})
|
8
|
+
@opts = opts
|
9
|
+
end
|
10
|
+
|
11
|
+
def [](key)
|
12
|
+
super(key.to_f)
|
13
|
+
end
|
14
|
+
|
15
|
+
def []=(key, val)
|
16
|
+
super(key.to_f, val)
|
17
|
+
end
|
18
|
+
|
19
|
+
def min
|
20
|
+
keys.sort.first.to_i
|
21
|
+
end
|
22
|
+
|
23
|
+
def max
|
24
|
+
keys.sort.last.to_i
|
25
|
+
end
|
26
|
+
|
27
|
+
def histogram(windows)
|
28
|
+
windows = histogram_windows(windows) unless windows.is_a?(Array)
|
29
|
+
Hash[windows.map{ |w| [w,0] }].tap do |histo|
|
30
|
+
self.each do |k,v|
|
31
|
+
histo.detect do |win, wval|
|
32
|
+
histo[win] += v if win.include?(k)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def json_histogram(windows)
|
39
|
+
histogram(windows).to_a.sort do |a, b|
|
40
|
+
a[0].first <=> b[0].first
|
41
|
+
end.map do |r, v|
|
42
|
+
[r.size == 1.0 ? r.last.to_s : json_value(r), v.to_i]
|
43
|
+
end.to_json
|
44
|
+
end
|
45
|
+
|
46
|
+
def json_value(r)
|
47
|
+
"#{r.first.round(@opts[:precision]).to_s}-#{r.last.round(@opts[:precision]).to_s}"
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def histogram_windows(windows)
|
53
|
+
_min = min
|
54
|
+
_max = max
|
55
|
+
|
56
|
+
return [(0..1)] if (_max-_min == 0)
|
57
|
+
|
58
|
+
windows.times
|
59
|
+
.inject((_min.._max)
|
60
|
+
.step(((_max-_min)/windows.to_f)).to_a << _max){ |a,n|
|
61
|
+
a[n]=(a[n]..a[n+1]); a }.take(windows)
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
class FnordMetric::Logger
|
2
|
+
|
3
|
+
def self.import(logfile_path)
|
4
|
+
expire = FnordMetric.options[:event_queue_ttl]
|
5
|
+
redis = Redis.new
|
6
|
+
|
7
|
+
@opts[:channels] ||= []
|
8
|
+
@opts[:channels] = @opts[:channels].map(&:to_s)
|
9
|
+
|
10
|
+
dump_file = File.open(logfile_path, 'r')
|
11
|
+
num_lines = %x{wc -l #{logfile_path}}.to_i
|
12
|
+
puts "importing #{num_lines} events..."
|
13
|
+
|
14
|
+
dump_file.each_with_log(num_lines) do |line, ind|
|
15
|
+
(8**64).to_s(36).tap do |uuid|
|
16
|
+
redis.set "fnordmetric-event-#{uuid}", line
|
17
|
+
redis.lpush "fnordmetric-queue" , uuid
|
18
|
+
redis.expire "fnordmetric-event-#{uuid}", expire
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(opts)
|
24
|
+
@opts = opts
|
25
|
+
opts.fetch(:file)
|
26
|
+
|
27
|
+
FnordMetric.register(self)
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialized
|
31
|
+
logfile_path = @opts[:file]
|
32
|
+
|
33
|
+
events = Queue.new
|
34
|
+
dump_file = File.open(logfile_path, 'a+')
|
35
|
+
|
36
|
+
fetcher = Thread.new do
|
37
|
+
loop do
|
38
|
+
event = events.pop
|
39
|
+
|
40
|
+
dump_file.write(event.to_json+"\n")
|
41
|
+
dump_file.flush
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
listener = Thread.new do
|
46
|
+
backend = FnordMetric.backend
|
47
|
+
backend.subscribe do |event|
|
48
|
+
events << event if log_channel?(event["_channel"])
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
FnordMetric.log "logging to #{logfile_path}"
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def log_channel?(channel)
|
59
|
+
return !!@opts[:channels] if !channel
|
60
|
+
@opts[:channels].include?(channel.to_s)
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
class FnordMetric::Namespace
|
2
|
+
|
3
|
+
attr_reader :handlers, :gauges, :opts, :key, :dashboards, :flags
|
4
|
+
|
5
|
+
@@opts = [:event, :gauge, :widget, :set_title, :hide_active_users, :hide_overview,
|
6
|
+
:hide_gauge_explorer, :dashboard]
|
7
|
+
|
8
|
+
@@multi_gauges = [:timeseries_gauge, :toplist_gauge, :distribution_gauge]
|
9
|
+
|
10
|
+
def initialize(key, opts)
|
11
|
+
@gauges = Hash.new
|
12
|
+
@dashboards = Hash.new
|
13
|
+
@handlers = Hash.new.with_indifferent_access
|
14
|
+
@title = key
|
15
|
+
@opts = opts
|
16
|
+
@key = key
|
17
|
+
|
18
|
+
@flags = {
|
19
|
+
:hide_active_users => (FnordMetric.options[:enable_active_users] == false),
|
20
|
+
:hide_gauge_explorer => (FnordMetric.options[:enable_gauge_explorer] == false)
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def ready!(redis, sync_redis = nil)
|
25
|
+
@redis = redis
|
26
|
+
@sync_redis = sync_redis
|
27
|
+
load_gauges
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
def announce(event)
|
32
|
+
if !@flags[:hide_active_users]
|
33
|
+
announce_to_timeline(event)
|
34
|
+
announce_to_typelist(event)
|
35
|
+
end
|
36
|
+
|
37
|
+
if event[:_session]
|
38
|
+
event[:_session_key] = announce_to_session(event).session_key
|
39
|
+
end
|
40
|
+
|
41
|
+
if event[:_type].to_sym == :_enterprise
|
42
|
+
ctx = FnordMetric::Context.new(opts, FnordMetric::Enterprise::CompatibilityHandler)
|
43
|
+
ctx.call(event, @redis, self)
|
44
|
+
return self
|
45
|
+
end
|
46
|
+
|
47
|
+
if FnordMetric::ZeroConfigGauge::TYPES.include?(event[:_type].to_sym)
|
48
|
+
ctx = FnordMetric::Context.new(opts, FnordMetric::ZeroConfigGauge::Handler)
|
49
|
+
ctx.call(event, @redis, self)
|
50
|
+
return self
|
51
|
+
end
|
52
|
+
|
53
|
+
res = [
|
54
|
+
@handlers[event[:_type].to_s],
|
55
|
+
@handlers["*"]
|
56
|
+
].flatten.compact.each do |context|
|
57
|
+
context.call(event, @redis, self)
|
58
|
+
end.size
|
59
|
+
|
60
|
+
if res == 0
|
61
|
+
FnordMetric.error("no handler for event-type: #{event[:_type]}")
|
62
|
+
end
|
63
|
+
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
def announce_to_session(event)
|
68
|
+
FnordMetric::Session.create(@opts.clone.merge(
|
69
|
+
:namespace_key => @key,
|
70
|
+
:namespace_prefix => key_prefix,
|
71
|
+
:redis => @redis,
|
72
|
+
:event => event
|
73
|
+
))
|
74
|
+
end
|
75
|
+
|
76
|
+
def announce_to_timeline(event)
|
77
|
+
timeline_key = key_prefix(:timeline)
|
78
|
+
@redis.zadd(timeline_key, event[:_time], event[:_eid])
|
79
|
+
end
|
80
|
+
|
81
|
+
def announce_to_typelist(event)
|
82
|
+
typelist_key = key_prefix("type-#{event[:_type]}")
|
83
|
+
@redis.lpush(typelist_key, event[:_eid])
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
def key_prefix(append=nil)
|
88
|
+
[@opts[:redis_prefix], @key, append].compact.join("-")
|
89
|
+
end
|
90
|
+
|
91
|
+
def token
|
92
|
+
@key
|
93
|
+
end
|
94
|
+
|
95
|
+
def title
|
96
|
+
@title
|
97
|
+
end
|
98
|
+
|
99
|
+
def dashboards(name=nil, opts = {})
|
100
|
+
return @dashboards unless name
|
101
|
+
dash = FnordMetric::Dashboard.new(opts.merge(:title => name))
|
102
|
+
@dashboards[dash.token.to_s] ||= dash
|
103
|
+
end
|
104
|
+
|
105
|
+
def sessions(_ids, opts={})
|
106
|
+
return FnordMetric::Session.all(extend_opts(opts)) if _ids == :all
|
107
|
+
end
|
108
|
+
|
109
|
+
def events(_ids, opts={})
|
110
|
+
return FnordMetric::Event.all(extend_opts(opts)) if _ids == :all
|
111
|
+
return FnordMetric::Event.by_type(opts.delete(:type), extend_opts(opts)) if _ids == :by_type
|
112
|
+
return FnordMetric::Event.by_session_key(opts.delete(:session_key), extend_opts(opts)) if _ids == :by_session_key
|
113
|
+
end
|
114
|
+
|
115
|
+
def method_missing(m, *args, &block)
|
116
|
+
return send(:opt_multigauge, *args.unshift(m), &block) if @@multi_gauges.include?(m)
|
117
|
+
raise "unknown option: #{m}" unless @@opts.include?(m)
|
118
|
+
send(:"opt_#{m}", *args, &block)
|
119
|
+
end
|
120
|
+
|
121
|
+
def opt_hide_active_users
|
122
|
+
@flags[:hide_active_users] = true
|
123
|
+
end
|
124
|
+
|
125
|
+
def opt_hide_gauge_explorer
|
126
|
+
@flags[:hide_gauge_explorer] = true
|
127
|
+
end
|
128
|
+
|
129
|
+
def opt_hide_overview
|
130
|
+
@flags[:hide_overview] = true
|
131
|
+
end
|
132
|
+
|
133
|
+
def opt_set_title(title)
|
134
|
+
@title = title
|
135
|
+
end
|
136
|
+
|
137
|
+
def opt_event(event_type, opts={}, &block)
|
138
|
+
FnordMetric::Context.new(opts, block).tap do |context|
|
139
|
+
@handlers[event_type.to_s] ||= []
|
140
|
+
@handlers[event_type.to_s] << context
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def opt_gauge(gauge_key, opts={})
|
145
|
+
opts.merge!(:key => gauge_key)
|
146
|
+
store_gauge(gauge_key, opts) if opts[:zero_config]
|
147
|
+
opts.merge!(:key_prefix => key_prefix)
|
148
|
+
klass = "FnordMetric::#{(opts[:type] || "").to_s.camelize}Gauge".constantize
|
149
|
+
@gauges[gauge_key] ||= klass.new(opts)
|
150
|
+
end
|
151
|
+
|
152
|
+
def opt_multigauge(gauge_type, gauge_key, opts={})
|
153
|
+
opts.merge!(:key => gauge_key, :key_prefix => key_prefix)
|
154
|
+
klass = "FnordMetric::#{gauge_type.to_s.camelize}"
|
155
|
+
@gauges[gauge_key] ||= klass.constantize.new(opts)
|
156
|
+
end
|
157
|
+
|
158
|
+
def opt_widget(dashboard, widget)
|
159
|
+
widget = build_widget(widget) if widget.is_a?(Hash)
|
160
|
+
dashboards(dashboard).add_widget(widget)
|
161
|
+
end
|
162
|
+
|
163
|
+
def opt_dashboard(dashboard, opts)
|
164
|
+
dashboards(dashboard, opts)
|
165
|
+
end
|
166
|
+
|
167
|
+
def build_widget(opts)
|
168
|
+
_gauges = [opts[:gauges]].flatten.map do |g|
|
169
|
+
@gauges[g] || FnordMetric::ZeroConfigGauge.new(g, self)
|
170
|
+
end
|
171
|
+
widget_klass = "FnordMetric::#{opts.fetch(:type).to_s.capitalize}Widget"
|
172
|
+
widget_klass.constantize.new(opts.merge(:gauges => _gauges))
|
173
|
+
end
|
174
|
+
|
175
|
+
def store_gauge(gauge_key, opts)
|
176
|
+
gaugelist_key = key_prefix("zero-config-gauges")
|
177
|
+
sync_redis.hset(gaugelist_key, gauge_key, opts.to_json)
|
178
|
+
end
|
179
|
+
|
180
|
+
def load_gauges
|
181
|
+
gaugelist_key = key_prefix("zero-config-gauges")
|
182
|
+
sync_redis.hgetall(gaugelist_key).each do |gauge_key, gauge_opts|
|
183
|
+
gopts = JSON.parse(gauge_opts).symbolize_keys
|
184
|
+
gopts.delete(:zero_config)
|
185
|
+
opt_gauge(gauge_key.to_sym, gopts)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def sync_redis
|
190
|
+
@sync_redis || @redis
|
191
|
+
end
|
192
|
+
|
193
|
+
def extend_opts(opts)
|
194
|
+
opts.merge(
|
195
|
+
:namespace_prefix => key_prefix,
|
196
|
+
:redis_prefix => @opts[:redis_prefix],
|
197
|
+
:redis => @redis
|
198
|
+
)
|
199
|
+
end
|
200
|
+
|
201
|
+
def to_json
|
202
|
+
flags.merge(
|
203
|
+
:token => token,
|
204
|
+
:title => title
|
205
|
+
).to_json
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
class FnordMetric::Session
|
2
|
+
|
3
|
+
attr_accessor :updated_at, :name, :picture
|
4
|
+
|
5
|
+
@@meta_attributes = %w(name picture)
|
6
|
+
|
7
|
+
def self.create(opts)
|
8
|
+
redis = opts.fetch(:redis)
|
9
|
+
event = opts[:event]
|
10
|
+
|
11
|
+
hash = Digest::MD5.hexdigest(event[:_session])
|
12
|
+
set_key = "#{opts[:namespace_prefix]}-session"
|
13
|
+
|
14
|
+
self.new(hash).tap do |session|
|
15
|
+
session.add_redis(redis, set_key)
|
16
|
+
session.add_event(event)
|
17
|
+
session.expire(opts[:session_data_ttl])
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.find(session_key, opts)
|
22
|
+
set_key = "#{opts[:namespace_prefix]}-session"
|
23
|
+
self.new(session_key, [opts[:redis], set_key])
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.all(opts)
|
27
|
+
set_key = "#{opts[:namespace_prefix]}-session"
|
28
|
+
limit = (opts[:limit].try(:to_i)||0)-1
|
29
|
+
session_ids = opts[:redis].zrevrange(set_key, 0, limit, :withscores => true)
|
30
|
+
|
31
|
+
unless session_ids.first.is_a?(Array)
|
32
|
+
session_ids = session_ids.in_groups_of(2).map do |session_id, ts|
|
33
|
+
[session_id, Float(ts)]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
session_ids.map do |session_key, ts|
|
38
|
+
find(session_key, opts).tap{ |s| s.updated_at = "%.f" % ts }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize(session_key, redis_opts=nil)
|
43
|
+
@session_key = session_key
|
44
|
+
add_redis(*redis_opts) if redis_opts
|
45
|
+
end
|
46
|
+
|
47
|
+
def session_key
|
48
|
+
@session_key
|
49
|
+
end
|
50
|
+
|
51
|
+
def picture
|
52
|
+
@picture
|
53
|
+
end
|
54
|
+
|
55
|
+
def name
|
56
|
+
@name
|
57
|
+
end
|
58
|
+
|
59
|
+
def data(key=nil)
|
60
|
+
key ? @data[key] : @data
|
61
|
+
end
|
62
|
+
|
63
|
+
def event_ids
|
64
|
+
@event_ids || []
|
65
|
+
end
|
66
|
+
|
67
|
+
def events
|
68
|
+
[]
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_json
|
72
|
+
{ :session_key => session_key }.tap do |hash|
|
73
|
+
hash.merge!(:_picture => @picture) if @picture
|
74
|
+
hash.merge!(:_name => @name) if @name
|
75
|
+
hash.merge!(:_updated_at => @updated_at) if @updated_at
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def redis_key(append=nil)
|
80
|
+
[@redis_prefix, @session_key, append].compact.join("-")
|
81
|
+
end
|
82
|
+
|
83
|
+
def add_redis(redis, prefix)
|
84
|
+
@redis_prefix = prefix
|
85
|
+
@redis = redis
|
86
|
+
end
|
87
|
+
|
88
|
+
def touch(time=Time.now.to_i)
|
89
|
+
@redis.zadd(@redis_prefix, time, @session_key)
|
90
|
+
end
|
91
|
+
|
92
|
+
def expire(time)
|
93
|
+
@redis.expire(redis_key(:events), time)
|
94
|
+
@redis.expire(redis_key(:data), time)
|
95
|
+
end
|
96
|
+
|
97
|
+
def add_event(event)
|
98
|
+
@redis.zadd(redis_key(:events), event[:_time], event[:_eid])
|
99
|
+
|
100
|
+
add_data(:_picture, event[:url]) if event[:_type] == "_set_picture"
|
101
|
+
add_data(:_name, event[:name]) if event[:_type] == "_set_name"
|
102
|
+
add_event_data(event) if event[:_type] == "_set_data"
|
103
|
+
touch(event[:_time])
|
104
|
+
end
|
105
|
+
|
106
|
+
def add_event_data(event)
|
107
|
+
event.each do |key, value|
|
108
|
+
add_data(key, value) unless key.to_s.starts_with?("_")
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def add_data(key, value)
|
113
|
+
@redis.hset(redis_key(:data), key, value)
|
114
|
+
end
|
115
|
+
|
116
|
+
def fetch_data!
|
117
|
+
@data = Hash.new
|
118
|
+
@redis.hgetall(redis_key(:data)).each do |key, value|
|
119
|
+
if key.to_s.starts_with?("_")
|
120
|
+
fetch_meta_key(key, value)
|
121
|
+
else
|
122
|
+
@data[key.intern] = value
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def fetch_meta_key(key, value)
|
128
|
+
meta_key = key[1..-1]
|
129
|
+
if @@meta_attributes.include?(meta_key)
|
130
|
+
instance_variable_set(:"@#{meta_key}", value)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def fetch_event_ids!(since=-1)
|
135
|
+
# FIXME: use WITHSCORE to get the timestamps and return event objects
|
136
|
+
@event_ids = @redis.zrevrange(redis_key(:events), 0, since)
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
_opts = FnordMetric.options
|
2
|
+
|
3
|
+
if _opts[:web_interface]
|
4
|
+
FnordMetric::Web.new(
|
5
|
+
:host => _opts[:web_interface][0],
|
6
|
+
:port => _opts[:web_interface][1]
|
7
|
+
)
|
8
|
+
end
|
9
|
+
|
10
|
+
if _opts[:inbound_stream]
|
11
|
+
FnordMetric::Acceptor.new(
|
12
|
+
:protocol => _opts[:inbound_protocol],
|
13
|
+
:host => _opts[:inbound_stream][0],
|
14
|
+
:port => _opts[:inbound_stream][1]
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
if _opts[:start_worker]
|
19
|
+
FnordMetric::Worker.new()
|
20
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
class FnordMetric::Timeseries
|
2
|
+
|
3
|
+
def initialize(timeline = {})
|
4
|
+
@timeline = Hash.new{ |h,k| h[k] = [0,nil] }
|
5
|
+
@timeline.merge!(timeline)
|
6
|
+
end
|
7
|
+
|
8
|
+
def incr_fraction(time, numerator, denominator)
|
9
|
+
incr_numerator(time, numerator) if numerator
|
10
|
+
incr_denominator(time, denominator) if denominator
|
11
|
+
end
|
12
|
+
|
13
|
+
def incr_numerator(time, value)
|
14
|
+
@timeline[time.to_i][0] += value
|
15
|
+
end
|
16
|
+
|
17
|
+
def incr_denominator(time, value)
|
18
|
+
@timeline[time.to_i][-1] ||= 0
|
19
|
+
@timeline[time.to_i][-1] += value
|
20
|
+
end
|
21
|
+
|
22
|
+
def timeseries(range, window, &block)
|
23
|
+
res = Hash.new{ |h,k| h[k] = [0,0] }
|
24
|
+
|
25
|
+
(((range.size)/window.to_f).ceil+1).times.map do |n|
|
26
|
+
res[((range.first+window*(n-1))/window.to_f).floor*window] = [0,0]
|
27
|
+
end
|
28
|
+
|
29
|
+
@timeline.each do |time, vals|
|
30
|
+
next unless range.include?(time)
|
31
|
+
wtime = (time/window.to_f).floor * window
|
32
|
+
if block
|
33
|
+
res[wtime] = block.call(*vals)
|
34
|
+
else
|
35
|
+
res[wtime][0] += vals[0]
|
36
|
+
res[wtime][1] += vals[1]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
FnordMetric::Timeseries.new(res)
|
41
|
+
end
|
42
|
+
|
43
|
+
def sum(range = (ticks.first..ticks.last))
|
44
|
+
@timeline
|
45
|
+
.inject(0){ |s,(t,v)| s + (range.include?(t) ? value_at(t) : 0) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def trend(range = (ticks.first..ticks.last))
|
49
|
+
range ||= (ticks.first..ticks.last)
|
50
|
+
|
51
|
+
rvals = @timeline.to_a
|
52
|
+
.select{ |t,v| range.include?(t) }
|
53
|
+
.sort{ |a,b| a.first <=> b.first }
|
54
|
+
.map{ |t,v| value_at(t) }
|
55
|
+
|
56
|
+
return 0 if rvals.size == 0
|
57
|
+
(rvals.last - rvals.first).to_f / rvals.first
|
58
|
+
end
|
59
|
+
|
60
|
+
def ticks
|
61
|
+
@timeline.keys.sort
|
62
|
+
end
|
63
|
+
|
64
|
+
def value_at(time)
|
65
|
+
if @timeline[time][1].to_i > 0
|
66
|
+
@timeline[time][0] / @timeline[time][1].to_f
|
67
|
+
else
|
68
|
+
@timeline[time][0]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_json(&block)
|
73
|
+
@timeline.to_a
|
74
|
+
.sort{ |a,b| a[0] <=> b[0] }
|
75
|
+
.map { |t,v| { :x => t, :y => block.call(*v), :v0 => v[0], :v1 => v[1] } }
|
76
|
+
.to_json
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
class FnordMetric::Toplist
|
2
|
+
|
3
|
+
attr_accessor :timelines, :total
|
4
|
+
|
5
|
+
def initialize(timeline = {})
|
6
|
+
@total = 0
|
7
|
+
@toplist = Hash.new{ |h,k| h[k] = 0 }
|
8
|
+
|
9
|
+
@timelines = Hash.new do |h,k|
|
10
|
+
h[k] = Hash.new{ |h,k| h[k] = 0 }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def incr_item(time, item, value)
|
15
|
+
@toplist[item] += value.to_f
|
16
|
+
@timelines[item][time] += value.to_f
|
17
|
+
@total += value.to_f
|
18
|
+
end
|
19
|
+
|
20
|
+
def prepare!
|
21
|
+
@toplist_arr = @toplist.to_a.sort do |a,b|
|
22
|
+
b.last <=> a.last
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def toplist(take = 100)
|
27
|
+
prepare! unless @toplist_arr
|
28
|
+
@toplist_arr[0..(take-1)]
|
29
|
+
end
|
30
|
+
|
31
|
+
def percentage(item)
|
32
|
+
(@toplist[item].to_f / total.to_f) * 100.0
|
33
|
+
end
|
34
|
+
|
35
|
+
def value(item)
|
36
|
+
@toplist[item].to_f
|
37
|
+
end
|
38
|
+
|
39
|
+
def trend(item)
|
40
|
+
times = @timelines[item].keys.sort
|
41
|
+
|
42
|
+
(@timelines[item][times.last] -
|
43
|
+
@timelines[item][times.first]) /
|
44
|
+
@timelines[item][times.first]
|
45
|
+
end
|
46
|
+
|
47
|
+
def rank(item)
|
48
|
+
prepare! unless @toplist_arr
|
49
|
+
|
50
|
+
@toplist_arr.index([item, value(item)]) + 1
|
51
|
+
end
|
52
|
+
|
53
|
+
def trending(take = 100)
|
54
|
+
@toplist.to_a.map{ |k,v|
|
55
|
+
[k, trend(k)]
|
56
|
+
}.sort{ |a,b|
|
57
|
+
b.last <=> a.last
|
58
|
+
}[0..(take-1)]
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class FnordMetric::UDPClient
|
2
|
+
|
3
|
+
def initialize(host, port)
|
4
|
+
@sock = UDPSocket.new
|
5
|
+
@sock.connect(host, port)
|
6
|
+
end
|
7
|
+
|
8
|
+
def event(event_data)
|
9
|
+
begin
|
10
|
+
if event_data.is_a?(Hash)
|
11
|
+
event_data = event_data.to_json
|
12
|
+
else
|
13
|
+
JSON.parse(event_data) # void ;)
|
14
|
+
end
|
15
|
+
rescue JSON::ParserError
|
16
|
+
FnordMetric.log("event_lost: can't parse json")
|
17
|
+
else
|
18
|
+
@sock.send(event_data, 0)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class FnordMetric::Util
|
2
|
+
|
3
|
+
def self.parse_time(str)
|
4
|
+
str = str.downcase
|
5
|
+
|
6
|
+
if (str == "now")
|
7
|
+
Time.now.to_i
|
8
|
+
elsif str =~ /^([0-9]+(?:\.[0-9]+)?)$/
|
9
|
+
$1.to_i
|
10
|
+
elsif str =~ /^-([0-9]+(?:\.[0-9]+)?)$/
|
11
|
+
Time.now.to_i - $1.to_i
|
12
|
+
elsif str =~ /^-([0-9]+(?:\.[0-9]+)?)s(ec(ond)?(s?))?$/
|
13
|
+
Time.now.to_i - $1.to_f
|
14
|
+
elsif str =~ /^-([0-9]+(?:\.[0-9]+)?)m(in(ute)?(s?))?$/
|
15
|
+
Time.now.to_i - ($1.to_f * 60)
|
16
|
+
elsif str =~ /^-([0-9]+(?:\.[0-9]+)?)h(our(s?))?$/
|
17
|
+
Time.now.to_i - ($1.to_f * 3600)
|
18
|
+
elsif str =~ /^-([0-9]+(?:\.[0-9]+)?)d(ay(s?))?$/
|
19
|
+
Time.now.to_i - ($1.to_i * 86400)
|
20
|
+
else
|
21
|
+
raise "invalid time specifiation: #{str}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|