Scout
 

How To Create a Scout Plugin

by James Edward Gray II

Scout uses a 4-step process to execute plugins:
1. Client requests the plugin plan from the Server.
2. Server sends the plugins to the Client.
3. Client runs each plugin.
4. Each plugin posts an optional report, alert, and/or error to the server.

We've been building and sharing Scout plugins for common server operations in the plugin directory. There are plugins ranging from monitoring the memory usage of processes to tracking your popularity on the Internet. What if you need a variation of a plugin though? How about creating a brand new plugin?

This is what makes Scout really powerful - you can customize it with your own plugins. If you think others might benefit from your plugin, you can also submit it to the directory.

Writing a plugin for Scout is very easy. You only need to know a few simple rules and you can generally be gathering new information with just 10 to 15 minutes worth of work. Let me show you what I mean.


Overview

  1. Setup
    Installing the Scout Client, building a plugin shell.
  2. Reports, Alerts, and Errors
    Putting data into Scout, explaining the difference between alerts and errors.
  3. Testing Plugins
    Running plugins from the command line, explaining the optional parameters.
  4. Input From External Tools
    Grabbing data from an external application within a Scout Plugin.
  5. Storing Data Between Runs
    Giving your plugins memory.
  6. Passing Options
    How to have plugins execute with unique options, testing options handling.
  7. Help
    Where to get further assistance creating Scout Plugins.

Setup Back to Top

What's this "gem install" command?

This command is used to install a Ruby Gem. Ruby must be installed for the client to run. Ruby Gems is installed with Ruby and is the packaging system of choice for Ruby.

Preparing to build a Scout plugin is extremely simple. Step one, make sure you have Scout installed:

$ sudo gem install scout --source http://gems.scoutapp.com

Create a minimal plugin file that Scout can work with. Very little is needed here. Basically the steps are:

  1. Create a class_name.rb file to hold your plugin
  2. Build a standard Ruby class that inherits from `Scout::Plugin`
  3. Add a `run()` method that returns a `Hash` of information to record

Yes, it's really that trivial. Following those rules, I placed the following code in itunes_track.rb:

class ItunesTrack < Scout::Plugin
  def run
    { }
  end
end

I doesn't do much yet, but that's a minimal Scout plugin. I'm going to collect the statistics on the currently playing track in iTunes, of course. What could be more critical?

Reports, Alerts, and Errors Back to Top

When to Send an Error vs. an Alert

Use errors when there is a problem running the plugin (like when a required library is missing).

Use alerts when the plugin runs fine, but an event occurred that requires special attention (like an increased server load).

Most often you will want to send Scout some report data. The `Hash` returned by your method is the data that will be sent to Scout. You may optionally want to send an important alert with or without report data. And when a plugin fails to run correctly, you can send an error.

A report is a `Hash`. You make up the field names and values so they are meaningful to you. Alerts and Errors are `Hash`es as well with one required key, `:subject`, and one optional key, `:body`. Alerts are used to notify when certain conditions have been met or a threshold has been passed. Errors are only used if there is a problem running the plugin.

Here's a plugin that shows how we might use report data, alerts, and errors:

class TypicalReports < Scout::Plugin
  def run
    begin
      if Date.today == Date.parse("Dec 25 2008")
        { :report => { :holiday => "Christmas" } }
      elsif Date.today == Date.parse("Oct 31 2008")
        { :report => { :holiday => "Halloween" },
          :alert =>  {:subject => "Trick or Treat!"} }
      else
        { }
      end
    rescue
      { :error => {
          :subject => "Could not run the plugin",
          :body    => "Please check the code and try again."
        } }
    end
  end
end

This Plugin is quite trivial, simply sending back data only on certain holidays, and alerts only on those special holidays. Let's find out how to test this plugin.

Testing Plugins Back to Top

Helpful Testing Command Line Options

You can get a full list of options by typing 'scout -h' at the command line.

-v => Turn on logging

-l debug => Increase the logging level.

