Naplug Gem Version

Naplug is a Nagios plugin library for Ruby focused on plugin internals: organization, status, performance data, output and exit code handling. It contains (but does not include by default) functionality related to option and argument parsing, allowing plugin developers the choice to use the built-in parser or any of the many other fine CLI tools available for this purpose.

Naplug aims to ease the task of writing Nagios plugins in Ruby by handling the paperwork, allowing the plugin developer to concentrate on the test logic of the plugin. Some of the internal implementation is largely modeled after the very excellent Worlkflow library.

Naplug allows plugins to contain other plugins (referred to as plugs), which are a useful abstraction to break up significant tasks that the plugin as a whole must perform in order to determine the state of a service or host. The status and output of these plugs is thus used to determine the overall status of the plugin and build the output depending on said status.

While Naplug handles the nitty-gritty of Nagios plugins, it is important to have familiarity with the Nagios Plugin Developer Guidelines.

Note

  • Naplug 1.x is incompatible with Naplug 0.x (0.x was never released as a Gem)
  • Naplug 1.x is only supported on Ruby 1.9 and above; it will not be backported to 1.8

Overview

Naplug approaches Nagios plugins as Ruby classes (note that plugin is a reserved keyword at both the class and instance levels). To use Naplug, install the gem and:

#!/usr/bin/env ruby
require 'naplug'

class MyPlugin
  include Naplug
  plugin do |p|
    ... <do plugin work> ...
  end  
end

MyPlugin.new.exec!

All examples in this document will omit the require for readability.

A very simple plugin that always returns an OK status:

class AlwaysOkPlugin

  include Naplug

  plugin do |p|
    p.status.ok!
    p.output! "Optimism level: 100%"
  end
end

AlwaysOkPlugin.new.exec!

In the above example, a new class, AlwaysOkPlugin, is defined (the class name is arbitrary), and within this class, a plugin is created, which performs some work to set the status and output of the plugin. Once the class is defined, a new instance of the plugin is created and executed. The exec! method executes the plugin, evaluates status, produces correctly formatted output, and exits with the appropriate exit code:

naplug@plugin:~: alwaysok 
OK: Optimism level: 100%
naplug@plugin:~: echo $?
0 

A less optimistic example, this time with arguments:

class AlmostAlwaysOkPlugin

  include Naplug

  plugin do |p|

    p.output! "Optimism level: #{p[:optimism]}%"

    case p[:optimism]
      when 23..100 then p.status.ok!
      when 6..22   then p.status.warning!
      when 0..5    then p.status.critical!
      else
        p.output! "utterly confused"
    end

  end
end

plugin = AlmostAlwaysOkPlugin.new :optimism => Random.rand(100)
plugin.exec!

Which yields:

naplug@plugin:~: almostalwaysok 
OK: Optimism level: 96%
naplug@plugin:~: echo $?
0

And

naplug@plugin:~: almostalwaysok 
WARNING: Optimism level: 9%
naplug@plugin:~: echo $?
1

Plugins

Plugins are defined inside a new class with the plugin keyword. Plugins are always initialized in an UNKNOWN state and with their output set to uninitialized plugin, since at that point, the status of the plugin has not been determined. This ensures that misbehaved plugins correctly notify Nagios that they are failing in some way (for instance, if there's an unhandled exception, at which point the output will be set to useful information about the exception).

Tags

Plugins can be tagged, and tags must be unique within a class. Tags are used to identify a plugin, which is useful when multiple plugins are defined in a single class, which may be necessary in cases where several implementations of tests are required. A plugin's tag defaults to main when not specified.

Plugins can be accessed through tag methods, and executed through tag! methods.

class MultiPlugin

  include Naplug

  plugin :foo do |p|
    ...
  end

  plugin :bar do |p|
    ...
  end

end

plugin = MultiPlugin.new
case condition
  when true then plugin.foo!
  else plugin.bar!
end

When defining multiple plugins, invoking exec! will execute the main plugin (if defined; otherwise, exec! is unable to decide which one to execute). When defining a single plugin, exec! will execute it regardess of tag.

Arguments

A plugin can accept [mostly] arbitrary arguments, which are entirely optional and are available through the [] notation. Naplug (again, mostly) attaches no special meaning to them, i.e., they can be used in any way they need to be used.

A more realistic example that checks the staleness of a marker file:

class MarkerFilePlugin

  include Naplug

  plugin do |p|
      if Time.now - File.mtime(p[:marker_file]) > p[:critical]
        p.status.critical!
        p.output! "marker file #{p[:marker_file]} mtime greater than #{p[:critical]} seconds"
      else
        p.status.ok!
        p.output! "marker #{p[:marker_file]} is up to date"
      end
  end
end

plugin = MarkerFilePlugin.new :marker_file => '/tmp/my_marker', :critical => 120
plugin.exec!    

There are some worthwhile observations about the above example. A missing marker file prevents determining the stalesness of said file (infinite staleness?), implicitly resulting in an UNKNOWN status and output corresponding to the message of the exception. For finer control of this behavior, exceptions can be raised inside the plugin, which will be handled internally:

plugin do |p|
  raise Errno::ENOENT, p[:marker_file] unless File.exists? p[:marker_file]
  ...
end

The exception object is available through the payload. This only applies to exceptions raised inside the plugin block.

Arguments can also be specified via the args! method:

class ArgumentsPlugin

  include Naplug

  plugin do |p|
    ...
  end

end

plugin = ArgumentsPlugin.new :foo => 'old argument'
plugin.args! :foo => 'new argument'

The above code will override the :foo argument with a value of new argument.

Exceptions and eject!

Plugins operate in restricted runtime environments: Nagios expects the proper exit code and output. Naplug makes every effort to properly handle unexpected exceptions when executing plugins, and where it can't, it propagates them bundled in the Naplug::Error exception, which is about the only exception (from Naplug's point of view) that needs to be handled:

