ustate-client 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -11,10 +11,27 @@ which can be queried or forwarded to various handlers. A state is simply:
11
11
  state: Any string less than 255 bytes, e.g. "ok", "warning", "critical",
12
12
  time: The time that the service entered this state, in unix time,
13
13
  description: Freeform text,
14
- metric_f: A floating-point number associated with this state, e.g. the number of reqs/sec
14
+ metric_f: A floating-point number associated with this state, e.g. the number of reqs/sec,
15
+ once: A boolean, described below.
15
16
  }
16
17
 
17
- At http://showyou.com, we use UState to monitor the health and performance of hundreds of services across our infrastructure, including CPU, queries/second, latency bounds, disk usage, queues, and others.
18
+ Normally, every state received by the server fires Index#on_state. When
19
+ state.state changes, Index#on_state_change is called. You can, for example,
20
+ register to send a single email whenever a state changes to :warning.
21
+
22
+ :once states are transient. They fire Index#on_state and #on_state_once, but do
23
+ *not* update the index. They can be used for events which are instantaneous;
24
+ instead of sending {state: error} and {state: ok}, send {state: error,
25
+ once:true}.
26
+
27
+ For example, recoverable errors may not hang your application, but
28
+ should be processed by the email notifier. Sending a :once state with
29
+ the error description means you can receive an email for each error,
30
+ instead of two for entering and exiting the error state.
31
+
32
+ At http://showyou.com, we use UState to monitor the health and performance of
33
+ hundreds of services across our infrastructure, including CPU, queries/second,
34
+ latency bounds, disk usage, queues, and others.
18
35
 
19
36
  UState also includes a simple dashboard Sinatra app.
20
37
 
@@ -39,22 +56,89 @@ For the dashboard:
39
56
 
40
57
  gem install sinatra thin erubis sass
41
58
 
42
- Getting started
43
- ===============
59
+ Demo
60
+ ====
44
61
 
45
62
  To try it out, install all the gems above, and clone the repository. Start the server with
46
63
 
47
- bin/server [--host host] [--port port]
64
+ bin/server
48
65
 
49
66
  UState listens on TCP socket host:port, and accepts connections from clients. Start a basic testing client with
50
67
 
51
- bin/test
68
+ bin/test
52
69
 
53
70
  The tester spews randomly generated statistics at a server on the default local host and port. To see it in action, run the dashboard:
54
71
 
55
- cd lib/ustate/dash
56
- ../../../bin/dash
72
+ cd lib/ustate/dash
73
+ ../../../bin/dash
74
+
75
+ Server
76
+ ======
77
+
78
+ The server loads a file in the working directory named config.rb. Override with
79
+ --config-file. Its contents are instance-evaled in the context of the current
80
+ server. You can use this to extend ustate with additional behavior.
81
+
82
+ Email
83
+ -----
84
+
85
+ config.rb:
86
+ # Email comes from this address (required):
87
+ emailer.from = 'ustate@your.net'
88
+
89
+ # Use this SMTP relay (default 127.0.0.1)
90
+ emailer.host = '123.4.56.7'
91
+
92
+ # Receive mail when a state transition matches any of ...
93
+ emailer.tell 'you@gmail.com', 'state = "error" or state = "critical"'
94
+ emailer.tell 'you@gmail.com', 'service =~ "mysql%"'
95
+
96
+ Custom hooks
97
+ ------------
98
+
99
+ config.rb:
100
+ # Log all states received to console.
101
+ index.on_state do |s|
102
+ p s
103
+ end
104
+
105
+ # Forward state transitions to another server.
106
+ require 'ustate/client'
107
+ client = UState::Client.new :host => '123.45.67.8'
108
+ index.on_state_change do |old, new|
109
+ client << new
110
+ end
111
+ index.on_state_once do |state|
112
+ client << state
113
+ end
114
+
115
+ Client
116
+ ======
57
117
 
