ustate-client 0.0.2 → 0.0.3

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.
@@ -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.