class ExceptionPlugin

  include Naplug

  plugin do |p|
    raise p[:exception], "raised exception: #{p[:exception]}"
  end

end

begin
  plugin = ExceptionPlugin.new :exception => StandardError
  plugin.exec!
rescue Naplug::Error => e
  plugin.eject! e
end

Which produces:

naplug@plugin:~: examples/exception 
UNKNOWN: exception:18: raised exception: StandardError
naplug@plugin:~: echo $?
3

The eject! method, which accepts a message string or an exception object as an argument, provides a last-ditch effort, out-of-band, escape hatch to bail out of executing a plugin, producing an UNKNOWN status and output from the message string or exception object.

While Naplug will internally handle exceptions within a plugin, it may be desirable to handle them especifically:

class ExceptionPlusPlugin

  include Naplug

  EXCEPTIONS = [ ArgumentError, ZeroDivisionError, TypeError ]

  plugin do |p|

    exception = EXCEPTIONS[p[:exception]]

    begin
      raise exception, "raising exception: #{exception}"
    rescue ArgumentError => e
      raise
    rescue ZeroDivisionError => e
      p.status.ok!
      p.output! "divided by zero is infinity"
    rescue => e
      p.status.critical!
      p.output! "got exception #{e.class}"
    end

  end

end

begin
  plugin = ExceptionPluginPlus.new :exception => Random.rand(3)
  plugin.exec!
rescue Naplug::Error => e
  plugin.eject! e
end

Which produces:

naplug@plugin:~: examples/exception+
UNKNOWN: exception+:24: raising exception: ArgumentError
naplug@plugin:~: examples/exception+
CRITICAL: got exception TypeError
naplug@plugin:~: examples/exception+
OK: divided by zero is infinity

Performance Data

Naplug supports plugin performance data, which is only available after a plugin has been execed. Within plugins, two methods are relevant: perfdata! and perfdata, which set and get performance data metrics, respectively:

class PerfDataPlugin

  include Naplug

  plugin :p do |p|
    p.status.ok!
    p.output! "#{p.tag}:#{p[:instance]} with argument metric #{p[:metric]}"
    p.perfdata! :metric, p[:metric]
    p.perfdata! '"_met ric!', 30, :max => 70
  end

end

pd = PerfDataPlugin.new :instance => :x1, :p => { :metric => 10 }
pd.exec!

This yields:

naplug@plugin:~: perfdata_uniplug 
OK: p:x1 with argument metric 10 | metric=10;;;; '"_met ric!'=30;;;;70

It is also possible to access performance data from within the user-defined plugin class through the perfdata method, which returns an array of PerformanceData objects.

Plugs: Plugins within Plugins

Up until now, Naplug has essentially provided syntactic sugar to define and use what amounts to single-purpose plugins, along with some convenience methods to represent status and produce output. But plugins sometimes need to perform a number of possibly independent tasks to reach a final, aggregated status.

In Naplug, these tasks are nested plugins or subplugins, and are referred to as plugs scoped to a parent plugin. When a plugin is created, we can define plugs inside the plugin through the plugin instance method. Again, these can be tagged, and plug tags must be unique, this time within a plugin.

class PlugPlugin

  include Naplug

  plugin do |p|

    plugin :plug1 do |p1|
      ...
    end

    plugin :plug2 do |p2|
      ...
    end

  end
end

Defining plugs imposes one important limitation: no other code besides plug definitions is allowed (in reality, it is allowed, just never really during executed).

class PluggedPlugin

  include Naplug

  plugin do |p|

    <do something here>     # will not be executed

    plugin :plug1 do |p1|
      ...
    end

    plugin :plug2 do |p2|
      ...
    end

    <do somthing else here>  # will not be executed
  end

