|
|
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.
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 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?
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.
-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):
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.
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:
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. 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:
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:
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.
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? Head to our support area.