ustate-client 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +21 -0
- data/README.markdown +111 -0
- data/lib/ustate/client/query.rb +17 -0
- data/lib/ustate/client.rb +108 -0
- data/lib/ustate/dash/config.rb +0 -0
- data/lib/ustate/dash/controller/css.rb +5 -0
- data/lib/ustate/dash/controller/index.rb +5 -0
- data/lib/ustate/dash/helper/renderer.rb +209 -0
- data/lib/ustate/dash/state.rb +75 -0
- data/lib/ustate/dash/views/css.scss +39 -0
- data/lib/ustate/dash/views/index.erubis +3 -0
- data/lib/ustate/dash/views/layout.erubis +16 -0
- data/lib/ustate/dash.rb +111 -0
- data/lib/ustate/message.rb +16 -0
- data/lib/ustate/query.rb +7 -0
- data/lib/ustate/query_string.rb +1066 -0
- data/lib/ustate/query_string.treetop +116 -0
- data/lib/ustate/server/backends/base.rb +103 -0
- data/lib/ustate/server/backends/tcp.rb +33 -0
- data/lib/ustate/server/backends.rb +4 -0
- data/lib/ustate/server/connection.rb +82 -0
- data/lib/ustate/server/graphite.rb +11 -0
- data/lib/ustate/server/index.rb +134 -0
- data/lib/ustate/server.rb +66 -0
- data/lib/ustate/state.rb +18 -0
- data/lib/ustate/version.rb +3 -0
- data/lib/ustate.rb +9 -0
- metadata +71 -0
@@ -0,0 +1,116 @@
|
|
1
|
+
module UState
|
2
|
+
grammar QueryString
|
3
|
+
# (state =~ "foo" or state == "bar") and service == "foo"
|
4
|
+
# Binding order proceeds from loosest to tightest.
|
5
|
+
|
6
|
+
rule or
|
7
|
+
first:and rest:(space 'or' space and)* {
|
8
|
+
def sql
|
9
|
+
rest.elements.map { |x| x.and }.
|
10
|
+
inject(first.sql) do |a, sub|
|
11
|
+
a | sub.sql
|
12
|
+
end
|
13
|
+
end
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
rule and
|
18
|
+
first:primary rest:(space 'and' space primary)* {
|
19
|
+
def sql
|
20
|
+
rest.elements.map { |x| x.primary }.
|
21
|
+
inject(first.sql) do |a, sub|
|
22
|
+
a & sub.sql
|
23
|
+
end
|
24
|
+
end
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
rule primary
|
29
|
+
'(' space? x:or space? ')' {
|
30
|
+
def sql
|
31
|
+
x.sql
|
32
|
+
end
|
33
|
+
}
|
34
|
+
/
|
35
|
+
predicate
|
36
|
+
end
|
37
|
+
|
38
|
+
rule predicate
|
39
|
+
equals / not_equals / approximately
|
40
|
+
end
|
41
|
+
|
42
|
+
rule approximately
|
43
|
+
field space? '=~' space? string {
|
44
|
+
def sql
|
45
|
+
Sequel::SQL::StringExpression.like field.sql, string.sql
|
46
|
+
end
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
rule not_equals
|
51
|
+
field space? '!=' space? value {
|
52
|
+
def sql
|
53
|
+
Sequel::SQL::BooleanExpression.from_value_pairs({field.sql => value.sql}, :AND, true)
|
54
|
+
end
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
rule equals
|
59
|
+
field space? ('=' / '==') space? value {
|
60
|
+
def sql
|
61
|
+
Sequel::SQL::BooleanExpression.from_value_pairs field.sql => value.sql
|
62
|
+
end
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
rule value
|
67
|
+
double_quoted_string / float / integer
|
68
|
+
end
|
69
|
+
|
70
|
+
rule integer
|
71
|
+
'-'? [0-9]+ {
|
72
|
+
def ruby_value
|
73
|
+
Integer(text_value)
|
74
|
+
end
|
75
|
+
alias sql ruby_value
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
rule float
|
80
|
+
'-'? [0-9]+ '.' [0-9]+ {
|
81
|
+
def ruby_value
|
82
|
+
Float(text_value)
|
83
|
+
end
|
84
|
+
|
85
|
+
alias sql ruby_value
|
86
|
+
}
|
87
|
+
end
|
88
|
+
|
89
|
+
rule field
|
90
|
+
# [a-zA-Z] [a-zA-Z_0-9]*
|
91
|
+
("state" / "host" / "service" / "description" / "metric_f") {
|
92
|
+
def sql
|
93
|
+
text_value.to_sym
|
94
|
+
end
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
rule string
|
99
|
+
double_quoted_string
|
100
|
+
end
|
101
|
+
|
102
|
+
# This doesn't work! Taken from the docs... :(
|
103
|
+
rule double_quoted_string
|
104
|
+
'"' (!'"' . / '\"')* '"' {
|
105
|
+
def ruby_value
|
106
|
+
text_value[1..-2].gsub('\"', '"')
|
107
|
+
end
|
108
|
+
alias sql ruby_value
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
rule space
|
113
|
+
[\s]+
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# Largely stolen from Thin
|
2
|
+
|
3
|
+
class UState::Server::Backends::Base
|
4
|
+
TIMEOUT = 5
|
5
|
+
MAXIMUM_CONNECTIONS = 1024
|
6
|
+
MAX_CONNECTIONS = 512
|
7
|
+
|
8
|
+
attr_accessor :server
|
9
|
+
attr_accessor :timeout
|
10
|
+
attr_accessor :maximum_connections
|
11
|
+
attr_writer :ssl, :ssl_options
|
12
|
+
def ssl?; @ssl; end
|
13
|
+
|
14
|
+
def initialize(opts = {})
|
15
|
+
@connections = []
|
16
|
+
@timeout = opts[:timeout] || TIMEOUT
|
17
|
+
@maximum_connections = opts[:maximum_connections] || MAXIMUM_CONNECTIONS
|
18
|
+
end
|
19
|
+
|
20
|
+
def start
|
21
|
+
@stopping = false
|
22
|
+
starter = proc do
|
23
|
+
connect
|
24
|
+
@running = true
|
25
|
+
end
|
26
|
+
|
27
|
+
# Allow for early run-up of eventmachine
|
28
|
+
if EventMachine.reactor_running?
|
29
|
+
starter.call
|
30
|
+
else
|
31
|
+
EventMachine.run &starter
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Graceful stop
|
36
|
+
def stop
|
37
|
+
@running = false
|
38
|
+
@stopping = true
|
39
|
+
|
40
|
+
# Stop accepting connections
|
41
|
+
disconnect
|
42
|
+
stop! if @connections.empty?
|
43
|
+
end
|
44
|
+
|
45
|
+
# Force stop
|
46
|
+
def stop!
|
47
|
+
@running = false
|
48
|
+
@stopping = false
|
49
|
+
|
50
|
+
EventMachine.stop if EventMachine.reactor_running?
|
51
|
+
@connections.each { |connection| connection.close_connection }
|
52
|
+
close
|
53
|
+
end
|
54
|
+
|
55
|
+
# Configure backend
|
56
|
+
def config
|
57
|
+
EventMachine.epoll
|
58
|
+
|
59
|
+
@maximum_connections =
|
60
|
+
EventMachine.set_descriptor_table_size(@maximum_connections)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Free up resources used by the backend
|
64
|
+
def close
|
65
|
+
end
|
66
|
+
|
67
|
+
def running?
|
68
|
+
@running
|
69
|
+
end
|
70
|
+
|
71
|
+
# Called by a connection when it's unbound
|
72
|
+
def connection_finished(connection)
|
73
|
+
@connections.delete connection
|
74
|
+
|
75
|
+
# Finalize graceful stop if there's no more active connections.
|
76
|
+
stop! if @stopping and @connections.empty?
|
77
|
+
end
|
78
|
+
|
79
|
+
# No connections?
|
80
|
+
def empty?
|
81
|
+
@connections.empty?
|
82
|
+
end
|
83
|
+
|
84
|
+
# No of connections
|
85
|
+
def size
|
86
|
+
@connections.size
|
87
|
+
end
|
88
|
+
|
89
|
+
protected
|
90
|
+
|
91
|
+
# Initialize a new connection to a client
|
92
|
+
def initialize_connection(connection)
|
93
|
+
connection.backend = self
|
94
|
+
connection.index = @server.index
|
95
|
+
connection.comm_inactivity_timeout = @timeout
|
96
|
+
|
97
|
+
# if @ssl
|
98
|
+
# connection.start_tls(@ssl_options
|
99
|
+
# end
|
100
|
+
|
101
|
+
@connections << connection
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# Stolen from Thin
|
2
|
+
|
3
|
+
class UState::Server
|
4
|
+
class Backends::TCP < Backends::Base
|
5
|
+
require 'socket'
|
6
|
+
|
7
|
+
attr_accessor :host, :port
|
8
|
+
|
9
|
+
HOST = '127.0.0.1'
|
10
|
+
PORT = 55956
|
11
|
+
|
12
|
+
def initialize(opts = {})
|
13
|
+
@host = opts[:host] || HOST
|
14
|
+
@port = opts[:port] || PORT
|
15
|
+
super opts
|
16
|
+
end
|
17
|
+
|
18
|
+
# Connect the server
|
19
|
+
def connect
|
20
|
+
puts "Listening on #{@host}:#{@port}"
|
21
|
+
@signature = EventMachine.start_server(@host, @port, Connection, &method(:initialize_connection))
|
22
|
+
end
|
23
|
+
|
24
|
+
# Stops server
|
25
|
+
def disconnect
|
26
|
+
EventMachine.stop_server @signature
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
"#{@host}:#{@port}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# Instantiated by EventMachine for each new connection
|
2
|
+
# Mostly from Thin.
|
3
|
+
class UState::Server::Connection < EventMachine::Connection
|
4
|
+
attr_accessor :backend
|
5
|
+
attr_accessor :index
|
6
|
+
|
7
|
+
# Called to prepare the connection for a request
|
8
|
+
def post_init
|
9
|
+
@state = :length
|
10
|
+
@buffer = ""
|
11
|
+
end
|
12
|
+
|
13
|
+
# Called when data is received
|
14
|
+
def receive_data(data = '')
|
15
|
+
@buffer << data
|
16
|
+
|
17
|
+
case @state
|
18
|
+
when :length
|
19
|
+
# Length header
|
20
|
+
if @buffer.bytesize >= 4
|
21
|
+
@length = @buffer.slice!(0,4).unpack('N').first
|
22
|
+
@state = :data
|
23
|
+
receive_data unless @buffer.empty?
|
24
|
+
end
|
25
|
+
when :data
|
26
|
+
# Data
|
27
|
+
if @buffer.bytesize >= @length
|
28
|
+
receive_message @buffer.slice!(0, @length)
|
29
|
+
@state = :length
|
30
|
+
receive_data unless @buffer.empty?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Called with a message type and data.
|
36
|
+
def receive_message(data)
|
37
|
+
begin
|
38
|
+
message = UState::Message.decode data
|
39
|
+
if states = message.states
|
40
|
+
# State update
|
41
|
+
states.each do |state|
|
42
|
+
@index << state
|
43
|
+
end
|
44
|
+
send UState::Message.new(ok: true)
|
45
|
+
elsif q = message.query
|
46
|
+
res = @index.query(q)
|
47
|
+
send UState::Message.new(ok: true, states: res)
|
48
|
+
else
|
49
|
+
send UState::Message.new(ok: false, error: "unknown message type")
|
50
|
+
end
|
51
|
+
rescue Exception => e
|
52
|
+
puts e
|
53
|
+
puts e.backtrace.join("\n")
|
54
|
+
m = UState::Message.new(ok: false, error: e.message)
|
55
|
+
send m
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def send(message)
|
60
|
+
send_data message.encode_with_length
|
61
|
+
end
|
62
|
+
|
63
|
+
# Called when the connection is unbound from the socket and can no longer be
|
64
|
+
# used to process requests.
|
65
|
+
def unbind
|
66
|
+
@backend.connection_finished self
|
67
|
+
end
|
68
|
+
|
69
|
+
def remote_address
|
70
|
+
socket_address
|
71
|
+
end
|
72
|
+
|
73
|
+
def terminate_request
|
74
|
+
close_connection_after_writing rescue nil
|
75
|
+
end
|
76
|
+
|
77
|
+
protected
|
78
|
+
|
79
|
+
def socket_address
|
80
|
+
Socket.unpack_sockaddr_in(get_peername)[1]
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module UState
|
2
|
+
class Server::Index
|
3
|
+
# Combines state messages. Responds to queries for particular states.
|
4
|
+
# Forwards messages to various receivers.
|
5
|
+
# Inserts are NOT threadsafe.
|
6
|
+
|
7
|
+
class ParseFailed < Server::Error; end
|
8
|
+
|
9
|
+
require 'sequel'
|
10
|
+
|
11
|
+
THREADS = 1000
|
12
|
+
BUFFER_SIZE = 10
|
13
|
+
|
14
|
+
attr_reader :db, :queue
|
15
|
+
|
16
|
+
def initialize(opts = {})
|
17
|
+
@db = Sequel.sqlite
|
18
|
+
|
19
|
+
@threads = opts[:threads] || THREADS
|
20
|
+
@pool = []
|
21
|
+
|
22
|
+
setup_db
|
23
|
+
end
|
24
|
+
|
25
|
+
def clear
|
26
|
+
setup_db
|
27
|
+
end
|
28
|
+
|
29
|
+
def <<(s)
|
30
|
+
process s
|
31
|
+
end
|
32
|
+
|
33
|
+
def thread(s)
|
34
|
+
Thread.new do
|
35
|
+
process s
|
36
|
+
@pooltex.synchronize do
|
37
|
+
@pool.delete Thread.current
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def on_state_change(old, new)
|
43
|
+
end
|
44
|
+
|
45
|
+
def on_state(state)
|
46
|
+
end
|
47
|
+
|
48
|
+
def process(s)
|
49
|
+
if current = @db[:states][host: s.host, service: s.service]
|
50
|
+
# Update
|
51
|
+
if current[:time] <= s.time
|
52
|
+
if current[:state] != s.state
|
53
|
+
on_state_change current, s
|
54
|
+
end
|
55
|
+
|
56
|
+
# Update
|
57
|
+
@db[:states].filter(host: s.host, service: s.service).update(
|
58
|
+
state: s.state,
|
59
|
+
time: s.time,
|
60
|
+
description: s.description,
|
61
|
+
metric_f: s.metric_f
|
62
|
+
)
|
63
|
+
end
|
64
|
+
else
|
65
|
+
# Insert
|
66
|
+
@db[:states].insert(
|
67
|
+
host: s.host,
|
68
|
+
service: s.service,
|
69
|
+
state: s.state,
|
70
|
+
time: s.time,
|
71
|
+
description: s.description,
|
72
|
+
metric_f: s.metric_f
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
on_state s
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns an array of States matching Query.
|
80
|
+
def query(q)
|
81
|
+
parser = QueryStringParser.new
|
82
|
+
if q.string
|
83
|
+
unless expression = parser.parse(q.string)
|
84
|
+
raise ParseFailed, "error parsing #{q.string.inspect} at line #{parser.failure_line}:#{parser.failure_column}: #{parser.failure_reason}"
|
85
|
+
end
|
86
|
+
filter = expression.sql
|
87
|
+
else
|
88
|
+
# No string? All states.
|
89
|
+
filter = true
|
90
|
+
end
|
91
|
+
|
92
|
+
ds = @db[:states].filter filter
|
93
|
+
ds.all.map do |row|
|
94
|
+
row_to_state row
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Converts a row to a State
|
99
|
+
def row_to_state(row)
|
100
|
+
State.new(row)
|
101
|
+
end
|
102
|
+
|
103
|
+
def setup_db
|
104
|
+
@db.drop_table :states rescue nil
|
105
|
+
@db.create_table :states do
|
106
|
+
String :host
|
107
|
+
String :service
|
108
|
+
String :state
|
109
|
+
String :description, :text => true
|
110
|
+
Integer :time
|
111
|
+
Float :metric_f
|
112
|
+
primary_key [:host, :service]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def start
|
117
|
+
stop!
|
118
|
+
@pool = []
|
119
|
+
end
|
120
|
+
|
121
|
+
# Finish up
|
122
|
+
def stop
|
123
|
+
@pool.each do |thread|
|
124
|
+
thread.join
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def stop!
|
129
|
+
@pool.each do |thread|
|
130
|
+
thread.kill
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|