ustate-client 0.0.1
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/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
|