sensu 0.9.9 → 0.9.10
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +16 -0
- data/lib/sensu/client.rb +34 -32
- data/lib/sensu/constants.rb +1 -1
- data/lib/sensu/extensions.rb +1 -1
- data/lib/sensu/extensions/handlers/debug.rb +1 -1
- data/lib/sensu/extensions/mutators/only_check_output.rb +1 -1
- data/lib/sensu/io.rb +4 -0
- data/lib/sensu/rabbitmq.rb +0 -3
- data/lib/sensu/server.rb +48 -24
- data/lib/sensu/settings.rb +63 -56
- data/sensu.gemspec +2 -0
- metadata +38 -22
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,19 @@
|
|
1
|
+
## 0.9.10 - 2013-01-30
|
2
|
+
|
3
|
+
### Features
|
4
|
+
|
5
|
+
Handlers can be subdued like checks, suppression windows.
|
6
|
+
|
7
|
+
### Non-backwards compatible changes
|
8
|
+
|
9
|
+
Extensions have access to settings.
|
10
|
+
|
11
|
+
### Other
|
12
|
+
|
13
|
+
Client queue names are now determined by the broker (RabbitMQ).
|
14
|
+
|
15
|
+
Improved zombie reaping.
|
16
|
+
|
1
17
|
## 0.9.9 - 2013-01-14
|
2
18
|
|
3
19
|
### Features
|
data/lib/sensu/client.rb
CHANGED
@@ -134,42 +134,44 @@ module Sensu
|
|
134
134
|
|
135
135
|
def setup_subscriptions
|
136
136
|
@logger.debug('subscribing to client subscriptions')
|
137
|
-
@
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
:
|
144
|
-
|
145
|
-
|
146
|
-
@check_request_queue.bind(@amq.fanout(exchange_name))
|
147
|
-
end
|
148
|
-
@check_request_queue.subscribe do |payload|
|
149
|
-
begin
|
150
|
-
check = JSON.parse(payload, :symbolize_names => true)
|
151
|
-
@logger.info('received check request', {
|
152
|
-
:check => check
|
137
|
+
@check_request_queue = @amq.queue('', :auto_delete => true) do |queue|
|
138
|
+
@settings[:client][:subscriptions].uniq.each do |exchange_name|
|
139
|
+
@logger.debug('binding queue to exchange', {
|
140
|
+
:queue => {
|
141
|
+
:name => queue.name
|
142
|
+
},
|
143
|
+
:exchange => {
|
144
|
+
:name => exchange_name
|
145
|
+
}
|
153
146
|
})
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
147
|
+
queue.bind(@amq.fanout(exchange_name))
|
148
|
+
end
|
149
|
+
queue.subscribe do |payload|
|
150
|
+
begin
|
151
|
+
check = JSON.parse(payload, :symbolize_names => true)
|
152
|
+
@logger.info('received check request', {
|
159
153
|
:check => check
|
160
154
|
})
|
161
|
-
check[:
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
155
|
+
if @settings.check_exists?(check[:name])
|
156
|
+
check.merge!(@settings[:checks][check[:name]])
|
157
|
+
execute_check(check)
|
158
|
+
elsif @safe_mode
|
159
|
+
@logger.warn('check is not defined', {
|
160
|
+
:check => check
|
161
|
+
})
|
162
|
+
check[:output] = 'Check is not defined (safe mode)'
|
163
|
+
check[:status] = 3
|
164
|
+
check[:handle] = false
|
165
|
+
publish_result(check)
|
166
|
+
else
|
167
|
+
execute_check(check)
|
168
|
+
end
|
169
|
+
rescue JSON::ParserError => error
|
170
|
+
@logger.warn('check request payload must be valid json', {
|
171
|
+
:payload => payload,
|
172
|
+
:error => error.to_s
|
173
|
+
})
|
167
174
|
end
|
168
|
-
rescue JSON::ParserError => error
|
169
|
-
@logger.warn('check request payload must be valid json', {
|
170
|
-
:payload => payload,
|
171
|
-
:error => error.to_s
|
172
|
-
})
|
173
175
|
end
|
174
176
|
end
|
175
177
|
end
|
data/lib/sensu/constants.rb
CHANGED
data/lib/sensu/extensions.rb
CHANGED
data/lib/sensu/io.rb
CHANGED
data/lib/sensu/rabbitmq.rb
CHANGED
data/lib/sensu/server.rb
CHANGED
@@ -94,12 +94,20 @@ module Sensu
|
|
94
94
|
end
|
95
95
|
end
|
96
96
|
|
97
|
-
def
|
97
|
+
def action_subdued?(check, handler=nil)
|
98
98
|
subdue = false
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
99
|
+
subdue_at = handler ? 'handler' : 'publisher'
|
100
|
+
conditions = Array.new
|
101
|
+
if check[:subdue]
|
102
|
+
conditions.push(check[:subdue])
|
103
|
+
end
|
104
|
+
if handler && handler[:subdue]
|
105
|
+
conditions.push(handler[:subdue])
|
106
|
+
end
|
107
|
+
conditions.each do |condition|
|
108
|
+
if condition.has_key?(:begin) && condition.has_key?(:end)
|
109
|
+
begin_time = Time.parse(condition[:begin])
|
110
|
+
end_time = Time.parse(condition[:end])
|
103
111
|
if end_time < begin_time
|
104
112
|
if Time.now < end_time
|
105
113
|
begin_time = Time.parse('12:00:00 AM')
|
@@ -111,19 +119,26 @@ module Sensu
|
|
111
119
|
subdue = true
|
112
120
|
end
|
113
121
|
end
|
114
|
-
if
|
115
|
-
days =
|
122
|
+
if condition.has_key?(:days)
|
123
|
+
days = condition[:days].map(&:downcase)
|
116
124
|
if days.include?(Time.now.strftime('%A').downcase)
|
117
125
|
subdue = true
|
118
126
|
end
|
119
127
|
end
|
120
|
-
if subdue &&
|
121
|
-
subdue =
|
128
|
+
if subdue && condition.has_key?(:exceptions)
|
129
|
+
subdue = condition[:exceptions].none? do |exception|
|
122
130
|
Time.now >= Time.parse(exception[:begin]) && Time.now <= Time.parse(exception[:end])
|
123
131
|
end
|
124
132
|
end
|
133
|
+
if subdue
|
134
|
+
if subdue_at == (condition[:at] || 'handler')
|
135
|
+
break
|
136
|
+
else
|
137
|
+
subdue = false
|
138
|
+
end
|
139
|
+
end
|
125
140
|
end
|
126
|
-
subdue
|
141
|
+
subdue
|
127
142
|
end
|
128
143
|
|
129
144
|
def filter_attributes_match?(hash_one, hash_two)
|
@@ -202,8 +217,8 @@ module Sensu
|
|
202
217
|
})
|
203
218
|
next
|
204
219
|
end
|
205
|
-
if
|
206
|
-
@logger.info('
|
220
|
+
if action_subdued?(event[:check], handler)
|
221
|
+
@logger.info('action is subdued', {
|
207
222
|
:event => event,
|
208
223
|
:handler => handler
|
209
224
|
})
|
@@ -266,31 +281,36 @@ module Sensu
|
|
266
281
|
end
|
267
282
|
|
268
283
|
def mutate_event_data(mutator_name, event, &block)
|
269
|
-
on_error = Proc.new do |error|
|
270
|
-
@logger.error('mutator error', {
|
271
|
-
:event => event,
|
272
|
-
:mutator => mutator,
|
273
|
-
:error => error.to_s
|
274
|
-
})
|
275
|
-
end
|
276
284
|
case
|
277
285
|
when mutator_name.nil?
|
278
286
|
block.call(event.to_json)
|
279
287
|
when @settings.mutator_exists?(mutator_name)
|
280
288
|
mutator = @settings[:mutators][mutator_name]
|
289
|
+
on_error = Proc.new do |error|
|
290
|
+
@logger.error('mutator error', {
|
291
|
+
:event => event,
|
292
|
+
:mutator => mutator,
|
293
|
+
:error => error.to_s
|
294
|
+
})
|
295
|
+
end
|
281
296
|
execute_command(mutator[:command], event.to_json, on_error) do |output, status|
|
282
297
|
if status == 0
|
283
298
|
block.call(output)
|
284
299
|
else
|
285
|
-
on_error.call('non-zero exit status (' + status + '): ' + output)
|
300
|
+
on_error.call('non-zero exit status (' + status.to_s + '): ' + output)
|
286
301
|
end
|
287
302
|
end
|
288
303
|
when @extensions.mutator_exists?(mutator_name)
|
289
|
-
@extensions[:mutators][mutator_name]
|
304
|
+
extension = @extensions[:mutators][mutator_name]
|
305
|
+
extension.run(event, @settings.to_hash) do |output, status|
|
290
306
|
if status == 0
|
291
307
|
block.call(output)
|
292
308
|
else
|
293
|
-
|
309
|
+
@logger.error('mutator error', {
|
310
|
+
:event => event,
|
311
|
+
:extension => extension,
|
312
|
+
:error => 'non-zero exit status (' + status.to_s + '): ' + output
|
313
|
+
})
|
294
314
|
end
|
295
315
|
end
|
296
316
|
else
|
@@ -365,7 +385,7 @@ module Sensu
|
|
365
385
|
end
|
366
386
|
@handlers_in_progress_count -= 1
|
367
387
|
when 'extension'
|
368
|
-
handler.run(event_data) do |output, status|
|
388
|
+
handler.run(event_data, @settings.to_hash) do |output, status|
|
369
389
|
output.split(/\n+/).each do |line|
|
370
390
|
@logger.info(line)
|
371
391
|
end
|
@@ -536,8 +556,12 @@ module Sensu
|
|
536
556
|
@master_timers << EM::Timer.new(scheduling_delay) do
|
537
557
|
interval = testing? ? 0.5 : check[:interval]
|
538
558
|
@master_timers << EM::PeriodicTimer.new(interval) do
|
539
|
-
unless
|
559
|
+
unless action_subdued?(check)
|
540
560
|
publish_check_request(check)
|
561
|
+
else
|
562
|
+
@logger.info('action is subdued', {
|
563
|
+
:check => check
|
564
|
+
})
|
541
565
|
end
|
542
566
|
end
|
543
567
|
end
|
data/lib/sensu/settings.rb
CHANGED
@@ -20,11 +20,15 @@ module Sensu
|
|
20
20
|
@indifferent_access = true
|
21
21
|
end
|
22
22
|
|
23
|
-
def
|
23
|
+
def to_hash
|
24
24
|
unless @indifferent_access
|
25
25
|
indifferent_access!
|
26
26
|
end
|
27
|
-
@settings
|
27
|
+
@settings
|
28
|
+
end
|
29
|
+
|
30
|
+
def [](key)
|
31
|
+
to_hash[key]
|
28
32
|
end
|
29
33
|
|
30
34
|
SETTINGS_CATEGORIES.each do |category|
|
@@ -137,6 +141,55 @@ module Sensu
|
|
137
141
|
exit 2
|
138
142
|
end
|
139
143
|
|
144
|
+
def validate_subdue(condition, details={})
|
145
|
+
type = details.has_key?(:check) ? 'check' : 'handler'
|
146
|
+
unless condition.is_a?(Hash)
|
147
|
+
invalid(type + ' subdue must be a hash', details)
|
148
|
+
end
|
149
|
+
if condition.has_key?(:at)
|
150
|
+
unless %w[handler publisher].include?(condition[:at])
|
151
|
+
invalid(type + ' subdue at must be either handler or publisher', details)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
if condition.has_key?(:begin) || condition.has_key?(:end)
|
155
|
+
begin
|
156
|
+
Time.parse(condition[:begin])
|
157
|
+
Time.parse(condition[:end])
|
158
|
+
rescue
|
159
|
+
invalid(type + ' subdue begin & end times must be valid', details)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
if condition.has_key?(:days)
|
163
|
+
unless condition[:days].is_a?(Array)
|
164
|
+
invalid(type + ' subdue days must be an array', details)
|
165
|
+
end
|
166
|
+
condition[:days].each do |day|
|
167
|
+
days = %w[sunday monday tuesday wednesday thursday friday saturday]
|
168
|
+
unless day.is_a?(String) && days.include?(day.downcase)
|
169
|
+
invalid(type + ' subdue days must be valid days of the week', details)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
if condition.has_key?(:exceptions)
|
174
|
+
unless condition[:exceptions].is_a?(Array)
|
175
|
+
invalid(type + ' subdue exceptions must be an array', details)
|
176
|
+
end
|
177
|
+
condition[:exceptions].each do |exception|
|
178
|
+
unless exception.is_a?(Hash)
|
179
|
+
invalid(type + ' subdue exceptions must each be a hash', details)
|
180
|
+
end
|
181
|
+
if exception.has_key?(:begin) || exception.has_key?(:end)
|
182
|
+
begin
|
183
|
+
Time.parse(exception[:begin])
|
184
|
+
Time.parse(exception[:end])
|
185
|
+
rescue
|
186
|
+
invalid(type + ' subdue exception begin & end times must be valid', details)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
140
193
|
def validate_checks
|
141
194
|
unless @settings[:checks].is_a?(Hash)
|
142
195
|
invalid('checks must be a hash')
|
@@ -209,60 +262,9 @@ module Sensu
|
|
209
262
|
end
|
210
263
|
end
|
211
264
|
if check.has_key?(:subdue)
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
})
|
216
|
-
end
|
217
|
-
if check[:subdue].has_key?(:begin) || check[:subdue].has_key?(:end)
|
218
|
-
begin
|
219
|
-
Time.parse(check[:subdue][:begin])
|
220
|
-
Time.parse(check[:subdue][:end])
|
221
|
-
rescue
|
222
|
-
invalid('check subdue begin & end times must be valid', {
|
223
|
-
:check => check
|
224
|
-
})
|
225
|
-
end
|
226
|
-
end
|
227
|
-
if check[:subdue].has_key?(:days)
|
228
|
-
unless check[:subdue][:days].is_a?(Array)
|
229
|
-
invalid('check subdue days must be an array', {
|
230
|
-
:check => check
|
231
|
-
})
|
232
|
-
end
|
233
|
-
check[:subdue][:days].each do |day|
|
234
|
-
days = %w[sunday monday tuesday wednesday thursday friday saturday]
|
235
|
-
unless day.is_a?(String) && days.include?(day.downcase)
|
236
|
-
invalid('check subdue days must be valid days of the week', {
|
237
|
-
:check => check
|
238
|
-
})
|
239
|
-
end
|
240
|
-
end
|
241
|
-
end
|
242
|
-
if check[:subdue].has_key?(:exceptions)
|
243
|
-
unless check[:subdue][:exceptions].is_a?(Array)
|
244
|
-
invalid('check subdue exceptions must be an array', {
|
245
|
-
:check => check
|
246
|
-
})
|
247
|
-
end
|
248
|
-
check[:subdue][:exceptions].each do |exception|
|
249
|
-
unless exception.is_a?(Hash)
|
250
|
-
invalid('check subdue exceptions must each be a hash', {
|
251
|
-
:check => check
|
252
|
-
})
|
253
|
-
end
|
254
|
-
if exception.has_key?(:begin) || exception.has_key?(:end)
|
255
|
-
begin
|
256
|
-
Time.parse(exception[:begin])
|
257
|
-
Time.parse(exception[:end])
|
258
|
-
rescue
|
259
|
-
invalid('check subdue exception begin & end times must be valid', {
|
260
|
-
:check => check
|
261
|
-
})
|
262
|
-
end
|
263
|
-
end
|
264
|
-
end
|
265
|
-
end
|
265
|
+
validate_subdue(check[:subdue], {
|
266
|
+
:check => check
|
267
|
+
})
|
266
268
|
end
|
267
269
|
end
|
268
270
|
end
|
@@ -459,6 +461,11 @@ module Sensu
|
|
459
461
|
end
|
460
462
|
end
|
461
463
|
end
|
464
|
+
if handler.has_key?(:subdue)
|
465
|
+
validate_subdue(handler[:subdue], {
|
466
|
+
:handler => handler
|
467
|
+
})
|
468
|
+
end
|
462
469
|
end
|
463
470
|
end
|
464
471
|
end
|
data/sensu.gemspec
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
1
2
|
require File.join(File.dirname(__FILE__), 'lib', 'sensu', 'constants')
|
2
3
|
|
3
4
|
Gem::Specification.new do |s|
|
@@ -13,6 +14,7 @@ Gem::Specification.new do |s|
|
|
13
14
|
s.has_rdoc = false
|
14
15
|
|
15
16
|
s.add_dependency('eventmachine', '1.0.0')
|
17
|
+
s.add_dependency('amq-client', '0.9.11')
|
16
18
|
s.add_dependency('amqp', '0.9.8')
|
17
19
|
s.add_dependency('json')
|
18
20
|
s.add_dependency('cabin', '0.4.4')
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sensu
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 47
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 9
|
9
|
-
-
|
10
|
-
version: 0.9.
|
9
|
+
- 10
|
10
|
+
version: 0.9.10
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Sean Porter
|
@@ -16,7 +16,7 @@ autorequire:
|
|
16
16
|
bindir: bin
|
17
17
|
cert_chain: []
|
18
18
|
|
19
|
-
date: 2013-01-
|
19
|
+
date: 2013-01-30 00:00:00 -08:00
|
20
20
|
default_executable:
|
21
21
|
dependencies:
|
22
22
|
- !ruby/object:Gem::Dependency
|
@@ -36,9 +36,25 @@ dependencies:
|
|
36
36
|
type: :runtime
|
37
37
|
version_requirements: *id001
|
38
38
|
- !ruby/object:Gem::Dependency
|
39
|
-
name:
|
39
|
+
name: amq-client
|
40
40
|
prerelease: false
|
41
41
|
requirement: &id002 !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - "="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
hash: 45
|
47
|
+
segments:
|
48
|
+
- 0
|
49
|
+
- 9
|
50
|
+
- 11
|
51
|
+
version: 0.9.11
|
52
|
+
type: :runtime
|
53
|
+
version_requirements: *id002
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: amqp
|
56
|
+
prerelease: false
|
57
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
42
58
|
none: false
|
43
59
|
requirements:
|
44
60
|
- - "="
|
@@ -50,11 +66,11 @@ dependencies:
|
|
50
66
|
- 8
|
51
67
|
version: 0.9.8
|
52
68
|
type: :runtime
|
53
|
-
version_requirements: *
|
69
|
+
version_requirements: *id003
|
54
70
|
- !ruby/object:Gem::Dependency
|
55
71
|
name: json
|
56
72
|
prerelease: false
|
57
|
-
requirement: &
|
73
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
58
74
|
none: false
|
59
75
|
requirements:
|
60
76
|
- - ">="
|
@@ -64,11 +80,11 @@ dependencies:
|
|
64
80
|
- 0
|
65
81
|
version: "0"
|
66
82
|
type: :runtime
|
67
|
-
version_requirements: *
|
83
|
+
version_requirements: *id004
|
68
84
|
- !ruby/object:Gem::Dependency
|
69
85
|
name: cabin
|
70
86
|
prerelease: false
|
71
|
-
requirement: &
|
87
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
72
88
|
none: false
|
73
89
|
requirements:
|
74
90
|
- - "="
|
@@ -80,11 +96,11 @@ dependencies:
|
|
80
96
|
- 4
|
81
97
|
version: 0.4.4
|
82
98
|
type: :runtime
|
83
|
-
version_requirements: *
|
99
|
+
version_requirements: *id005
|
84
100
|
- !ruby/object:Gem::Dependency
|
85
101
|
name: em-redis-unified
|
86
102
|
prerelease: false
|
87
|
-
requirement: &
|
103
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
88
104
|
none: false
|
89
105
|
requirements:
|
90
106
|
- - "="
|
@@ -96,11 +112,11 @@ dependencies:
|
|
96
112
|
- 1
|
97
113
|
version: 0.4.1
|
98
114
|
type: :runtime
|
99
|
-
version_requirements: *
|
115
|
+
version_requirements: *id006
|
100
116
|
- !ruby/object:Gem::Dependency
|
101
117
|
name: thin
|
102
118
|
prerelease: false
|
103
|
-
requirement: &
|
119
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
104
120
|
none: false
|
105
121
|
requirements:
|
106
122
|
- - "="
|
@@ -112,11 +128,11 @@ dependencies:
|
|
112
128
|
- 0
|
113
129
|
version: 1.5.0
|
114
130
|
type: :runtime
|
115
|
-
version_requirements: *
|
131
|
+
version_requirements: *id007
|
116
132
|
- !ruby/object:Gem::Dependency
|
117
133
|
name: async_sinatra
|
118
134
|
prerelease: false
|
119
|
-
requirement: &
|
135
|
+
requirement: &id008 !ruby/object:Gem::Requirement
|
120
136
|
none: false
|
121
137
|
requirements:
|
122
138
|
- - "="
|
@@ -128,11 +144,11 @@ dependencies:
|
|
128
144
|
- 0
|
129
145
|
version: 1.0.0
|
130
146
|
type: :runtime
|
131
|
-
version_requirements: *
|
147
|
+
version_requirements: *id008
|
132
148
|
- !ruby/object:Gem::Dependency
|
133
149
|
name: rake
|
134
150
|
prerelease: false
|
135
|
-
requirement: &
|
151
|
+
requirement: &id009 !ruby/object:Gem::Requirement
|
136
152
|
none: false
|
137
153
|
requirements:
|
138
154
|
- - ">="
|
@@ -142,11 +158,11 @@ dependencies:
|
|
142
158
|
- 0
|
143
159
|
version: "0"
|
144
160
|
type: :development
|
145
|
-
version_requirements: *
|
161
|
+
version_requirements: *id009
|
146
162
|
- !ruby/object:Gem::Dependency
|
147
163
|
name: rspec
|
148
164
|
prerelease: false
|
149
|
-
requirement: &
|
165
|
+
requirement: &id010 !ruby/object:Gem::Requirement
|
150
166
|
none: false
|
151
167
|
requirements:
|
152
168
|
- - ">="
|
@@ -156,11 +172,11 @@ dependencies:
|
|
156
172
|
- 0
|
157
173
|
version: "0"
|
158
174
|
type: :development
|
159
|
-
version_requirements: *
|
175
|
+
version_requirements: *id010
|
160
176
|
- !ruby/object:Gem::Dependency
|
161
177
|
name: em-http-request
|
162
178
|
prerelease: false
|
163
|
-
requirement: &
|
179
|
+
requirement: &id011 !ruby/object:Gem::Requirement
|
164
180
|
none: false
|
165
181
|
requirements:
|
166
182
|
- - ">="
|
@@ -170,7 +186,7 @@ dependencies:
|
|
170
186
|
- 0
|
171
187
|
version: "0"
|
172
188
|
type: :development
|
173
|
-
version_requirements: *
|
189
|
+
version_requirements: *id011
|
174
190
|
description: A monitoring framework that aims to be simple, malleable, and scalable. Uses the publish/subscribe model.
|
175
191
|
email:
|
176
192
|
- portertech@gmail.com
|