sensu 0.17.0.beta → 0.17.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,226 @@
1
+ require "multi_json"
2
+
3
+ module Sensu
4
+ module Client
5
+ # EventMachine connection handler for the Sensu client"s socket.
6
+ #
7
+ # The Sensu client listens on localhost, port 3030 (by default), for
8
+ # UDP and TCP traffic. This allows software running on the host to
9
+ # push check results (that may contain metrics) into Sensu, without
10
+ # needing to know anything about Sensu"s internal implementation.
11
+ #
12
+ # The socket only accepts 7-bit ASCII-encoded data.
13
+ #
14
+ # Although the Sensu client accepts UDP and TCP traffic, you must be
15
+ # aware of the UDP protocol limitations. Any data you send over UDP
16
+ # must fit in a single datagram and you will not receive a response
17
+ # (no confirmation).
18
+ #
19
+ # == UDP Protocol ==
20
+ #
21
+ # If the socket receives a message containing whitespace and the
22
+ # string +"ping"+, it will ignore it.
23
+ #
24
+ # The socket assumes all other messages will contain a single,
25
+ # complete, JSON hash. The hash must be a valid JSON check result.
26
+ # Deserialization failures will be logged at the ERROR level by the
27
+ # Sensu client, but the sender of the invalid data will not be
28
+ # notified.
29
+ #
30
+ # == TCP Protocol ==
31
+ #
32
+ # If the socket receives a message containing whitespace and the
33
+ # string +"ping"+, it will respond with the message +"pong"+.
34
+ #
35
+ # The socket assumes any other stream will be a single, complete,
36
+ # JSON hash. A deserialization failure will be logged at the WARN
37
+ # level by the Sensu client and respond with the message
38
+ # +"invalid"+. An +"ok"+ response indicates the Sensu client
39
+ # successfully received the JSON hash and will publish the check
40
+ # result.
41
+ #
42
+ # Streams can be of any length. The socket protocol does not require
43
+ # any headers, instead the socket tries to parse everything it has
44
+ # been sent each time a chunk of data arrives. Once the JSON parses
45
+ # successfully, the Sensu client publishes the result. After
46
+ # +WATCHDOG_DELAY+ (default is 500 msec) since the most recent chunk
47
+ # of data was received, the agent will give up on the sender, and
48
+ # instead respond +"invalid"+ and close the connection.
49
+ class Socket < EM::Connection
50
+ class DataError < StandardError; end
51
+
52
+ attr_accessor :logger, :settings, :transport, :protocol
53
+
54
+ # The number of seconds that may elapse between chunks of data
55
+ # from a sender before it is considered dead, and the connection
56
+ # is close.
57
+ WATCHDOG_DELAY = 0.5
58
+
59
+ #
60
+ # Sensu::Socket operating mode enum.
61
+ #
62
+
63
+ # ACCEPT mode. Append chunks of data to a buffer and test to see
64
+ # whether the buffer contents are valid JSON.
65
+ MODE_ACCEPT = :ACCEPT
66
+
67
+ # REJECT mode. No longer receiving data from sender. Discard
68
+ # chunks of data in this mode, the connection is being closed.
69
+ MODE_REJECT = :REJECT
70
+
71
+ # Initialize instance variables that will be used throughout the
72
+ # lifetime of the connection. This method is called when the
73
+ # network connection has been established, and immediately after
74
+ # responding to a sender.
75
+ def post_init
76
+ @protocol ||= :tcp
77
+ @data_buffer = ""
78
+ @parse_error = nil
79
+ @watchdog = nil
80
+ @mode = MODE_ACCEPT
81
+ end
82
+
83
+ # Send a response to the sender, close the
84
+ # connection, and call post_init().
85
+ #
86
+ # @param [String] data to send as a response.
87
+ def respond(data)
88
+ if @protocol == :tcp
89
+ send_data(data)
90
+ close_connection_after_writing
91
+ end
92
+ post_init
93
+ end
94
+
95
+ # Cancel the current connection watchdog.
96
+ def cancel_watchdog
97
+ if @watchdog
98
+ @watchdog.cancel
99
+ end
100
+ end
101
+
102
+ # Reset (or start) the connection watchdog.
103
+ def reset_watchdog
104
+ cancel_watchdog
105
+ @watchdog = EM::Timer.new(WATCHDOG_DELAY) do
106
+ @mode = MODE_REJECT
107
+ @logger.warn("discarding data buffer for sender and closing connection", {
108
+ :data => @data_buffer,
109
+ :parse_error => @parse_error
110
+ })
111
+ respond("invalid")
112
+ end
113
+ end
114
+
115
+ # Validate check result attributes.
116
+ #
117
+ # @param [Hash] check result to validate.
118
+ def validate_check_result(check)
119
+ unless check[:name] =~ /^[\w\.-]+$/
120
+ raise DataError, "check name must be a string and cannot contain spaces or special characters"
121
+ end
122
+ unless check[:output].is_a?(String)
123
+ raise DataError, "check output must be a string"
124
+ end
125
+ unless check[:status].is_a?(Integer)
126
+ raise DataError, "check status must be an integer"
127
+ end
128
+ end
129
+
130
+ # Publish a check result to the Sensu transport.
131
+ #
132
+ # @param [Hash] check result.
133
+ def publish_check_result(check)
134
+ payload = {
135
+ :client => @settings[:client][:name],
136
+ :check => check.merge(:issued => Time.now.to_i)
137
+ }
138
+ @logger.info("publishing check result", {
139
+ :payload => payload
140
+ })
141
+ @transport.publish(:direct, "results", MultiJson.dump(payload))
142
+ end
143
+
144
+ # Process a check result. Set check result attribute defaults,
145
+ # validate the attributes, publish the check result to the Sensu
146
+ # transport, and respond to the sender with the message +"ok"+.
147
+ #
148
+ # @param [Hash] check result to be validated and published.
149
+ # @raise [DataError] if +check+ is invalid.
150
+ def process_check_result(check)
151
+ check[:status] ||= 0
152
+ validate_check_result(check)
153
+ publish_check_result(check)
154
+ respond("ok")
155
+ end
156
+
157
+ # Parse a JSON check result. For UDP, immediately raise a parser
158
+ # error. For TCP, record parser errors, so the connection
159
+ # +watchdog+ can report them.
160
+ #
161
+ # @param [String] data to parse for a check result.
162
+ def parse_check_result(data)
163
+ begin
164
+ check = MultiJson.load(data)
165
+ cancel_watchdog
166
+ process_check_result(check)
167
+ rescue MultiJson::ParseError, ArgumentError => error
168
+ if @protocol == :tcp
169
+ @parse_error = error.to_s
170
+ else
171
+ raise error
172
+ end
173
+ end
174
+ end
175
+
176
+ # Process the data received. This method validates the data
177
+ # encoding, provides ping/pong functionality, and passes potential
178
+ # check results on for further processing.
179
+ #
180
+ # @param [String] data to be processed.
181
+ def process_data(data)
182
+ if data.bytes.find { |char| char > 0x80 }
183
+ @logger.warn("socket received non-ascii characters")
184
+ respond("invalid")
185
+ elsif data.strip == "ping"
186
+ @logger.debug("socket received ping")
187
+ respond("pong")
188
+ else
189
+ @logger.debug("socket received data", {
190
+ :data => data
191
+ })
192
+ begin
193
+ parse_check_result(data)
194
+ rescue => error
195
+ @logger.error("failed to process check result from socket", {
196
+ :data => data,
197
+ :error => error.to_s
198
+ })
199
+ respond("invalid")
200
+ end
201
+ end
202
+ end
203
+
204
+ # This method is called whenever data is received. For UDP, it
205
+ # will only be called once, the original data length can be
206
+ # expected. For TCP, this method may be called several times, data
207
+ # received is buffered. TCP connections require a +watchdog+.
208
+ #
209
+ # @param [String] data received from the sender.
210
+ def receive_data(data)
211
+ unless @mode == MODE_REJECT
212
+ case @protocol
213
+ when :udp
214
+ process_data(data)
215
+ when :tcp
216
+ if EM.reactor_running?
217
+ reset_watchdog
218
+ end
219
+ @data_buffer << data
220
+ process_data(@data_buffer)
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end
@@ -1,9 +1,12 @@
1
1
  module Sensu
