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.
Updated Updated (Jun 7, 2008) to reflect the new 2.0 client features. Check out Scout Plugin Updates for a quick overview of the differences, improvements, and new features of the Scout 2.0 client.

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 `build_report()` method that builds up the data you wish to collect

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

class ItunesTrack < Scout::Plugin
  def build_report
    { }
  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. 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 `Hashes` 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 build_report
    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!")
      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.

Just for reference, you can use one of many different helper methods to build up your report. Here are some examples:

# Add report data:
reports << {:data => "here"}
report(:data => "here")
add_report(:data => "here")

# Add an alert: 
alerts << {:subject => "subject", :body => "body"}
alert("subject", "body")
alert(:subject => "subject", :body => "body")
add_alert("subject", "body")
add_alert(:subject => "subject", :body => "body")

# Add an error: 
errors << {:subject => "subject", :body => "body"}
error("subject", "body")
error(:subject => "subject", :body => "body")
add_error("subject", "body")
add_error(:subject => "subject", :body => "body")

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 in the test mode, passing the path to the plugin file.

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

$ scout test typical_reports.rb
{:reports=>[{:holiday=>"Halloween"}],
  :alerts=>[{:subject=>"Trick or Treat!"}],
  :errors=>[],
  :memory=>{}}

Notice that the output is a `Hash` that contains the report information and the alerts that we will receive when it runs. We'll talk about the memory in a moment. But, that's 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 build_report
    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:

$ scout test itunes_track.rb
{:reports=>
  [{:track_name=>
    "Crank That (Soulja Boy)\n"}],
  :alerts=>[],
  :errors=>[],
  :memory=>{}}

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. The memory only lives for one request, so if you want to "keep" something in memory for extended periods, you'll have to tell Scout to remember it on each run. Scout provides a few easy ways to get and set the memory:

# access the `disk_space_usage` data currently in the memory:
memory(:disk_space_usage)

# remember the `disk_space_usage` data in the memory for next run:
remember(:disk_space_usage = 42)

# delete the `disk_space_usage` data from the memory for next run:
memory.delete(:disk_space_usage)

# clear the entire memory:
memory.clear

There are several ways to remember pieces of data in memory beteween runs, these helper methods all accomplish the same thing:

remember(:name, value)
remember(:name1, value1, :name2, value2)
remember(:name => value)
remember(:name1 => value1, :name2 => value2)
remember(:name1, value1, :name2 => value2)

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 `alert_user` to "remember" that I've already sent an alert to the user. Another way to think about it: unless I've already sent the alert, we'll alert the user.

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 build_report
    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!") unless memory(:sent_alert)
      remember(:sent_alert => true)
    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

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 alert), then we generate an alert.

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 test itunes_track.rb
{:reports=>[],
  :alerts=>[{:subject=>"Your mac is currently tuneless!"}],
  :errors=>[],
  :memory=>{:sent_alert=>true}}

$ scout test itunes_track.rb
{:reports=>[], :alerts=>[], :errors=>[], :memory=>{:sent_alert=>true}}

$ scout test itunes_track.rb
{:reports=>[], :alerts=>[], :errors=>[], :memory=>{:sent_alert=>true}}

Passing Options

Back to Top

Options allow for customization

Options allow you to customize:

- paths to executables (/usr/bin/perl or /usr/local/bin/perl)
- optional alerts (allow the user to specify how verbose the plugin is)
- custom functionality (if the option is turned on, enable extra functionality)
- optional extra command line arguments or specific options your plugin requires

Be sure to browse the Plugin Directory to see many examples of passing options.

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 your plugin's 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 build_report
    @process = option(:applescript_executable) || "osascript"
    current_track = IO.popen(@process, "r+") do |as|
      as << APPLESCRIPT
      as.close_write
      as.read
    end
    if current_track.empty?
      alert(:subject => "Your mac is currently tuneless!") unless memory(:sent_alert)
      remember(:sent_alert => true)
    else
      report(:track_name => current_track)
    end
  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 Option using: `option(:applescript_executable)` and if that option is not set, we just default to `osascript`.

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.

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

$ scout test itunes_track.rb 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? Head to our support area.

Already have a Scout account?   Where do I login? Highgroove Studios Powered By Rails Machine