end

Order of Execution

When exec! is invoked on a plugin, plugs are executed in the order in which they are defined, which is a side-effect of the fact that plugs are inserted into a Hash to keep track of them: Hashes enumerate their values in the order that the corresponding keys were inserted. Execution order can only be controlled manually:

plugin.exec :plug2
plugin.exex :plug1

Arguments

With the introduction of plugs, arguments do become more structured, as arguments keys are matched to plugin and plug tags to route them appropriately.

plugin = PlugPlugin.new(:plug1 => { :critical => 120, :warning => 60 },
                        :plug2 => { :ok => 0, :warning => 5, :critical => 10 })

Any keys not matching plug tags are considered to be shared among all plugs:

plugin = PlugPlugin.new(:file => '/tmp/file',
                        :plug1 => { :critical => 120, :warning => 60 }, 
                        :plug2 => { :ok => 0, :warning => 5, :critical => 10 })

Tagged arguments have priority over shared ones.

plugin = PlugPlugin.new(:file => '/tmp/file', 
                        :plug1 => { :file => '/var/tmp/file', :critical => 120 },
                        :plug2 => { :ok => 0, :warning => 5, :critical => 10 })

Performance Data

As mentioned earlier, Naplug supports plugin performance data, through two methods of Plugin, perfdata! and perfdata:

class PerfDataPlugPlugin

  include Naplug

  plugin :p do |p|

    plugin :p1 do |p1|
      p1.status.ok!
      p1.output! "#{p.tag}:#{p1.tag} #{p[:instance]}"
      p1.perfdata! :metric, p1[:metric], :max => 70
      p1.perfdata! '"_met ric!', 30, :max => 70
    end

    plugin :p2 do |p2|
      p2.status.ok!
      p2.output! "#{p.tag}:#{p2.tag} #{p[:instance]}"
      p2.perfdata! 'p2/metric', p2[:metric], :crit => 70
    end

    plugin :p3 do |p3|
      p3.status.ok!
     p3.output! "#{p.tag}:#{p3.tag} #{p[:instance]}"
    end
  end
end

# Note: the following code is meant to show some of the behavior related to Performance Data:
plugin = PerfDataPlugPlugin.new :instance => :x1, :p1 => { :metric => 10 }, :p2 => { :metric => 50 }
plugin.exec

# Plugin Perfdata Ancestors
plugin.perfdata.each do |pd|
  puts "#{pd.tag} has #{pd.to_a.size} labels and its tree is #{pd.ancestors :mode => :tags}"
end

plugin.perfdata.each do |pd|
  puts "plugin #{pd.tag} has #{pd.labels}"
end

which produces:

plugin@naplug:~: perfdata_multiplug
perfdata p1 has 2 labels and its tree is p/p1
perfdata p2 has 1 labels and its tree is p/p2
plugin p1 has ["metric", "'\"_met ric!'"]
plugin p2 has ["p2/metric"]

Some of this functionality is intended to ease integration with, for instance, Graphite:

g = Graphite.new :hostname => 'graphite', :port => 2013
h = `hostname`.gsub(/\./,'_')

plugin.perfdata.each do |pd|
  pd.to_a.each do |l|
    g.push "#{h}_app_#{l}
  end
end
g.flush!