118
+ You can use the git repo, or the gem.
119
+
120
+ gem install ustate-client
121
+
122
+ Then:
123
+
124
+ require 'ustate'
125
+ require 'ustate/client'
126
+
127
+ # Create a client
128
+ c = UState::Client.new(
129
+ host: "my.host", # Default localhost
130
+ port: 1234 # Default 55956
131
+ )
132
+
133
+ # Insert a state
134
+ c << {
135
+ state: "ok",
136
+ service: "My service"
137
+ }
138
+
139
+ # Query for states
140
+ c.query.states # => [UState::State(state: 'ok', service: 'My service')]
141
+ c.query('state != "ok"').states # => []
58
142
 
59
143
  The Dashboard
60
144
  =============
@@ -90,20 +174,18 @@ You can also query states using a very basic expression language. The grammar is
90
174
  state = "ok"
91
175
  (service =~ "disk%") or (state == "critical" and host =~ "%.trioptimum.com")
92
176
 
93
- Search queries will return a message with repeated States matching that expression. An empty expression matches all states.
177
+ Search queries will return a message with repeated States matching that expression. An null expression will return no states.
94
178
 
95
179
  Performance
96
180
  ===========
97
181
 
98
- It's Ruby. It ain't gonna be fast. However, on my 4-year-old core 2 duo, I see >600 inserts/sec or queries/sec. The client is fully threadsafe, and performs well concurrently. I will continue to tune UState for latency and throughput, and welcome patches.
182
+ On a macbook pro 8,3, I see >1300 queries/sec or >1200 inserts/sec. The client is fully threadsafe, and performs well concurrently. I will continue to tune UState for latency and throughput, and welcome patches.
99
183
 
100
184
  For large installations, I plan to implement a selective forwarder. Local ustate servers can accept high volumes of states from a small set of nodes, and forward updates at a larger granularity to supervisors, and so forth, in a tree. The query language should be able to support proxying requests to the most recent source of a state, so very large sets of services can be maintained at high granularity.
101
185
 
102
186
  Goals
103
187
  =====
104
188
 
105
- Immediately, I'll be porting our internal email alerter to UState. Users register for interest in certain types of states or transitions, and receive emails when those events occur.
106
-
107
189
  In the medium term, I'll be connecting UState to Graphite (or perhaps another
108
190
  graphing tool) for metrics archival and soft-realtime graphs. I have an
109
191
  internal gnuplot system which is clunky and deserves retirement.