Often you will want to try out a plugin on the server it will run on to be sure it works as you expect. Publishing the plugin, uploading everything to Scout and repeatedly running the client is too much hassle for those cases. That's why Scout can also simply run a plugin locally. Simply run the Scout Client with the -p or --plugin option, passing the path to the plugin file.

Here's how we can try out the code above, pretending that today is Halloween:

$ scout -p typical_reports.rb
{ :report => { :holiday => "Halloween" },
  :alert =>  { :subject => "Trick or Treat!"} }

Enough background. Let's get back to our critical iTunes plugin.

Input From External Tools Back to Top

Gathering data from a Rails App

A common task is gathering application-specific data from a Ruby on Rails application (number of users, order data, etc.). It's easy with a plugin. Here's a plugin shell that loads up the Rails environment.

Most plugins just gather data from the server using external tools and send that data on up to Scout. That strategy will work for us here too. We can ask AppleScript what's playing using Ruby's tools for talking to external processes and build our report off of that output.

Here's the code for that:

class ItunesTrack < Scout::Plugin
  APPLESCRIPT = <<-END_AS
  tell application "iTunes"
    try
        if not (exists current track) then return
        get name of current track
    end try
  end tell
  END_AS

  def run
    current_track = IO.popen("osascript", "r+") do |as|
      as << APPLESCRIPT
      as.close_write
      as.read
    end
    if current_track.empty?
      {:alert => {:subject => "Your mac is currently tuneless!"} }
    else
      {:report => {:track_name => current_track} }
    end
  rescue Exception
    { :error => { :subject => "Couldn't use `osascript` as expected.",
                  :body    => "An exception was thrown:  #{$!.message}" } }
  end
end

As you can see, the only remotely tricky code in here is the call to `IO.popen()`. I use that call to open a pipe to the osascript tool. Once open, I push some AppleScript code into the pipe and close my ability to write to it so it will run the code. Then I just read back the response and act on it.

Running this Scout plugin locally is easy:

The '-o' option

This passes in options to the plugin. Learn more about options below.
$ scout -p itunes_track.rb -o itunes_track.yml
{:track_name=>"Crank That (Soulja Boy)\n"}

There are many ways to collect data from external processes. Be sure to browse the Plugin Directory to see many examples of reading data from various system commands.

Storing Data Between Runs Back to Top

Where is memory stored?

Memory is stored locally in the client_history.yml file.

Scout provides a way for you to store data between runs. We call this a plugin's memory. The most common use-case for this is to set some kind of flag so that your plugin doesn't continue to generate alerts. To get something from the memory, simply access it through the @memory hash. To put something in the memory, simply assign it a key and value. To remove something from memory, simply set its value to nil.

# access the `disk_space_usage` data currently in the memory:
@memory[:disk_space_usage]

# put the `disk_space_usage` data in the memory for next run:
report[:memory] = {:disk_space_usage = true}

# clear the `disk_space_usage` data from the memory for next run:
report[:memory] = {:disk_space_usage = nil}
Anything in the memory will automatically be available to you on the next plugin run. I'm going to edit our ItunesTrack Scout plugin to only alert me once when a song is not playing. I'll set a variable in the memory called `no_track` and I'll only generate the alert when we know there is a track playing.

class ItunesTrack < Scout::Plugin
  APPLESCRIPT = <<-END_AS
  tell application "iTunes"
    try
        if not (exists current track) then return
        get name of current track
    end try
  end tell
  END_AS

  def run
    current_track = IO.popen("osascript", "r+") do |as|
      as << APPLESCRIPT
      as.close_write
      as.read
    end

    report = {}
    alert_user = @memory[:no_track].nil?
    if current_track.empty? and alert_user
      report[:alert] = {:subject => "Your mac is currently tuneless!"}
      report[:memory] = {:no_track => true}
    elsif current_track.empty? and !alert_user
      report[:memory] = {:no_track => true}
    else
      report[:track_name] = current_track
      report[:memory] = {:no_track => nil}  
    end
    report
  rescue Exception
    { :error => { :subject => "Couldn't use `osascript` as expected.",
                  :body    => "An exception was thrown:  #{$!.message}" } }
  end
end