2
2
  unless defined?(Sensu::VERSION)
3
- VERSION = '0.17.0.beta'
3
+ # Sensu release version.
4
+ VERSION = "0.17.0.beta.1"
4
5
 
6
+ # Sensu check severities.
5
7
  SEVERITIES = %w[ok warning critical unknown]
6
8
 
9
+ # Process signals that trigger a Sensu process stop.
7
10
  STOP_SIGNALS = %w[INT TERM]
8
11
  end
9
12
  end
data/lib/sensu/daemon.rb CHANGED
@@ -1,53 +1,69 @@
1
- require 'rubygems'
1
+ require "rubygems"
2
2
 
3
- gem 'multi_json', '1.10.1'
3
+ gem "multi_json", "1.10.1"
4
+ gem "eventmachine", "1.0.3"
4
5
 
5
- gem 'sensu-em', '2.4.1'
6
- gem 'sensu-logger', '1.0.0'
7
- gem 'sensu-settings', '1.2.0'
8
- gem 'sensu-extension', '1.0.0'
9
- gem 'sensu-extensions', '1.0.0'
10
- gem 'sensu-transport', '2.4.0'
11
- gem 'sensu-spawn', '1.1.0'
6
+ gem "sensu-em", "2.4.1"
7
+ gem "sensu-logger", "1.0.0"
8
+ gem "sensu-settings", "1.2.0"
9
+ gem "sensu-extension", "1.1.2"
10
+ gem "sensu-extensions", "1.1.0"
11
+ gem "sensu-transport", "2.4.0"
12
+ gem "sensu-spawn", "1.1.0"
12
13
 
