god 0.13.4 → 0.13.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/History.txt +13 -0
  3. data/doc/god.asciidoc +50 -8
  4. data/ext/god/extconf.rb +1 -0
  5. data/ext/god/kqueue_handler.c +9 -1
  6. data/ext/god/netlink_handler.c +17 -0
  7. data/god.gemspec +12 -3
  8. data/lib/god.rb +3 -1
  9. data/lib/god/contacts/slack.rb +100 -0
  10. data/lib/god/contacts/statsd.rb +46 -0
  11. data/lib/god/contacts/webhook.rb +1 -0
  12. data/lib/god/process.rb +10 -5
  13. data/test/helper.rb +8 -3
  14. data/test/test_airbrake.rb +1 -1
  15. data/test/test_behavior.rb +2 -2
  16. data/test/test_campfire.rb +1 -1
  17. data/test/test_condition.rb +3 -3
  18. data/test/test_conditions_disk_usage.rb +1 -1
  19. data/test/test_conditions_http_response_code.rb +1 -1
  20. data/test/test_conditions_process_running.rb +1 -1
  21. data/test/test_conditions_socket_responding.rb +1 -1
  22. data/test/test_conditions_tries.rb +3 -3
  23. data/test/test_contact.rb +9 -9
  24. data/test/test_driver.rb +4 -3
  25. data/test/test_email.rb +1 -1
  26. data/test/test_event_handler.rb +2 -2
  27. data/test/test_god.rb +9 -7
  28. data/test/test_handlers_kqueue_handler.rb +1 -1
  29. data/test/test_hipchat.rb +1 -1
  30. data/test/test_jabber.rb +1 -1
  31. data/test/test_logger.rb +3 -3
  32. data/test/test_metric.rb +1 -1
  33. data/test/test_process.rb +7 -3
  34. data/test/test_prowl.rb +1 -1
  35. data/test/test_registry.rb +1 -1
  36. data/test/test_slack.rb +56 -0
  37. data/test/test_socket.rb +1 -1
  38. data/test/test_statsd.rb +22 -0
  39. data/test/test_sugar.rb +1 -1
  40. data/test/test_system_portable_poller.rb +1 -1
  41. data/test/test_system_process.rb +1 -1
  42. data/test/test_task.rb +4 -4
  43. data/test/test_timeline.rb +1 -1
  44. data/test/test_trigger.rb +5 -1
  45. data/test/test_watch.rb +1 -1
  46. data/test/test_webhook.rb +1 -1
  47. metadata +240 -223
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4e92e64f9fc2eaea16ace1b200c266186acbfe80
4
+ data.tar.gz: 3f1a94de8fa67f06e41b96e1176faf71e02ee7ab
5
+ SHA512:
6
+ metadata.gz: 97bc476662014ecdd34126fc2a1e1fa0352e477c5e7770eca6440b63770fed590d97f1e0e0326b00f8715b9b6db4ab7771b35cfe00583b6ea0f8aa86b9d8bc5c
7
+ data.tar.gz: 8f36cbb7934ee98a1b2c78af71740ed2a502ccd08479b0a01f15d233224873d7ff15cdf7874282713e99a4aa2b02e02bf70b4a630f4d5a3e0518179dfdf2c54f
@@ -1,3 +1,16 @@
1
+ == 0.13.5 / 2015-01-09
2
+ * Minor Enhancements
3
+ * statsd notifier (#144)
4
+ * Slack notifier (#172)
5
+ * Bug fixes
6
+ * Add support for Ruby 2.2.0 (#206)
7
+ * Fix tests for Ruby 2.0 (#205)
8
+ * Make group assignment more like login (#159)
9
+ * Flush file descriptors to prevent too many processes from being started (#141)
10
+ * Allow SSL URLs (#168)
11
+ * Documentation fixes
12
+ * Clarify Hipchat privilege requirements (#176)
13
+
1
14
  == 0.13.4 / 2014-03-05
2
15
  * Minor Enhancements
3
16
  * Hipchat reporter (#162)
@@ -436,8 +436,8 @@ A `watch` represents a single process that has concrete start, stop, and/or
436
436
  restart operations. You can define as many watches as you like. In the example
437
437
  above, I've got some Rails instances running in Mongrels that I need to keep
438
438
  alive. Every watch must have a unique `name` so that it can be identified
439
- later on. The `start` and `stop` attributes specify the commands to start
440
- and stop the process. If no `restart` attribute is set, restart will be
439
+ later on. The `start` and `stop` attributes specify the commands to start
440
+ and stop the process. If no `restart` attribute is set, restart will be
441
441
  represented by a call to stop followed by a call to start. The
442
442
  optional `grace` attribute sets the amount of time following a
443
443
  start/stop/restart command to wait before resuming normal monitoring
@@ -575,7 +575,7 @@ Watching Non-Daemon Processes
575
575
  Need to watch a script that doesn't have built in daemonization? No problem!
576
576
  God will daemonize and keep track of your process for you. If you don't
577
577
  specify a `pid_file` attribute for a watch, it will be auto-daemonized and a
578
- PID file will be stored for it in `/var/run/god`.
578
+ PID file will be stored for it in `/var/run/god`.
579
579
 
580
580
 
581
581
  ```ruby
@@ -585,9 +585,9 @@ God.pid_file_directory = '/home/tom/pids'
585
585
  God.watch do |w|
586
586
  w.name = 'mongrel'
587
587
  w.pid_file = w.pid_file = File.join(RAILS_ROOT, "log/mongrel.pid")
588
-
588
+
589
589
  w.start = "mongrel_rails start -P #{RAILS_ROOT}/log/mongrel.pid -d"
590
-
590
+
591
591
  # ...
592
592
  end
593
593
 
@@ -595,15 +595,15 @@ end
595
595
  God.watch do |w|
596
596
  w.name = 'worker'
597
597
  # w.pid_file = is not set
598
-
598
+
599
599
  w.start = "rake resque:worker"
600
-
600
+
601
601
  # ...
602
602
  end
603
603
  ```
604
604
 
605
605
 
606
- If you'd rather have the PID file stored in a different location, you can
606
+ If you'd rather have the PID file stored in a different location, you can
607
607
  set it at the top of your config:
608
608
 
609
609
  ```ruby
@@ -1022,6 +1022,8 @@ ssl - A Boolean determining whether or not to use SSL
1022
1022
  from - The String representing who the message should be sent as.
1023
1023
  ```
1024
1024
 
1025
+ NOTE: in Hipchat you must have a token with 'admin' privileges. 'Notification' privileges will not be enough.
1026
+
1025
1027
  Email
1026
1028
  ~~~~~
1027
1029
 
@@ -1243,6 +1245,46 @@ end
1243
1245
  apikey - The String API key.
1244
1246
  ```
1245
1247
 
1248
+ Slack
1249
+ ~~~~~
1250
+
1251
+ Send a message to a channel in Slack (https://slack.com/).
1252
+
1253
+ First, set up an Incoming Webhook in your Slack account.
1254
+
1255
+ Then, in your God configuration, set the defaults:
1256
+
1257
+ ```ruby
1258
+ God::Contacts::Slack.defaults do |d|
1259
+ d.account = "foo"
1260
+ d.token = "abc123abc123abc123"
1261
+ c.notify_channel = true
1262
+ c.format = '%{host} alert: %{message}'
1263
+ end
1264
+ ```
1265
+
1266
+ `account` is the name of your Slack account; if you view slack at
1267
+ "foo.slack.com", then your account is "foo". `token` is from your
1268
+ newly-created webhook, and will be a string of unintelligible
1269
+ characters.
1270
+
1271
+ The `notify_channel` and `format` settings are optional. The first
1272
+ controls whether the message includes `@channel` (sending notifications
1273
+ to everyone in the channel); the second controls how the message is
1274
+ formatted. Acceptable values within the format are `priority`, `host`,
1275
+ `message`, `category`, and `time`.
1276
+
1277
+ Once you've set the defaults, create contacts for the channels that you
1278
+ want to notify. You can create as many as you like, and they'll look
1279
+ something like this:
1280
+
1281
+ ```ruby
1282
+ God.contact(:slack) do |c|
1283
+ c.name = '#ops'
1284
+ c.channel = '#ops'
1285
+ end
1286
+ ```
1287
+
1246
1288
  /////////////////////////////////////////////////////////////////////////////
1247
1289
  /////////////////////////////////////////////////////////////////////////////
1248
1290
 
@@ -9,6 +9,7 @@ def create_dummy_makefile
9
9
  end
10
10
  end
11
11
 
12
+ have_func('rb_wait_for_single_fd')
12
13
  case RUBY_PLATFORM
13
14
  when /bsd/i, /darwin/i
14
15
  unless have_header('sys/event.h')
@@ -5,6 +5,10 @@
5
5
  #include <sys/time.h>
6
6
  #include <errno.h>
7
7
 
8
+ #ifdef HAVE_RB_WAIT_FOR_SINGLE_FD
9
+ #include <ruby/io.h>
10
+ #endif
11
+
8
12
  static VALUE mGod;
9
13
  static VALUE cKQueueHandler;
10
14
  static VALUE cEventHandler;
@@ -63,6 +67,10 @@ kqh_handle_events()
63
67
  {
64
68
  int nevents, i, num_to_fetch;
65
69
  struct kevent *events;
70
+
71
+ #ifdef HAVE_RB_WAIT_FOR_SINGLE_FD
72
+ rb_wait_for_single_fd(kq, RB_WAITFD_IN, NULL);
73
+ #else
66
74
  fd_set read_set;
67
75
 
68
76
  FD_ZERO(&read_set);
@@ -70,7 +78,7 @@ kqh_handle_events()
70
78
 
71
79
  // Don't actually run this method until we've got an event
72
80
  rb_thread_select(kq + 1, &read_set, NULL, NULL, NULL);
73
-
81
+ #endif
74
82
  // Grabbing num_events once for thread safety
75
83
  num_to_fetch = num_events;
76
84
  events = (struct kevent*)malloc(num_to_fetch * sizeof(struct kevent));
@@ -1,6 +1,12 @@
1
1
  #ifdef __linux__ /* only build on linux */
2
2
 
3
3
  #include <ruby.h>
4
+
5
+ #ifdef HAVE_RB_WAIT_FOR_SINGLE_FD
6
+ #include <ruby/io.h>
7
+ #endif
8
+
9
+
4
10
  #include <sys/types.h>
5
11
  #include <unistd.h>
6
12
  #include <sys/socket.h>
@@ -31,6 +37,16 @@ nlh_handle_events()
31
37
 
32
38
  VALUE extra_data;
33
39
 
40
+ #ifdef HAVE_RB_WAIT_FOR_SINGLE_FD
41
+ int ret;
42
+ if((ret = rb_wait_for_single_fd(nl_sock, RB_WAITFD_IN, NULL)) < 0){
43
+ rb_raise(rb_eStandardError, "%s", strerror(errno));
44
+ }
45
+ /* If there were no events detected, return */
46
+ if(!(ret & RB_WAITFD_IN)){
47
+ return INT2FIX(0);
48
+ }
49
+ #else
34
50
  fd_set fds;
35
51
 
36
52
  FD_ZERO(&fds);
@@ -44,6 +60,7 @@ nlh_handle_events()
44
60
  if (! FD_ISSET(nl_sock, &fds)) {
45
61
  return INT2FIX(0);
46
62
  }
63
+ #endif
47
64
 
48
65
  /* if there are events, make calls */
49
66
  if (-1 == recv(nl_sock, buff, sizeof(buff), 0)) {
@@ -3,8 +3,8 @@ Gem::Specification.new do |s|
3
3
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
4
4
 
5
5
  s.name = 'god'
6
- s.version = '0.13.4'
7
- s.date = '2014-03-05'
6
+ s.version = '0.13.5'
7
+ s.date = '2015-01-09'
8
8
 
9
9
  s.summary = "Process monitoring framework."
10
10
  s.description = "An easy to configure, easy to extend monitoring framework written in Ruby."
@@ -25,18 +25,23 @@ Gem::Specification.new do |s|
25
25
 
26
26
  s.add_development_dependency('json', '~> 1.6')
27
27
  s.add_development_dependency('rake')
28
+ s.add_development_dependency('minitest')
28
29
  s.add_development_dependency('rdoc', '~> 3.10')
29
30
  s.add_development_dependency('twitter', '~> 4.0')
30
31
  s.add_development_dependency('prowly', '~> 0.3')
31
32
  s.add_development_dependency('xmpp4r', '~> 0.5')
32
33
  s.add_development_dependency('dike', '~> 0.0.3')
33
- s.add_development_dependency('rcov', '~> 0.9')
34
+ # s.add_development_dependency('rcov', '~> 0.9')
34
35
  s.add_development_dependency('daemons', '~> 1.1')
35
36
  s.add_development_dependency('mocha', '~> 0.10')
36
37
  s.add_development_dependency('gollum', '~> 1.3.1')
38
+ #the last version to support 1.8.7 is 0.99.6
39
+ s.add_development_dependency('mustache', ['~> 0.99.0', '< 0.99.7'])
37
40
  s.add_development_dependency('airbrake', '~> 3.1.7')
38
41
  s.add_development_dependency('nokogiri', '~> 1.5.0')
39
42
  s.add_development_dependency('activesupport', [ '>= 2.3.10', '< 4.0.0' ])
43
+ s.add_development_dependency('statsd-ruby')
44
+ s.add_development_dependency('i18n', '< 0.7.0')
40
45
  # = MANIFEST =
41
46
  s.files = %w[
42
47
  Announce.txt
@@ -87,6 +92,8 @@ Gem::Specification.new do |s|
87
92
  lib/god/contacts/jabber.rb
88
93
  lib/god/contacts/prowl.rb
89
94
  lib/god/contacts/scout.rb
95
+ lib/god/contacts/slack.rb
96
+ lib/god/contacts/statsd.rb
90
97
  lib/god/contacts/twitter.rb
91
98
  lib/god/contacts/webhook.rb
92
99
  lib/god/driver.rb
@@ -162,7 +169,9 @@ Gem::Specification.new do |s|
162
169
  test/test_process.rb
163
170
  test/test_prowl.rb
164
171
  test/test_registry.rb
172
+ test/test_slack.rb
165
173
  test/test_socket.rb
174
+ test/test_statsd.rb
166
175
  test/test_sugar.rb
167
176
  test/test_system_portable_poller.rb
168
177
  test/test_system_process.rb
data/lib/god.rb CHANGED
@@ -90,9 +90,11 @@ load_contact(:email)
90
90
  load_contact(:jabber)
91
91
  load_contact(:prowl)
92
92
  load_contact(:scout)
93
+ load_contact(:statsd)
93
94
  load_contact(:twitter)
94
95
  load_contact(:webhook)
95
96
  load_contact(:airbrake)
97
+ load_contact(:slack)
96
98
 
97
99
  $:.unshift File.join(File.dirname(__FILE__), *%w[.. ext god])
98
100
 
@@ -158,7 +160,7 @@ end
158
160
 
159
161
  module God
160
162
  # The String version number for this package.
161
- VERSION = '0.13.4'
163
+ VERSION = '0.13.5'
162
164
 
163
165
  # The Integer number of lines of backlog to keep for the logger.
164
166
  LOG_BUFFER_SIZE_DEFAULT = 100
@@ -0,0 +1,100 @@
1
+ # Send a message to a Slack channel
2
+ #
3
+ # account - The name of your Slack account (visible in URL, e.g. foo.slack.com)
4
+ # token - The token of the webhook created in Slack
5
+ # channel - The name of the channel to send the message to, prefixed with #
6
+ # notify_channel - Whether to send an "@channel" in the message, to alert everyone in the channel
7
+ # format - An optional format string to change how the alert is displayed
8
+
9
+ require 'net/http'
10
+ require 'uri'
11
+
12
+ CONTACT_DEPS[:slack] = ['json']
13
+ CONTACT_DEPS[:slack].each do |d|
14
+ require d
15
+ end
16
+
17
+ module God
18
+ module Contacts
19
+
20
+ class Slack < Contact
21
+ class << self
22
+ attr_accessor :account, :token, :channel, :notify_channel, :format, :username, :emoji
23
+ end
24
+
25
+ self.channel = "#general"
26
+ self.notify_channel = false
27
+ self.format = "%{priority} alert on %{host}: %{message} (%{category}, %{time})"
28
+
29
+ def valid?
30
+ valid = true
31
+ valid &= complain("Attribute 'account' must be specified", self) unless arg(:account)
32
+ valid &= complain("Attribute 'token' must be specified", self) unless arg(:token)
33
+ valid
34
+ end
35
+
36
+ attr_accessor :account, :token, :channel, :notify_channel, :format, :username, :emoji
37
+
38
+ def text(data)
39
+ text = ""
40
+ text << "<!channel> " if arg(:notify_channel)
41
+
42
+ if RUBY_VERSION =~ /^1\.8/
43
+ text << arg(:format).gsub(/%\{(\w+)\}/) do |match|
44
+ data[$1.to_sym]
45
+ end
46
+ else
47
+ text << arg(:format) % data
48
+ end
49
+
50
+ text
51
+ end
52
+
53
+ def notify(message, time, priority, category, host)
54
+ text = text({
55
+ :message => message,
56
+ :time => time,
57
+ :priority => priority,
58
+ :category => category,
59
+ :host => host
60
+ })
61
+
62
+ request(text)
63
+ end
64
+
65
+ def api_url
66
+ URI.parse("https://#{arg(:account)}.slack.com/services/hooks/incoming-webhook?token=#{arg(:token)}")
67
+ end
68
+
69
+ def request(text)
70
+ http = Net::HTTP.new(api_url.host, api_url.port)
71
+ http.use_ssl = true
72
+
73
+ req = Net::HTTP::Post.new(api_url.request_uri)
74
+ req.body = {
75
+ :link_names => 1,
76
+ :text => text,
77
+ :channel => arg(:channel)
78
+ }.tap { |payload|
79
+ payload[:username] = arg(:username) if arg(:username)
80
+ payload[:icon_emoji] = arg(:emoji) if arg(:emoji)
81
+ }.to_json
82
+
83
+ res = http.request(req)
84
+
85
+ case res
86
+ when Net::HTTPSuccess
87
+ self.info = "successfully notified slack on channel #{arg(:channel)}"
88
+ else
89
+ self.info = "failed to send webhook to #{arg(:url)}: #{res.error!}"
90
+ end
91
+ rescue Object => e
92
+ applog(nil, :info, "failed to send webhook to #{arg(:url)}: #{e.message}")
93
+ applog(nil, :debug, e.backtrace.join("\n"))
94
+ end
95
+
96
+ end
97
+
98
+ end
99
+ end
100
+
@@ -0,0 +1,46 @@
1
+ # Send a notice to statsd
2
+ #
3
+ # host - statsd host
4
+ # port - statsd port (optional)
5
+
6
+ require 'statsd-ruby'
7
+
8
+ module God
9
+ module Contacts
10
+
11
+ class Statsd < Contact
12
+ class << self
13
+ attr_accessor :host, :port
14
+ end
15
+
16
+ attr_accessor :host, :port
17
+
18
+ def valid?
19
+ valid = true
20
+ valid &= complain("Attribute 'statsd_host' must be specified", self) unless arg(:host)
21
+ valid
22
+ end
23
+
24
+ def notify(message, time, priority, category, hostname)
25
+ statsd = ::Statsd.new host, (port ? port.to_i : 8125) # 8125 is the default statsd port
26
+
27
+ hostname.gsub! /\./, '_'
28
+ app = message.gsub /([^\s]*).*/, '\1'
29
+
30
+ [
31
+ 'cpu out of bounds',
32
+ 'memory out of bounds',
33
+ 'process is flapping'
34
+ ].each do |event_type|
35
+ statsd.increment "god.#{event_type.gsub(/\s/, '_')}.#{hostname}.#{app}" if message.include? event_type
36
+ end
37
+
38
+ self.info = 'sent statsd alert'
39
+ rescue => e
40
+ applog(nil, :info, "failed to send statsd alert: #{e.message}")
41
+ applog(nil, :debug, e.backtrace.join("\n"))
42
+ end
43
+ end
44
+
45
+ end
46
+ end
@@ -41,6 +41,7 @@ module God
41
41
 
42
42
  uri = URI.parse(arg(:url))
43
43
  http = Net::HTTP.new(uri.host, uri.port)
44
+ http.use_ssl = true if uri.scheme == "https"
44
45
 
45
46
  req = nil
46
47
  res = nil
@@ -32,16 +32,18 @@ module God
32
32
  begin
33
33
  uid_num = Etc.getpwnam(self.uid).uid if self.uid
34
34
  gid_num = Etc.getgrnam(self.gid).gid if self.gid
35
+ gid_num = Etc.getpwnam(self.uid).gid if self.gid.nil? && self.uid
35
36
 
36
37
  ::Dir.chroot(self.chroot) if self.chroot
37
- ::Process.groups = [gid_num] if self.gid
38
- ::Process::Sys.setgid(gid_num) if self.gid
38
+ ::Process.groups = [gid_num] if gid_num
39
+ ::Process.initgroups(self.uid, gid_num) if self.uid && gid_num
40
+ ::Process::Sys.setgid(gid_num) if gid_num
39
41
  ::Process::Sys.setuid(uid_num) if self.uid
40
42
  rescue ArgumentError, Errno::EPERM, Errno::ENOENT
41
43
  exit(1)
42
44
  end
43
45
 
44
- File.writable?(file_in_chroot(file)) ? exit(0) : exit(1)
46
+ File.writable?(file_in_chroot(file)) ? exit!(0) : exit!(1)
45
47
  end
46
48
 
47
49
  wpid, status = ::Process.waitpid2(pid)
@@ -246,6 +248,7 @@ module God
246
248
  r.close
247
249
  pid = self.spawn(command)
248
250
  puts pid.to_s # send pid back to forker
251
+ exit!(0)
249
252
  end
250
253
 
251
254
  ::Process.waitpid(opid, 0)
@@ -295,11 +298,13 @@ module God
295
298
  File.umask self.umask if self.umask
296
299
  uid_num = Etc.getpwnam(self.uid).uid if self.uid
297
300
  gid_num = Etc.getgrnam(self.gid).gid if self.gid
301
+ gid_num = Etc.getpwnam(self.uid).gid if self.gid.nil? && self.uid
298
302
 
299
303
  ::Dir.chroot(self.chroot) if self.chroot
300
304
  ::Process.setsid
301
- ::Process.groups = [gid_num] if self.gid
302
- ::Process::Sys.setgid(gid_num) if self.gid
305
+ ::Process.groups = [gid_num] if gid_num
306
+ ::Process.initgroups(self.uid, gid_num) if self.uid && gid_num
307
+ ::Process::Sys.setgid(gid_num) if gid_num
303
308
  ::Process::Sys.setuid(uid_num) if self.uid
304
309
  self.dir ||= '/'
305
310
  Dir.chdir self.dir