This would push the collected performance data metrics to Graphite with an invented API. Note that Nagios performance data labels are not always legal graphite metric names ("\"_met ric!' above). Naplug makes no attempt to address this.

A Plugged Plugin Example

Take a service for which we wish to monitor three conditions:

  • that the service is running one and only one process
  • that the log file has seen activity within the last 60 seconds
  • that some metric related to the service (number of files in a queue) is within acceptable thresholds

Each of these tasks can be a plug, and Naplug will take care of aggregating the statuses to yield a plugin status (worst always wins).

require 'sys/proctable'
require 'naplug'

class MultiPlugServicePlugin

    include Naplug

    plugin do |p|

      plug :proc_count do |p1|
        pids = Sys::ProcTable.ps.each do |ps|...
        case pids.size
          when 1
            p1.status.ok
            p1.output "process #{p1[:name]} running with pid #{pids[0]}"
          when 0
            p1.status.critical
            p1.output "process #{p1[:name]} not running"
          else
            p1.status.critical
            p1.output "multiple #{p1[:name]} processes found, pids #{pids.join(',')}"
        end
      end

      plug :log_mtime do |p2|
        delta = Time.now - File.mtime(p2[:log_file])
        if delta > p2[:critical]
            p2.status.critical
            p2.output "p2[:name] log file #{p2[:log_file]} mtime greater than #{p2[:critical]} seconds"
          else
            p2.status.ok
            p2.output "marker #{p2[:log_file]} is up to date"
          end
      end

      plug :queue_depth do |p3|
        num_files = Dir.entries(p3[:dir]).length - 2
        p3.output "queue depth: #{num_files} items"

        case num_files
          when 0..100    then p3.status.ok
          when 101..1000 then p3.status.warning
          else                p3.status.critical
         end          
      end

    end        
end

plugin = MultiPlugServicePlugin.new :name => 'foobard'
plugin[:log_mtime] = { :log_file => '/var/log/foobard.log' }
plugin[:queue_depth] = { :dir => '/var/spool/foobard' }
plugin.exec!

Naplug Methods

Class Methods

Whenever Naplug in included in a class, the following class methods are available:

  • plugin, which is used to create plugins
  • tags, which returns an array of defined plugin tags

Instance Methods

In addition to the above class methods, the followingh instance methods are available:

  • args and args! to retrieve and set arguments
  • exec!, exec and eval to exec-to-exit, exec and evaluate plugins, respectively
  • has_plugins?, which evaluates to true if a plugin has plugs
  • [] and []= to get and set specific arguments
  • to_str to produce formatted plugin output
  • eject!, to quickly bail out
  • enable! and enabled?, disable! and disabled?, for enable and disabled plugs

Overriding these will likely cause Naplug to misbehave, to say the least.

Other methods can be defined in the class as necessary, and they can be used in the defined plugins or plugs, generally to provide helpers services. These should be defined as private or protected as necessary.

Status

Status is a special object that represent the status of a plugin for each of the defined states in the Nagios Plugin Guidelines: OK, WARNING, CRITICAL and UNKNOWN. Each of these states is itself an instance method which sets the state, and you can obtain the string and numeric representation through the usual methods to_s and to_i. The initial (and default) status of a Status object is UNKNOWN. Statuses are comparable in that larger statuses represent worse states, a feature that will come handy shortly.

require 'naplug/status'

puts "All statuses:"
Naplug::Status.states.each do |state|
  status = Naplug::Status.new state
  puts "  status #{status} has exit code #{status.to_i}"
end

puts "Working with a status:"
status = Naplug::Status.new
puts "  status #{status} has exit code #{status.to_i}"
status.ok!
puts "  status #{status} has exit code #{status.to_i}"

puts "Comparing statuses:"
status1 = Naplug::Status.new :warning
if status < status1
  puts "  status [#{status}] < status1 [#{status1}] is true"
end

which produces

naplug@plugin:~: status 
All statuses:
  status OK has exit code 0
  status WARNING has exit code 1
  status CRITICAL has exit code 2
  status UNKNOWN has exit code 3
Working with a status:
  status UNKNOWN has exit code 3 after initialization
  status OK has exit code 0 after status.ok
Comparing statuses:
  status [OK] < status1 [WARNING] is true

Naplug Helpers

Naplug includes helpers, which are not loaded by default.

Naplug::Helpers::CLI

Naplug is very much focused on plugin internals, leaving the work of command-line parsing to external entities. This allows plugin developers to use their preferred choice of parsers, such as thor, Slop or OptionsParser.

A helper is however built-in, and uses the small and very flexible [trollop] library, which must be installed as a gem. To use:

require 'naplug'
require 'naplug/helpers/cli'

class FooPlugin

  VERSION = '1.0.0'

  include Naplug

  plugin do |p|
    ...
  end
end

class FooPluginCLI

  include Naplug::Helpers::CLI

  opts = options do
    version FooPlugin::VERSION
    banner "#{File.basename($0)}"
    opt :warning, 'number of mtime WARNING seconds (required)', :type => :int, :required => true
    opt :critical, 'number of mtime CRITICAL seconds (required)', :type => :int, :required => true
  end

  plugin = FooPlugin.new opts
  plugin.exec!

end

Naplug does change the behavior of Trollop so that when arguments generate an error, these are handled correctly as a plugin (producing an UNKNOWN status).r

Futures

There following are some ideas on future Naplug features.

Order of Execution

A future release will allow the execution order to be changed through an order! instance method, which will accept a list of tags in the desired order of execution.

plugin.order! :plug2, :plug1

If tags are omitted from the list, the missing plugs are pushed to the end of the line in the last order set.

Enabling and Disabling Plugs

Currently, when plugs are defined, they are assumed to be enabled and will be executed when exec! is invoked. There may be cases when it may be desirable or necessary to disable specific plugins, which will be accomplished through the disable! instance method. A disabled plug can be re-enabled via the enable! plugin method:

plugin.disable! :plug2

Disabled plugs will not be executed and will not be taken into account when evaluating status. The active state of a plugin can be queried via the enabled? and disabled? methods.

plugin.enabled? :plug2 => false
plugin.disabled? :plug2 => true

Aditionally, is_<tag>_enabled? and is_<tag>_disabled? methods will be available for each plug.