13
- require 'time'
14
- require 'uri'
14
+ require "time"
15
+ require "uri"
15
16
 
16
- require 'sensu/logger'
17
- require 'sensu/settings'
18
- require 'sensu/extensions'
19
- require 'sensu/transport'
20
- require 'sensu/spawn'
17
+ require "sensu/logger"
18
+ require "sensu/settings"
19
+ require "sensu/extensions"
20
+ require "sensu/transport"
21
+ require "sensu/spawn"
21
22
 
22
- require 'sensu/constants'
23
- require 'sensu/utilities'
24
- require 'sensu/cli'
25
- require 'sensu/redis'
23
+ require "sensu/constants"
24
+ require "sensu/utilities"
25
+ require "sensu/cli"
26
+ require "sensu/redis"
26
27
 
28
+ # Symbolize hash keys when parsing JSON.
27
29
  MultiJson.load_options = {:symbolize_keys => true}
28
30
 
29
31
  module Sensu
30
32
  module Daemon
31
33
  include Utilities
32
34
 
33
- attr_reader :state
34
-
35
+ # Initialize the Sensu process. Set the initial service state, set
36
+ # up the logger, load settings, load extensions, and optionally
37
+ # daemonize the process and/or create a PID file. A subclass may
38
+ # override this method.
39
+ #
40
+ # @param options [Hash]
35
41
  def initialize(options={})
36
42
  @state = :initializing
37
- @timers = {
38
- :run => Array.new
39
- }
43
+ @timers = {:run => []}
40
44
  setup_logger(options)
41
45
  load_settings(options)
42
46
  load_extensions(options)
43
47
  setup_process(options)
44
48
  end
45
49
 
50
+ # Set up the Sensu logger and its process signal traps for log
51
+ # rotation and debug log level toggling. This method creates the
52
+ # logger instance variable: `@logger`.
53
+ #
54
+ # https://github.com/sensu/sensu-logger
55
+ #
56
+ # @param options [Hash]
46
57
  def setup_logger(options={})
47
58
  @logger = Logger.get(options)
48
59
  @logger.setup_signal_traps
49
60
  end
50
61
 
62
+ # Log setting or extension loading concerns, sensitive information
63
+ # is redacted.
64
+ #
65
+ # @param concerns [Array] to be logged.
66
+ # @param level [Symbol] to log the concerns at.
51
67
  def log_concerns(concerns=[], level=:warn)
52
68
  concerns.each do |concern|
53
69
  message = concern.delete(:message)
@@ -55,18 +71,34 @@ module Sensu
55
71
  end
56
72
  end
57
73
 
74
+ # Load Sensu settings and validate them. If there are validation
75
+ # failures, log them (concerns), then cause the Sensu process to
76
+ # exit (2). This method creates the settings instance variable:
77
+ # `@settings`.
78
+ #
79
+ # https://github.com/sensu/sensu-settings
80
+ #
81
+ # @param options [Hash]
58
82
  def load_settings(options={})
59
83
  @settings = Settings.get(options)
60
84
  log_concerns(@settings.warnings)
61
85
  failures = @settings.validate
62
86
  unless failures.empty?
63
- @logger.fatal('invalid settings')
87
+ @logger.fatal("invalid settings")
64
88
  log_concerns(failures, :fatal)
65
- @logger.fatal('SENSU NOT RUNNING!')
89
+ @logger.fatal("SENSU NOT RUNNING!")
66
90
  exit 2
67
91
  end
68
92
  end
69
93
 
94
+ # Load Sensu extensions and log any concerns. Set the logger and
95
+ # settings for each extension instance. This method creates the
96
+ # extensions instance variable: `@extensions`.
97
+ #
98
+ # https://github.com/sensu/sensu-extensions
99
+ # https://github.com/sensu/sensu-extension
100
+ #
101
+ # @param options [Hash]
70
102
  def load_extensions(options={})
