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.
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:
Create a minimal plugin file that Scout can work with. Very little is needed here. Basically the steps are:
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?
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.
-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:
Enough background. Let's get back to our critical iTunes plugin.
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:
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.
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}
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:
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:
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:
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.
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