Now, if the current track is empty (meaning a song is not playing) and we have not already alerted the user (using our memory to see if we've already sent the no_track alert), then we generate an alert. Otherwise, we just simply make sure to remember that we've already sent the alert by continually returning the `no_track` memory flag.

Running this plugin, we now see the alert and memory being set when no song is playing. And then the alert is not sent on subsequent runs:

$ scout -p itunes_track.rb
{:alert=> {:subject=>"Your mac is currently tuneless!"},
 :memory=>{:no_track=>true}}

$ scout -p itunes_track.rb
 {:memory=>{:no_track=>true}}

$ scout -p itunes_track.rb
 {:memory=>{:no_track=>true}}

Passing Options Back to Top

If you would like to let the user specify options for the plugin, such as thresholds, limits, even variables, you can do so very easily. Simply create a YAML file with the same name (with a .yml extension) and directory as the plugin. This is important so that Scout can load your plugin's options. If you file name is process_memory.rb then your options file should be process_memory.yml. In this options YAML file, you can specify any number of options, each containing an option name, a display name, some notes, and a default value.

I'm going to update our ItunesTrack Scout plugin to allow the user to specify one option: the name of the AppleScript Executable. First, I'll create a file called itunes_track.yml:

options:
  applescript_executable:
    name: AppleScript Executable
    notes: Specify the full path to the AppleScript Executable.
    default: osascript

By giving each option a name, and specifying a display name, notes, and a default value, when users add this plugin to their client, they will see the following:

Plugin_options

To use these options in your plugin, simply access them using the `@options` Hash. Here's our updated iTunesTrack Scout plugin:

class ItunesTrack < Scout::Plugin
  APPLESCRIPT = <<-END_AS
  tell application "iTunes"
    try
        if not (exists current track) then return
        get name of current track
    end try
  end tell
  END_AS

  def run
    @process = @options["applescript_executable"] || "osascript"
    current_track = IO.popen(@process, "r+") do |as|
      as << APPLESCRIPT
      as.close_write
      as.read
    end
    report = {}
    alert_user = @memory[:no_track].nil?
    if current_track.empty? and alert_user
      report[:alert] = {:subject => "Your mac is currently tuneless!"}
      report[:memory] = {:no_track => true}
    elsif current_track.empty? and !alert_user
      report[:memory] = {:no_track => true}
    else
      report[:track_name] = current_track
      report[:memory] = {:no_track => nil}  
    end
    report

  rescue Exception
    { :error => { :subject => "Couldn't use `#{@process}` as expected.",
                  :body    => "An exception was thrown:  #{$!.message}" } }
  end
end

Notice that we are able to access the AppleScript Executable using: `@options["applescript_executable"]`.

One quick note: When testing a plugin that has options, you can simply pass the options as a Hash (surrounded by single quotes), or send in the YAML options file. Just use the -o or --plugin-options directive:

$ scout -p itunes_track.rb -o '{"applescript_executable"=>"osascript"}'
{:memory=>{:no_track=>nil}, :track_name=>"...Baby One More Time\n"}

$ scout -p itunes_track.rb -o itunes_track.yml
{:memory=>{:no_track=>nil}, :track_name=>"...Baby One More Time\n"}

When running your plugin locally and using the YAML options file, the default specified for each option will be sent as input. This means when running this Scout plugin locally, we will always send the value `osascript` for the applescript_executable option.

While this is not a lot of code, the fact is that most Scout plugins are just variations on this simple pattern. You generally want to read some data from an external source, parse it, and send it up to Scout by returning the proper `Hash`. My parsing step is trivial here, but a few regular expressions will usually find the data you need even as the tool output grows more complex.

Next time you have a need to track some data, consider building a Scout plugin. As you can see, there's not much work involved.

Help Back to Top

Have a plugin development question? Checkout the Scout Forums. We monitor the forums actively during business hours. General Scout Q&A is answered in our Help Area. We also welcome your questions and feedback at support@highgroove.com.

Back to Top
 
Scout Web Monitoring and Reporting Software

A service of Highgroove Studios

Powered By Rails Machine