@@ -0,0 +1,92 @@
1
+ module UState
2
+ class Emailer
3
+ require 'net/smtp'
4
+
5
+ attr_accessor :from
6
+ attr_accessor :host
7
+ attr_accessor :name
8
+
9
+ # Registers self with index.
10
+ # Options:
11
+ # :host: The SMTP host to connect to. Default 'localhost'
12
+ # :name: The From name used. Default "ustate".
13
+ # :from: The From address used: e.g. "ustate@your_domain.com"
14
+ def initialize(index, opts = {})
15
+ opts = {
16
+ :name => 'ustate',
17
+ :host => 'localhost'
18
+ }.merge opts
19
+
20
+ @from = opts[:from]
21
+ @name = opts[:name]
22
+ @host = opts[:host]
23
+
24
+ @tell = {}
25
+
26
+ index.on_state_change &method(:receive)
27
+ index.on_state_once &method(:receive)
28
+ end
29
+
30
+ # Send an email to address about state.
31
+ def email(address, s)
32
+ raise ArgumentError, "no from address" unless @from
33
+
34
+ # Subject
35
+ subject = "#{s.host} #{s.service} #{s.state}"
36
+ if s.once
37
+ subject << " transient "
38
+ else
39
+ subject << " is "
40
+ end
41
+ subject << s.state
42
+
43
+ # Body
44
+ body = "#{subject}: #{s.description}"
45
+
46
+ # SMTP message
47
+ message = <<EOF
48
+ From: #{@name} <#{@from}>
49
+ To: <#{address}>
50
+ Subject: #{subject.gsub("\n", ' ')}
51
+
52
+ #{body}
53
+ EOF
54
+
55
+ Net::SMTP.start(@host) do |smtp|
56
+ smtp.send_message message, @from, address
57
+ end
58
+ end
59
+
60
+ # Dispatch emails to each address which is interested in this state
61
+ def receive(*states)
62
+ state = states.last
63
+ Thread.new do
64
+ @tell.each do |address, q|
65
+ if q === state
66
+ email address, state
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ # Notify email when a state matching query_string is
73
+ # received. Multiple calls are ORed together:
74
+ #
75
+ # emailer.tell 'aphyr@aphyr.com', 'state = "error"'
76
+ # emailer.tell 'aphyr@aphyr.com', 'host =~ "frontend%"'
77
+ def tell(email, query_string)
78
+ parser = QueryStringParser.new
79
+ q = parser.parse(query_string)
80
+ unless q
81
+ raise ArgumentError, "error parsing #{query_string.inspect} at line #{parser.failure_line}:#{parser.failure_column}: #{parser.failure_reason}"
82
+ end
83
+ q = q.query
84
+
85
+ @tell[email] = if existing = @tell[email]
86
+ Query::Or.new existing, q
87
+ else
88
+ q
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,12 @@
1
+ class UState::Query
2
+ class And
3
+ def initialize(a,b)
4
+ @a = a
5
+ @b = b
6
+ end
7
+
8
+ def ===(state)
9
+ @a === state and @b === state
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,24 @@
1
+ class UState::Query
2
+ class Approximately
3
+ def initialize(field, value)
4
+ @field = field
5
+ @value = case value
6
+ when String
7
+ r = value.chars.inject('') do |r, c|
8
+ if c == '%'
9
+ r << '.*'
10
+ else
11
+ r << Regexp.escape(c)
12
+ end
13
+ end
14
+ /^#{r}$/
15
+ else
16
+ value
17
+ end
18
+ end
19
+
20
+ def ===(state)
21
+ @value === state.send(@field)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,6 @@
1
+ require 'ustate/query/and'
2
+ require 'ustate/query/approximately'
3
+ require 'ustate/query/equals'
4
+ require 'ustate/query/not_equals'
5
+ require 'ustate/query/not'
6
+ require 'ustate/query/or'
@@ -0,0 +1,12 @@
1
+ class UState::Query
2
+ class Equals
3
+ def initialize(field, value)
4
+ @field = field
5
+ @value = value
6
+ end
7
+
8
+ def ===(state)
9
+ state.send(@field) == @value
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ class UState::Query
2
+ class Not
3
+ def initialize(a)
4
+ @a = a
5
+ end
6
+
7
+ def ===(state)
8
+ not @a === state
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ class UState::Query
2
+ class NotEquals
3
+ def initialize(field, value)
4
+ @field = field
5
+ @value = value
6
+ end
7
+
8
+ def ===(state)
9
+ state.send(@field) != @value
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ class UState::Query
2
+ class Or
3
+ def initialize(a,b)
4
+ @a = a
5
+ @b = b
6
+ end
7
+
8
+ def ===(state)
9
+ @a === state or @b === state
10
+ end
11
+ end
12
+ end
@@ -34,6 +34,12 @@ module UState
34
34
  end
35
35
 
36
36
  module Or2
37
+ def query
38
+ rest.elements.map { |x| x.and }.inject(first.query) do |a, sub|
39
+ Query::Or.new a, sub.query
40
+ end
41
+ end
42
+
37
43
  def sql
38
44
  rest.elements.map { |x| x.and }.
39
45
  inject(first.sql) do |a, sub|
@@ -135,6 +141,12 @@ module UState
135
141
  end
136
142
 
137
143
  module And2
