ustate-client 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +47 -0
- data/lib/ustate/aggregator.rb +69 -0
- data/lib/ustate/client.rb +4 -1
- data/lib/ustate/dash/helper/renderer.rb +59 -21
- data/lib/ustate/dash/views/index.erubis +2 -0
- data/lib/ustate/dash/views/layout.erubis +7 -2
- data/lib/ustate/emailer.rb +6 -4
- data/lib/ustate/graphite.rb +101 -0
- data/lib/ustate/metric_thread.rb +68 -0
- data/lib/ustate/query.rb +17 -0
- data/lib/ustate/query_string.rb +58 -2
- data/lib/ustate/query_string.treetop +10 -1
- data/lib/ustate/server.rb +15 -0
- data/lib/ustate/server/index.rb +88 -5
- data/lib/ustate/state.rb +153 -16
- data/lib/ustate/version.rb +1 -1
- metadata +41 -30
- data/lib/ustate/dash/state.rb +0 -75
data/README.markdown
CHANGED
@@ -83,6 +83,7 @@ Email
|
|
83
83
|
-----
|
84
84
|
|
85
85
|
config.rb:
|
86
|
+
|
86
87
|
# Email comes from this address (required):
|
87
88
|
emailer.from = 'ustate@your.net'
|
88
89
|
|
@@ -93,10 +94,56 @@ config.rb:
|
|
93
94
|
emailer.tell 'you@gmail.com', 'state = "error" or state = "critical"'
|
94
95
|
emailer.tell 'you@gmail.com', 'service =~ "mysql%"'
|
95
96
|
|
97
|
+
Aggregating states
|
98
|
+
---
|
99
|
+
|
100
|
+
UState can fold together states matching some criteria to provide a more
|
101
|
+
general overview of a complex system. Folds are executed in a separate thread
|
102
|
+
and polled from the index every aggregator.interval seconds.
|
103
|
+
|
104
|
+
config.rb:
|
105
|
+
|
106
|
+
# Add together the metrics for all feed mergers on any host.
|
107
|
+
# The resulting state has service name 'feed merger', but no host.
|
108
|
+
aggregator.sum 'service = "feed merger" and host != null', host: nil
|
109
|
+
|
110
|
+
# Average latencies
|
111
|
+
aggregator.average 'service = "api latency"'
|
112
|
+
|
113
|
+
# You can also pass any block to aggregator.fold. The block will be called
|
114
|
+
# with an array of states matching your query.
|
115
|
+
aggregator.fold 'service = "custom"' do |states|
|
116
|
+
UState::State.new(
|
117
|
+
service: 'some crazy result',
|
118
|
+
metric_f: states.map(&:metric_f).max
|
119
|
+
)
|
120
|
+
end
|
121
|
+
|
122
|
+
Graphite
|
123
|
+
---
|
124
|
+
|
125
|
+
UState can forward metrics to Graphite. Just specify a query matching states
|
126
|
+
you'd like to forward. Forwarding is performed in a separate thread, and polled from the index every graphite.interval seconds.
|
127
|
+
|
128
|
+
config.rb:
|
129
|
+
|
130
|
+
graphite.host = 'foo'
|
131
|
+
graphite.port = 12345
|
132
|
+
|
133
|
+
# Submit states every 5 seconds
|
134
|
+
graphite.interval = 5
|
135
|
+
|
136
|
+
# Send everything without a host
|
137
|
+
graphite.graph 'host = null'
|
138
|
+
|
139
|
+
# And also include the disk use on all nodes
|
140
|
+
graphite.graph 'service = "disk"'
|
141
|
+
|
96
142
|
Custom hooks
|
97
143
|
------------
|
98
144
|
|
99
145
|
config.rb:
|
146
|
+
|
100
147
|
# Log all states received to console.
|
101
148
|
index.on_state do |s|
|
102
149
|
p s
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module UState
|
2
|
+
class Aggregator
|
3
|
+
# Combines states periodically.
|
4
|
+
|
5
|
+
INTERVAL = 1
|
6
|
+
|
7
|
+
attr_accessor :interval
|
8
|
+
attr_accessor :folds
|
9
|
+
def initialize(index, opts = {})
|
10
|
+
@index = index
|
11
|
+
|
12
|
+
@folds = {}
|
13
|
+
@interval = opts[:interval] || INTERVAL
|
14
|
+
|
15
|
+
start
|
16
|
+
end
|
17
|
+
|
18
|
+
# Combines states matching query with State.average
|
19
|
+
def average(query, init = State.new)
|
20
|
+
fold query do |states|
|
21
|
+
State.average states, init
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Combines states matching query with the given block. The block
|
26
|
+
# receives an array of states which presently match.
|
27
|
+
#
|
28
|
+
# Example:
|
29
|
+
# fold 'service = "api % reqs/sec"' do |states|
|
30
|
+
# states.inject(State.new(service: 'api reqs/sec')) do |combined, state|
|
31
|
+
# combined.metric_f += state.metric_f
|
32
|
+
# combined
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
def fold(query, &block)
|
36
|
+
@folds[block] = if existing = @folds[block]
|
37
|
+
"(#{existing}) or (#{q})"
|
38
|
+
else
|
39
|
+
query
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Polls index for states matching each fold, applies fold, and inserts into
|
44
|
+
# index.
|
45
|
+
def start
|
46
|
+
@runner = Thread.new do
|
47
|
+
loop do
|
48
|
+
interval = (@interval.to_f / @folds.size) rescue @interval
|
49
|
+
@folds.each do |f, query|
|
50
|
+
matching = @index.query(Query.new(string: query))
|
51
|
+
unless matching.empty?
|
52
|
+
if combined = f[matching]
|
53
|
+
@index << combined
|
54
|
+
end
|
55
|
+
end
|
56
|
+
sleep interval
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Combines states matching query with State.sum
|
63
|
+
def sum(query, init = State.new)
|
64
|
+
fold query do |states|
|
65
|
+
State.sum states, init
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/ustate/client.rb
CHANGED
@@ -89,7 +89,7 @@ class UState::Client
|
|
89
89
|
@locket.synchronize do
|
90
90
|
begin
|
91
91
|
tries += 1
|
92
|
-
yield (@socket
|
92
|
+
yield (@socket || connect)
|
93
93
|
rescue IOError => e
|
94
94
|
raise if tries > 3
|
95
95
|
connect and retry
|
@@ -102,6 +102,9 @@ class UState::Client
|
|
102
102
|
rescue Errno::ECONNRESET => e
|
103
103
|
raise if tries > 3
|
104
104
|
connect and retry
|
105
|
+
rescue InvalidResponse => e
|
106
|
+
raise if tries > 3
|
107
|
+
connect and retry
|
105
108
|
end
|
106
109
|
end
|
107
110
|
end
|
@@ -101,11 +101,17 @@ module UState
|
|
101
101
|
# names have common prefixes removed.
|
102
102
|
# hosts: an array of hosts for rows. Default is every host present in
|
103
103
|
# states, sorted.
|
104
|
+
# transpose: Hosts go across, services go down. Enables :global_maxima.
|
105
|
+
# global_maximum: Compute default maxima for services globally,
|
106
|
+
# instead of a different maximum for each service.
|
104
107
|
def state_chart(states, opts = {})
|
105
108
|
o = {
|
106
109
|
:maxima => {},
|
107
110
|
:service_names => {}
|
108
111
|
}.merge opts
|
112
|
+
if o[:transpose] and not o.include?(:global_maximum)
|
113
|
+
o[:global_maximum] = true
|
114
|
+
end
|
109
115
|
|
110
116
|
# Get all services
|
111
117
|
services = states.map { |s| s.service }.compact.uniq.sort
|
@@ -118,10 +124,18 @@ module UState
|
|
118
124
|
end.merge o[:service_names]
|
119
125
|
|
120
126
|
# Compute maximum for each service
|
121
|
-
maxima =
|
122
|
-
|
123
|
-
m
|
124
|
-
|
127
|
+
maxima = if o[:global_maximum]
|
128
|
+
max = states.map(&:metric).max
|
129
|
+
services.inject({}) do |m, s|
|
130
|
+
m[s] = max
|
131
|
+
m
|
132
|
+
end.merge o[:maxima]
|
133
|
+
else
|
134
|
+
states.inject(Hash.new(0)) do |m, s|
|
135
|
+
m[s.service] = [s.metric, m[s.service]].max
|
136
|
+
m
|
137
|
+
end.merge o[:maxima]
|
138
|
+
end
|
125
139
|
|
126
140
|
# Compute union of all hosts for these states, if no
|
127
141
|
# list of hosts explicitly given.
|
@@ -139,27 +153,51 @@ module UState
|
|
139
153
|
# Title
|
140
154
|
title = o[:title] || prefix.capitalize rescue 'Unknown'
|
141
155
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
156
|
+
if o[:transpose]
|
157
|
+
h2(title) +
|
158
|
+
table(
|
159
|
+
tr(
|
160
|
+
th,
|
161
|
+
*hosts.map do |host|
|
162
|
+
th host
|
163
|
+
end
|
164
|
+
),
|
146
165
|
*services.map do |service|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
166
|
+
tr(
|
167
|
+
th(service_names[service]),
|
168
|
+
*hosts.map do |host|
|
169
|
+
s = by[[host, service]]
|
170
|
+
td(
|
171
|
+
s ? state_bar(s, max: maxima[service]) : nil
|
172
|
+
)
|
173
|
+
end
|
174
|
+
)
|
175
|
+
end,
|
176
|
+
:class => 'chart'
|
177
|
+
)
|
178
|
+
else
|
179
|
+
h2(title) +
|
180
|
+
table(
|
151
181
|
tr(
|
152
|
-
th
|
182
|
+
th,
|
153
183
|
*services.map do |service|
|
154
|
-
|
155
|
-
td(
|
156
|
-
s ? state_bar(s, max: maxima[service]) : nil
|
157
|
-
)
|
184
|
+
th service_names[service]
|
158
185
|
end
|
159
|
-
)
|
160
|
-
|
161
|
-
|
162
|
-
|
186
|
+
),
|
187
|
+
*hosts.map do |host|
|
188
|
+
tr(
|
189
|
+
th(host),
|
190
|
+
*services.map do |service|
|
191
|
+
s = by[[host, service]]
|
192
|
+
td(
|
193
|
+
s ? state_bar(s, max: maxima[service]) : nil
|
194
|
+
)
|
195
|
+
end
|
196
|
+
)
|
197
|
+
end,
|
198
|
+
:class => 'chart'
|
199
|
+
)
|
200
|
+
end
|
163
201
|
end
|
164
202
|
|
165
203
|
# Renders a state as a short tag.
|
@@ -1,3 +1,5 @@
|
|
1
1
|
<div class="box"><%= state_list query 'state != "ok"' %></div>
|
2
2
|
|
3
3
|
<div class="box"><%= state_chart query 'service = "cpu" or service = "memory" or service =~ "disk%" or service = "load"' %></div>
|
4
|
+
|
5
|
+
<div class="box"><%= state_chart query('host = "host0"'), transpose: true %></div>
|
@@ -1,11 +1,16 @@
|
|
1
1
|
<html>
|
2
2
|
<head>
|
3
3
|
<title>Dashboard</title>
|
4
|
-
<meta http-equiv="refresh" content="1" />
|
5
4
|
<link rel="stylesheet" type="text/css" href="/css" />
|
5
|
+
|
6
|
+
<script type="text/javascript">
|
7
|
+
setTimeout(function() {
|
8
|
+
location.reload(true);
|
9
|
+
}, 10000);
|
10
|
+
</script>
|
6
11
|
</head>
|
7
12
|
|
8
|
-
<body>
|
13
|
+
<body onload="refresh();">
|
9
14
|
<div>
|
10
15
|
<h1>Dashboard</h1>
|
11
16
|
<span style="position: absolute; top: 4px; right: 4px;">(as of <%= time Time.now.to_i %>)</span>
|
data/lib/ustate/emailer.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
module UState
|
2
2
|
class Emailer
|
3
|
+
# Sends emails when the index receives state transitions matching specific
|
4
|
+
# queries.
|
5
|
+
|
3
6
|
require 'net/smtp'
|
4
7
|
|
5
8
|
attr_accessor :from
|
@@ -32,7 +35,7 @@ module UState
|
|
32
35
|
raise ArgumentError, "no from address" unless @from
|
33
36
|
|
34
37
|
# Subject
|
35
|
-
subject = "#{s.host} #{s.service}
|
38
|
+
subject = "#{s.host} #{s.service}"
|
36
39
|
if s.once
|
37
40
|
subject << " transient "
|
38
41
|
else
|
@@ -59,11 +62,10 @@ EOF
|
|
59
62
|
|
60
63
|
# Dispatch emails to each address which is interested in this state
|
61
64
|
def receive(*states)
|
62
|
-
state = states.last
|
63
65
|
Thread.new do
|
64
66
|
@tell.each do |address, q|
|
65
|
-
if q === state
|
66
|
-
email address,
|
67
|
+
if states.any? { |state| p state; q === state }
|
68
|
+
email address, states.last
|
67
69
|
end
|
68
70
|
end
|
69
71
|
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module UState
|
2
|
+
class Graphite
|
3
|
+
# Forwards states to Graphite.
|
4
|
+
HOST = '127.0.0.1'
|
5
|
+
PORT = 2003
|
6
|
+
INTERVAL = 5
|
7
|
+
|
8
|
+
attr_accessor :query
|
9
|
+
attr_accessor :host
|
10
|
+
attr_accessor :port
|
11
|
+
attr_accessor :interval
|
12
|
+
|
13
|
+
def initialize(index, opts = {})
|
14
|
+
@index = index
|
15
|
+
@query = opts[:query]
|
16
|
+
@host = opts[:host] || HOST
|
17
|
+
@port = opts[:port] || PORT
|
18
|
+
@interval = opts[:interval] || INTERVAL
|
19
|
+
@locket = Mutex.new
|
20
|
+
|
21
|
+
start
|
22
|
+
end
|
23
|
+
|
24
|
+
def connect
|
25
|
+
@socket = TCPSocket.new(@host, @port)
|
26
|
+
end
|
27
|
+
|
28
|
+
def graph(q)
|
29
|
+
if @query
|
30
|
+
@query = "(#{@query}) or (#{q})"
|
31
|
+
else
|
32
|
+
@query = q
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def forward(state)
|
37
|
+
# Figure out what time to use.
|
38
|
+
present = Time.now.to_i
|
39
|
+
if (present - state.time) >= @interval
|
40
|
+
time = present
|
41
|
+
else
|
42
|
+
time = state.time
|
43
|
+
end
|
44
|
+
|
45
|
+
# Construct message
|
46
|
+
string = "#{path(state)} #{state.metric} #{time}"
|
47
|
+
|
48
|
+
# Validate string
|
49
|
+
if string["\n"]
|
50
|
+
raise ArgumentError, "#{string} has a newline"
|
51
|
+
end
|
52
|
+
|
53
|
+
with_connection do |s|
|
54
|
+
s.puts string
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Formats a state into a Graphite metric path.
|
59
|
+
def path(state)
|
60
|
+
if state.host
|
61
|
+
host = state.host.split('.').reverse.join('.')
|
62
|
+
"#{host}.#{state.service.gsub(' ', '.')}"
|
63
|
+
else
|
64
|
+
state.service.gsub(' ', '.')
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def start
|
69
|
+
@runner = Thread.new do
|
70
|
+
loop do
|
71
|
+
@index.query(Query.new(string: @query)).each do |state|
|
72
|
+
forward state
|
73
|
+
end
|
74
|
+
sleep @interval
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def with_connection
|
80
|
+
tries = 0
|
81
|
+
@locket.synchronize do
|
82
|
+
begin
|
83
|
+
tries += 1
|
84
|
+
yield (@socket or connect)
|
85
|
+
rescue IOError => e
|
86
|
+
raise if tries > 3
|
87
|
+
connect and retry
|
88
|
+
rescue Errno::EPIPE => e
|
89
|
+
raise if tries > 3
|
90
|
+
connect and retry
|
91
|
+
rescue Errno::ECONNREFUSED => e
|
92
|
+
raise if tries > 3
|
93
|
+
connect and retry
|
94
|
+
rescue Errno::ECONNRESET => e
|
95
|
+
raise if tries > 3
|
96
|
+
connect and retry
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
class UState::MetricThread
|
2
|
+
# A metric thread is simple: it wraps some metric object which responds to <<,
|
3
|
+
# and every interval seconds, calls #flush which replaces the object and calls
|
4
|
+
# a user specified function.
|
5
|
+
|
6
|
+
INTERVAL = 10
|
7
|
+
|
8
|
+
attr_accessor :interval
|
9
|
+
attr_accessor :metric
|
10
|
+
|
11
|
+
# client = UState::Client.new
|
12
|
+
# m = MetricThread.new Mtrc::Rate do |rate|
|
13
|
+
# client << rate
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# loop do
|
17
|
+
# sleep rand
|
18
|
+
# m << rand
|
19
|
+
# end
|
20
|
+
def initialize(klass, *klass_args, &f)
|
21
|
+
@klass = klass
|
22
|
+
@klass_args = klass_args
|
23
|
+
@f = f
|
24
|
+
@interval = INTERVAL
|
25
|
+
|
26
|
+
@metric = new_metric
|
27
|
+
|
28
|
+
start
|
29
|
+
end
|
30
|
+
|
31
|
+
def <<(*a)
|
32
|
+
@metric.<<(*a)
|
33
|
+
end
|
34
|
+
|
35
|
+
def new_metric
|
36
|
+
@klass.new *@klass_args
|
37
|
+
end
|
38
|
+
|
39
|
+
def flush
|
40
|
+
old, @metric = @metric, new_metric
|
41
|
+
@f[old]
|
42
|
+
end
|
43
|
+
|
44
|
+
def start
|
45
|
+
raise RuntimeError, "already running" if @runner
|
46
|
+
|
47
|
+
@running = true
|
48
|
+
@runner = Thread.new do
|
49
|
+
while @running
|
50
|
+
sleep @interval
|
51
|
+
begin
|
52
|
+
flush
|
53
|
+
rescue Exception => e
|
54
|
+
end
|
55
|
+
end
|
56
|
+
@runner = nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def stop
|
61
|
+
stop!
|
62
|
+
@runner.join
|
63
|
+
end
|
64
|
+
|
65
|
+
def stop!
|
66
|
+
@running = false
|
67
|
+
end
|
68
|
+
end
|
data/lib/ustate/query.rb
CHANGED
@@ -3,5 +3,22 @@ module UState
|
|
3
3
|
include Beefcake::Message
|
4
4
|
|
5
5
|
optional :string, :string, 1
|
6
|
+
|
7
|
+
# Converts a Query or string to a query AST.
|
8
|
+
def self.query(q)
|
9
|
+
case q
|
10
|
+
when String
|
11
|
+
parser = QueryStringParser.new
|
12
|
+
q = parser.parse(q)
|
13
|
+
unless q
|
14
|
+
raise ArgumentError, "error parsing #{query_string.inspect} at line #{parser.failure_line}:#{parser.failure_column}: #{parser.failure_reason}"
|
15
|
+
end
|
16
|
+
q.query
|
17
|
+
when Query
|
18
|
+
query q.string
|
19
|
+
else
|
20
|
+
raise ArgumentError, "don't know what to do with #{q.class} #{q.inspect}"
|
21
|
+
end
|
22
|
+
end
|
6
23
|
end
|
7
24
|
end
|
data/lib/ustate/query_string.rb
CHANGED
@@ -635,8 +635,13 @@ module UState
|
|
635
635
|
if r3
|
636
636
|
r0 = r3
|
637
637
|
else
|
638
|
-
|
639
|
-
|
638
|
+
r4 = _nt_null
|
639
|
+
if r4
|
640
|
+
r0 = r4
|
641
|
+
else
|
642
|
+
@index = i0
|
643
|
+
r0 = nil
|
644
|
+
end
|
640
645
|
end
|
641
646
|
end
|
642
647
|
end
|
@@ -1048,6 +1053,57 @@ module UState
|
|
1048
1053
|
r0
|
1049
1054
|
end
|
1050
1055
|
|
1056
|
+
module Null0
|
1057
|
+
def ruby_value
|
1058
|
+
nil
|
1059
|
+
end
|
1060
|
+
alias sql ruby_value
|
1061
|
+
end
|
1062
|
+
|
1063
|
+
def _nt_null
|
1064
|
+
start_index = index
|
1065
|
+
if node_cache[:null].has_key?(index)
|
1066
|
+
cached = node_cache[:null][index]
|
1067
|
+
if cached
|
1068
|
+
cached = SyntaxNode.new(input, index...(index + 1)) if cached == true
|
1069
|
+
@index = cached.interval.end
|
1070
|
+
end
|
1071
|
+
return cached
|
1072
|
+
end
|
1073
|
+
|
1074
|
+
i0 = index
|
1075
|
+
if has_terminal?('null', false, index)
|
1076
|
+
r1 = instantiate_node(SyntaxNode,input, index...(index + 4))
|
1077
|
+
@index += 4
|
1078
|
+
else
|
1079
|
+
terminal_parse_failure('null')
|
1080
|
+
r1 = nil
|
1081
|
+
end
|
1082
|
+
if r1
|
1083
|
+
r0 = r1
|
1084
|
+
r0.extend(Null0)
|
1085
|
+
else
|
1086
|
+
if has_terminal?('nil', false, index)
|
1087
|
+
r2 = instantiate_node(SyntaxNode,input, index...(index + 3))
|
1088
|
+
@index += 3
|
1089
|
+
else
|
1090
|
+
terminal_parse_failure('nil')
|
1091
|
+
r2 = nil
|
1092
|
+
end
|
1093
|
+
if r2
|
1094
|
+
r0 = r2
|
1095
|
+
r0.extend(Null0)
|
1096
|
+
else
|
1097
|
+
@index = i0
|
1098
|
+
r0 = nil
|
1099
|
+
end
|
1100
|
+
end
|
1101
|
+
|
1102
|
+
node_cache[:null][start_index] = r0
|
1103
|
+
|
1104
|
+
r0
|
1105
|
+
end
|
1106
|
+
|
1051
1107
|
def _nt_space
|
1052
1108
|
start_index = index
|
1053
1109
|
if node_cache[:space].has_key?(index)
|
@@ -92,7 +92,7 @@ module UState
|
|
92
92
|
end
|
93
93
|
|
94
94
|
rule value
|
95
|
-
double_quoted_string / float / integer
|
95
|
+
double_quoted_string / float / integer / null
|
96
96
|
end
|
97
97
|
|
98
98
|
rule integer
|
@@ -137,6 +137,15 @@ module UState
|
|
137
137
|
}
|
138
138
|
end
|
139
139
|
|
140
|
+
rule null
|
141
|
+
('null' / 'nil') {
|
142
|
+
def ruby_value
|
143
|
+
nil
|
144
|
+
end
|
145
|
+
alias sql ruby_value
|
146
|
+
}
|
147
|
+
end
|
148
|
+
|
140
149
|
rule space
|
141
150
|
[\s]+
|
142
151
|
end
|
data/lib/ustate/server.rb
CHANGED
@@ -14,9 +14,14 @@ module UState
|
|
14
14
|
require 'treetop'
|
15
15
|
require 'ustate/query_string'
|
16
16
|
require 'ustate/query/ast'
|
17
|
+
require 'ustate/metric_thread'
|
18
|
+
require 'mtrc'
|
17
19
|
|
18
20
|
attr_accessor :backends
|
19
21
|
attr_accessor :index
|
22
|
+
attr_writer :aggregator
|
23
|
+
attr_writer :emailer
|
24
|
+
attr_writer :graphite
|
20
25
|
|
21
26
|
def initialize(opts = {})
|
22
27
|
# Backends
|
@@ -30,11 +35,21 @@ module UState
|
|
30
35
|
setup_signals
|
31
36
|
end
|
32
37
|
|
38
|
+
def aggregator(opts = {})
|
39
|
+
require 'ustate/aggregator'
|
40
|
+
@aggregator ||= UState::Aggregator.new(@index, opts)
|
41
|
+
end
|
42
|
+
|
33
43
|
def emailer(opts = {})
|
34
44
|
require 'ustate/emailer'
|
35
45
|
@emailer ||= UState::Emailer.new(@index, opts)
|
36
46
|
end
|
37
47
|
|
48
|
+
def graphite(opts = {})
|
49
|
+
require 'ustate/graphite'
|
50
|
+
@graphite ||= UState::Graphite.new(@index, opts)
|
51
|
+
end
|
52
|
+
|
38
53
|
def start
|
39
54
|
@index.start
|
40
55
|
|
data/lib/ustate/server/index.rb
CHANGED
@@ -10,8 +10,17 @@ module UState
|
|
10
10
|
|
11
11
|
THREADS = 1000
|
12
12
|
BUFFER_SIZE = 10
|
13
|
+
# Forget about states after
|
14
|
+
EXPIRY = 1000
|
15
|
+
|
16
|
+
# Update metrics every
|
17
|
+
INSERT_RATE_INTERVAL = 5
|
18
|
+
INSERT_TIMES_INTERVAL = 5
|
13
19
|
|
14
|
-
attr_reader :db, :queue
|
20
|
+
attr_reader :db, :queue
|
21
|
+
attr_accessor :expiry
|
22
|
+
attr_accessor :insert_rate_interval
|
23
|
+
attr_accessor :insert_times_interval
|
15
24
|
|
16
25
|
def initialize(opts = {})
|
17
26
|
@db = Sequel.sqlite
|
@@ -23,6 +32,10 @@ module UState
|
|
23
32
|
@on_state_once = []
|
24
33
|
@on_state = []
|
25
34
|
|
35
|
+
@expiry = opts[:expiry] || EXPIRY
|
36
|
+
|
37
|
+
@insert_rate_interval = opts[:insert_rate_interval] || INSERT_RATE_INTERVAL
|
38
|
+
@insert_times_interval = opts[:insert_times_interval] || INSERT_TIMES_INTERVAL
|
26
39
|
setup_db
|
27
40
|
end
|
28
41
|
|
@@ -31,7 +44,11 @@ module UState
|
|
31
44
|
end
|
32
45
|
|
33
46
|
def <<(s)
|
47
|
+
t0 = Time.now
|
34
48
|
process s
|
49
|
+
dt = Time.now - t0
|
50
|
+
@insert_times << dt
|
51
|
+
@insert_rate << 1
|
35
52
|
end
|
36
53
|
|
37
54
|
def thread(s)
|
@@ -83,7 +100,7 @@ module UState
|
|
83
100
|
# Update
|
84
101
|
if current[:time] <= s.time
|
85
102
|
if current[:state] != s.state
|
86
|
-
on_state_change current, s
|
103
|
+
on_state_change row_to_state(current), s
|
87
104
|
end
|
88
105
|
|
89
106
|
# Update
|
@@ -128,6 +145,36 @@ module UState
|
|
128
145
|
end
|
129
146
|
end
|
130
147
|
|
148
|
+
# Remove states older than @expiry
|
149
|
+
def reap
|
150
|
+
@db[:states].filter { |s|
|
151
|
+
s.time < (Time.now - @expiry).to_i
|
152
|
+
}.each do |row|
|
153
|
+
on_state_change(
|
154
|
+
row_to_state(row),
|
155
|
+
row_to_state(
|
156
|
+
row.merge(
|
157
|
+
state: 'unknown',
|
158
|
+
description: "ustate has not heard from this service since #{Time.at(row[:time])}",
|
159
|
+
metric_f: nil,
|
160
|
+
time: Time.now.to_i
|
161
|
+
)
|
162
|
+
)
|
163
|
+
)
|
164
|
+
@db[:states].filter(host: row[:host], state: row[:state]).delete
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Periodically expire old states.
|
169
|
+
def reaper
|
170
|
+
Thread.new do
|
171
|
+
loop do
|
172
|
+
sleep 1
|
173
|
+
reap
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
131
178
|
# Converts a row to a State
|
132
179
|
def row_to_state(row)
|
133
180
|
State.new(row)
|
@@ -149,13 +196,49 @@ module UState
|
|
149
196
|
def start
|
150
197
|
stop!
|
151
198
|
@pool = []
|
199
|
+
@reaper = reaper
|
200
|
+
|
201
|
+
@insert_rate = MetricThread.new(Mtrc::Rate) do |r|
|
202
|
+
self << State.new(
|
203
|
+
service: "ustate insert rate",
|
204
|
+
state: "ok",
|
205
|
+
host: Socket.gethostname,
|
206
|
+
time: Time.now.to_i,
|
207
|
+
metric_f: r.rate.to_f,
|
208
|
+
)
|
209
|
+
end
|
210
|
+
@insert_rate.interval = @insert_rate_interval
|
211
|
+
|
212
|
+
@insert_times = MetricThread.new(Mtrc::SortedSamples) do |r|
|
213
|
+
self << State.new(
|
214
|
+
service: "ustate insert 50",
|
215
|
+
state: "ok",
|
216
|
+
host: Socket.gethostname,
|
217
|
+
time: Time.now.to_i,
|
218
|
+
metric_f: r % 50,
|
219
|
+
)
|
220
|
+
self << State.new(
|
221
|
+
service: "ustate insert 95",
|
222
|
+
state: "ok",
|
223
|
+
host: Socket.gethostname,
|
224
|
+
time: Time.now.to_i,
|
225
|
+
metric_f: r % 95,
|
226
|
+
)
|
227
|
+
self << State.new(
|
228
|
+
service: "ustate insert 99",
|
229
|
+
state: "ok",
|
230
|
+
host: Socket.gethostname,
|
231
|
+
time: Time.now.to_i,
|
232
|
+
metric_f: r % 99,
|
233
|
+
)
|
234
|
+
end
|
235
|
+
@insert_times.interval = @insert_times_interval
|
152
236
|
end
|
153
237
|
|
154
238
|
# Finish up
|
155
239
|
def stop
|
156
|
-
@
|
157
|
-
|
158
|
-
end
|
240
|
+
@insert_rate.stop rescue nil
|
241
|
+
@insert_times.stop rescue nil
|
159
242
|
end
|
160
243
|
|
161
244
|
def stop!
|
data/lib/ustate/state.rb
CHANGED
@@ -1,19 +1,156 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
1
|
+
module UState
|
2
|
+
class State
|
3
|
+
include Beefcake::Message
|
4
|
+
|
5
|
+
optional :time, :int64, 1
|
6
|
+
optional :state, :string, 2
|
7
|
+
optional :service, :string, 3
|
8
|
+
optional :host, :string, 4
|
9
|
+
optional :description, :string, 5
|
10
|
+
optional :once, :bool, 6
|
11
|
+
optional :metric_f, :float, 15
|
12
|
+
|
13
|
+
# Average a set of states together. Chooses the mean metric, the mode
|
14
|
+
# state, mode service, and the mean time. If init is provided, its values
|
15
|
+
# override (where present) the computed ones.
|
16
|
+
def self.average(states, init = State.new)
|
17
|
+
init = case init
|
18
|
+
when State
|
19
|
+
init.dup
|
20
|
+
else
|
21
|
+
State.new init
|
22
|
+
end
|
23
|
+
|
24
|
+
# Metric
|
25
|
+
init.metric_f ||= states.inject(0.0) { |a, state|
|
26
|
+
a + (state.metric || 0)
|
27
|
+
} / states.size
|
28
|
+
if init.metric_f.nan?
|
29
|
+
init.metric_f = 0.0
|
30
|
+
end
|
31
|
+
|
32
|
+
# State
|
33
|
+
init.state ||= mode states.map(&:state)
|
34
|
+
init.service ||= mode states.map(&:service)
|
35
|
+
|
36
|
+
# Time
|
37
|
+
init.time ||= begin
|
38
|
+
(states.inject(0) do |a, state|
|
39
|
+
a + state.time.to_f
|
40
|
+
end / states.size).to_i
|
41
|
+
rescue
|
42
|
+
end
|
43
|
+
init.time ||= Time.now.to_i
|
44
|
+
|
45
|
+
init
|
46
|
+
end
|
47
|
+
|
48
|
+
# Sum a set of states together. Adds metrics, takes the mode state, mode
|
49
|
+
# service and the mean time. If init is provided, its values override
|
50
|
+
# (where present) the computed ones.
|
51
|
+
def self.sum(states, init = State.new)
|
52
|
+
init = case init
|
53
|
+
when State
|
54
|
+
init.dup
|
55
|
+
else
|
56
|
+
State.new init
|
57
|
+
end
|
58
|
+
|
59
|
+
# Metric
|
60
|
+
init.metric_f ||= states.inject(0.0) { |a, state|
|
61
|
+
a + (state.metric || 0)
|
62
|
+
}
|
63
|
+
if init.metric_f.nan?
|
64
|
+
init.metric_f = 0.0
|
65
|
+
end
|
66
|
+
|
67
|
+
# State
|
68
|
+
init.state ||= mode states.map(&:state)
|
69
|
+
init.service ||= mode states.map(&:service)
|
70
|
+
|
71
|
+
# Time
|
72
|
+
init.time ||= begin
|
73
|
+
(states.inject(0) do |a, state|
|
74
|
+
a + state.time.to_f
|
75
|
+
end / states.size).to_i
|
76
|
+
rescue
|
77
|
+
end
|
78
|
+
init.time ||= Time.now.to_i
|
79
|
+
|
80
|
+
init
|
81
|
+
end
|
82
|
+
|
83
|
+
# Finds the maximum of a set of states. Metric is the maximum. State is the
|
84
|
+
# highest, as defined by Dash.config.state_order. Time is the mean.
|
85
|
+
def self.max(states, init = State.new)
|
86
|
+
init = case init
|
87
|
+
when State
|
88
|
+
init.dup
|
89
|
+
else
|
90
|
+
State.new init
|
91
|
+
end
|
92
|
+
|
93
|
+
# Metric
|
94
|
+
init.metric_f ||= states.inject(0.0) { |a, state|
|
95
|
+
a + (state.metric || 0)
|
96
|
+
}
|
97
|
+
if init.metric_f.nan?
|
98
|
+
init.metric_f = 0.0
|
99
|
+
end
|
100
|
+
|
101
|
+
# State
|
102
|
+
init.state ||= states.inject(nil) do |max, state|
|
103
|
+
state.state if Dash.config[:state_order][state.state] > Dash.config[:state_order][max]
|
104
|
+
end
|
105
|
+
|
106
|
+
# Time
|
107
|
+
init.time ||= begin
|
108
|
+
(states.inject(0) { |a, state|
|
109
|
+
a + state.time.to_f
|
110
|
+
} / states.size).to_i
|
111
|
+
rescue
|
112
|
+
end
|
113
|
+
init.time ||= Time.now.to_i
|
114
|
+
|
115
|
+
init
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.mode(array)
|
119
|
+
array.inject(Hash.new(0)) do |counts, e|
|
120
|
+
counts[e] += 1
|
121
|
+
counts
|
122
|
+
end.sort_by { |e, count| count }.last.first rescue nil
|
123
|
+
end
|
124
|
+
|
125
|
+
# Partition a list of states by a field
|
126
|
+
# Returns a hash of field_value => state
|
127
|
+
def self.partition(states, field)
|
128
|
+
states.inject(Hash.new { [] }) do |p, state|
|
129
|
+
p[state.send(field)] << state
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Sorts states by a field. nil values first.
|
134
|
+
def self.sort(states, field)
|
135
|
+
states.sort do |a, b|
|
136
|
+
a = a.send field
|
137
|
+
b = b.send field
|
138
|
+
if a.nil?
|
139
|
+
-1
|
140
|
+
elsif b.nil?
|
141
|
+
1
|
142
|
+
else
|
143
|
+
a <=> b
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def metric
|
149
|
+
@metric || metric_f
|
150
|
+
end
|
15
151
|
|
16
|
-
|
17
|
-
|
152
|
+
def metric=(m)
|
153
|
+
@metric = m
|
154
|
+
end
|
18
155
|
end
|
19
156
|
end
|
data/lib/ustate/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ustate-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,12 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2011-09-
|
13
|
-
default_executable:
|
12
|
+
date: 2011-09-13 00:00:00.000000000Z
|
14
13
|
dependencies:
|
15
14
|
- !ruby/object:Gem::Dependency
|
16
15
|
name: beefcake
|
17
|
-
requirement: &
|
16
|
+
requirement: &69429590 !ruby/object:Gem::Requirement
|
18
17
|
none: false
|
19
18
|
requirements:
|
20
19
|
- - ! '>='
|
@@ -22,10 +21,10 @@ dependencies:
|
|
22
21
|
version: 0.3.5
|
23
22
|
type: :runtime
|
24
23
|
prerelease: false
|
25
|
-
version_requirements: *
|
24
|
+
version_requirements: *69429590
|
26
25
|
- !ruby/object:Gem::Dependency
|
27
26
|
name: trollop
|
28
|
-
requirement: &
|
27
|
+
requirement: &69428990 !ruby/object:Gem::Requirement
|
29
28
|
none: false
|
30
29
|
requirements:
|
31
30
|
- - ! '>='
|
@@ -33,7 +32,18 @@ dependencies:
|
|
33
32
|
version: 1.16.2
|
34
33
|
type: :runtime
|
35
34
|
prerelease: false
|
36
|
-
version_requirements: *
|
35
|
+
version_requirements: *69428990
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: mtrc
|
38
|
+
requirement: &69428610 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.0.4
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *69428610
|
37
47
|
description:
|
38
48
|
email: aphyr@aphyr.com
|
39
49
|
executables: []
|
@@ -41,41 +51,42 @@ extensions: []
|
|
41
51
|
extra_rdoc_files: []
|
42
52
|
files:
|
43
53
|
- lib/ustate.rb
|
44
|
-
- lib/ustate/
|
45
|
-
- lib/ustate/query/equals.rb
|
46
|
-
- lib/ustate/query/or.rb
|
47
|
-
- lib/ustate/query/approximately.rb
|
48
|
-
- lib/ustate/query/and.rb
|
49
|
-
- lib/ustate/query/ast.rb
|
50
|
-
- lib/ustate/query/not.rb
|
54
|
+
- lib/ustate/graphite.rb
|
51
55
|
- lib/ustate/query_string.treetop
|
52
|
-
- lib/ustate/
|
53
|
-
- lib/ustate/
|
54
|
-
- lib/ustate/
|
56
|
+
- lib/ustate/message.rb
|
57
|
+
- lib/ustate/metric_thread.rb
|
58
|
+
- lib/ustate/aggregator.rb
|
55
59
|
- lib/ustate/version.rb
|
56
|
-
- lib/ustate/
|
57
|
-
- lib/ustate/client/query.rb
|
58
|
-
- lib/ustate/query_string.rb
|
59
|
-
- lib/ustate/server/index.rb
|
60
|
+
- lib/ustate/server/graphite.rb
|
60
61
|
- lib/ustate/server/backends.rb
|
61
|
-
- lib/ustate/server/backends/tcp.rb
|
62
62
|
- lib/ustate/server/backends/base.rb
|
63
|
-
- lib/ustate/server/
|
63
|
+
- lib/ustate/server/backends/tcp.rb
|
64
|
+
- lib/ustate/server/index.rb
|
64
65
|
- lib/ustate/server/connection.rb
|
66
|
+
- lib/ustate/server.rb
|
67
|
+
- lib/ustate/dash.rb
|
68
|
+
- lib/ustate/state.rb
|
69
|
+
- lib/ustate/query.rb
|
70
|
+
- lib/ustate/client/query.rb
|
65
71
|
- lib/ustate/client.rb
|
66
|
-
- lib/ustate/message.rb
|
67
|
-
- lib/ustate/dash/helper/renderer.rb
|
68
|
-
- lib/ustate/dash/views/css.scss
|
69
72
|
- lib/ustate/dash/views/index.erubis
|
70
73
|
- lib/ustate/dash/views/layout.erubis
|
71
|
-
- lib/ustate/dash/
|
74
|
+
- lib/ustate/dash/views/css.scss
|
72
75
|
- lib/ustate/dash/controller/index.rb
|
73
76
|
- lib/ustate/dash/controller/css.rb
|
74
77
|
- lib/ustate/dash/config.rb
|
75
|
-
- lib/ustate/dash.rb
|
78
|
+
- lib/ustate/dash/helper/renderer.rb
|
79
|
+
- lib/ustate/query/or.rb
|
80
|
+
- lib/ustate/query/not_equals.rb
|
81
|
+
- lib/ustate/query/ast.rb
|
82
|
+
- lib/ustate/query/equals.rb
|
83
|
+
- lib/ustate/query/approximately.rb
|
84
|
+
- lib/ustate/query/not.rb
|
85
|
+
- lib/ustate/query/and.rb
|
86
|
+
- lib/ustate/query_string.rb
|
87
|
+
- lib/ustate/emailer.rb
|
76
88
|
- LICENSE
|
77
89
|
- README.markdown
|
78
|
-
has_rdoc: true
|
79
90
|
homepage: https://github.com/aphyr/ustate
|
80
91
|
licenses: []
|
81
92
|
post_install_message:
|
@@ -96,7 +107,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
96
107
|
version: '0'
|
97
108
|
requirements: []
|
98
109
|
rubyforge_project: ustate-client
|
99
|
-
rubygems_version: 1.
|
110
|
+
rubygems_version: 1.8.10
|
100
111
|
signing_key:
|
101
112
|
specification_version: 3
|
102
113
|
summary: Client for the distributed state server ustate.
|
data/lib/ustate/dash/state.rb
DELETED
@@ -1,75 +0,0 @@
|
|
1
|
-
class UState::State
|
2
|
-
# Average a set of states together. Chooses the meane metric, the mode
|
3
|
-
# state, and the mean time. If init is provided, its values override (where present) the
|
4
|
-
# computed ones.
|
5
|
-
def average(states, init = State.new)
|
6
|
-
# Metric
|
7
|
-
init.metric ||= begin
|
8
|
-
states.inject(0) { |a, state|
|
9
|
-
a + (state.metric || 0)
|
10
|
-
} / states.size
|
11
|
-
rescue
|
12
|
-
nil
|
13
|
-
end
|
14
|
-
|
15
|
-
# State
|
16
|
-
init.state ||= states.inject(Hash.new(0)) do |counts, s|
|
17
|
-
counts[s.state] += 1
|
18
|
-
counts
|
19
|
-
end.sort_by { |state, count| count }.first.first rescue nil
|
20
|
-
|
21
|
-
init.time ||= begin
|
22
|
-
states.inject(0) do |a, state|
|
23
|
-
a + state.time.to_f
|
24
|
-
end / states.size
|
25
|
-
rescue
|
26
|
-
end
|
27
|
-
|
28
|
-
init
|
29
|
-
end
|
30
|
-
|
31
|
-
# Finds the maximum of a set of states. Metric is the maximum. State is the highest,
|
32
|
-
# as defined by Dash.config.state_order. Time is the mean.
|
33
|
-
def max(states, init = {})
|
34
|
-
init.metric ||= states.inject(0) { |a, state|
|
35
|
-
a + (state.metric || 0)
|
36
|
-
}
|
37
|
-
|
38
|
-
init.state] ||= states.inject(nil) do |max, state|
|
39
|
-
state.state if Dash.config[:state_order][state.state] > Dash.config[:state_order][max]
|
40
|
-
end
|
41
|
-
|
42
|
-
init.time ||= begin
|
43
|
-
states.inject(0) { |a, state|
|
44
|
-
a + state.time.to_f
|
45
|
-
} / states.size
|
46
|
-
rescue
|
47
|
-
nil
|
48
|
-
end
|
49
|
-
|
50
|
-
init
|
51
|
-
end
|
52
|
-
|
53
|
-
# Partition a list of states by a field
|
54
|
-
# Returns a hash of field_value => state
|
55
|
-
def partition(states, field)
|
56
|
-
states.inject(Hash.new { [] }) do |p, state|
|
57
|
-
p[state.send(field)] << state
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
# Sorts states by a field. nil values first.
|
62
|
-
def sort(states, field)
|
63
|
-
states.sort do |a, b|
|
64
|
-
a = a.send field
|
65
|
-
b = b.send field
|
66
|
-
if a.nil?
|
67
|
-
-1
|
68
|
-
elsif b.nil?
|
69
|
-
1
|
70
|
-
else
|
71
|
-
a <=> b
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|