systemd_mon 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 71beedd875598eeb882df66a2a2982aad63fffe3
4
+ data.tar.gz: e08a8386c4489f8f42461ed8aa5bf2a65eb78bf6
5
+ SHA512:
6
+ metadata.gz: 016247aaa8c34f2cb6eaddd14459bc58d52ccd12e32557ca3a5d4e5a30c06e5222db72393f5d630d293ec69533897d7a5d6fdd7d1a161bec2a02ae5acaf43ed2
7
+ data.tar.gz: ca44541313c92f37e790f15ba07862885288d1d4e026bdb1560f681f1bc3b3cda721ec6b608b18c6507a0b65cf763bc75bd039f010e6818243e020b71f28c29b
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in systemd_alert.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Jon Cairns
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # SystemdMon
2
+
3
+ Monitor systemd units and trigger alerts for failed states. The command line tool runs as a daemon, using dbus to get notifications of changes to systemd services. If a service enters a failed state, or returns from a failed state to an active state, notifications will be triggered.
4
+
5
+ Built-in notifications include email and slack, but more can be added via the ruby API.
6
+
7
+ ## Installation
8
+
9
+ Install the gem using:
10
+
11
+ gem install systemd_mon
12
+
13
+ ## Usage
14
+
15
+ To run the command line tool, you will first need to create a YAML configuration file to specify which systemd units you want to monitor, and which notifications you want to trigger. A full example looks like this:
16
+
17
+ ```yaml
18
+ ---
19
+ notifiers:
20
+ # These are options passed to the 'mail' gem
21
+ email:
22
+ address: smtp.gmail.com
23
+ port: 587
24
+ domain: mydomain.com
25
+ user_name: "user@mydomain.com"
26
+ password: "supersecr3t"
27
+ authentication: "plain"
28
+ enable_starttls_auto: true
29
+ slack:
30
+ team: myteam
31
+ token: supersecr3ttoken
32
+ channel: mychannel
33
+ username: doge
34
+ units:
35
+ - unicorn.service
36
+ - nginx.service
37
+ - sidekiq.service
38
+ ```
39
+
40
+ Then start the command line tool with:
41
+
42
+ $ systemd_mon path/to/systemd_mon.yml
43
+
44
+ ## Contributing
45
+
46
+ 1. Fork it ( https://github.com/joonty/systemd_mon/fork )
47
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
48
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
49
+ 4. Push to the branch (`git push origin my-new-feature`)
50
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/bin/systemd_mon ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'systemd_mon/cli'
3
+
4
+ SystemdMon::CLI.new.start
@@ -0,0 +1,5 @@
1
+ require "systemd_mon/version"
2
+ require "systemd_mon/logger"
3
+ require "systemd_mon/notifier_loader"
4
+
5
+ module SystemdMon; end
@@ -0,0 +1,39 @@
1
+ require 'systemd_mon/unit_with_state'
2
+
3
+ module SystemdMon
4
+ class CallbackManager
5
+ def initialize(queue)
6
+ self.queue = queue
7
+ self.states = Hash.new { |h, u| h[u] = UnitWithState.new(u) }
8
+ end
9
+
10
+ def start(change_callback, each_state_change_callback)
11
+ loop do
12
+ unit, state = queue.deq
13
+ Logger.debug { state }
14
+ unit_state = states[unit]
15
+ unit_state << state
16
+
17
+ if each_state_change_callback
18
+ with_error_handling { each_state_change_callback.call(unit_state) }
19
+ end
20
+
21
+ if change_callback && unit_state.state_change.important?
22
+ with_error_handling { change_callback.call(unit_state) }
23
+ end
24
+
25
+ unit_state.reset! if unit_state.state_change.important?
26
+ end
27
+ end
28
+
29
+ def with_error_handling
30
+ yield
31
+ rescue => e
32
+ Logger.error "Uncaught exception (#{e.class}) in callback: #{e.message}"
33
+ Logger.debug_error { "\n\t#{e.backtrace.join("\n\t")}\n" }
34
+ end
35
+
36
+ protected
37
+ attr_accessor :queue, :states
38
+ end
39
+ end
@@ -0,0 +1,81 @@
1
+ require 'yaml'
2
+ require 'systemd_mon'
3
+ require 'systemd_mon/monitor'
4
+ require 'systemd_mon/error'
5
+ require 'systemd_mon/dbus_manager'
6
+
7
+ module SystemdMon
8
+ class CLI
9
+ def initialize
10
+ self.me = "systemd_mon"
11
+ self.verbose = true
12
+ end
13
+
14
+ def start
15
+ yaml_config_file = ARGV.first
16
+ self.options = load_and_validate_options(yaml_config_file)
17
+ self.verbose = options['verbose'] || false
18
+ Logger.verbose = verbose
19
+
20
+ start_monitor
21
+
22
+ rescue SystemdMon::Error => e
23
+ err_string = e.message
24
+ if verbose
25
+ err_string << " - #{e.original.message} (#{e.original.class})"
26
+ err_string << "\n\t#{e.original.backtrace.join("\n\t")}"
27
+ end
28
+ fatal_error(err_string)
29
+ rescue => e
30
+ err_string = e.message
31
+ if verbose
32
+ err_string << " (#{e.class})"
33
+ err_string << "\n\t#{e.backtrace.join("\n\t")}"
34
+ end
35
+ fatal_error(err_string)
36
+ end
37
+
38
+ protected
39
+ def start_monitor
40
+ monitor = Monitor.new(DBusManager.new)
41
+
42
+ # Load units to monitor
43
+ monitor.register_units options['units']
44
+
45
+ options['notifiers'].each do |name, notifier_options|
46
+ klass = NotifierLoader.new.get_class(name)
47
+ monitor.add_notifier klass.new(notifier_options)
48
+ end
49
+
50
+ monitor.start
51
+ end
52
+
53
+ def load_and_validate_options(yaml_config_file)
54
+ options = load_options(yaml_config_file)
55
+
56
+ unless options.has_key?('notifiers') && options['notifiers'].any?
57
+ fatal_error("no notifiers have been defined, there is no reason to continue")
58
+ end
59
+ unless options.has_key?('units') && options['units'].any?
60
+ fatal_error("no units have been added for watching, there is no reason to continue")
61
+ end
62
+ options
63
+ end
64
+
65
+ def load_options(yaml_config_file)
66
+ unless yaml_config_file && File.exists?(yaml_config_file)
67
+ fatal_error "First argument must be a path to a YAML configuration file"
68
+ end
69
+
70
+ YAML.load_file(yaml_config_file)
71
+ end
72
+
73
+ def fatal_error(message, code = 255)
74
+ $stderr.puts " #{me} error: #{message}"
75
+ exit code
76
+ end
77
+
78
+ protected
79
+ attr_accessor :verbose, :options, :me
80
+ end
81
+ end
@@ -0,0 +1,31 @@
1
+ require 'dbus'
2
+ require 'systemd_mon/error'
3
+ require 'systemd_mon/dbus_unit'
4
+
5
+ module SystemdMon
6
+ class DBusManager
7
+ def initialize
8
+ self.dbus = DBus::SystemBus.instance
9
+ self.systemd_service = dbus.service("org.freedesktop.systemd1")
10
+ self.systemd_object = systemd_service.object("/org/freedesktop/systemd1")
11
+ systemd_object.introspect
12
+ systemd_object.Subscribe
13
+ end
14
+
15
+ def fetch_unit(unit_name)
16
+ path = systemd_object.GetUnit(unit_name).first
17
+ DBusUnit.new(unit_name, path, systemd_service.object(path))
18
+ rescue DBus::Error
19
+ raise SystemdMon::UnknownUnitError, "Unknown or unloaded systemd unit '#{unit_name}'"
20
+ end
21
+
22
+ def runner
23
+ main = DBus::Main.new
24
+ main << dbus
25
+ main
26
+ end
27
+
28
+ protected
29
+ attr_accessor :systemd_service, :systemd_object, :dbus
30
+ end
31
+ end
@@ -0,0 +1,57 @@
1
+ require 'systemd_mon/state'
2
+
3
+ module SystemdMon
4
+ class DBusUnit
5
+ attr_reader :name
6
+
7
+ IFACE_UNIT = "org.freedesktop.systemd1.Unit"
8
+ IFACE_PROPS = "org.freedesktop.DBus.Properties"
9
+
10
+ def initialize(name, path, dbus_object)
11
+ self.name = name
12
+ self.path = path
13
+ self.dbus_object = dbus_object
14
+ prepare_dbus_objects!
15
+ end
16
+
17
+ def register_listener!(queue)
18
+ queue.enq [self, build_state] # initial state
19
+ dbus_object.on_signal("PropertiesChanged") do |iface|
20
+ if iface == IFACE_UNIT
21
+ queue.enq [self, build_state]
22
+ end
23
+ end
24
+ end
25
+
26
+ def on_change(&callback)
27
+ self.change_callback = callback
28
+ end
29
+
30
+ def on_each_state_change(&callback)
31
+ self.each_state_change_callback = callback
32
+ end
33
+
34
+ def property(name)
35
+ dbus_object.Get(IFACE_UNIT, name).first
36
+ end
37
+
38
+ protected
39
+ attr_accessor :path, :dbus_object, :change_callback, :each_state_change_callback
40
+ attr_writer :name
41
+
42
+ def build_state
43
+ State.new(
44
+ property("ActiveState"),
45
+ property("SubState"),
46
+ property("LoadState"),
47
+ property("UnitFileState")
48
+ )
49
+ end
50
+
51
+ def prepare_dbus_objects!
52
+ dbus_object.introspect
53
+ self.dbus_object.default_iface = IFACE_PROPS
54
+ self
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,17 @@
1
+ module SystemdMon
2
+
3
+ # Save original exception for use in verbose mode
4
+ class Error < StandardError
5
+ attr_reader :original
6
+
7
+ def initialize(msg, original=$!)
8
+ super(msg)
9
+ @original = original
10
+ end
11
+ end
12
+
13
+ class MonitorError < Error; end
14
+ class UnknownUnitError < Error; end
15
+ class NotificationError < Error; end
16
+ class NotifierError < Error; end
17
+ end
@@ -0,0 +1,18 @@
1
+ module SystemdMon::Formatters
2
+ class Base
3
+ def initialize(unit)
4
+ self.unit = unit
5
+ end
6
+
7
+ def as_html
8
+ raise "The formatter #{self.class} does not provide an html formatted string"
9
+ end
10
+
11
+ def as_text
12
+ raise "The formatter #{self.class} does not provide a plain text string"
13
+ end
14
+
15
+ protected
16
+ attr_accessor :unit
17
+ end
18
+ end
@@ -0,0 +1,32 @@
1
+ require 'systemd_mon/formatters/base'
2
+ module SystemdMon::Formatters
3
+ class StateTableFormatter < Base
4
+ def as_text
5
+ table = render_table
6
+ lengths = table.transpose.map { |v| v.map(&:length).max }
7
+
8
+ full_width = lengths.inject(&:+) + (lengths.length * 3) + 1
9
+ div = " " + ("-" * full_width) + "\n"
10
+ s = div.dup
11
+ table.each do |row|
12
+ s << " | "
13
+ row.each_with_index { |col, i|
14
+ s << col.ljust(lengths[i]) + " | "
15
+ }
16
+ s << "\n" + div.dup
17
+ end
18
+ s
19
+ end
20
+
21
+ protected
22
+ def render_table
23
+ changed = unit.state_change.diff
24
+ table = []
25
+ table << ["Time"].concat(changed.map{|v| v.first.display_name})
26
+ changed.transpose.each do |vals|
27
+ table << [vals.first.timestamp.strftime("%H:%M:%S.%3N %z")].concat(vals.map{|v| v.value})
28
+ end
29
+ table
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ module SystemdMon
2
+ class Logger
3
+ def self.verbose=(flag)
4
+ @verbose = flag
5
+ end
6
+
7
+ def self.verbose
8
+ @verbose
9
+ end
10
+
11
+ def self.debug(message = nil, stream = $stdout)
12
+ if verbose
13
+ if block_given?
14
+ $stdout.puts yield
15
+ else
16
+ $stdout.puts message
17
+ end
18
+ end
19
+ end
20
+
21
+ def self.error(message = nil)
22
+ $stderr.puts message
23
+ end
24
+
25
+ def self.debug_error(message = nil)
26
+ debug message, $stderr
27
+ end
28
+
29
+ def self.puts(message = nil)
30
+ $stdout.puts message
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,98 @@
1
+ require 'thread'
2
+ require 'systemd_mon/logger'
3
+ require 'systemd_mon/callback_manager'
4
+ require 'systemd_mon/notification_centre'
5
+ require 'systemd_mon/notification'
6
+ require 'systemd_mon/error'
7
+
8
+ module SystemdMon
9
+ class Monitor
10
+ def initialize(dbus_manager)
11
+ self.hostname = `hostname`.strip
12
+ self.dbus_manager = dbus_manager
13
+ self.units = []
14
+ self.change_callback = lambda(&method(:unit_change_callback))
15
+ self.notification_centre = NotificationCentre.new
16
+ Thread.abort_on_exception = true
17
+ end
18
+
19
+ def add_notifier(notifier)
20
+ notification_centre << notifier
21
+ self
22
+ end
23
+
24
+ def register_unit(unit_name)
25
+ self.units << dbus_manager.fetch_unit(unit_name)
26
+ self
27
+ end
28
+
29
+ def register_units(*unit_names)
30
+ self.units.concat unit_names.flatten.map { |unit_name|
31
+ dbus_manager.fetch_unit(unit_name)
32
+ }
33
+ self
34
+ end
35
+
36
+ def on_change(&callback)
37
+ self.change_callback = callback
38
+ self
39
+ end
40
+
41
+ def on_each_state_change(&callback)
42
+ self.each_state_change_callback = callback
43
+ self
44
+ end
45
+
46
+ def start
47
+ startup_check!
48
+ at_exit { notification_centre.notify_stop! hostname }
49
+ notification_centre.notify_start! hostname
50
+
51
+ Logger.puts "Monitoring changes to #{units.count} units"
52
+ Logger.debug { " - " + units.map(&:name).join("\n - ") + "\n\n" }
53
+ Logger.debug { "Using notifiers: #{notification_centre.classes.join(", ")}"}
54
+
55
+ state_q = Queue.new
56
+
57
+ units.each do |unit|
58
+ unit.register_listener! state_q
59
+ end
60
+
61
+ [start_callback_thread(state_q),
62
+ start_dbus_thread].each(&:join)
63
+ end
64
+
65
+ protected
66
+ attr_accessor :units, :dbus_manager, :change_callback, :each_state_change_callback, :hostname, :notification_centre
67
+
68
+ def startup_check!
69
+ unless units.any?
70
+ raise MonitorError, "At least one systemd unit should be registered before monitoring can start"
71
+ end
72
+ unless notification_centre.any?
73
+ raise MonitorError, "At least one notifier should be registered before monitoring can start"
74
+ end
75
+ self
76
+ end
77
+
78
+ def start_dbus_thread
79
+ Thread.new do
80
+ dbus_manager.runner.run
81
+ end
82
+ end
83
+
84
+ def start_callback_thread(state_q)
85
+ Thread.new do
86
+ manager = CallbackManager.new(state_q)
87
+ manager.start change_callback, each_state_change_callback
88
+ end
89
+ end
90
+
91
+ def unit_change_callback(unit)
92
+ Logger.puts "#{unit.name} #{unit.state_change.status_text}: #{unit.state.active} (#{unit.state.sub})"
93
+ Logger.debug unit.state_change.to_s
94
+ Logger.puts
95
+ notification_centre.notify! Notification.new(hostname, unit)
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,30 @@
1
+ module SystemdMon
2
+ class Notification
3
+ attr_reader :unit, :type, :hostname
4
+
5
+ def initialize(hostname, unit)
6
+ self.hostname = hostname
7
+ self.unit = unit
8
+ self.type = determine_type
9
+ end
10
+
11
+ def self.types
12
+ [:alert, :info, :ok]
13
+ end
14
+
15
+ protected
16
+ attr_writer :unit, :type, :hostname
17
+
18
+ def determine_type
19
+ if unit.state_change.ok?
20
+ if unit.state_change.first.fail?
21
+ :ok
22
+ else
23
+ :info
24
+ end
25
+ else
26
+ :alert
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,77 @@
1
+ require 'systemd_mon/error'
2
+ require 'systemd_mon/logger'
3
+
4
+ module SystemdMon
5
+ class NotificationCentre
6
+ include Enumerable
7
+
8
+ def initialize
9
+ self.notifiers = []
10
+ end
11
+
12
+ def classes
13
+ notifiers.map(&:class)
14
+ end
15
+
16
+ def each
17
+ notifiers.each do |notifier|
18
+ yield notifier
19
+ end
20
+ end
21
+
22
+ def add_notifier(notifier)
23
+ unless notifier.respond_to?(:notify!)
24
+ raise NotifierError, "Notifier #{notifier.class} must respond to 'notify!'"
25
+ end
26
+ self.notifiers << notifier
27
+ end
28
+
29
+ def notify_start!(hostname)
30
+ each_notifier do |notifier|
31
+ if notifier.respond_to?(:notify_start!)
32
+ Logger.puts "Notifying SystemdMon start via #{notifier.class}"
33
+ notifier.notify_start! hostname
34
+ else
35
+ Logger.debug { "#{notifier.class} doesn't respond to 'notify_start!', not sending notification" }
36
+ end
37
+ end
38
+ end
39
+
40
+ def notify_stop!(hostname)
41
+ each_notifier do |notifier|
42
+ if notifier.respond_to?(:notify_stop!)
43
+ Logger.puts "Notifying SystemdMon stop via #{notifier.class}"
44
+ notifier.notify_stop! hostname
45
+ else
46
+ Logger.debug { "#{notifier.class} doesn't respond to 'notify_start!', not sending notification" }
47
+ end
48
+ end
49
+ end
50
+
51
+ def notify!(notification)
52
+ each_notifier do |notifier|
53
+ Logger.puts "Notifying state change of #{notification.unit.name} via #{notifier.class}"
54
+ notifier.notify! notification
55
+ end
56
+ end
57
+
58
+ alias :<< :add_notifier
59
+
60
+ protected
61
+ attr_accessor :notifiers
62
+
63
+ def each_notifier
64
+ notifiers.map { |notifier|
65
+ Thread.new do
66
+ begin
67
+ yield notifier
68
+ rescue => e
69
+ Logger.error "Failed to send notification via #{notifier.class}:\n"
70
+ Logger.error " #{e.class}: #{e.message}\n"
71
+ Logger.debug_error { "\n\t#{e.backtrace.join('\n\t')}\n" }
72
+ end
73
+ end
74
+ }.each(&:join)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,21 @@
1
+ module SystemdMon
2
+ class NotifierLoader
3
+ def get_class(name)
4
+ class_name = camel_case(name)
5
+ get_class_const(class_name)
6
+ rescue NameError
7
+ require "systemd_mon/notifiers/#{name}"
8
+ get_class_const(class_name)
9
+ end
10
+
11
+ protected
12
+ def camel_case(name)
13
+ return name if name !~ /_/ && name =~ /[A-Z]+.*/
14
+ name.split('_').map { |e| e.capitalize }.join
15
+ end
16
+
17
+ def get_class_const(name)
18
+ Notifiers.const_get(name)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,39 @@
1
+ require 'systemd_mon/logger'
2
+
3
+ module SystemdMon::Notifiers
4
+ class Base
5
+ def initialize(options)
6
+ self.options = options
7
+ self.me = self.class.name
8
+ end
9
+
10
+ # Subclasses must respond to a unit change
11
+ def notify!(notification)
12
+ raise "Notifier #{self.class} does not respond to notify!"
13
+ end
14
+
15
+ # Subclasses can choose to do something when SystemdMon starts
16
+ # E.g. with
17
+ #
18
+ # def notify_start!(hostname)
19
+ # end
20
+
21
+ # Subclasses can choose to do something when SystemdMon stops
22
+ # E.g. with
23
+ #
24
+ # def notify_stop!(hostname)
25
+ # end
26
+
27
+ def log(message)
28
+ SystemdMon::Logger.puts "#{me}: #{message}"
29
+ end
30
+
31
+ def debug(message = nil, &blk)
32
+ message = "#{me}: #{message}" if message
33
+ SystemdMon::Logger.debug message, &blk
34
+ end
35
+
36
+ protected
37
+ attr_accessor :options, :me
38
+ end
39
+ end
@@ -0,0 +1,60 @@
1
+ require 'mail'
2
+ require 'systemd_mon/notifiers/base'
3
+ require 'systemd_mon/logger'
4
+ require 'systemd_mon/formatters/state_table_formatter'
5
+
6
+ module SystemdMon::Notifiers
7
+ class Email < Base
8
+ def initialize(*)
9
+ super
10
+ if options['smtp']
11
+ opts = options
12
+ Mail.defaults do
13
+ delivery_method :smtp, Hash[opts['smtp'].map { |h, k| [h.to_sym, k] }]
14
+ end
15
+ end
16
+
17
+ validate_options!
18
+ end
19
+
20
+ def notify!(notification)
21
+ unit = notification.unit
22
+ subject = "#{unit.name} on #{notification.hostname}: #{unit.state_change.status_text}"
23
+ message = "Systemd unit #{unit.name} on #{notification.hostname} #{unit.state_change.status_text}: #{unit.state.active} (#{unit.state.sub})\n\n"
24
+ if unit.state_change.length > 1
25
+ message << SystemdMon::Formatters::StateTableFormatter.new(unit).as_text
26
+ end
27
+ message << "\nRegards, SystemdMon"
28
+
29
+ send_mail subject, message
30
+
31
+ log "sent email notification"
32
+ end
33
+
34
+ protected
35
+ attr_accessor :options
36
+
37
+ def validate_options!
38
+ unless options.has_key?("to")
39
+ raise NotifierError, "The 'to' address must be set to use the email notifier"
40
+ end
41
+ true
42
+ end
43
+
44
+ def send_mail(subject, message)
45
+ debug("Sending email to #{options['to']}:")
46
+ debug(%Q{ -> Subject: "#{subject}"})
47
+ debug(%Q{ -> Message: "#{message}"})
48
+
49
+ mail = Mail.new do
50
+ subject subject
51
+ body message
52
+ end
53
+ mail.to = options['to']
54
+ if options['from']
55
+ mail.from options['from']
56
+ end
57
+ mail.deliver!
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,92 @@
1
+ require 'slack-notifier'
2
+ require 'systemd_mon/notifiers/base'
3
+
4
+ module SystemdMon::Notifiers
5
+ class Slack < Base
6
+ def initialize(*)
7
+ super
8
+ self.notifier = ::Slack::Notifier.new(
9
+ options.fetch('team'),
10
+ options.fetch('token'),
11
+ channel: options['channel'],
12
+ username: options['username'],
13
+ icon_emoji: options['icon_emoji'],
14
+ icon_url: options['icon_url'])
15
+ end
16
+
17
+ def notify_start!(hostname)
18
+ message = "Startup notification for SystemdMon"
19
+
20
+ attach = {
21
+ fallback: message,
22
+ text: "SystemdMon is starting on #{hostname}",
23
+ color: "good"
24
+ }
25
+
26
+ notifier.ping message, attachments: [attach]
27
+ end
28
+
29
+ def notify_stop!(hostname)
30
+ message = "Shutdown alert for SystemdMon"
31
+
32
+ attach = {
33
+ fallback: message,
34
+ text: "SystemdMon is stopping on #{hostname}",
35
+ color: "danger"
36
+ }
37
+
38
+ notifier.ping message, attachments: [attach]
39
+ end
40
+
41
+ def notify!(notification)
42
+ unit = notification.unit
43
+ message = "Systemd unit #{unit.name} on #{notification.hostname} #{unit.state_change.status_text}"
44
+
45
+ attach = {
46
+ fallback: "#{message}: #{unit.state.active} (#{unit.state.sub})",
47
+ color: color(notification.type),
48
+ fields: fields(notification)
49
+ }
50
+
51
+ debug("sending slack message with attachment: ")
52
+ debug(attach.inspect)
53
+
54
+ notifier.ping message, attachments: [attach]
55
+ log "sent slack notification"
56
+ end
57
+
58
+ protected
59
+ attr_accessor :notifier
60
+
61
+ def fields(notification)
62
+ f = [
63
+ {
64
+ title: "Hostname",
65
+ value: notification.hostname,
66
+ short: true
67
+ },
68
+ {
69
+ title: "Unit",
70
+ value: notification.unit.name,
71
+ short: true
72
+ }
73
+ ]
74
+
75
+ changes = notification.unit.state_change.diff.map(&:last)
76
+ f.concat(changes.map { |v|
77
+ { title: v.display_name, value: v.value, short: true }
78
+ })
79
+ end
80
+
81
+ def color(type)
82
+ case type
83
+ when :alert
84
+ 'danger'
85
+ when :info
86
+ '#0099CC'
87
+ else
88
+ 'good'
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,33 @@
1
+ require 'systemd_mon/state_value'
2
+
3
+ module SystemdMon
4
+ class State
5
+ include Enumerable
6
+
7
+ attr_reader :active, :sub, :loaded, :unit_file, :all_states
8
+
9
+ def initialize(active, sub, loaded, unit_file)
10
+ timestamp = Time.now
11
+ @active = StateValue.new("active", active, timestamp, %w(active), %w(inactive))
12
+ @sub = StateValue.new("status", sub, timestamp)
13
+ @loaded = StateValue.new("loaded", loaded, timestamp, %w(loaded))
14
+ @unit_file = StateValue.new("file", unit_file, timestamp, %w(enabled), %w(disabled))
15
+ @all_states = [@active, @sub, @loaded, @unit_file]
16
+ end
17
+
18
+ def each
19
+ @all_states.each do |state|
20
+ yield state
21
+ end
22
+ end
23
+
24
+ def ok?
25
+ all?(&:ok?)
26
+ end
27
+
28
+ def fail?
29
+ any?(&:fail?)
30
+ end
31
+ end
32
+
33
+ end
@@ -0,0 +1,113 @@
1
+ module SystemdMon
2
+ class StateChange
3
+ include Enumerable
4
+
5
+ attr_reader :states
6
+
7
+ def initialize(original_state = nil)
8
+ self.states = []
9
+ states << original_state if original_state
10
+ end
11
+
12
+ def last
13
+ states.last
14
+ end
15
+
16
+ def length
17
+ states.length
18
+ end
19
+
20
+ def <<(state)
21
+ self.states << state
22
+ @diff = nil
23
+ end
24
+
25
+ def each
26
+ states.each do |state|
27
+ yield state
28
+ end
29
+ end
30
+
31
+ def changes
32
+ states[1..-1]
33
+ end
34
+
35
+ def recovery?
36
+ first.fail? && last.ok?
37
+ end
38
+
39
+ def ok?
40
+ last.ok?
41
+ end
42
+
43
+ def fail?
44
+ last.fail?
45
+ end
46
+
47
+ def restart?
48
+ first.ok? && last.ok? && changes.any? { |s| s.active == "deactivating" }
49
+ end
50
+
51
+ def reload?
52
+ first.ok? && last.ok? && changes.any? { |s| s.active == "reloading" }
53
+ end
54
+
55
+ def still_fail?
56
+ length > 1 && first.fail? && last.fail?
57
+ end
58
+
59
+ def status_text
60
+ if recovery?
61
+ "recovered"
62
+ elsif restart?
63
+ "restarted"
64
+ elsif reload?
65
+ "reloaded"
66
+ elsif still_fail?
67
+ "still failed"
68
+ elsif fail?
69
+ "failed"
70
+ else
71
+ "started"
72
+ end
73
+ end
74
+
75
+ def important?
76
+ if length == 1
77
+ first.fail?
78
+ else
79
+ diff.map(&:last).any?(&:important?)
80
+ end
81
+ end
82
+
83
+ def diff
84
+ @diff ||= zipped.reject { |states|
85
+ match = states.first.value
86
+ states.all? { |s| s.value == match }
87
+ }
88
+ end
89
+
90
+ def zipped
91
+ if length == 1
92
+ first.all_states
93
+ else
94
+ first.all_states.zip(*changes.map(&:all_states))
95
+ end
96
+ end
97
+
98
+ def to_s
99
+ diff.inject("") { |s, (*states)|
100
+ first = states.shift
101
+ s << "#{first.name} state changed from #{first.value} to "
102
+ s << states.map(&:value).join(" then ")
103
+ s << "\n"
104
+ s
105
+ }
106
+ end
107
+
108
+ protected
109
+ attr_accessor :original, :changed
110
+ attr_writer :states
111
+ end
112
+
113
+ end
@@ -0,0 +1,48 @@
1
+ module SystemdMon
2
+ class StateValue
3
+ attr_reader :name, :value, :ok_states, :failure_states, :timestamp
4
+
5
+ def initialize(name, value, timestamp, ok_states = [], failure_states = [])
6
+ self.name = name
7
+ self.value = value
8
+ self.ok_states = ok_states
9
+ self.failure_states = failure_states
10
+ self.timestamp = timestamp
11
+ end
12
+
13
+ def display_name
14
+ name.capitalize
15
+ end
16
+
17
+ def important?
18
+ ok_states.include?(value) || failure_states.include?(value)
19
+ end
20
+
21
+ def ok?
22
+ if ok_states.any?
23
+ ok_states.include?(value)
24
+ else
25
+ true
26
+ end
27
+ end
28
+
29
+ def fail?
30
+ if failure_states.any?
31
+ failure_states.include?(value)
32
+ else
33
+ false
34
+ end
35
+ end
36
+
37
+ def to_s
38
+ value
39
+ end
40
+
41
+ def ==(other)
42
+ value == other
43
+ end
44
+
45
+ protected
46
+ attr_writer :name, :value, :ok_states, :failure_states, :timestamp
47
+ end
48
+ end
@@ -0,0 +1,33 @@
1
+ require 'systemd_mon/state_change'
2
+
3
+ module SystemdMon
4
+ class UnitWithState
5
+ attr_reader :unit, :state_change
6
+
7
+ def initialize(unit)
8
+ self.unit = unit
9
+ self.state_change = StateChange.new
10
+ end
11
+
12
+ def name
13
+ unit.name
14
+ end
15
+
16
+ def <<(state)
17
+ self.state_change << state
18
+ end
19
+
20
+ def current_state
21
+ state_change.last
22
+ end
23
+
24
+ def reset!
25
+ self.state_change = StateChange.new(current_state)
26
+ end
27
+
28
+ alias :state :current_state
29
+
30
+ protected
31
+ attr_writer :state_change, :unit
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ module SystemdMon
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'systemd_mon/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "systemd_mon"
8
+ spec.version = SystemdMon::VERSION
9
+ spec.authors = ["Jon Cairns"]
10
+ spec.email = ["jon@joncairns.com"]
11
+ spec.summary = %q{Monitor systemd units and trigger alerts for failed states}
12
+ spec.description = %q{Monitor systemd units and trigger alerts for failed states}
13
+ spec.homepage = "https://github.com/joonty/systemd_mon"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "ruby-dbus"
22
+ spec.add_development_dependency "bundler", "~> 1.6"
23
+ spec.add_development_dependency "rake"
24
+ end
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: systemd_mon
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Jon Cairns
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-08-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ruby-dbus
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.6'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Monitor systemd units and trigger alerts for failed states
56
+ email:
57
+ - jon@joncairns.com
58
+ executables:
59
+ - systemd_mon
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".gitignore"
64
+ - Gemfile
65
+ - LICENSE.txt
66
+ - README.md
67
+ - Rakefile
68
+ - bin/systemd_mon
69
+ - lib/systemd_mon.rb
70
+ - lib/systemd_mon/callback_manager.rb
71
+ - lib/systemd_mon/cli.rb
72
+ - lib/systemd_mon/dbus_manager.rb
73
+ - lib/systemd_mon/dbus_unit.rb
74
+ - lib/systemd_mon/error.rb
75
+ - lib/systemd_mon/formatters/base.rb
76
+ - lib/systemd_mon/formatters/state_table_formatter.rb
77
+ - lib/systemd_mon/logger.rb
78
+ - lib/systemd_mon/monitor.rb
79
+ - lib/systemd_mon/notification.rb
80
+ - lib/systemd_mon/notification_centre.rb
81
+ - lib/systemd_mon/notifier_loader.rb
82
+ - lib/systemd_mon/notifiers/base.rb
83
+ - lib/systemd_mon/notifiers/email.rb
84
+ - lib/systemd_mon/notifiers/slack.rb
85
+ - lib/systemd_mon/state.rb
86
+ - lib/systemd_mon/state_change.rb
87
+ - lib/systemd_mon/state_value.rb
88
+ - lib/systemd_mon/unit_with_state.rb
89
+ - lib/systemd_mon/version.rb
90
+ - systemd_mon.gemspec
91
+ homepage: https://github.com/joonty/systemd_mon
92
+ licenses:
93
+ - MIT
94
+ metadata: {}
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubyforge_project:
111
+ rubygems_version: 2.2.2
112
+ signing_key:
113
+ specification_version: 4
114
+ summary: Monitor systemd units and trigger alerts for failed states
115
+ test_files: []