144
+ def query
145
+ rest.elements.map { |x| x.primary }.inject(first.query) do |a, sub|
146
+ Query::And.new a, sub.query
147
+ end
148
+ end
149
+
138
150
  def sql
139
151
  rest.elements.map { |x| x.primary }.
140
152
  inject(first.sql) do |a, sub|
@@ -219,6 +231,10 @@ module UState
219
231
  end
220
232
 
221
233
  module Primary1
234
+ def query
235
+ x.query
236
+ end
237
+
222
238
  def sql
223
239
  x.sql
224
240
  end
@@ -348,6 +364,10 @@ module UState
348
364
  end
349
365
 
350
366
  module Approximately1
367
+ def query
368
+ Query::Approximately.new field.sql, string.sql
369
+ end
370
+
351
371
  def sql
352
372
  Sequel::SQL::StringExpression.like field.sql, string.sql
353
373
  end
@@ -424,6 +444,10 @@ module UState
424
444
  end
425
445
 
426
446
  module NotEquals1
447
+ def query
448
+ Query::NotEquals.new field.sql, value.sql
449
+ end
450
+
427
451
  def sql
428
452
  Sequel::SQL::BooleanExpression.from_value_pairs({field.sql => value.sql}, :AND, true)
429
453
  end
@@ -500,6 +524,10 @@ module UState
500
524
  end
501
525
 
502
526
  module Equals1
527
+ def query
528
+ Query::Equals.new field.sql, value.sql
529
+ end
530
+
503
531
  def sql
504
532
  Sequel::SQL::BooleanExpression.from_value_pairs field.sql => value.sql
505
533
  end
@@ -529,21 +557,21 @@ module UState
529
557
  s0 << r2
530
558
  if r2
531
559
  i4 = index
532
- if has_terminal?('=', false, index)
533
- r5 = instantiate_node(SyntaxNode,input, index...(index + 1))
534
- @index += 1
560
+ if has_terminal?('==', false, index)
561
+ r5 = instantiate_node(SyntaxNode,input, index...(index + 2))
562
+ @index += 2
535
563
  else
536
- terminal_parse_failure('=')
564
+ terminal_parse_failure('==')
537
565
  r5 = nil
538
566
  end
539
567
  if r5
540
568
  r4 = r5
541
569
  else
542
- if has_terminal?('==', false, index)
543
- r6 = instantiate_node(SyntaxNode,input, index...(index + 2))
544
- @index += 2
570
+ if has_terminal?('=', false, index)
571
+ r6 = instantiate_node(SyntaxNode,input, index...(index + 1))
572
+ @index += 1
545
573
  else
546
- terminal_parse_failure('==')
574
+ terminal_parse_failure('=')
547
575
  r6 = nil
548
576
  end
549
577
  if r6
@@ -5,6 +5,12 @@ module UState
5
5
 
6
6
  rule or
