Rubix

Rubix is a Ruby client for Zabbix. It has a few goals:

  • Provide a Connection and Response class that abstract away the complexity of authenticating with and sending API requests to Zabbix.

  • Provide an ORM for Zabbix resources like Hosts, HostGroups, Templates, &c. using the Zabbix API.

  • Provide a command-line script that wraps the zabbix_sender utility, allowing it to consume JSON data and to 'auto-vivify' hosts, hostgroups, applications, and items.

  • Provide some Monitor classes that make it easy to write scripts that periodically measure something and push it to Zabbix.

  • Have as few dependencies as possible: the core classes only depend on Ruby 1.8 standard library & JSON and scripts additionally depend on Configliere[http://github.com/mrflip/configliere].

  • Play nicely with Chef

There are a lot of other projects out there that connect Ruby to Zabbix. Here's a quick list:

zabbix

zabbix aws templates, scripts, chef automations

zabbixapi

Ruby module for work with zabbix api

zabbix-rb

send data to zabbix from ruby

zabbix_pusher

zabbix_pusher is a gem to parse zabbix templates and push the data to the corresponding zabbix server

zabbix-trappers

Collection of ruby scripts for zabbix trappers

rzabbix

Zabbix API client for Ruby

zabboard

zabbix analytics

zabbix-web

Zabbix frontend

zabcon

Zabcon is a command line interface for Zabbix written in Ruby

None of these projects was satisfactory for our purposes so I decided to write Rubix. The name is terrible but the code is better. Enjoy!

Connections, Requests, & Responses

Getting connected is easy

require 'rubix'

# Provide API URL & credentials.  These are the defaults.
Rubix.connect('http://localhost/api_jsonrpc.php', 'admin', 'zabbix')

As per the Zabbixi API documentation, each request to the Zabbix API needs four values:

id

an integer identifying the request ID.

auth

a string confirming that the API request is authenticated.

method

the name of the API method you're calling, e.g. - host.get, template.delete, &c.

params

parameters for the invocation of the method.

When you send a request, Rubix only requires you to specify the method and the params, handling the id and authentication quietly for you:

response = Rubix.connection.request('host.get', 'filter' => { 'host' => 'My Zabbix Host' })

case
when response.has_data?
  # Response is a success and "has data" -- it's not empty.  This
  # means we found our host.
  puts response.result
  #=> [{"hostid"=>"10017"}]
when response.success?
  # Response was succssful but doesn't "have data" -- it's empty, no
  # such host!
  puts "No such host"
else
  # Response was an error.  Uh oh!
  puts response.error_message
end

On the command line

Rubix comes with a command line utility zabbix_api which lets you issue these sorts of requests directly on the command line.

$ zabbix_api host.get '{"filter": {"host": "My Zabbix Host"}}'
[{"hostid"=>"10017"}]

zabbix_api lets you specify the credentials and will pretty-print responses for you. Try zabbix_api --help for more details.

ORM

If you don't want to deal with the particulars of the Zabbix API itself, Rubix provides a set of classes that you can use instead.

The following example goes through setting up an item on a host complete with host groups, templates, applications, and so on.

require 'rubix'
Rubix.connect('http://localhost/api_jsonrpc.php', 'admin', 'zabbix')

# Ensure the host group we want exists.
host_group = Rubix::HostGroup.find_or_create(:name => "My Zabbix Hosts")

# Now the template -- created templates are empty by default!
template = Rubix::Template.new(:name => "Template_Some_Service")
template.save

# Now the host.
host = Rubix::Host.new(:name => "My Host", :ip => '123.123.123.123', :templates => [template], :host_groups => [host_group])
host.save

# Now for the application
app = Rubix::Application.new(:name => 'Some App', :host => host)
app.save

# Now the item
item = Rubix::Item.new(:host => host, :key => 'foo.bar.baz', :description => "Some Item", :value_type => :unsigned_int, :applications => [app])
item.save

You can also update and destroy resources.

Only host groups, templates, hosts, applications, user macros, and items are available at present. Other Zabbix resources (actions, alerts, events, maps, users, &c.) can be similarly wrapped, they just haven't been yet because we don't use them as much as we use these core resources.

Sender

Rubix comes with a zabbix_pipe script which, coupled with the zabbix_sender utility, allows for writing data to Zabbix.

By the design of Zabbix, all data written via zabbix_sender (and therefore zabbix_pipe) must be written to items with type “Zabbix trapper”. This type instructs Zabbix to accept values written by zabbix_sender.

zabbix_pipe can consume data from STDIN, a file on disk, or a named pipe. It consumes data one line at a time from any of these sources and uses the zabbix_sender utility to send this data to a Zabbix server.

Here's an example of starting zabbix_pipe and sending some data onto Zabbix. (Credentials and addresses for the Zabbix server and the Zabbix API can all be customized; see the --help option.)

# Send one data point -- this is tab separated!
$ echo "foo.bar.baz 123" | zabbix_pipe --host='My Zabbix Host'
# Send a bunch of data points in a file
$ cat my_data.tsv | zabbix_pipe --host='My Zabbix Host'

You can also pass the file directly:

# Send a bunch of data points in a file
$ zabbix_pipe --host='My Zabbix Host' my_data.tsv

You can also listen from a named pipe. This is useful on a “production” system in which many processes may want to simply and easily write somewhere without worrying about what happens.

# In the first terminal
$ mkfifo /dev/zabbix
$ zabbix_pipe --pipe=/dev/zabbix --host='My Zabbix Host'

# In another terminal
$ echo "foo.bar.baz 123" > /dev/zabbix
$ cat my_data > /dev/zabbix

In any of these modes, you can also send JSON data directly.

$ echo '{"data": [{"key": "foo.bar.baz", "value": 123}, {"key": "foo.bar.baz", "value": 101}]}' > /dev/zabbix

This simple block of JSON doesn't really add any power to zabbix_pipe. What becomes more interesting is that we can wedge in data for diffferent hosts, items, &c. when using JSON input:

$ echo '{"host": "My first host", "data": [{"key": "foo.bar.baz", "value": 123}, {"key": "foo.bar.baz", "value": 101}]}' > /dev/zabbix
$ echo '{"host": "My second host", "data": [{"key": "foo.bar.baz", "value": 123}, {"key": "foo.bar.baz", "value": 101}]}' > /dev/zabbix
$ echo '{"host": "My third host", "data": [{"key": "foo.bar.baz", "value": 123}, {"key": "foo.bar.baz", "value": 101}]}' > /dev/zabbix

Rubix will switch hosts on the fly.

Auto-vivification

By default, for every item written into zabbix_pipe, Rubix will first check, using the Zabbix API, whether an item with the given key exists for the given host. If not, Rubix will create the host and the item. You can pass a few details along with your data (when writing in JSON format) or at startup of the zabbix_pipe to tune some of the properites of newly created hosts and items:

$ echo '{"host": "My host", "hostgroups": "Host Group 1,Host Group 2", "templates": "Template 1,Template 2", "applications": "App 1, App2", "data": [{"key": "foo.bar.baz", "value": 123}, {"key": "foo.bar.baz", "value": 101}]}' > /dev/zabbix

If the host 'My host' does not exist, Rubix will create it, putting in each of the host groups 'Host Group 1' and 'Host Group 2' (which will also be auto-vivified), and attach it to templates 'Template 1' and 'Template 2' (auto-vivified). If the item does not exist for the host 'My host', then it will be created and put inside applications 'App1' and 'App2' (auto-vivified). The value type of the item (unsigned int, float, character, text, &c.) will be chosen dynamically based on the value being written. The created items will all be of the type “Zabbix trapper” so that they can be written to.

Auto-vivification is intended to make it easy to register a lot of dynamic hosts and items in Zabbix. This is perfect for cloud deployments where resources to be monitored are often dynamic.

Failed writes

By the design of Zabbix, a newly created item of type 'Zabbix trapper' will not accept data for some interval, typically a minute or so, after being created. This means that when writing a series of values for some non-existent item 'foo.bar.baz', the first write will cause the item to be created (auto-vivified), the next several writes will fail as the Zabbix server is not accepting writes for this item yet, and then all writes will begin to succeed as the Zabbix server catches up. If it is absolutely essential for all writes to succeed, including the first, then zabbix_pipe needs to go to sleep for a while after creating a new item in order to give the Zabbix server time to catch up. This can be configured with the --create_item_sleep option. By default this is set to 0.

Skipping auto-vivification

Attempting to auto-vivify keys on every single write is expensive and does not scale. It's recommended, therefore, to run zabbix_pipe with the --fast flag in production settings. In this mode, zabbix_pipe will not attempt to auto-vivify anything – if items do not exist, writes will just fail, as they do with zabbix_sender itself.

A good pattern is to set up a production pipe (in --fast) mode at /dev/zabbix and to do all development/deployment with a separate instance of zabbix_pipe. Once development/deployment is complete and all hosts, groups, templates, applications, and items have been created, switch to writing all data in “production mode” using the --fast pipe at /dev/zabbix.

Monitors

Rubix also comes with some classes that make it easy to write simple monitors. The output of these monitors should match the expected input format of zabbix_pipe. This way they can be chained together. Here's an example of a simple monitor that calculates the currently used memory in bytes.

# in memory_monitor.rb
require 'rubix'

class MemoryMonitor < Rubix::Monitor
  def measure
    write do |data|
      mem_used = `free | tail -n+2 | head -n1`.chomp.split[2].to_i
      data << [['mem.used', mem_used]]
    end
  end
end

MemoryMonitor.run if $0 == __FILE__

The file memory_monitor.rb can now be run on the command line in various ways:

# Write the output to STDOUT
$ ruby memory_monitor.rb
'mem.used'  11595908

# Loop every 30 seconds
$ ruby memory_monitor.rb --loop=30

'mem.used'  11595760
'mem.used'  11595800
'mem.used'  11596016
'mem.used'  11596008

It can be piped into a running zabbix_pipe:

$ ruby memory_monitor.rb --loop=30 > /dev/zabbix &

Monitors with Chef

It's useful to be able to monitor resources defined in Chef and so Rubix comes with a ChefMonitor class.

# in webserver_monitor.rb
require 'rubix'
require 'net/http'
require 'uri'

class WebserverMonitor < Rubix::ChefMonitor

  def webserver
    @webserver ||= chef_node_from_node_name('my_webserver')
  end

  def uri
    URI.parse("http://#{webserver['ec2']['public_hostname']}")
  end

  def measure
    begin
      if Net::HTTP.get_response(uri).code.to_i == 200

availability = 1 else availability = 0 end

    rescue => e
      availability = 0
    end
    write do |data|
      data << ([['webserver.available', availability]])
    end
  end
end

WebserverMonitor.run if $0 == __FILE__

To run this monitor you'll have to pass in credentials used to authenticate with Chef:

$ sudo ruby webserver_monitor.rb --chef_server_url=http://api.opscode.com/organizations/my_company --chef_node_name=this_node --chef_client_key=/etc/chef/client.pem --loop=30 > /dev/zabbix

Make sure you can read the file at /etc/chef/client.pem!