ustate-client 0.0.3 → 0.0.4
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.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
|