71
103
  @extensions = Extensions.get(options)
72
104
  log_concerns(@extensions.warnings)
@@ -77,35 +109,49 @@ module Sensu
77
109
  end
78
110
  end
79
111
 
112
+ # Manage the current process, optionally daemonize and/or write
113
+ # the current process ID to a PID file.
114
+ #
115
+ # @param options [Hash]
80
116
  def setup_process(options)
81
- if options[:daemonize]
82
- daemonize
83
- end
84
- if options[:pid_file]
85
- write_pid(options[:pid_file])
86
- end
117
+ daemonize if options[:daemonize]
118
+ write_pid(options[:pid_file]) if options[:pid_file]
87
119
  end
88
120
 
121
+ # Start the Sensu service and set the service state to `:running`.
122
+ # This method will likely be overridden by a subclass.
89
123
  def start
90
124
  @state = :running
91
125
  end
92
126
 
127
+ # Pause the Sensu service and set the service state to `:paused`.
128
+ # This method will likely be overridden by a subclass.
93
129
  def pause
94
130
  @state = :paused
95
131
  end
96
132
 
133
+ # Resume the paused Sensu service and set the service state to
134
+ # `:running`. This method will likely be overridden by a subclass.
97
135
  def resume
98
136
  @state = :running
99
137
  end
100
138
 
139
+ # Stop the Sensu service and set the service state to `:stopped`.
140
+ # This method will likely be overridden by a subclass. This method
141
+ # should stop the EventMachine event loop.
101
142
  def stop
102
143
  @state = :stopped
103
- @logger.warn('stopping reactor')
144
+ @logger.warn("stopping reactor")
104
145
  EM::stop_event_loop
105
146
  end
106
147
 
148
+ # Set up process signal traps. This method uses the `STOP_SIGNALS`
149
+ # constant to determine which process signals will result in a
150
+ # graceful service stop. A periodic timer must be used to poll for
151
+ # received signals, as Mutex#lock cannot be used within the
152
+ # context of `trap()`.
107
153
  def setup_signal_traps
108
- @signals = Array.new
154
+ @signals = []
109
155
  STOP_SIGNALS.each do |signal|
110
156
  Signal.trap(signal) do
111
157
  @signals << signal
@@ -114,103 +160,109 @@ module Sensu
114
160
  EM::PeriodicTimer.new(1) do
115
161
  signal = @signals.shift
116
162
  if STOP_SIGNALS.include?(signal)
117
- @logger.warn('received signal', {
118
- :signal => signal
119
- })
163
+ @logger.warn("received signal", :signal => signal)
120
164
  stop
121
165
  end
122
166
  end
123
167
  end
124
168
 
169
+ # Set up the Sensu transport connection. Sensu uses a transport
170
+ # API, allowing it to use various message brokers. By default,
171
+ # Sensu will use the built-in "rabbitmq" transport. The Sensu
172
+ # service will stop gracefully in the event of a transport error,
173
+ # and pause/resume in the event of connectivity issues. This
174
+ # method creates the transport instance variable: `@transport`.
175
+ #
176
+ # https://github.com/sensu/sensu-transport
125
177
  def setup_transport
126
- transport_name = @settings[:transport][:name] || 'rabbitmq'
178
+ transport_name = @settings[:transport][:name] || "rabbitmq"
127
179
  transport_settings = @settings[transport_name]