7
7
  first:and rest:(space 'or' space and)* {
8
+ def query
9
+ rest.elements.map { |x| x.and }.inject(first.query) do |a, sub|
10
+ Query::Or.new a, sub.query
11
+ end
12
+ end
13
+
8
14
  def sql
9
15
  rest.elements.map { |x| x.and }.
10
16
  inject(first.sql) do |a, sub|
@@ -16,6 +22,12 @@ module UState
16
22
 
17
23
  rule and
18
24
  first:primary rest:(space 'and' space primary)* {
25
+ def query
26
+ rest.elements.map { |x| x.primary }.inject(first.query) do |a, sub|
27
+ Query::And.new a, sub.query
28
+ end
29
+ end
30
+
19
31
  def sql
20
32
  rest.elements.map { |x| x.primary }.
21
33
  inject(first.sql) do |a, sub|
@@ -27,6 +39,10 @@ module UState
27
39
 
28
40
  rule primary
29
41
  '(' space? x:or space? ')' {
42
+ def query
43
+ x.query
44
+ end
45
+
30
46
  def sql
31
47
  x.sql
32
48
  end
@@ -41,6 +57,10 @@ module UState
41
57
 
42
58
  rule approximately
43
59
  field space? '=~' space? string {
60
+ def query
61
+ Query::Approximately.new field.sql, string.sql
62
+ end
63
+
44
64
  def sql
45
65
  Sequel::SQL::StringExpression.like field.sql, string.sql
46
66
  end
@@ -49,6 +69,10 @@ module UState
49
69
 
50
70
  rule not_equals
51
71
  field space? '!=' space? value {
72
+ def query
73
+ Query::NotEquals.new field.sql, value.sql
74
+ end
75
+
52
76
  def sql
53
77
  Sequel::SQL::BooleanExpression.from_value_pairs({field.sql => value.sql}, :AND, true)
54
78
  end
@@ -56,7 +80,11 @@ module UState
56
80
  end
57
81
 
58
82
  rule equals
59
- field space? ('=' / '==') space? value {
83
+ field space? ('==' / '=') space? value {
84
+ def query
85
+ Query::Equals.new field.sql, value.sql
86
+ end
87
+
60
88
  def sql
61
89
  Sequel::SQL::BooleanExpression.from_value_pairs field.sql => value.sql
62
90
  end
@@ -12,7 +12,8 @@ module UState
12
12
  require 'ustate/server/index'
13
13
  require 'ustate/server/backends'
14
14
  require 'treetop'
15
- require 'ustate/query_string.rb'
15
+ require 'ustate/query_string'
16
+ require 'ustate/query/ast'
16
17
 
17
18
  attr_accessor :backends
18
19
  attr_accessor :index
@@ -29,6 +30,11 @@ module UState
29
30
  setup_signals
30
31
  end
31
32
 
33
+ def emailer(opts = {})
34
+ require 'ustate/emailer'
35
+ @emailer ||= UState::Emailer.new(@index, opts)
36
+ end
37
+
32
38
  def start
33
39
  @index.start
34
40
 
@@ -19,6 +19,10 @@ module UState
19
19
  @threads = opts[:threads] || THREADS
20
20
  @pool = []
21
21
 
22
+ @on_state_change = []
23
+ @on_state_once = []
24
+ @on_state = []
25
+
22
26
  setup_db
23
27
  end
24
28
 
@@ -39,13 +43,42 @@ module UState
39
43
  end
40
44
  end
41
45
 
42
- def on_state_change(old, new)
46
+ def on_state_change(old = nil, new = nil, &block)
47
+ if block_given?
48
+ @on_state_change |= [block]
49
+ else
50
+ @on_state_change.each do |callback|
51
+ callback.call old, new
52
+ end
53
+ end
54
+ end
55
+
56
+ def on_state_once(state = nil, &block)
57
+ if block_given?
58
+ @on_state_once |= [block]
59
+ else
60
+ @on_state_once.each do |callback|
61
+ callback.call state
62
+ end
63
+ end
43
64
  end
44
65
 
45
- def on_state(state)
66
+ def on_state(state = nil, &block)
67
+ if block_given?
68
+ @on_state |= [block]
69
+ else
70
+ @on_state.each do |callback|
71
+ callback.call state
72
+ end
73
+ end
46
74
  end
47
75
 
48
76
  def process(s)
77
+ if s.once
78
+ on_state_once s
79
+ return on_state s
80
+ end
81
+
49
82
  if current = @db[:states][host: s.host, service: s.service]
50
83
  # Update
51
84
  if current[:time] <= s.time
@@ -6,6 +6,7 @@ class UState::State
6
6
  optional :service, :string, 3
7
7
  optional :host, :string, 4
8
8
  optional :description, :string, 5
9
+ optional :once, :bool, 6
9
10
  optional :metric_f, :float, 15
10
11
 
11
12
  def metric
@@ -1,3 +1,3 @@
1
1
  module UState
2
- VERSION = '0.0.2'
2
+ VERSION = '0.0.3'
3
3
  end
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.2
4
+ version: 0.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,12 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-09-05 00:00:00.000000000Z
12
+ date: 2011-09-06 00:00:00.000000000 -07:00
13
+ default_executable:
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: beefcake
16
- requirement: &77275260 !ruby/object:Gem::Requirement
17
+ requirement: &15107700 !ruby/object:Gem::Requirement
17
18
  none: false
18
19
  requirements:
19
20
  - - ! '>='
@@ -21,10 +22,10 @@ dependencies:
21
22
  version: 0.3.5
22
23
  type: :runtime
23
24
  prerelease: false
24
- version_requirements: *77275260
25
+ version_requirements: *15107700
25
26
  - !ruby/object:Gem::Dependency
26
27
  name: trollop
27
- requirement: &77274800 !ruby/object:Gem::Requirement
28
+ requirement: &15107220 !ruby/object:Gem::Requirement
28
29
  none: false
29
30
  requirements:
30
31
  - - ! '>='
@@ -32,7 +33,7 @@ dependencies:
32
33
  version: 1.16.2
33
34
  type: :runtime
34
35
  prerelease: false
35
- version_requirements: *77274800
36
+ version_requirements: *15107220
36
37
  description:
37
38
  email: aphyr@aphyr.com
38
39
  executables: []
@@ -40,32 +41,41 @@ extensions: []
40
41
  extra_rdoc_files: []
41
42
  files:
42
43
  - lib/ustate.rb
44
+ - lib/ustate/query/not_equals.rb
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
43
51
  - lib/ustate/query_string.treetop
44
- - lib/ustate/message.rb
52
+ - lib/ustate/server.rb
53
+ - lib/ustate/emailer.rb
54
+ - lib/ustate/query.rb
45
55
  - lib/ustate/version.rb
46
- - lib/ustate/server/graphite.rb
56
+ - lib/ustate/state.rb
57
+ - lib/ustate/client/query.rb
58
+ - lib/ustate/query_string.rb
59
+ - lib/ustate/server/index.rb
47
60
  - lib/ustate/server/backends.rb
48
- - lib/ustate/server/backends/base.rb
49
61
  - lib/ustate/server/backends/tcp.rb
50
- - lib/ustate/server/index.rb
62
+ - lib/ustate/server/backends/base.rb
63
+ - lib/ustate/server/graphite.rb
51
64
  - lib/ustate/server/connection.rb
52
- - lib/ustate/server.rb
53
- - lib/ustate/dash.rb
54
- - lib/ustate/state.rb
55
- - lib/ustate/query.rb
56
- - lib/ustate/client/query.rb
57
65
  - lib/ustate/client.rb
66
+ - lib/ustate/message.rb
67
+ - lib/ustate/dash/helper/renderer.rb
68
+ - lib/ustate/dash/views/css.scss
58
69
  - lib/ustate/dash/views/index.erubis
59
70
  - lib/ustate/dash/views/layout.erubis
60
- - lib/ustate/dash/views/css.scss
61
71
  - lib/ustate/dash/state.rb
62
72
  - lib/ustate/dash/controller/index.rb
63
73
  - lib/ustate/dash/controller/css.rb
64
74
  - lib/ustate/dash/config.rb
65
- - lib/ustate/dash/helper/renderer.rb
66
- - lib/ustate/query_string.rb
75
+ - lib/ustate/dash.rb
67
76
  - LICENSE
68
77
  - README.markdown
78
+ has_rdoc: true
69
79
  homepage: https://github.com/aphyr/ustate
70
80
  licenses: []
71
81
  post_install_message:
@@ -86,7 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
86
96
  version: '0'
87
97
  requirements: []
88
98
  rubyforge_project: ustate-client
89
- rubygems_version: 1.8.10
99
+ rubygems_version: 1.6.2
90
100
  signing_key:
91
101
  specification_version: 3
92
102
  summary: Client for the distributed state server ustate.