128
- @logger.debug('connecting to transport', {
180
+ @logger.debug("connecting to transport", {
129
181
  :name => transport_name,
130
182
  :settings => transport_settings
131
183
  })
132
184
  Transport.logger = @logger
133
185
  @transport = Transport.connect(transport_name, transport_settings)
134
186
  @transport.on_error do |error|
135
- @logger.fatal('transport connection error', {
136
- :error => error.to_s
137
- })
187
+ @logger.fatal("transport connection error", :error => error.to_s)
138
188
  stop
139
189
  end
140
190
  @transport.before_reconnect do
141
191
  unless testing?
142
- @logger.warn('reconnecting to transport')
192
+ @logger.warn("reconnecting to transport")
143
193
  pause
144
194
  end
145
195
  end
146
196
  @transport.after_reconnect do
147
- @logger.info('reconnected to transport')
197
+ @logger.info("reconnected to transport")
148
198
  resume
149
199
  end
150
200
  end
151
201
 
202
+ # Set up the Redis connection. Sensu uses Redis as a data store,
203
+ # to store the client registry, current events, etc. The Sensu
204
+ # service will stop gracefully in the event of a Redis error, and
205
+ # pause/resume in the event of connectivity issues. This method
206
+ # creates the Redis instance variable: `@redis`.
152
207
  def setup_redis
153
- @logger.debug('connecting to redis', {
154
- :settings => @settings[:redis]
155
- })
208
+ @logger.debug("connecting to redis", :settings => @settings[:redis])
156
209
  @redis = Redis.connect(@settings[:redis])
157
210
  @redis.on_error do |error|
158
- @logger.fatal('redis connection error', {
159
- :error => error.to_s
160
- })
211
+ @logger.fatal("redis connection error", :error => error.to_s)
161
212
  stop
162
213
  end
163
214
  @redis.before_reconnect do
164
215
  unless testing?
165
- @logger.warn('reconnecting to redis')
216
+ @logger.warn("reconnecting to redis")
166
217
  pause
167
218
  end
168
219
  end
169
220
  @redis.after_reconnect do
170
- @logger.info('reconnected to redis')
221
+ @logger.info("reconnected to redis")
171
222
  resume
172
223
  end
173
224
  end
174
225
 
175
226
  private
176
227
 
228
+ # Write the current process ID (PID) to a file (PID file). This
229
+ # method will cause the Sensu service to exit (2) if the PID file
230
+ # cannot be written to.
231
+ #
232
+ # @param file [String] to write the current PID to.
177
233
  def write_pid(file)
178
234
  begin
179
- File.open(file, 'w') do |pid_file|
235
+ File.open(file, "w") do |pid_file|
180
236
  pid_file.puts(Process.pid)
181
237
  end
182
238
  rescue
183
- @logger.fatal('could not write to pid file', {
184
- :pid_file => file
185
- })
186
- @logger.fatal('SENSU NOT RUNNING!')
239
+ @logger.fatal("could not write to pid file", :pid_file => file)
240
+ @logger.fatal("SENSU NOT RUNNING!")
187
241
  exit 2
188
242
  end
189
243
  end
190
244
 
245
+ # Daemonize the current process. Seed the random number generator,
246
+ # fork (& exit), detach from controlling terminal, ignore SIGHUP,
247
+ # fork (& exit), use root '/' as the current working directory,
248
+ # and close STDIN/OUT/ERR since the process is no longer attached
249
+ # to a terminal.
191
250
  def daemonize
192
251
  Kernel.srand
193
- if Kernel.fork
194
- exit
195
- end
252
+ exit if Kernel.fork
196
253
  unless Process.setsid
197
- @logger.fatal('cannot detach from controlling terminal')
198
- @logger.fatal('SENSU NOT RUNNING!')
254
+ @logger.fatal("cannot detach from controlling terminal")
255
+ @logger.fatal("SENSU NOT RUNNING!")
199
256
  exit 2
200
257
  end
201
- Signal.trap('SIGHUP', 'IGNORE')
202
- if Kernel.fork
203
- exit
204
- end
205
- Dir.chdir('/')
258
+ Signal.trap("SIGHUP", "IGNORE")
259
+ exit if Kernel.fork
260
+ Dir.chdir("/")
206
261
  ObjectSpace.each_object(IO) do |io|
207
262
  unless [STDIN, STDOUT, STDERR].include?(io)
208
263
  begin
209
- unless io.closed?
210
- io.close
211
- end
212
- rescue
213
- end
264
+ io.close unless io.closed?
265
+ rescue; end
214
266
  end
215
267
  end
216
268
  end
data/lib/sensu/redis.rb CHANGED
@@ -1,16 +1,21 @@
1
- gem 'em-redis-unified', '0.5.0'
1
+ gem "em-redis-unified", "0.5.0"
2
2
 
3
- require 'em-redis'
3
+ require "em-redis"
4
4
 
5
5
  module Sensu
6
6
  class Redis
7
+ # Connect to Redis and ensure that the Redis version is at least
8
+ # 1.3.14, in order to support certain commands.
9
+ #
10
+ # @param options [Hash]
11
+ # @return [Object] Redis connection object.
7
12
  def self.connect(options={})
8
- options ||= Hash.new
13
+ options ||= {}
9
14
  connection = EM::Protocols::Redis.connect(options)
10
15
  connection.info do |info|
11
- if info[:redis_version] < '1.3.14'
16
+ if info[:redis_version] < "1.3.14"
12
17
  klass = EM::Protocols::Redis::RedisError
13
- message = 'redis version must be >= 2.0 RC 1'
18
+ message = "redis version must be >= 2.0 RC 1"
14
19
  connection.error(klass, message)
15
20
  end
16
21
  end