<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:georss='http://www.georss.org/georss' xmlns:gd='http://schemas.google.com/g/2005' xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-8927077563876229110</id><updated>2012-01-05T12:24:57.670-08:00</updated><category term='voting'/><category term='web application'/><category term='xml'/><category term='Google Maps'/><category term='CSS-P'/><category term='Google Spreadsheet'/><category term='Fluid Design'/><category term='Street Level View'/><category term='jQuery'/><category term='democracy'/><category term='CSS'/><category term='software'/><category term='Server Monitor'/><category term='free'/><category term='Google Docs'/><category term='CodeProject'/><category term='open source'/><category term='ballot'/><title type='text'>Stephen Akins - Blog</title><subtitle type='html'>Google Docs, mashups, and other technical topics</subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://stephenakins.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8927077563876229110/posts/default?max-results=100'/><link rel='alternate' type='text/html' href='http://stephenakins.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><author><name>Stephen Akins</name><uri>https://profiles.google.com/106963705487836408694</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//lh6.googleusercontent.com/-OBjWBqY9XUM/AAAAAAAAAAI/AAAAAAAABww/ozuFLWf-lsE/s512-c/photo.jpg'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>9</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>100</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-8927077563876229110.post-6113085493290837281</id><published>2011-11-16T16:39:00.000-08:00</published><updated>2011-12-20T13:41:49.261-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Google Spreadsheet'/><category scheme='http://www.blogger.com/atom/ns#' term='Street Level View'/><category scheme='http://www.blogger.com/atom/ns#' term='Google Maps'/><title type='text'>Want to know when Google is adding your area to Street Level View?</title><content type='html'>Google doesn't tell many people when they are driving around in their cars, photographing an area for Street Level View (I understand Google notifies Women's Shelters and other organizations that are sensitive about exterior photographs of their buildings). &amp;nbsp;There are several obvious reasons for not being too public about their mapping intentions, of course. &amp;nbsp;Most importantly, I imagine, is that Google wants to keep their data from being "tampered" with by people who would make some special display specifically for the mapping vehicle (e.g. a "Hi Mom!" banner). &lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://4.bp.blogspot.com/-m1L8FvA9X1c/TsRZryt8WiI/AAAAAAAABCo/a6z0h7l_dY0/s1600/kendaraan_google.jpg" imageanchor="1" style="clear: right; float: right; margin-bottom: 1em; margin-left: 1em;"&gt;&lt;img border="0" src="http://4.bp.blogspot.com/-m1L8FvA9X1c/TsRZryt8WiI/AAAAAAAABCo/a6z0h7l_dY0/s1600/kendaraan_google.jpg" /&gt;&lt;/a&gt;&lt;/div&gt;I, however, really want to know anyway. &amp;nbsp;I live on a small island that relies heavily on tourism. &amp;nbsp;Our island has many artists and artisans who work out of studios on their own properties. &amp;nbsp;I would like to have these artists have displays of their work on the ends of their driveways when Google conducts a street level view on our island (our island hasn't been covered yet, but I think it may be covered in the future). &amp;nbsp;I think it would be good for the locals, and for the tourists who look at our island online.&lt;br /&gt;&lt;br /&gt;As I said, google doesn't broadcast where they intend to cover next; but they DO broadcast where they are &lt;i&gt;currently&lt;/i&gt;. &amp;nbsp;Google has a special site that you can visit to see the current location of their vehicles, categorized by country:&lt;br /&gt;&lt;br /&gt;&lt;a href="http://maps.google.com/help/maps/streetview/learn/where-is-street-view.html"&gt;http://maps.google.com/help/maps/streetview/learn/where-is-street-view.html&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;As I said, the chart on this page shows the &lt;i&gt;current&lt;/i&gt;&amp;nbsp;location and only seems to list the city or general area they are currently mapping. &amp;nbsp;However, I think it's a start and below I'll show you how you can use this data to get an alert when Google starts mapping a new area in your country.&lt;br /&gt;&lt;br /&gt;The method I'll describe here is a very simple one. &amp;nbsp;The country data on the page above is populated through an XML feed. &amp;nbsp;I decided to import the data into a Google Docs spreadsheet, and make the spreadsheet notify me via email if the cell that contains the data for my country changes. &amp;nbsp;The steps I took are as follows:&lt;br /&gt;&lt;br /&gt;&lt;ol&gt;&lt;li&gt;Create a spreadsheet in Google Docs&lt;/li&gt;&lt;li&gt;In the first cell on the default worksheet (A1) insert the following function:&lt;br /&gt;=ImportFeed("http://spreadsheets.google.com/feeds/list/0AjZ9lY-SjtYacnNVdGhsckJrM3k5X1hFd3BIWlhWcFE/oda/public/basic")&lt;/li&gt;&lt;li&gt;Select Tools &amp;gt; Notification Rules and add a new rule&lt;/li&gt;&lt;li&gt;Click the checkbox next to "Any of these cells are changed: ". &amp;nbsp;In the cell range box, put the name of the cell that contains the data for your country (for my country, Canada, the cell was C7)&lt;/li&gt;&lt;li&gt;Click the checkbox next to "Email - Right Away" under "Notify Me With..."&lt;/li&gt;&lt;li&gt;Save your spreadsheet and exit! &amp;nbsp;&lt;/li&gt;&lt;/ol&gt;&lt;div&gt;The spreadsheet doesn't poll the source every second, but you should get a notification shortly after the data changes (certainly within the hour). &amp;nbsp;Again, this isn't exactly "notice", but hopefully it will give you some warning.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;If there is a lot of activity in your country (and you're getting too many notifications) you can use the FIND function to locate your area/city name within your country data and then base your notification on the result.&lt;/div&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8927077563876229110-6113085493290837281?l=stephenakins.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://stephenakins.blogspot.com/feeds/6113085493290837281/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stephenakins.blogspot.com/2011/11/want-to-know-when-google-is-adding-your.html#comment-form' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8927077563876229110/posts/default/6113085493290837281'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8927077563876229110/posts/default/6113085493290837281'/><link rel='alternate' type='text/html' href='http://stephenakins.blogspot.com/2011/11/want-to-know-when-google-is-adding-your.html' title='Want to know when Google is adding your area to Street Level View?'/><author><name>Stephen Akins</name><uri>https://profiles.google.com/106963705487836408694</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//lh6.googleusercontent.com/-OBjWBqY9XUM/AAAAAAAAAAI/AAAAAAAABww/ozuFLWf-lsE/s512-c/photo.jpg'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://4.bp.blogspot.com/-m1L8FvA9X1c/TsRZryt8WiI/AAAAAAAABCo/a6z0h7l_dY0/s72-c/kendaraan_google.jpg' height='72' width='72'/><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8927077563876229110.post-4740246072871408459</id><published>2011-07-19T11:14:00.000-07:00</published><updated>2011-12-20T13:42:41.773-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='open source'/><category scheme='http://www.blogger.com/atom/ns#' term='web application'/><category scheme='http://www.blogger.com/atom/ns#' term='free'/><category scheme='http://www.blogger.com/atom/ns#' term='software'/><category scheme='http://www.blogger.com/atom/ns#' term='xml'/><category scheme='http://www.blogger.com/atom/ns#' term='voting'/><category scheme='http://www.blogger.com/atom/ns#' term='ballot'/><category scheme='http://www.blogger.com/atom/ns#' term='democracy'/><title type='text'>Accessballot</title><content type='html'>I've completed the initial version of a English US EAC compliant paper ballot generating API.  It takes XML candidate/issue data and uses it to generate a PDF of a ballot that is 100% compliant with the US EAC ballot design standards.&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-KO41MTK_Qe0/TiXLBq1IizI/AAAAAAAAArU/CKftAKTGliw/s1600/ballot_box02.png" imageanchor="1" style="clear: left; float: right; margin-bottom: 1em; margin-right: 1em;"&gt;&lt;img border="0" height="300" src="http://2.bp.blogspot.com/-KO41MTK_Qe0/TiXLBq1IizI/AAAAAAAAArU/CKftAKTGliw/s320/ballot_box02.png" width="250" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;The application is Opensource and you can find it on Google Code at the following address:&lt;br /&gt;&lt;a href="http://code.google.com/p/accessballot/"&gt;http://code.google.com/p/accessballot/&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;You can also use the hosted version on the Accessvote.com (which is free of charge).  The following is a sample ballot generated using the free API:&lt;br /&gt;&lt;a href="http://accessvote.com/api/us_eac/?s=http://accessvote.com/sample_ballots/us_eac-sample.xml"&gt;http://accessvote.com/api/us_eac/?s=http://accessvote.com/sample_ballots/us_eac-sample.xml&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;If you would like more information on how to use it/integrate it into your own applications, you can view the project wiki at the following location:&lt;br /&gt;&lt;a href="http://code.google.com/p/accessballot/w/list"&gt;http://code.google.com/p/accessballot/w/list&lt;/a&gt;.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8927077563876229110-4740246072871408459?l=stephenakins.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://stephenakins.blogspot.com/feeds/4740246072871408459/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stephenakins.blogspot.com/2011/07/accessballot.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8927077563876229110/posts/default/4740246072871408459'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8927077563876229110/posts/default/4740246072871408459'/><link rel='alternate' type='text/html' href='http://stephenakins.blogspot.com/2011/07/accessballot.html' title='Accessballot'/><author><name>Stephen Akins</name><uri>https://profiles.google.com/106963705487836408694</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//lh6.googleusercontent.com/-OBjWBqY9XUM/AAAAAAAAAAI/AAAAAAAABww/ozuFLWf-lsE/s512-c/photo.jpg'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://2.bp.blogspot.com/-KO41MTK_Qe0/TiXLBq1IizI/AAAAAAAAArU/CKftAKTGliw/s72-c/ballot_box02.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8927077563876229110.post-6367752788165957299</id><published>2011-01-26T00:09:00.000-08:00</published><updated>2011-01-31T11:47:21.708-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Fluid Design'/><category scheme='http://www.blogger.com/atom/ns#' term='CSS'/><category scheme='http://www.blogger.com/atom/ns#' term='CSS-P'/><category scheme='http://www.blogger.com/atom/ns#' term='jQuery'/><title type='text'>Making DIVs, using the CSS "Float Left" property, all have uniform heights; automatically adjusting to match the tallest DIV in the row</title><content type='html'>&lt;h3&gt;Update: I modified the code so it would work with the onresize() event&lt;/h3&gt;&lt;br /&gt;Would you believe that was the best title I could come up with?  Honestly, if you can think of a better one shoot me a line - I want to hear from you.&lt;br /&gt;&lt;br /&gt;Okay, I'm a big fan of Fluid Design.  I love to design websites/webapps that flow into the space available, from giant screened desktops to mobile phones, and always look like they were designed with exactly that screen size in mind.  &lt;br /&gt;&lt;br /&gt;One thing that CSS-P brought to web design that didn't exist back when we were using &lt;i&gt;invisible nested tables&lt;/i&gt; to do webpage layout is the ability to have rows with a dynamic number of "columns".  So, for instance, if you are showing rows of thumbnails, you can have more thumbnails on a row if the browser window is wide, and fewer if the window is narrower.  &lt;br /&gt;&lt;br /&gt;The method for doing this is to cause items to "Float Left".  Usually this means putting your content (say a thumbnail and label) into a DIV; and giving the DIV the CSS property of:&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;div.column&lt;br /&gt;{&lt;br /&gt;   float:left;&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;If the above were added to a page's stylesheet, the DIVs with the class "column" assigned to them would stack to the right of the item before it on the page, and if there are more than one item, each will stack to the right of the previous DIV until there is no more room on that line, and then the next will appear below that row, starting a new one.&lt;br /&gt;&lt;br /&gt;If the DIVs do not share the same height, each row will have their tops all lined up, but will have bottoms extend downwards as much as they need to to accommodate the content they contain.  And the next row, which also has all the tops lined up, will appear just below the bottom of the DIV, from the previous row, that was "tallest".  Like this:&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/_ei3QL9_U_fs/TT_MjVILioI/AAAAAAAAAmI/2YA10g32Bn0/s1600/irregular_height_columns.gif" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" height="276" src="http://1.bp.blogspot.com/_ei3QL9_U_fs/TT_MjVILioI/AAAAAAAAAmI/2YA10g32Bn0/s320/irregular_height_columns.gif" width="320" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;(So above the second row begins below the "tallest" item in the first row, which is &lt;b&gt;c)&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;br /&gt;This isn't bad, but many designers would rather have some uniformity to the DIV sizes and have, say, the container backgrounds and borders fill in the spaces below &lt;b&gt;a&lt;/b&gt;, &lt;b&gt;b&lt;/b&gt;, and &lt;b&gt;d&lt;/b&gt;. &amp;nbsp;We can do that, of course, by giving each container a specified height, instead of letting the content drive the height, but then we would have to make all the containers as tall as they are likely ever to get which isn't prefect either.&amp;nbsp; &lt;br /&gt;&lt;br /&gt;Ideally, &lt;i&gt;I think&lt;/i&gt;, it would be best to have all of the DIVs in a row be all be the same height, but only be as tall as is actually needed to accommodate the tallest DIV in the row. &amp;nbsp;Like this:&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/_ei3QL9_U_fs/TT_PrEoBapI/AAAAAAAAAmM/iSGf6lA9iGY/s1600/regular_height_columns.gif" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" height="276" src="http://3.bp.blogspot.com/_ei3QL9_U_fs/TT_PrEoBapI/AAAAAAAAAmM/iSGf6lA9iGY/s320/regular_height_columns.gif" width="320" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;(in the above example, the DIVs in the first row are as tall as the tallest DIV in the row, which is c, and all the DIVs in the second row are as tall as f, which is the tallest DIV in that row)&lt;br /&gt;&lt;br /&gt;Well, with a bit of jQuery you can have your DIVs all adjust their heights so they are uniform across the entire row.  You will have to add jQuery to your page (see &lt;a href="http://docs.jquery.com/How_jQuery_Works"&gt;How jQuery Works&lt;/a&gt;) and add the following Javascript to your page:&lt;br /&gt;&lt;pre class="prettyprint"&gt;var currentTallest = 0;&lt;br /&gt;var currentRowStart = 0;&lt;br /&gt;var rowDivs = new Array();&lt;br /&gt;&lt;br /&gt;function setConformingHeight(el, newHeight) {&lt;br /&gt; // set the height to something new, but remember the original height in case things change&lt;br /&gt; el.data("originalHeight", (el.data("originalHeight") == undefined) ? (el.height()) : (el.data("originalHeight")));&lt;br /&gt; el.height(newHeight);&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;function getOriginalHeight(el) {&lt;br /&gt; // if the height has changed, send the originalHeight&lt;br /&gt; return (el.data("originalHeight") == undefined) ? (el.height()) : (el.data("originalHeight"));&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;function columnConform() {&lt;br /&gt;&lt;br /&gt; // find the tallest DIV in the row, and set the heights of all of the DIVs to match it.&lt;br /&gt; $('div.column').each(function(index) {&lt;br /&gt;&lt;br /&gt;  if(currentRowStart != $(this).position().top) {&lt;br /&gt;&lt;br /&gt;   // we just came to a new row.  Set all the heights on the completed row&lt;br /&gt;   for(currentDiv = 0 ; currentDiv &lt; rowDivs.length ; currentDiv++) setConformingHeight(rowDivs[currentDiv], currentTallest);&lt;br /&gt;&lt;br /&gt;   // set the variables for the new row&lt;br /&gt;   rowDivs.length = 0; // empty the array&lt;br /&gt;   currentRowStart = $(this).position().top;&lt;br /&gt;   currentTallest = getOriginalHeight($(this));&lt;br /&gt;   rowDivs.push($(this));&lt;br /&gt;&lt;br /&gt;  } else {&lt;br /&gt;&lt;br /&gt;   // another div on the current row.  Add it to the list and check if it's taller&lt;br /&gt;   rowDivs.push($(this));&lt;br /&gt;   currentTallest = (currentTallest &lt; getOriginalHeight($(this))) ? (getOriginalHeight($(this))) : (currentTallest);&lt;br /&gt;&lt;br /&gt;  }&lt;br /&gt;  // do the last row&lt;br /&gt;  for(currentDiv = 0 ; currentDiv &lt; rowDivs.length ; currentDiv++) setConformingHeight(rowDivs[currentDiv], currentTallest);&lt;br /&gt;&lt;br /&gt; });&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;$(window).resize(function() {&lt;br /&gt; columnConform();&lt;br /&gt;});&lt;br /&gt;&lt;br /&gt;$(document).ready(function() {&lt;br /&gt; columnConform();&lt;br /&gt;});&lt;br /&gt;&lt;/pre&gt;(the above code assumes you have assigned the CSS class called "column" to each of the DIVs that are "Floating Left")&lt;br /&gt;&lt;br /&gt;If you can read Javascript/jQuery, you will find the code comments will sufficiently explain how it works; but in a nutshell: The script goes through each DIV, determining which are on the same row by comparing the X value of the top of each container. &amp;nbsp;It keeps track of which is the tallest, and then sets the height for each DIV in the row based on that value.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8927077563876229110-6367752788165957299?l=stephenakins.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://stephenakins.blogspot.com/feeds/6367752788165957299/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stephenakins.blogspot.com/2011/01/uniform-div-heights-for-liquid-css-p.html#comment-form' title='11 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8927077563876229110/posts/default/6367752788165957299'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8927077563876229110/posts/default/6367752788165957299'/><link rel='alternate' type='text/html' href='http://stephenakins.blogspot.com/2011/01/uniform-div-heights-for-liquid-css-p.html' title='Making DIVs, using the CSS &quot;Float Left&quot; property, all have uniform heights; automatically adjusting to match the tallest DIV in the row'/><author><name>Stephen Akins</name><uri>https://profiles.google.com/106963705487836408694</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//lh6.googleusercontent.com/-OBjWBqY9XUM/AAAAAAAAAAI/AAAAAAAABww/ozuFLWf-lsE/s512-c/photo.jpg'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://1.bp.blogspot.com/_ei3QL9_U_fs/TT_MjVILioI/AAAAAAAAAmI/2YA10g32Bn0/s72-c/irregular_height_columns.gif' height='72' width='72'/><thr:total>11</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8927077563876229110.post-3847249475294584252</id><published>2010-12-09T12:27:00.000-08:00</published><updated>2011-10-04T08:38:17.600-07:00</updated><title type='text'>Link Tracking using jQuery and the Google Analytics Asynchronous Tracking Code</title><content type='html'>The Google Analytics tracking code is triggered when pages that include the code are called. But what about when users request PDFs and other documents that aren't web pages or click on links to external web pages on other websites? &amp;nbsp;How do we track these events also?&lt;br /&gt;&lt;br /&gt;Here is some jQuery you can add to your pages that will allow you to track when people click on these links. &amp;nbsp;You will have to add the Google Analytics Asynchronous Tracking code (found here:&amp;nbsp;&lt;a href="http://code.google.com/apis/analytics/docs/tracking/asyncTracking.html"&gt;http://code.google.com/apis/analytics/docs/tracking/asyncTracking.html&lt;/a&gt;) and add jQuery to your page (see here:&amp;nbsp;&lt;a href="http://docs.jquery.com/How_jQuery_Works"&gt;http://docs.jquery.com/How_jQuery_Works&lt;/a&gt;) to use this code:&lt;a href="http://2.bp.blogspot.com/_ei3QL9_U_fs/TQI81nytThI/AAAAAAAAAkw/FlFQjAWaujc/s1600/Untitleddrawing.png" imageanchor="1" style="clear: right; float: right; margin-bottom: 1em; margin-left: 1em;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/_ei3QL9_U_fs/TQI81nytThI/AAAAAAAAAkw/FlFQjAWaujc/s1600/Untitleddrawing.png" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;$(document).ready(function(){&lt;br /&gt;&lt;br /&gt; $('a').click(function(){&lt;br /&gt;&lt;br /&gt;  href = ($(this).attr('href') == undefined) ? ('') : ($(this).attr('href'));&lt;br /&gt;  href_lower = href.toLowerCase();&lt;br /&gt;  &lt;br /&gt;  if(href_lower.substr(-3) == "pdf" || href_lower.substr(-3) == "xls" || href_lower.substr(-3) == "doc") {&lt;br /&gt;   _gaq.push(['_trackEvent', 'document', 'download', href_lower.substr(-3), $(this).text()]);&lt;br /&gt;   _gaq.push(['_trackPageview', href]);&lt;br /&gt;  }&lt;br /&gt; &lt;br /&gt;  if(href_lower.substr(0, 4).toLowerCase() == "http") {&lt;br /&gt;   _gaq.push(['_trackEvent', 'external_link', 'open', 'href', $(this).text()]);&lt;br /&gt;   _gaq.push(['_trackPageview', href]);&lt;br /&gt;  }&lt;br /&gt;  &lt;br /&gt;  if ($(this).attr('target') != undefined &amp;&amp; $(this).attr('target').toLowerCase() != '_blank' &amp;&amp; href_lower.substr(0,10) != "javascript") {&lt;br /&gt;   setTimeout(function() { location.href = href; }, 200);&lt;br /&gt;   return false;&lt;br /&gt;  }&lt;br /&gt;&lt;br /&gt; });&lt;br /&gt;&lt;br /&gt;});&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;So, the example above will track clicks on internal links opening files with the extensions &lt;i&gt;pdf&lt;/i&gt;, &lt;i&gt;xls&lt;/i&gt;, and &lt;i&gt;doc&lt;/i&gt;&amp;nbsp;and will&amp;nbsp;categorize&amp;nbsp;them in your Google Analytics event tracking reports as "documents". &amp;nbsp;It will also track outgoing links (links with URLs beginning with "http") and categorize them as "external_links". &lt;br /&gt;&lt;br /&gt;If you need more categories, add more if statements. &amp;nbsp;In the method above I create one click event that is attached to every link on the page, rather than just attaching specific events to the links that require them. &amp;nbsp;If a link satisfies more than one criteria (e.g. it's a link both to a PDF, and it's on an external website) then both events will be created. &amp;nbsp;In the end I thought this approach was probably more customizable and efficient overall, especially if there are several event categories.&lt;br /&gt;&lt;br /&gt;The setTimeout bit is to allow links that replace the page content (i.e. regular links) a moment before calling the page; so that the GA function has time to execute before opening the page.  Be careful when using plugins like Fancybox; add links to such plugins to the exception list in the condition wrapped around the setTimeout function as these functions remove the href attribute and the combination of that and the setTimeout can cause unpredictable behaviour.&lt;br /&gt;&lt;br /&gt;You may want to track events for clicks made by specific links like the nav or featured content. &amp;nbsp;You can achieve this using a similar method:&lt;br /&gt;&lt;br /&gt;&lt;blockquote&gt;&lt;br /&gt;$("&lt;span class="Apple-style-span" style="background-color: #fff2cc;"&gt;#main_menu&lt;/span&gt; a").mouseup(function(){&lt;br /&gt;&lt;span class="Apple-tab-span" style="white-space: pre;"&gt; &lt;/span&gt;if($(this).attr('href').toLowerCase() != "javascript:;" &amp;amp;&amp;amp; $(this).attr('href') != "#") {&lt;br /&gt;&lt;span class="Apple-tab-span" style="white-space: pre;"&gt;  &lt;/span&gt;_gaq.push(['_trackEvent', '&lt;span class="Apple-style-span" style="background-color: #fff2cc;"&gt;main_menu&lt;/span&gt;', 'open', $(this).attr('href')]);&lt;br /&gt;&lt;span class="Apple-tab-span" style="white-space: pre;"&gt; &lt;/span&gt;}&lt;br /&gt;});&lt;br /&gt;&lt;br /&gt;&lt;/blockquote&gt;Change the ID in the &lt;span class="Apple-style-span" style="background-color: #fff2cc;"&gt;yellow&lt;/span&gt; to whatever you call the container that your menu is kept in. &amp;nbsp;I added the check for &lt;i&gt;JavaScript:;&lt;/i&gt; and &lt;i&gt;#&lt;/i&gt; just in case there are links that don't open pages (e.g. links that reveal further options).&lt;br /&gt;&lt;br /&gt;Leave comments if you have other suggestions for mixing jQuery and GA for better tracking.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8927077563876229110-3847249475294584252?l=stephenakins.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://stephenakins.blogspot.com/feeds/3847249475294584252/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stephenakins.blogspot.com/2010/12/link-tracking-using-jquery-and-google.html#comment-form' title='8 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8927077563876229110/posts/default/3847249475294584252'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8927077563876229110/posts/default/3847249475294584252'/><link rel='alternate' type='text/html' href='http://stephenakins.blogspot.com/2010/12/link-tracking-using-jquery-and-google.html' title='Link Tracking using jQuery and the Google Analytics Asynchronous Tracking Code'/><author><name>Stephen Akins</name><uri>https://profiles.google.com/106963705487836408694</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//lh6.googleusercontent.com/-OBjWBqY9XUM/AAAAAAAAAAI/AAAAAAAABww/ozuFLWf-lsE/s512-c/photo.jpg'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://2.bp.blogspot.com/_ei3QL9_U_fs/TQI81nytThI/AAAAAAAAAkw/FlFQjAWaujc/s72-c/Untitleddrawing.png' height='72' width='72'/><thr:total>8</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8927077563876229110.post-5484527725396490068</id><published>2010-12-08T09:53:00.000-08:00</published><updated>2010-12-08T10:16:15.827-08:00</updated><title type='text'>Google Nexus S Fun</title><content type='html'>I happened to catch the second clue in the Google Nexus (@googlenexus) Nexus S contest about 20 minutes or so after they posted it.  I didn't get the first puzzle; I thought I did but I was wrong.  This one I was on the right track right away.  The clue was:&lt;br /&gt;&lt;br /&gt;&lt;blockquote&gt;Puzzle Challenge #2: There is more than meets the eye at http://goo.gl/MKoc8. You'll find the answer in a tweet.&lt;/blockquote&gt;&lt;br /&gt;So the FIRST thing I did when I went to the page was look at the source code.  At the very bottom of the page I found this:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;.&lt;br /&gt;                               I                I                               &lt;br /&gt;                                I      ++      I                                &lt;br /&gt;                                 IIIIIIIIIIIIII                                 &lt;br /&gt;                               IIIIIIIIIIIIIIIIII                               &lt;br /&gt;                             IIIIIIIIIIIIIIIIIIIIII                             &lt;br /&gt;                            IIII   IIIIIIIIII  IIIII                            &lt;br /&gt;                           IIIIIIIIIIIIIIIIIIIIIIIIII                           &lt;br /&gt;                          IIIIIIIIIIIIIIIIIIIIIIIIIII                           &lt;br /&gt;                          IIIIIIIIIIIIIIIIIIIIIIIIIIII                          &lt;br /&gt;                          IIIIIIIIIIIIIIIIIIIIIIIIIIII                          &lt;br /&gt;                    IIII  IIIIIIIIIIIIIIIIIIIIIIIIIIII  IIII                    &lt;br /&gt;                   IIIIII IIIIIIIIIIIIIIIIIIIIIIIIIIII IIIIII                   &lt;br /&gt;                   IIIIII IIIIIIIIIIIIIIIIIIIIIIIIIIII IIIIII                   &lt;br /&gt;                   IIIIII 0110100001110100011101000111 IIIIII                   &lt;br /&gt;                   IIIIII 0000001110100010111100101111 IIIIII                   &lt;br /&gt;                   IIIIII 0110011101101111011011110010 IIIIII                   &lt;br /&gt;                   IIIIII 1110011001110110110000101111 IIIIII                   &lt;br /&gt;                   IIIIII 0110101001010000010011110100 IIIIII                   &lt;br /&gt;                   IIIIII 100101101100IIIIIIIIIIIIIIII IIIIII                   &lt;br /&gt;                   IIIIII IIIIIIIIIIIIIIIIIIIIIIIIIIII IIIIII                   &lt;br /&gt;                   IIIIII IIIIIIIIIIIIIIIIIIIIIIIIIIII IIIIII                   &lt;br /&gt;                   +IIII  IIIIIIIIIIIIIIIIIIIIIIIIIIII  IIII:                   &lt;br /&gt;                          IIIIIIIIIIIIIIIIIIIIIIIIIIII                          &lt;br /&gt;                          IIIIIIIIIIIIIIIIIIIIIIIIIIII                          &lt;br /&gt;                           IIIIIIIIIIIIIIIIIIIIIIIIII                           &lt;br /&gt;                                IIIIII    IIIIII                                &lt;br /&gt;                                IIIIII    IIIIII                                &lt;br /&gt;                                IIIIII    IIIIII                                &lt;br /&gt;                                IIIIII    IIIIII                                &lt;br /&gt;                                IIIIII    IIIIII                                &lt;br /&gt;                                IIIII,    IIIIII                                &lt;br /&gt;                                  I:        ~I                     &lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;I noticed the binary around the android's belly so I translated it into ascii text and got the following URL:&lt;br /&gt;&lt;br /&gt;http://goo.gl/jPOIl&lt;br /&gt;&lt;br /&gt;I went to the URL and was faced with a puzzle with a timer.  The puzzle involved rearranging letters, by swapping two neighbours, in a string so they were in the "correct" order.  You could see how many were correct at the bottom of the page.  It was pretty simple and I got the solution before the timer ran out (it took me a few seconds to recognize I was being timed).  &lt;br /&gt;&lt;br /&gt;&lt;img height="288" src="http://lh6.ggpht.com/_ei3QL9_U_fs/TP_EH86j24I/AAAAAAAAAkc/H9gUixQkhhw/Picture%201.png" width="640" /&gt;&lt;br /&gt;&lt;br /&gt;Clicking the &lt;i&gt;Tweet your victory!&lt;/i&gt;&amp;nbsp;button opened a web tweet box with the message:&lt;br /&gt;&lt;br /&gt;&lt;blockquote&gt;@googlenexus Run, run, as fast as you can! You can't catch me, I'm the Gingerbread Android!&lt;/blockquote&gt;&lt;br /&gt;&lt;br /&gt;I hope it's not against the rules to post the solution; I looked quickly through the legalese and didn't see anything forbidding it.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8927077563876229110-5484527725396490068?l=stephenakins.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://stephenakins.blogspot.com/feeds/5484527725396490068/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stephenakins.blogspot.com/2010/12/google-nexus-s-fun.html#comment-form' title='7 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8927077563876229110/posts/default/5484527725396490068'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8927077563876229110/posts/default/5484527725396490068'/><link rel='alternate' type='text/html' href='http://stephenakins.blogspot.com/2010/12/google-nexus-s-fun.html' title='Google Nexus S Fun'/><author><name>Stephen Akins</name><uri>https://profiles.google.com/106963705487836408694</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//lh6.googleusercontent.com/-OBjWBqY9XUM/AAAAAAAAAAI/AAAAAAAABww/ozuFLWf-lsE/s512-c/photo.jpg'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://lh6.ggpht.com/_ei3QL9_U_fs/TP_EH86j24I/AAAAAAAAAkc/H9gUixQkhhw/s72-c/Picture%201.png' height='72' width='72'/><thr:total>7</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8927077563876229110.post-2270509959050575666</id><published>2010-07-27T16:49:00.000-07:00</published><updated>2010-07-27T17:06:52.676-07:00</updated><title type='text'>Getting my Images on Canvas</title><content type='html'>I've been rendering some of my models to print them on canvas recently.&lt;br /&gt;&lt;br /&gt;&lt;a href="http://goo.gl/photos/oPJH" imageanchor="1" style="clear:right;margin-bottom:1em;margin-left:1em"&gt;&lt;img border="0" src="http://lh6.ggpht.com/_ei3QL9_U_fs/TE9uDo1M5EI/AAAAAAAAAcI/Ey7WlSBhh9o/s512/w3.jpeg"&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;I had to wait to buy a new computer to render my stuff.  Can you believe it though?  I'm already upgrading.  You'd think 8 gb would be lots of RAM, wouldn't you?  My new RAM is in the mail.&lt;br /&gt;&lt;br /&gt;I transferred one of the images to canvas already.  It's only a little over 2' by a little over 4' but it cost me nearly $300 to print after taxes!  My neighbour is going to stretch it onto a frame for me.  I'm pretty excited.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8927077563876229110-2270509959050575666?l=stephenakins.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://stephenakins.blogspot.com/feeds/2270509959050575666/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stephenakins.blogspot.com/2010/07/getting-my-images-on-canvas.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8927077563876229110/posts/default/2270509959050575666'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8927077563876229110/posts/default/2270509959050575666'/><link rel='alternate' type='text/html' href='http://stephenakins.blogspot.com/2010/07/getting-my-images-on-canvas.html' title='Getting my Images on Canvas'/><author><name>Stephen Akins</name><uri>https://profiles.google.com/106963705487836408694</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//lh6.googleusercontent.com/-OBjWBqY9XUM/AAAAAAAAAAI/AAAAAAAABww/ozuFLWf-lsE/s512-c/photo.jpg'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://lh6.ggpht.com/_ei3QL9_U_fs/TE9uDo1M5EI/AAAAAAAAAcI/Ey7WlSBhh9o/s72-c/w3.jpeg' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8927077563876229110.post-7659592564175427822</id><published>2009-10-28T10:10:00.001-07:00</published><updated>2010-12-22T14:34:34.790-08:00</updated><title type='text'>Google Android Desktop Image</title><content type='html'>Here is a copy of a Google Android desktop image I made in Maya.  It's really simple; didn't take me long, but I thought other people may enjoy it too.  Let me know if you think it could be made better in some way; I love suggestions.&lt;br /&gt;&lt;br /&gt;&lt;a id="bucf" href="http://docs.google.com/File?id=d43qvtp_111gmpcgdcx_b" target="_blank"&gt;&lt;img style="width: 320px; height: 200px; float: left; margin-left: 0pt; margin-right: 1em;" src="http://docs.google.com/File?id=d43qvtp_111gmpcgdcx_b" /&gt;&lt;/a&gt;1680x1050&lt;br clear="all"&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;a id="y98n" href="http://docs.google.com/File?id=d43qvtp_113v5778sgz_b" target="_blank"&gt;&lt;img style="width: 320px; height: 256px; float: left; margin-left: 0pt; margin-right: 1em;" src="http://docs.google.com/File?id=d43qvtp_113v5778sgz_b" /&gt;&lt;/a&gt;1280x1024&lt;br clear="all"&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;a id="y98n" href="http://docs.google.com/File?id=d43qvtp_123dp8chtt4_b" target="_blank"&gt;&lt;img style="width: 320px; height: 256px; float: left; margin-left: 0pt; margin-right: 1em;" src="http://docs.google.com/File?id=d43qvtp_123dp8chtt4_b" /&gt;&lt;/a&gt;1280x1024&lt;br clear="all"&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;a id="m2qf" href="http://docs.google.com/File?id=d43qvtp_114dcnqd6f6_b" target="_blank"&gt;&lt;img style="width: 320px; height: 240px; float: left; margin-left: 0pt; margin-right: 1em;" src="http://docs.google.com/File?id=d43qvtp_114dcnqd6f6_b" /&gt;&lt;/a&gt;1024x768&lt;br /&gt;&lt;div id="g:9i" style="text-align: left;clear: left;"&gt;&lt;br /&gt;&lt;p&gt;btw, I lit the scene using a "light probe" from &lt;a href="http://ict.debevec.org/~debevec/Probes/"&gt;Paul Debevec's authoritative page&lt;/a&gt; on the topic.  The exact light map that I used is of Galileo's Tomb and you can see the reflection of light from the windows that light the room in the Android's glossy head. ;).  If anyone would like my Maya model, let me know and I'll post it here.&lt;/p&gt;&lt;/div&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8927077563876229110-7659592564175427822?l=stephenakins.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://stephenakins.blogspot.com/feeds/7659592564175427822/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stephenakins.blogspot.com/2009/10/here-is-copy-of-google-android-de.html#comment-form' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8927077563876229110/posts/default/7659592564175427822'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8927077563876229110/posts/default/7659592564175427822'/><link rel='alternate' type='text/html' href='http://stephenakins.blogspot.com/2009/10/here-is-copy-of-google-android-de.html' title='Google Android Desktop Image'/><author><name>Stephen Akins</name><uri>https://profiles.google.com/106963705487836408694</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//lh6.googleusercontent.com/-OBjWBqY9XUM/AAAAAAAAAAI/AAAAAAAABww/ozuFLWf-lsE/s512-c/photo.jpg'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8927077563876229110.post-3118839109303255825</id><published>2009-06-10T11:12:00.001-07:00</published><updated>2009-06-10T20:30:17.830-07:00</updated><title type='text'>Pagerank Experiment</title><content type='html'>&lt;p&gt;Yes, "PageRank" is effectively meaningless now.&amp;nbsp; (it's offical because I say so ;))&lt;/p&gt;&lt;p&gt;I had been reading people talking about how what is most commonly referred to as Pagerank doesn't exist any more and that Pagerank tools were pretty well useless.  However, I didn't want to take their word for it without verifying it for myself.&amp;nbsp; So a little while ago I created an experiment to test how much PageRank actually reflects where one is positioned in a google search engine results page.&amp;nbsp; When I refer to "Pagerank" I mean the value (between 0 and 10) that is associated with a site or web page and that is meant to be a reflection of how popular content is based on who is pointing to that content (google "pagerank tool" to get a list of a tools you can use online to check the pagerank of any site).&lt;/p&gt;&lt;p&gt;In my experiment I took a domain that had no pagerank (gumbozumbo.com) and I put some content on the site (namely a Flash/Software chess clock that I made some time ago and I now offer for free on the gumbozumbo.com domain).&amp;nbsp; In my experiment I targetted the search term "free chess clock", which was easy to do because I dedicated the entire domain to providing/promoting this free application and the light content was very keyword targetted (for the record, I did nothing underhanded or illegitimate here.&amp;nbsp; I offered real and valuable content and did not misrepresent it in any way).&amp;nbsp; &lt;/p&gt;&lt;p&gt;Now, if you use any PageRank checker on the gumbozumbo.com domain name, you will see that it has a reported PageRank of Zero (even though I solicited a &lt;i&gt;few&lt;/i&gt; very relevant links to the content).&lt;br&gt;&lt;img style="width: 344px; height: 107px;" src="http://docs.google.com/File?id=d43qvtp_39fc55cddg_b"&gt;&lt;/p&gt;&lt;p&gt;&lt;font size="1"&gt;&lt;span style="color: rgb(102, 102, 102);"&gt;(ranking courtesy of &lt;/span&gt;&lt;a style="color: rgb(102, 102, 102);" title="Page Rank Checker" target="_blank" href="http://www.prchecker.info/check_page_rank.php" id="mjst"&gt;prchecker.info&lt;/a&gt;&lt;span style="color: rgb(102, 102, 102);"&gt;)&lt;/span&gt;&lt;/font&gt;&lt;/p&gt;&lt;p&gt;However, if you search for "free chess clock" in Google, you will see that it ranks &lt;b&gt;first&lt;/b&gt;; ahead of free chess clocks on other domains that have high Pagerank.&lt;/p&gt;&lt;img style="width: 466px; height: 59px;" src="http://docs.google.com/File?id=d43qvtp_40fp33bgc8_b"&gt;&lt;p&gt;Clearly highly focused content has a much greater affect on Google search engine rankings than similar content on sites that are not as focused, even if those sites have high Pagerank.&lt;/p&gt;&lt;p&gt;I hope discussing the gumbozumbo domain in this blog isn't going to throw the result off (grin).&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8927077563876229110-3118839109303255825?l=stephenakins.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://stephenakins.blogspot.com/feeds/3118839109303255825/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stephenakins.blogspot.com/2009/06/gumbozumbo-pagerank-experiment.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8927077563876229110/posts/default/3118839109303255825'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8927077563876229110/posts/default/3118839109303255825'/><link rel='alternate' type='text/html' href='http://stephenakins.blogspot.com/2009/06/gumbozumbo-pagerank-experiment.html' title='Pagerank Experiment'/><author><name>Stephen Akins</name><uri>https://profiles.google.com/106963705487836408694</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//lh6.googleusercontent.com/-OBjWBqY9XUM/AAAAAAAAAAI/AAAAAAAABww/ozuFLWf-lsE/s512-c/photo.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8927077563876229110.post-2556448186097702381</id><published>2009-06-02T19:28:00.000-07:00</published><updated>2010-05-06T10:35:13.841-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Server Monitor'/><category scheme='http://www.blogger.com/atom/ns#' term='Google Spreadsheet'/><category scheme='http://www.blogger.com/atom/ns#' term='Google Docs'/><category scheme='http://www.blogger.com/atom/ns#' term='CodeProject'/><title type='text'>Google Spreadsheet Server Monitoring</title><content type='html'>Monitor your websites using a Google Spreadsheet and some PHP&lt;h2&gt;What do you mean the website is down? &lt;/h2&gt;So, your client calls you and tells you that the contact form on their website isn't working.  One of their customers called them to tell them, and it looks like it's been down for awhile.  Your client wonders why you're the last one to know - why do they pay for maintenance anyway?&lt;br /&gt;&lt;br /&gt;This is the situation we faced too many times, years ago, and why we started monitoring our servers.  We quickly went from being the last one to know when a website stopped working properly, to being the first.  We also began collecting a lot of valuable data about the quality of our web hosting services.  Further more, we did a kind of testing that really meant something real to us.  Instead of just checking to see if a server was up we created "sensors", that we placed on client websites, and would do things like make a simple call to the website's actual database, emulating what the website did as closely as possible.  This told us more about what the actual user experience was like, and about whether our servers were doing what they were supposed to, than just pinging a server to see if it was up.&lt;br /&gt;&lt;br /&gt;A few weeks ago I was thinking about server monitor software and I realized that most of the mechanics behind the software is actually pretty simple; the more difficult part is the reporting side of things.  Fortunately &lt;a title="This Spreadsheet application is part of a suite of applications Google has available for free from your web browser" target="_blank" href="http://docs.google.com/support/bin/topic.py?topic=15115" id="gxp7"&gt;Google Spreadsheet&lt;/a&gt;  has the ability to read data from external sources and wonderful graphs and gadgets (like speedometers) for translating the server monitoring data; and to display our information meaningfully and handily.  I admit I'm a big fan of the Google docs webapps and I decided, mostly for fun, to try my hand at writing a server monitor with a little PHP and one Google &lt;span style="background-color: rgb(255, 255, 255);"&gt;&lt;a title="Defininition of a Spreadsheet" target="_blank" href="http://en.wikipedia.org/wiki/Spreadsheet" id="kxct"&gt;spreadsheet&lt;/a&gt;&lt;/span&gt;.&lt;br /&gt;&lt;br /&gt;&lt;h2&gt;Google Docs to the Rescue?&lt;/h2&gt;I decided to make a project out of building a server monitor that used a Google Spreadsheet for its front end.  I was right in that the core was quite simple, but I admit I added a few unforeseen yet indispensable "enhancements" along the way (like data "archiving").  It worked (and was fun to do) so I decided to write a blog about how it works and also showing people how they may do something like it for themselves (and hopefully inspire people to create things like it - I may also do a series on other things that might be done like search engine ranking reports, etc).  I provide all my code here, and instructions for creating the spreadsheet.&lt;br /&gt;&lt;br /&gt;I started by defining what I wanted it to do, partly inspired by the kinds of things I know I can do with Google Docs.  This was my list:&lt;br /&gt;&lt;ol&gt;&lt;li&gt;It would send out an email/sms notification if something went down&lt;/li&gt;&lt;li&gt;It would send out an email/sms if it went back up again&lt;/li&gt;&lt;li&gt;I could view the status of all the sensors &lt;/li&gt;&lt;li&gt;I could view detailed history for any single sensor &lt;/li&gt;&lt;li&gt;It would email me a daily report&lt;/li&gt;&lt;li&gt;I could share the &lt;sup&gt;&lt;a href="http://www.blogger.com/post-edit.g?blogID=8927077563876229110&amp;amp;postID=2556448186097702381#FOOTNOTE-1"&gt;1&lt;/a&gt;&lt;/sup&gt;live data with other people, and publish it back out to the Internet (again, live data)&lt;/li&gt;&lt;/ol&gt;Well, It worked out rather well.  The only real drawback is it's not as immediate as I'd have liked (I want data updated by the second if I can get it).  However, Google docs isn't going to poll your datasource every second (and for good reason!) so immediate updates aren't going to happen.  However you can force an update if you need to and it refreshes often enough, I think, to stay quite useful.&lt;br /&gt;&lt;br /&gt;Below I describe how you can make one for yourself.  I'm not sure if I need to say this, but:  &lt;i&gt;I did this project and wrote this blog to amuse myself; and I provide this information here for your own benefit/amusement.  It's up to you whether or not it's as dependable as is you want a server monitor to be and I'm not responsible if it doesn't work as well as you think it should (nor is it Google's)&lt;/i&gt;.&lt;br /&gt;&lt;br /&gt;&lt;hr class="pb"&gt;&lt;h2&gt;What you'll need: &lt;/h2&gt;You'll need a Google account (of course) and a server (I used a shared Linux server at &lt;a title="LunarPages Web Hosting" target="_blank" href="http://www.lunarpages.com/" id="m7sg"&gt;LunarPages&lt;/a&gt;) that &lt;i&gt;&lt;b&gt;isn't&lt;/b&gt;&lt;/i&gt; on a webserver you are monitoring.  You'll have to write some server side script (I used PHP) and you'll need a database (I used MySQL).  The sensors you create will be in whatever you use on your website currently (I show a couple of examples further on).  &lt;b&gt;You will also have to add two jobs to the &lt;a title="Cron is a time-based job scheduler in Unix-like computer operating systems" target="_blank" href="http://en.wikipedia.org/wiki/Cron" id="mma0"&gt;cron&lt;/a&gt;&lt;/b&gt; so you'll need to make sure you have permission to create cron jobs (most of our hosting providers provide an interface for creating cron jobs in their control panel).&lt;br /&gt;&lt;br /&gt;&lt;h2&gt;How it Works &lt;/h2&gt;&lt;a id="bwtd" href="http://docs.google.com/File?id=d43qvtp_3dc7wj5hn_b" target="_blank"&gt;&lt;img style="float: right; margin-left: 1em; width: 442px; margin-right: 0pt; height: 598px;" src="http://docs.google.com/File?id=d43qvtp_3dc7wj5hn_b" /&gt;&lt;/a&gt;The basic design has a group of small "things" (scripts and worksheets) working together to make it all work.&lt;br /&gt;&lt;br /&gt;A cron job calls a script that tests all the sensors, and sends out notifications if necessary.  The results are put in a database.  The spreadsheet populates itself with the data from the database by calling some scripts, which pass back the data using &lt;a title="Comma Separated Values" target="_blank" href="http://en.wikipedia.org/wiki/Comma-separated_values" id="q4nj"&gt;CSV&lt;/a&gt;, and the spreadsheet uses that data to create all of our fancy charts and graphs.  The settings for the application (e.g. the list of sensors) are also stored in the spreadsheet and the PHP scripts use that information to determine which sensors to call, etc.  Finally a daily script sends out a summary email report and "compresses" old data to save space.&lt;br /&gt;&lt;br /&gt;&lt;h2&gt;Step 1: Creating a Sensor &lt;/h2&gt;A sensor, in our terms, is a fairly simple thing.  In fact it &lt;i&gt;could&lt;/i&gt; be an existing web page if all you want to do is see if the site's web server is serving up pages. The server monitor simply tests to see if a sensor (which is a web page) returns an error code in it's header.  With something like a &lt;i&gt;database sensor&lt;/i&gt; we will "artificially" return an &lt;a target="_blank" title="any code that is not 200 is an error code; when I throw my own error response code I use &amp;quot;500 Internal Server Error&amp;quot;" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html" id="d:60"&gt;error code in the response header&lt;/a&gt;  if there is a failure to send a query to the database.&lt;br /&gt;&lt;br /&gt;Really you can create a sensor to test anything your want, even things on an application level (e.g. test to see if a variable has an expected value).  Ideally a sensor would be able to tell that a website is acting entirely the way that it is supposed to, and short of regularly [24/7] parsing each page on the website for error codes, missing images, broken links and basically a rigorous testing régime, I think the sensor approach is about the best one can do (I would &lt;i&gt;love&lt;/i&gt; to hear that I'm wrong - please comment below if you think I am).&lt;br /&gt;&lt;br /&gt;I suggest, however, even if you are just going to test to see if a webserver is serving up pages that you make a special page, something simple, that has no linked resources, and is only called by your server monitor.  Something like this:&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;&amp;lt;html&amp;gt;&lt;br /&gt;&amp;lt;body&amp;gt;&lt;br /&gt;Web server is functioning properly&lt;br /&gt;&amp;lt;/body&amp;gt;&lt;br /&gt;&amp;lt;/html&amp;gt;&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;and call it something like /sensors/websensor.html&lt;br /&gt;&lt;br /&gt;For a database sensor I suggest making a very simple call to one of the database tables actually used by your website.  Further more, if you use an include file for connecting to your database on your website, I suggest you use the same include file your site uses.  More than once a sensor has told us of a problem when someone accidentally overwrote a connection string file with a file from a test/staging server (Human error is the biggest problem actually).&lt;br /&gt;&lt;br /&gt;A typical database sensor, written in PHP, might look something like this:&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;&amp;lt;?php&lt;br /&gt;require_once('&lt;span class="editme"&gt;../Connections/your_connection_string_include_file.php&lt;/span&gt;');&lt;br /&gt;&lt;br /&gt;$sql = "SELECT &lt;span class="editme"&gt;id&lt;/span&gt; FROM &lt;span class="editme"&gt;your_table&lt;/span&gt; LIMIT 1";&lt;br /&gt;$dbtest = mysql_query($sql, &lt;span class="editme"&gt;$database_connection&lt;/span&gt;) or die(mysql_error());&lt;br /&gt;$dbtest_totalrows = mysql_num_rows($dbtest);&lt;br /&gt;&lt;br /&gt;if($dbtest_totalrows &amp;gt; 0) {&lt;br /&gt;&amp;nbsp;?&amp;gt;Database is functioning properly&amp;lt;?php&lt;br /&gt;} else {&lt;br /&gt;&amp;nbsp;header("http_response_code: 500");&lt;br /&gt;&amp;nbsp;?&amp;gt;Database not functioning properly&amp;lt;?php&lt;br /&gt;}&lt;br /&gt;?&amp;gt;&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;Now we have a sensor that will tell you if the webserver is serving up pages AND is able to access your database.  Please note that it doesn't matter what the server side code is here, I just used PHP in my example.  We have sensors in multiple languages testing multiple aspects of our web sites; all that matters is if the sensor returns an error code in the response header or not.&lt;br /&gt;&lt;h2&gt;Step 2: Creating our Spreadsheet&lt;/h2&gt;&lt;br /&gt;&lt;div&gt;We're going to step away from our text editors/IDEs long enough to start our spreadsheet now.  We'll start by creating the firs&lt;span style="background-color: rgb(255, 255, 255);"&gt;t &lt;sup&gt;&lt;a href="http://www.blogger.com/post-edit.g?blogID=8927077563876229110&amp;amp;postID=2556448186097702381#FOOTNOTE-2"&gt;2&lt;/a&gt;&lt;/sup&gt;   worksheet in the spreadsheet, which I call the "sensor list".  I &lt;/span&gt;&lt;b style="background-color: rgb(255, 255, 255);"&gt;&lt;i&gt;strongly&lt;/i&gt;&lt;/b&gt;&lt;span style="background-color: rgb(255, 255, 255);"&gt; suggest that you build your spreadsheet just the same way I did, in terms of labels and what rows and columns data is put in, and then play with it afterward when it's all working.  It will be easiest to follow me if you start out more or less exactly as I describe.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;This first worksheet is going to do two things: It's the place where we are going to list the sensors that the application will test (our Sensors Tester script is going to read this list to determine which sensors to call).  Also, beside each sensor on the list (columns A &amp;amp; B), we're going to display the sensor's current status in terms of green, yellow and red "lights" (but we'll save that for Step 4).  Set up your spreadsheet so that it looks like this:&lt;br /&gt;&lt;br /&gt;(note that I have left the A and B columns blank for now - that's where we're going to display the sensor's status in Step 4)&lt;br /&gt;&lt;br /&gt;&lt;div style="text-align: left;" id="g8-3"&gt;&lt;a target="_blank" href="http://docs.google.com/File?id=d43qvtp_5gpv4wjnt_b"&gt;&lt;img src="http://docs.google.com/File?id=d43qvtp_5gpv4wjnt_b" style="width: 200px; height: 40.8511px;" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;The first column in our data (column C, the "Sensor ID" column), is what the server logs are keyed to.  I used this method for two reasons:  Using an integer means less storage space in the database (each log record can be associated with the appropriate sensor with as little as one byte of data) &lt;i&gt;and&lt;/i&gt; if I keyed it to an existing field (e.g. sensor name) I wouldn't be able to edit that field without orphaning the sensor's previous data.&lt;br /&gt;&lt;br /&gt;The second column is the name that will be used by the application when referring to the sensor (for instance, the email notifications will this name in their alerts).  This will also be the name used on Graphs so try not to make and of these labels long if you can avoid it.&lt;br /&gt;&lt;br /&gt;The third column is the email address that is used when sending out notifications for this server (use commas to list more than one address).  Personally I like to have the monitor text message my cell phone when a server goes down.  This can usually be done quite easily as many cell phone providers provide an email address that you can use to text your cell phone.&lt;br /&gt;&lt;br /&gt;&lt;div&gt;This is all we have to do to create new sensors.  Create the sensor itself and install it on the corresponding server, and add the sensor to this list.  Automatically the sensor will begin being scanned, we will be notified when there are issues, and we can see the sensor's status are read reports on it (as soon as there is enough data to do so).&lt;br /&gt;&lt;br /&gt;&lt;span style="font-weight:bold;"&gt;IMPORTANT:&lt;/span&gt; Please note that Google Docs doesn't update the published document immediately after making changes; rather there is a lag time between when you change the document and when it republishes it.  You can usually make the spreadsheet update the published version immediately if you turn off publishing and then turn it back on again.&lt;br /&gt;&lt;br /&gt;We also need to create another worksheet that will contain some settings.  We'll call this worksheet "Sensor and Report Settings", and in it you should create the following fields:&lt;br /&gt;&lt;br /&gt;&lt;div style="text-align: left;" id="a3kp"&gt;&lt;a target="_blank" href="http://docs.google.com/File?id=d43qvtp_6c7t8qqwm_b"&gt;&lt;img src="http://docs.google.com/File?id=d43qvtp_6c7t8qqwm_b" style="width: 160px; height: 34.1033px;" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;&lt;div&gt;The yellow cell, B2, should contain the following formula:&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;&lt;div&gt;&lt;span style="font-family:arial;"&gt;=VLookup(B1,'Sensor List'!C3:D7, 2, FALSE)&lt;/span&gt;&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;&lt;div&gt;Rather than create a separate set of worksheets for each sensor report, we're going to make &lt;i&gt;one&lt;/i&gt; set of work sheets that will display the data from any sensor.  We will control which sensor is being reported on by changing the number (sensorID) in the green box on this worksheet.  The above formula gives you a little positive feedback by displaying the name of the sensor that you've just selected&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;Before our application can read these settings we must &lt;i&gt;publish&lt;/i&gt; the spreadsheet.  At the top select &lt;i&gt;Share &amp;gt; Publish as web page...&lt;/i&gt; and you will get a dialog box where you can publish the document.  Click &lt;i&gt;Publish Now&lt;/i&gt; and then click &lt;i&gt;More publishing options&lt;/i&gt; on the bottom of the dialog box.  This is where you can create a URL for specific ranges of data.  We're going to generate two URLs, one for the sensor list on the first worksheet, and the second to for the settings on the second worksheet. In the pop-up dialog that appears when you click &lt;i&gt;More publishing options&lt;/i&gt; set the &lt;i&gt;File Format&lt;/i&gt; to &lt;i&gt;CSV,&lt;/i&gt; under &lt;i&gt;What sheets&lt;/i&gt; select &lt;i&gt;Sheet "Sensor List" only&lt;/i&gt;, and under &lt;i&gt;What cells&lt;/i&gt; enter C3:F50 (I picked 50 at random, the number only has to be higher than the last sensor on your list, but equal to or less than the number of rows currently on the spreadsheet).  Make a copy of this URL for yourself and generate one for the settings on the &lt;i&gt;Sensor and Report Settings&lt;/i&gt; worksheet (cell range B3:B6).&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;&lt;div style="text-align: left;" id="a:7u"&gt;&lt;a target="_blank" href="http://docs.google.com/File?id=d43qvtp_7cxpvv4c8_b"&gt;&lt;img src="http://docs.google.com/File?id=d43qvtp_7cxpvv4c8_b" style="width: 160px; height: 90.0465px;" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;&lt;h2&gt;Step 3: Testing our Sensors&lt;/h2&gt;&lt;br /&gt;Okay, now we have:&lt;br /&gt;&lt;ol&gt;&lt;li&gt;a list of sensors to test,&lt;/li&gt;&lt;li&gt;at least one sensor script installed on a web server that we will test.&lt;/li&gt;&lt;/ol&gt;&lt;br /&gt;Now, on the server that will be &lt;i&gt;doing&lt;/i&gt; the testing (again, the server that you are using for your testing should &lt;b&gt;not&lt;/b&gt; be on a server that you plan to test), we will add some PHP script and a MySQL database that will do the actual testing and send out notifications (if needed) and store the test results in our database.  Clearly this is the nexus of our application.&lt;br /&gt;&lt;br /&gt;Let's start by creating our database (I named my database &lt;i&gt;sensors&lt;/i&gt;).  Here is SQL for creating the tables that I am using:&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;DROP TABLE IF EXISTS `sensor_log`;&lt;br /&gt;&lt;br /&gt;CREATE TABLE IF NOT EXISTS `sensor_log` (&lt;br /&gt;&amp;nbsp;`ID` int(11) NOT NULL auto_increment,&lt;br /&gt;&amp;nbsp;`sensorID` smallint(2) NOT NULL,&lt;br /&gt;&amp;nbsp;`lag` smallint(2) NOT NULL,&lt;br /&gt;&amp;nbsp;`status_code` smallint(2) NOT NULL,&lt;br /&gt;&amp;nbsp;`created_date` timestamp NOT NULL default CURRENT_TIMESTAMP,&lt;br /&gt;&amp;nbsp;PRIMARY KEY  (`ID`),&lt;br /&gt;&amp;nbsp;KEY `sensorID` (`sensorID`),&lt;br /&gt;&amp;nbsp;KEY `status_code` (`status_code`)&lt;br /&gt;) ENGINE=MyISAM  DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;&lt;br /&gt;&lt;br /&gt;DROP TABLE IF EXISTS `sensor_log_archive`;&lt;br /&gt;&lt;br /&gt;CREATE TABLE IF NOT EXISTS `sensor_log_archive` (&lt;br /&gt;&amp;nbsp;`ID` int(11) NOT NULL auto_increment,&lt;br /&gt;&amp;nbsp;`sensorID` int(2) NOT NULL,&lt;br /&gt;&amp;nbsp;`average_lag` int(2) NOT NULL,&lt;br /&gt;&amp;nbsp;`downtime` int(3) NOT NULL,&lt;br /&gt;&amp;nbsp;`sensor_date` date NOT NULL,&lt;br /&gt;&amp;nbsp;`created_date` timestamp NOT NULL default CURRENT_TIMESTAMP,&lt;br /&gt;&amp;nbsp;PRIMARY KEY  (`ID`),&lt;br /&gt;&amp;nbsp;KEY `sensorID` (`sensorID`)&lt;br /&gt;) ENGINE=MyISAM  DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;This is a very straight forward set of tables.  The &lt;i&gt;sensor_log&lt;/i&gt; table is where we store the results of our tests, and the &lt;i&gt;sensor_log_archive&lt;/i&gt; table is where we store our "compressed" data (our script archives data by taking the aggregate results for an entire day for each sensor and inserts it into the archive table, thus we reduce the amount of space by a factor of nearly 100 to 1).&lt;br /&gt;&lt;br /&gt;Now we'll start creating our testing script.  I call this script &lt;i&gt;testallsensors.php&lt;/i&gt;. and I keep the script in a folder called &lt;i&gt;sensors&lt;/i&gt;.  The first thing you're going to need is a connection string to your database.  Again I keep this in a separate file, like always, and call the file &lt;i&gt;database_connection.php&lt;/i&gt;.  My database connection looks something like this:&lt;br /&gt;&lt;br /&gt;&lt;h4&gt;database_connection.php&lt;/h4&gt;&lt;pre class="prettyprint"&gt;&amp;lt;?php&lt;br /&gt;&lt;br /&gt;$database_hostname = "&lt;span class="editme"&gt;localhost&lt;/span&gt;";&lt;br /&gt;$database_username = "&lt;span class="editme"&gt;dbusername&lt;/span&gt;";&lt;br /&gt;$database_password = "&lt;span class="editme"&gt;dbpassword&lt;/span&gt;";&lt;br /&gt;$database_name = "&lt;span class="editme"&gt;dbname&lt;/span&gt;";&lt;br /&gt;&lt;br /&gt;$database_connection = mysql_pconnect($database_hostname, $database_username, $database_password) or trigger_error(mysql_error(),E_USER_ERROR);&lt;br /&gt;&lt;br /&gt;mysql_select_db($database_name, $database_connection);&lt;br /&gt;&lt;br /&gt;?&amp;gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;(As you can see, I connect to the server &lt;i&gt;and&lt;/i&gt; select my database in my connection script.  I rarely have an application that accesses two databases on the same server so I find this most useful)&lt;br /&gt;&lt;br /&gt;We're also going to read some data from our spreadsheet.  I use a readCSV function that I found in the comments area of one of the PHP Manual pages, that I modified very slightly for this purpose (see http://www.php.net/fgetcsv).  I keep the function in an include file as I use on multiple pages.  It looks like this:&lt;br /&gt;&lt;br /&gt;&lt;h4&gt;csv.php&lt;/h4&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;&amp;lt;?php&lt;br /&gt;&lt;br /&gt;define("CSV_Start",&amp;nbsp;0);&lt;br /&gt;define("CSV_Quoted",   1);&lt;br /&gt;define("CSV_Quoted2",  2);&lt;br /&gt;define("CSV_Unquoted", 3);&lt;br /&gt;&lt;br /&gt;function readCSV($fh, $len, $delimiter = ',', $enclosure = '"') {&lt;br /&gt;&amp;nbsp;$data = Array();&lt;br /&gt;&amp;nbsp;$fildNr = 0;&lt;br /&gt;&amp;nbsp;$state = CSV_Start;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;$data[0] = "";&lt;br /&gt;&lt;br /&gt;&amp;nbsp;do {&lt;br /&gt;&amp;nbsp;&amp;nbsp;if(($line = fgets($fh, $len)) == FALSE) return FALSE;&lt;br /&gt;&amp;nbsp;&amp;nbsp;for ($ix = 0; $ix &amp;lt; strlen($line); $ix++) {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;if ($line[$ix] == $delimiter) {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if ($state != CSV_Quoted) {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$fildNr++;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$data[$fildNr] = "";&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$state = CSV_Start;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} else {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$data[$fildNr] .= $line[$ix];&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;} elseif ($line[$ix] == $enclosure) {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if ($state == CSV_Start) {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$state = CSV_Quoted;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} elseif ($state == CSV_Quoted) {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$state = CSV_Quoted2;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} elseif ($state == CSV_Quoted2) {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$data[$fildNr] .= $line[$ix];&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$state = CSV_Quoted;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} else {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$data[$fildNr] .= $line[$ix];&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;} else {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$data[$fildNr] .= $line[$ix];&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if ($state == CSV_Quoted2) {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} elseif ($state == CSV_Start) {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$state = CSV_Unquoted;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;} while ($state == CSV_Quoted);&lt;br /&gt;&lt;br /&gt;&amp;nbsp;return $data;  &lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;?&amp;gt;&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;Okay, now we'll start &lt;i&gt;testallsensors.php&lt;/i&gt;.  The first thing we'll do is import the settings from our spreadsheet.  We'll start by ECHOing the settings to the script output so we can verify that the settings are being imported correctly.  (make sure you replace the URL so that it's the URL for the settings that you determined in Step 2).&lt;br /&gt;&lt;h4&gt;testallsensors.php (stage one)&lt;/h4&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;&amp;lt;?php&lt;br /&gt;&lt;br /&gt;require_once('database_connection.php');&lt;br /&gt;require_once('csv.php');&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;$handle = fopen("http://spreadsheets.google.com/pub?&lt;span class="editme"&gt;key=XXXXXXXXXXXXXXXX&amp;output=csv&amp;gid=2&amp;range=B3:B6&lt;/span&gt;", "r");&lt;br /&gt;&lt;br /&gt;if(($results = readCSV($handle, 1000)) == FALSE) break;&lt;br /&gt;$settings_recipients = str_replace("\n", "", $results[0]);&lt;br /&gt;&lt;br /&gt;if(($results = readCSV($handle, 1000)) == FALSE) break;&lt;br /&gt;$settings_failures = str_replace("\n", "", $results[0]);&lt;br /&gt;&lt;br /&gt;if(($results = readCSV($handle, 1000)) == FALSE) break;&lt;br /&gt;$settings_retry_minutes = str_replace("\n", "", $results[0]);&lt;br /&gt;&lt;br /&gt;if(($results = readCSV($handle, 1000)) == FALSE) break;&lt;br /&gt;$settings_archive_days = str_replace("\n", "", $results[0]);&lt;br /&gt;&lt;br /&gt;fclose($handle);&lt;br /&gt;&lt;br /&gt;echo("$settings_recipients|$settings_failures|$settings_retry_minutes|$settings_archive_days");&lt;br /&gt;&lt;br /&gt;?&amp;gt;&lt;br /&gt;&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;If everything went well, you should see the settings you entered into your spreadsheet when you run this script.  If it didn't work, check the URL and make sure that your spreadsheet is &lt;i&gt;Published&lt;/i&gt;.&lt;br /&gt;&lt;br /&gt;Lets go ahead an check out the rest of this script.  I'll discuss it in detail below:&lt;br /&gt;&lt;h4&gt;testallsensors.php (stage two)&lt;/h4&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;&amp;lt;?php&lt;br /&gt;&lt;br /&gt;require_once('database_connection.php');&lt;br /&gt;require_once('csv.php');&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;function timestamp() {&lt;br /&gt;&amp;nbsp;$mytime = split(" ", microtime(true));&lt;br /&gt;&amp;nbsp;$mytime = ($mytime[0] + $mytime[1]);&lt;br /&gt;&amp;nbsp;return round($mytime*1000);&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;// Get the maintenance settings&lt;br /&gt;&lt;br /&gt;$handle = fopen("http://spreadsheets.google.com/pub?key=&lt;span class="editme"&gt;XXXXXXXXXXXX&amp;output=csv&amp;gid=2&amp;range=B3:B6&lt;/span&gt;", "r");&lt;br /&gt;&lt;br /&gt;if(($results = readCSV($handle, 1000)) == FALSE) break;&lt;br /&gt;$settings_recipients = str_replace("\n", "", $results[0]);&lt;br /&gt;&lt;br /&gt;if(($results = readCSV($handle, 1000)) == FALSE) break;&lt;br /&gt;$settings_failures = str_replace("\n", "", $results[0]);&lt;br /&gt;&lt;br /&gt;if(($results = readCSV($handle, 1000)) == FALSE) break;&lt;br /&gt;$settings_retry_minutes = str_replace("\n", "", $results[0]);&lt;br /&gt;&lt;br /&gt;if(($results = readCSV($handle, 1000)) == FALSE) break;&lt;br /&gt;$settings_archive_days = str_replace("\n", "", $results[0]);&lt;br /&gt;&lt;br /&gt;fclose($handle);&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;// The back end could serve multiple spreadsheets each containing their own list of sensors&lt;br /&gt;// if you want to do this, add each additional sensor list to this array:&lt;br /&gt;&lt;br /&gt;$handle_list = array (&lt;br /&gt;&amp;nbsp;"http://spreadsheets.google.com/pub?key=&lt;span class="editme"&gt;XXXXXXXXXXXXXXX&amp;output=csv&amp;gid=0&amp;range=C3:F99&lt;/span&gt;"&lt;br /&gt;);&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;$retries = 0;&lt;br /&gt;$retry = false;&lt;br /&gt;&lt;br /&gt;// we keep looping until we are sure a sensor has not gone down (i.e. a sensor that was previously up has not failed $settings_failures times).&lt;br /&gt;do {&lt;br /&gt;&lt;br /&gt;&amp;nbsp;$any_failures = false;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;for($handle_index=0 ; $handle_index&amp;lt;count($handle_list) ; $handle_index++) {&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;$handle = fopen($handle_list[$handle_index], "r");&lt;br /&gt;&amp;nbsp;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;while(($results = readCSV($handle, 1000)) != FALSE) {&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;// go through each sensor on the list, test it, record it, and send out a notification if necessary&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;$sensor_sensorID = intval($results[0]);&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;$sensor_name = trim($results[1]);&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;$sensor_url = trim($results[2]);&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;$sensor_emails = trim($results[3]);&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;if(!$sensor_sensorID || $sensor_name=="" || $sensor_url=="" || $sensor_emails=="") continue;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;// if it's a retry, then find out if this sensor failed last time&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;$sensor_failed = false;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;if($retry) {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$sql = "SELECT status_code FROM sensor_log WHERE sensorID=$sensor_sensorID ORDER BY created_date DESC LIMIT 1;";&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$result = mysql_query($sql, $database_connection) or die(mysql_error());&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$row = mysql_fetch_assoc($result);&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$sensor_failed = (intval($row['status_code'])==200) ? false : true;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;mysql_free_result($result);&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;if(($retry &amp;&amp; $sensor_failed) || !$retry) {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$start = timestamp();&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$response = @ file_get_contents($sensor_url, "r");&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;list($version,$status_code,$msg) = explode(' ',$http_response_header[0], 3);&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$end = timestamp();&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$difference = $end-$start;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// check to see if this sensor has had previous trouble&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$sql = "SELECT status_code FROM sensor_log WHERE sensorID=$sensor_sensorID ORDER BY ID DESC LIMIT $settings_failures";&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$dbtest = mysql_query($sql, $database_connection) or die(mysql_error());&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$dbtest_totalrows = mysql_num_rows($dbtest);&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$error_count = $settings_failures-$dbtest_totalrows;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for($i=0 ; $i&amp;lt;$dbtest_totalrows ; $i++) {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$row = mysql_fetch_assoc($dbtest);&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if($row['status_code'] != 200) $error_count++;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// add this last test to the database&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$sql = "INSERT INTO sensor_log (sensorID, status_code, lag) VALUES ($sensor_sensorID, $status_code, $difference)";&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$dbtest = mysql_query($sql, $database_connection) or die(mysql_error());&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// if this is the first good reading in awhile, then the server just went back up.&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if($error_count == $settings_failures &amp;&amp; $status_code == 200) {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// sensor has come back up after being down&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$subject =  "$sensor_name is back up!";&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$msg = "The server went back up!\n\n";&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ini_set('sendmail_from', '&lt;span class="editme"&gt;nopreply@yoururl.com&lt;/span&gt;');&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$to = $sensor_emails; // separate email addresses with commas&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$mailheaders = "From: Server Monitor &amp;lt;&lt;span class="editme"&gt;nopreply@yoururl.com&lt;/span&gt;&amp;gt; \n";&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;mail($to, $subject, $msg, $mailheaders);&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$any_failures = ($status_code != 200 &amp;&amp; $error_count &amp;lt; $settings_failures) ? true : $any_failures;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if($retries == $settings_failures &amp;&amp; $status_code != 200) {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// send the email notification&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$subject =  "$sensor_name has gone down!";&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$msg = "The server went down!\n\n";&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ini_set('sendmail_from', '&lt;span class="editme"&gt;nopreply@yoururl.com&lt;/span&gt;');&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$to = $sensor_emails; // separate email addresses with commas&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$mailheaders = "From: Server Monitor &amp;lt;noreply@yoururl.com&amp;gt; \n";&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;mail($to, $subject, $msg, $mailheaders);&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;fclose($handle);&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;}&lt;br /&gt;&lt;br /&gt;&amp;nbsp;if($any_failures) {&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;$retries++;&lt;br /&gt;&amp;nbsp;&amp;nbsp;sleep(abs($settings_retry_minutes * 60));&lt;br /&gt;&amp;nbsp;&amp;nbsp;$retry = true;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;} else {&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;$retry = false;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;}&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;} while ($retry);&lt;br /&gt;&lt;br /&gt;?&amp;gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;Okay, there are a few things that need explaining here.  I'll describe in plain language what's going on:&lt;br /&gt;&lt;br /&gt;First the script reads the settings, as we reviewed in &lt;i&gt;testallsensors.php (stage one)&lt;/i&gt;.&lt;br /&gt;&lt;br /&gt;The very outside loop is the &lt;i&gt;retry loop&lt;/i&gt;.  If any sensor fails (i.e. returns a &lt;a title="Definition of Status Codes" target="_blank" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html" id="z-ky"&gt;status code&lt;/a&gt;  other than 200) that wasn't failing previously, this loop continues until the sensor is either good, or the script has exhausted it's number of retries (the number of retries, and the length of time this script &lt;i&gt;sleeps&lt;/i&gt; between each retry is determined in our settings).  Each time a test is made, the lag time and the response code is INSERTed into the &lt;i&gt;sensor_log&lt;/i&gt; table.&lt;br /&gt;&lt;br /&gt;The loop nested inside the &lt;i&gt;retry loop&lt;/i&gt; is a loop that goes through each sensor list (if there is more than one).  I built the back end so it could serve more than one list of sensors (i.e. lists from multiple spreadsheets); I do this by looping through an array of sensor list URLs (the example has just one URL but you can add others).  As it reads each sensor from the list, it calls it, gets the result and times the response time (what I refer to as lag).  &lt;br /&gt;&lt;br /&gt;I built it so one can have multiple sensor lists for organizational purposes.  I thought there was pretty good chance that people would want to create separate spreadsheets for different customers etc.  There's a lot to be said for having only one list though (one place to go to view the status of all of your sensors) and even if you have one sensor list you can, of course, create as many reports as you want for any sensorID on any number of spreadsheets.  Just remember to &lt;b&gt;never reuse a sensorID&lt;/b&gt; (as least not with the same backend and database).  If you use the same sensorID on any sensor list twice, with the same back end/database, their data will get mixed together.&lt;br /&gt;&lt;br /&gt;Within the inner loop we do our sensor test and if it comes back &lt;i&gt;up&lt;/i&gt; after being previously &lt;i&gt;down&lt;/i&gt; then an "Up" notification goes out.  If a sensor returns &lt;i&gt;$settings_failures&lt;/i&gt; failures after previously being &lt;i&gt;up&lt;/i&gt;, then a "Down" notification also goes out.  And whatever happens the result and the lag are inserting into the database with the current timestamp and the sensorID.&lt;br /&gt;&lt;br /&gt;At the end of the loop we see if a sensor is &lt;i&gt;failing&lt;/i&gt; (by which I mean, if it came back &lt;i&gt;down&lt;/i&gt; but has not returned &lt;i&gt;$settings_failures&lt;/i&gt; failures yet.  If there is such a circumstance, the process sleeps for &lt;i&gt;$settings_retry_minutes&lt;/i&gt; before it tries again.&lt;br /&gt;&lt;br /&gt;Run a few tests with the script and make sure that it's properly reading the information from your spreadsheet, that it's conducting it's tests and INSERTing the test results in the database.  Finally, simulate one of your sensors going down (you can do this by simply temporarily renaming a sensor so the script can't find it / gets a status code of &lt;a title="What's a 404?" target="_blank" href="http://malaysia.answers.yahoo.com/question/index?qid=20080715213024AA1OmF2" id="eqxc"&gt;404&lt;/a&gt;) and make sure you get the notifications as the script detects the error.  It's possible that your tests will time out on your webserver because of the sleep commands.  This won't be an issue later as the cron will be running the script and it won't be running it using the webserver, but the timeouts can make testing difficult.  If you get webserver timeouts, try shortening the &lt;i&gt;$settings_retry_minutes&lt;/i&gt; and/or &lt;i&gt;$settings_failures&lt;/i&gt; values temporarily for your tests or extend your server's timeout.&lt;br /&gt;&lt;br /&gt;Important Note: If you edit this code, make sure you don't cause the script to go into an endless loop.  Eventually, when you begin using the cron will thread a new instance of this script every 15 minutes and if the script isn't terminating correctly you could make your testing server very very unhappy.&lt;br /&gt;&lt;br /&gt;I'm using &lt;a title="a fairly dependable and inexpensive hosting service" target="_blank" href="http://lunarpages.com/" id="m_vn"&gt;LunarPages&lt;/a&gt; for hosting and they have a very easy to use option in their control panel for creating cron jobs, but every shared hosting service we use has a similar facility.  Usually you have to call the script by passing it to the PHP interpreter (e.g. &lt;i&gt;php /path/to/script/testallsensors.php&lt;/i&gt;).  I find running the script every 15 minutes is perfect.  More often than that isn't much more useful, and it may get your hosting services upset with you.  If you have issues creating a cron job call your hosting service and they'll surely help you out.&lt;br /&gt;&lt;br /&gt;When you are satisfied the script is working correctly, we'll move on displaying the current status for all the sensors in our spreadsheet.&lt;br /&gt;&lt;br /&gt;&lt;h2&gt;Step 5: Current Server Status&lt;/h2&gt;&lt;br /&gt;To display the current server status, first we need to pull results from the database and populate a new worksheet with them.  We'll do this by having the worksheet execute an &lt;i&gt;importDATA&lt;/i&gt; function that calls a script that returns CSV values that the function will use to populate the worksheet.  We'll start by creating the script that returns the CSV data:&lt;br /&gt;&lt;br /&gt;&lt;h4&gt;sensorsummary.php&lt;/h4&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;&amp;lt;?php&lt;br /&gt;&lt;br /&gt;require_once('database_connection.php');&lt;br /&gt;require_once('csv.php');&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;// We get the list of sensors from the spreadsheet&lt;br /&gt;&lt;br /&gt;$spreadsheet_sensorID = array();&lt;br /&gt;$spreadsheet_name = array();&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;$handle = fopen("http://spreadsheets.google.com/pub?&lt;span class="editme"&gt;key=XXXXXXXXXXXXXXXX&amp;output=csv&amp;gid=0&amp;range=C3:C99&lt;/span&gt;", "r");&lt;br /&gt;&lt;br /&gt;while(($results = readCSV($handle, 1000)) != FALSE) {&lt;br /&gt;&lt;br /&gt;&amp;nbsp;array_push($spreadsheet_sensorID, intval($results[0]));&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;$sql = "SELECT sensorID, lag, status_code FROM sensor_log WHERE ID IN (SELECT MAX(ID) AS lastID FROM sensor_log WHERE sensorID IN (" . join(",", $spreadsheet_sensorID) . ") GROUP BY sensorID);";&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;$result = mysql_query($sql, $database_connection) or die(mysql_error());&lt;br /&gt;$result_totalrows = mysql_num_rows($result);&lt;br /&gt;&lt;br /&gt;for($i=0 ; $i&amp;lt;$result_totalrows ; $i++) {&lt;br /&gt;&amp;nbsp;$row = mysql_fetch_assoc($result);&lt;br /&gt;&amp;nbsp;echo "" . $row['sensorID'] . "," . $row['lag'] . "," . (($row['status_code']==200) ? (200) : (0)) . "\n";&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;mysql_free_result($result);&lt;br /&gt;&lt;br /&gt;?&amp;gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;Simply put, this script looks at all of the sensors on a sensor list and returns their current status (again, as CSV data, which I used as it was easiest).  &lt;/div&gt;&lt;br /&gt;&lt;div&gt;If you have more than one spreadsheet/sensor list using your back end you will either have to create one of these scripts for each of your spreadsheets or pass something to this script to that identifies which sensor list URL it should use.&lt;br /&gt;&lt;br /&gt;Once you have tested the script and made sure it is indeed outputting the sensor status data correctly, you can go ahead and import the data into your spreadsheet.  Go to your spreadsheet and create a worksheet called &lt;i&gt;Sensor Status Data&lt;/i&gt;.  The worksheet should look like this:&lt;br /&gt;&lt;br /&gt;&lt;div style="text-align: left;"&gt;&lt;a id="t:wh" href="http://docs.google.com/File?id=d43qvtp_8drd79jcd_b" target="_blank"&gt;&lt;img style="width: 160px; height: 78.0861px;" src="http://docs.google.com/File?id=d43qvtp_8drd79jcd_b" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;In the cell A2, insert the following function:&lt;br /&gt;&lt;br /&gt;&lt;div style="margin-left: 40px;"&gt;=importData("http://&lt;span style="color: rgb(153, 153, 153);"&gt;www.yourserver.com/sensors/&lt;/span&gt;sensorsummary.php?temp=" &amp;amp; INT(NOW()/TIME(0;10;0)))&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;The &lt;i&gt;temp&lt;/i&gt; value appended onto the end of the function causes the filename to change every 10 minutes; this helps to keep the data fairly current.  Please understand that Google can't poll your script every few seconds or anything like that; nor would it be a good idea anyway (that's a LOT of traffic).  Normally the spreadsheet updates the pulled data at variable freqencies, presumably depending on how busy their servers are.  There is a certain amount of lag time here, especially during heavy traffic periods, but you can force an update if you really need to make sure the spreadsheet is as current as possible, and you can call &lt;i&gt;testallsensors.php&lt;/i&gt; if you need to re-pole the servers being tested (you can force the data to reload manually by editing the cell with the function and changing the cell contents - usually I just add a space to the end of the cell contents).&lt;br /&gt;&lt;br /&gt;I also made a second version of this script that pulls in some more details and orders the data into columns rather than rows.  It really is very close to the same data used here &lt;i&gt;sensorsummary.php&lt;/i&gt;, but I'll include it here for convenience sake; some of the graphs and gadgets that are available in Google Spreadsheet require the data to be organized like this:&lt;br /&gt;&lt;br /&gt;&lt;h4&gt;sensorsummary_detailed.php&lt;/h4&gt;&lt;pre class="prettyprint"&gt;&amp;lt;?php&lt;br /&gt;&lt;br /&gt;require_once('database_connection.php');&lt;br /&gt;require_once('csv.php');&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;$current_ID = array();&lt;br /&gt;$current_lag = array();&lt;br /&gt;$current_code = array();&lt;br /&gt;&lt;br /&gt;$historical_ID = array();&lt;br /&gt;$historical_lag = array();&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;// We get the list of sensors from the spreadsheet&lt;br /&gt;&lt;br /&gt;$spreadsheet_sensorID = array();&lt;br /&gt;$spreadsheet_name = array();&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;$handle = fopen("http://spreadsheets.google.com/pub?&lt;span class="editme"&gt;key=XXXXXXXXXXXXXXXXXXXX&amp;output=csv&amp;gid=0&amp;range=C3:D99&lt;/span&gt;", "r");&lt;br /&gt;&lt;br /&gt;while(($results = readCSV($handle, 1000)) != FALSE) {&lt;br /&gt;&lt;br /&gt;&amp;nbsp;array_push($spreadsheet_sensorID, intval($results[0]));&lt;br /&gt;&amp;nbsp;array_push($spreadsheet_name, str_replace("\n", "", $results[1]));&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;// We get the historical average so we have something to compare against&lt;br /&gt;$sql = "SELECT sensorID, FLOOR(AVG(lag)) AS average_lag, AVG(status_code) AS average_code, HOUR(created_date) AS hourgroup FROM `sensor_log` WHERE HOUR(NOW())=HOUR(created_date) GROUP BY sensorID, hourgroup;";&lt;br /&gt;$result = mysql_query($sql, $database_connection) or die(mysql_error());&lt;br /&gt;$result_totalrows = mysql_num_rows($result);&lt;br /&gt;&lt;br /&gt;for($i=0 ; $i&amp;lt;$result_totalrows ; $i++) {&lt;br /&gt;&amp;nbsp;$row = mysql_fetch_assoc($result);&lt;br /&gt;&amp;nbsp;array_push($historical_lag, $row['average_lag']);&lt;br /&gt;&amp;nbsp;array_push($historical_ID, $row['sensorID']);&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;// We get the average for the last hour&lt;br /&gt;$sql = "SELECT sensorID, FLOOR(AVG(lag)) AS average_lag, AVG(status_code) AS average_code FROM `sensor_log` WHERE (TIMEDIFF(NOW(), created_date) &amp;lt; TIME('1:00:00')) GROUP BY sensorID;";&lt;br /&gt;$result = mysql_query($sql, $database_connection) or die(mysql_error());&lt;br /&gt;$result_totalrows = mysql_num_rows($result);&lt;br /&gt;&lt;br /&gt;for($i=0 ; $i&amp;lt;$result_totalrows ; $i++) {&lt;br /&gt;&amp;nbsp;$row = mysql_fetch_assoc($result);&lt;br /&gt;&lt;br /&gt;&amp;nbsp;// build arrays of the results, so we can turn them on their side&lt;br /&gt;&amp;nbsp;array_push($current_ID, $row['sensorID']);&lt;br /&gt;&amp;nbsp;array_push($current_lag, $row['average_lag']);&lt;br /&gt;&amp;nbsp;array_push($current_code, (($row['average_code']==200) ? (200) : (0)));&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;// write out a row of sensor names&lt;br /&gt;for($j=0 ; $j&amp;lt;count($spreadsheet_sensorID) ; $j++) {&lt;br /&gt;&amp;nbsp;for($i=0 ; $i&amp;lt;count($current_ID) ; $i++)&lt;br /&gt;&amp;nbsp;&amp;nbsp;if($spreadsheet_sensorID[$j] == $current_ID[$i])&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo "\"{$spreadsheet_name[$j]} ({$current_ID[$i]})\"";&lt;br /&gt;&amp;nbsp;if($j&amp;lt;count($spreadsheet_sensorID)-1) echo ",";&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;echo "\n";&lt;br /&gt;&lt;br /&gt;// write out a row of current lag times&lt;br /&gt;for($j=0 ; $j&amp;lt;count($spreadsheet_sensorID) ; $j++) {&lt;br /&gt;&amp;nbsp;for($i=0 ; $i&amp;lt;count($current_ID) ; $i++)&lt;br /&gt;&amp;nbsp;&amp;nbsp;if($spreadsheet_sensorID[$j] == $current_ID[$i])&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo "\"{$current_lag[$j]}\"";&lt;br /&gt;&amp;nbsp;if($j&amp;lt;count($spreadsheet_sensorID)-1) echo ",";&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;echo "\n";&lt;br /&gt;&lt;br /&gt;// write out a row of average lag times for the same hour&lt;br /&gt;for($j=0 ; $j&amp;lt;count($spreadsheet_sensorID) ; $j++) {&lt;br /&gt;&amp;nbsp;for($i=0 ; $i&amp;lt;count($historical_ID) ; $i++)&lt;br /&gt;&amp;nbsp;&amp;nbsp;if($spreadsheet_sensorID[$j] == $historical_ID[$i])&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo "\"{$historical_lag[$j]}\"";&lt;br /&gt;&amp;nbsp;if($j&amp;lt;count($spreadsheet_sensorID)-1) echo ",";&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;echo "\n";&lt;br /&gt;&lt;br /&gt;// write out a row of status codes (200 = entire hour is good, 0 = error)&lt;br /&gt;for($j=0 ; $j&amp;lt;count($spreadsheet_sensorID) ; $j++) {&lt;br /&gt;&amp;nbsp;for($i=0 ; $i&amp;lt;count($current_ID) ; $i++)&lt;br /&gt;&amp;nbsp;&amp;nbsp;if($spreadsheet_sensorID[$j] == $current_ID[$i])&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo "\"{$current_code[$j]}\"";&lt;br /&gt;&amp;nbsp;if($j&amp;lt;count($spreadsheet_sensorID)-1) echo ",";&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;echo "\n";&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;mysql_free_result($result);&lt;br /&gt;&lt;br /&gt;?&amp;gt;&lt;/pre&gt;&lt;br /&gt;&lt;h2&gt;Colouring Our Data&lt;/h2&gt;&lt;img id="zcvo" style="width: 648px; height: 393.049px; float: right; margin-left: 1em; margin-right: 0pt;" src="http://docs.google.com/File?id=d43qvtp_9d6zvtkf6_b" /&gt;This is useful stuff; there's nothing like having problems &lt;span style="background-color: rgb(234, 153, 153);"&gt;stand out in red&lt;/span&gt;.  Google Docs Spreadsheet has a very easy mechanism for colouring your cells based on rules.  In my spreadsheet I took all of the values, including the values in the worksheets that contain the imported data, and made them so that they changed colour based on their value.  This makes is really easy to spot trouble.&lt;br /&gt;&lt;h3&gt;Colouring Lag Values&lt;/h3&gt;Ideally I think I'd like to base the colours on tolerances within what would be considered normal for a specific sensor, but in my example I used a gross scale that I apply to all the values.  The values I used are:&lt;br /&gt;&lt;ol&gt;&lt;li&gt;&amp;lt; 500 = good (&lt;span style="background-color: rgb(182, 215, 168);"&gt;green&lt;/span&gt;),&lt;br /&gt;&lt;/li&gt;&lt;li&gt;500 - 1000 = medium (&lt;span style="background-color: rgb(255, 242, 204);"&gt;yellow&lt;/span&gt;),&lt;br /&gt;&lt;/li&gt;&lt;li&gt;&amp;gt; 1000 = bad (&lt;span style="background-color: rgb(234, 153, 153);"&gt;red&lt;/span&gt;)&lt;/li&gt;&lt;/ol&gt;Please note that some kinds of sensors are going to naturally take longer to return a result than others.  For instance, a "web sensor" doesn't have to have any server side code at all, where as a "database sensor" needs to open a connection to the database server, run a query and inspect the results.&lt;br /&gt;&lt;br /&gt;The diagram on the right shows the exact settings I used.  Make sure you select the entire column (or at least from row 2 to the bottom of your worksheet) that you intend to create your rules for before you create your rules.&lt;br /&gt;&lt;h3&gt;Colouring Errors&lt;/h3&gt;Errors are easier to colour because there are only two states: error (red) and no error (green).  On the front worksheet (the sensor list), beside each sensor in the first column, I made a "light" by inserting the error value from the worksheet that I pull the sensor status into (I conveniently return the values in the same order that they appear on the list).  I then colour the text so you can't see the value at all, just bright green or red by making the rule change the text colour so that it's the same as the background colour.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size:130%;"&gt;Okay!&lt;/span&gt; We've come a long way now.  We have the sensors being tested, notifications being sent out, data being stored, and the results coloured with current status lights beside each of the sensors on our sensor list.  Now we just need to create useful reports on individual sensors, daily sensor reports, and just to be thorough, we're going to archive/compress our old data.&lt;br /&gt;&lt;h2&gt;Creating our Detailed History Report&lt;/h2&gt;History reports allow us to get a bigger picture of a sensor's status and allow us to see in finer detail what went wrong and when.  When I first started this project I wasn't quite sure how I was going to create a history report (which requires multiple worksheets) for each sensor.  It soon occurred to me that if I create an &lt;i&gt;adaptable&lt;/i&gt; report, where I could change one setting and have the report populate itself with the data from any sensor, I would save the end user (in this case, &lt;i&gt;me&lt;/i&gt;!) a lot of work (and if we ever need to send someone a copy of a sensor history report, we can always "hard wire" a copy of the report for that specific use).&lt;br /&gt;&lt;br /&gt;The way that I chose to do this was by creating a cell (that I colour Green) on the Sensors and Report Settings worksheet where the user enters the sensor ID that they want to create a report for (if someone can think of a way to do this with some kind of select box or something I'd like to hear from you).  For convenvience sake, I chose this page to display a list of the sensors and their hourly averages (the list also shows the current hour compared to the same hour's recent historical average) so that the user has the sensor list handy.&lt;br /&gt;&lt;br /&gt;When the sensor ID is changed, two other worksheets are populated by calling two scripts that return 24 hour and 10 data historical data for the sensor indicated in the green box.  Usually the data is populated within a few seconds of changing the [sensor ID] number in this cell, because changing the cell value alters the URLs that the data is read from and that typically triggers a [nearly] immediate update.&lt;br /&gt;&lt;br /&gt;Then, finally, I have a fourth worksheet (titled the "Sensor Report") that shows two graphs based on the historical data for the sensor ID entered.  The 24 hour graph shows actual values, where as the 10 day graph shows hourly averages for that period.&lt;br /&gt;&lt;br /&gt;I pulled in the data by entering the following formulas in the A2 cells on the &lt;i&gt;24 Hour Error Trend Data&lt;/i&gt; and the &lt;i&gt;10 Day Error Trend Data&lt;/i&gt; worksheets:&lt;br /&gt;&lt;br /&gt;&lt;span style="background-color: rgb(204, 0, 0);"&gt;&lt;div id="i9pm" style="text-align: left;"&gt;&lt;h4&gt;for the &lt;i&gt;24 Hour Error Trend Data&lt;/i&gt; worksheet:&lt;/h4&gt;&lt;img style="width: 648px; height: 170.09px;" src="http://docs.google.com/File?id=d43qvtp_2hbt9h44j_b" /&gt;&lt;br /&gt;&lt;h4&gt;for the &lt;i&gt;10 Day Error Trend Data&lt;/i&gt; worksheet:&lt;/h4&gt;&lt;div id="ckhe" style="text-align: left;"&gt;&lt;img style="width: 648px; height: 169.913px;" src="http://docs.google.com/File?id=d43qvtp_34cnw62dp_b" /&gt;&lt;/div&gt;&lt;/div&gt;&lt;/span&gt;&lt;br /&gt;&lt;h3&gt;Graphing the Data&lt;/h3&gt;I used the &lt;span style="background-color: rgb(204, 0, 0);"&gt;&lt;span style="background-color: rgb(255, 255, 255);"&gt;&lt;i&gt;Interactive Time Series&lt;/i&gt; graphs for this report.  I found they were a good way of allowing the end user to examine any part of the data easily.  &lt;b&gt;Please note that these graphs will not be able to display any data until enough data collected first&lt;/b&gt;.  You can [patiently] wait until there's enough data or you can generate some test data in the database if you are feeling particularly impatient.&lt;br /&gt;&lt;br /&gt;Use the following for your &lt;i&gt;Range&lt;/i&gt; in the graph's settings:&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;div style="margin-left: 40px;"&gt;'24 Hour Error Trend Data'!A2:C130&lt;/div&gt;&lt;br /&gt;and&lt;br /&gt;&lt;div style="margin-left: 40px;"&gt;'10 Day Error Trend Data'!A2:C250&lt;/div&gt;&lt;br /&gt;(note that the end of these two ranges can't go beyond the end of the last row that you actually have in these two work sheets.  I padded the worksheets with extra rows because, at least with the 24 hour data, you can't know exactly how many actual readings there will be (because bad sensor readings generate extra follow-up readings to verify the trouble wasn't just some temporary network fluctuation - plus you may have triggers several test readings).&lt;br /&gt;&lt;br /&gt;And the following are the two PHP scripts that are called to pull in the data:&lt;br /&gt;&lt;br /&gt;&lt;h4&gt;24hours.php&lt;/h4&gt;&lt;pre class="prettyprint"&gt;&amp;lt;?php&lt;br /&gt;&lt;br /&gt;require_once('database_connection.php');&lt;br /&gt;&lt;br /&gt;$sensorID = 0;&lt;br /&gt;if(isset($_GET['sensorID'])) {&lt;br /&gt;&amp;nbsp;$sensorID = intval($_GET['sensorID']);&lt;br /&gt;}&lt;br /&gt;if(isset($_POST['sensorID'])) {&lt;br /&gt;&amp;nbsp;$sensorID = intval($_POST['sensorID']);&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;if($sensorID == 0) {&lt;br /&gt;&amp;nbsp;echo "error";&lt;br /&gt;&amp;nbsp;exit();&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;$sql = "SELECT lag, YEAR(created_date) AS yeargroup, MONTH(created_date) AS monthgroup, DAY(created_date) AS daygroup, HOUR(created_date) AS hourgroup, MINUTE(created_date) AS minutegroup, status_code, TIMEDIFF(NOW(), created_date) AS boo FROM sensor_log WHERE (TIMEDIFF(NOW(), created_date) &amp;lt; TIME('24:00:00')) AND sensorID=$sensorID ORDER BY created_date;";&lt;br /&gt;$result = mysql_query($sql, $database_connection) or die(mysql_error());&lt;br /&gt;$result_totalrows = mysql_num_rows($result);&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;for($i=0 ; $i&amp;lt;$result_totalrows ; $i++) {&lt;br /&gt;&amp;nbsp;$row = mysql_fetch_assoc($result);&lt;br /&gt;&amp;nbsp;$minutes = sprintf("%02d", $row['minutegroup']);&lt;br /&gt;&amp;nbsp;echo "" . $row['monthgroup'] . "/" . $row['daygroup'] . "/" . $row['yeargroup'] . " " . $row['hourgroup'] . ":" . $minutes . "," . $row['lag'] . "," . (($row['status_code']==200) ? (200) : (0)) . "\n";&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;mysql_free_result($result);&lt;br /&gt;&lt;br /&gt;?&amp;gt;&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;h4&gt;10days.php&lt;/h4&gt;&lt;pre class="prettyprint"&gt;&amp;lt;?php&lt;br /&gt;&lt;br /&gt;require_once('database_connection.php');&lt;br /&gt;&lt;br /&gt;$websiteID = 0;&lt;br /&gt;if(isset($_GET['websiteID'])) {&lt;br /&gt;$websiteID = intval($_GET['websiteID']);&lt;br /&gt;} else if(isset($_POST['websiteID'])) {&lt;br /&gt;$websiteID = intval($_POST['websiteID']);&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;if($websiteID == 0) {&lt;br /&gt;echo "error";&lt;br /&gt;exit();&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;$sql = "SELECT FLOOR(AVG(lag)) AS average_lag, YEAR(created_date) AS yeargroup, MONTH(created_date) AS monthgroup, DAY(created_date) AS daygroup, HOUR(created_date) AS hourgroup, AVG(status_code) AS average_code, TIMEDIFF(NOW(), created_date) AS boo FROM sensor_log WHERE (TIMEDIFF(NOW(), created_date) &lt; TIME('240:00:00')) AND websiteID=$websiteID GROUP BY yeargroup, monthgroup, daygroup, hourgroup ORDER BY created_date;";&lt;br /&gt;$result = mysql_query($sql, $database_connection) or die(mysql_error());&lt;br /&gt;$result_totalrows = mysql_num_rows($result);&lt;br /&gt;&lt;br /&gt;for($i=0 ; $i&lt;$result_totalrows ; $i++) {&lt;br /&gt;$row = mysql_fetch_assoc($result);&lt;br /&gt;echo "" . $row['monthgroup'] . "/" . $row['daygroup'] . "/" . $row['yeargroup'] . " " . $row['hourgroup'] . ":00," . $row['average_lag'] . "," . (($row['average_code']==200) ? (200) : (0)) . "\n";&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;mysql_free_result($result);&lt;br /&gt;&lt;br /&gt;?&amp;gt;&lt;/pre&gt;&lt;h2&gt;Creating the Daily Report and Maintaining Our Database&lt;/h2&gt;We're going to take care of both of these tasks with one script that we'll have the cron call every 24 hours:&lt;h3&gt;The Daily Report&lt;/h3&gt;The daily report is a report that we'll have emailed to us first thing in our day so that we can see at a glance how our servers have been doing in the last 24 hours.  The report I made is quite simple in the it shows the sensor list, as well as each sensor's up time and lag time, for the most recent 24 hours.  For each value I use a small function that calculates an appropriate RGB value (colour) for each of the values shown on the sensor list.&lt;div id="qr-9" style="text-align: left;"&gt;&lt;img style="width: 354px; height: 154px;" src="http://docs.google.com/File?id=d43qvtp_4c2c534d2_b" /&gt;&lt;/div&gt;&lt;h3&gt;"Archiving" old data&lt;/h3&gt;Also, as it would be unnecessary (or even excessive) to keep every ping value in perpetuity, we take data that is old (I use &amp;gt;2 weeks) and then average the values for each day we are archiving and place the averaged lag/up time values in another table (&lt;i&gt;sensor_log_archive&lt;/i&gt;); deleting the old sensor values as we go.  This should make your database nearly 100 times smaller than it would be otherwise.Here is the PHP script that I created for the report.  Remember that you will have to create a cronjob that will call the script once a day.&lt;h4&gt;daily.php&lt;/h4&gt;&lt;pre class="prettyprint"&gt;&amp;lt;?php&lt;br /&gt;&lt;br /&gt;/*&lt;br /&gt;&lt;br /&gt;&amp;nbsp;This script does two things.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;1) sends out the daily email report&lt;br /&gt;&amp;nbsp;2) archives old data&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;This script must be called by the cron daily&lt;br /&gt;&lt;br /&gt;*/&lt;br /&gt;&lt;br /&gt;require_once('database_connection.php');&lt;br /&gt;require_once('csv.php');&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;// Get the maintenance settings&lt;br /&gt;&lt;br /&gt;$handle = fopen("http://spreadsheets.google.com/pub?&lt;span class="editme"&gt;key=XXXXXXXXXXXXXXXXXXX&amp;output=csv&amp;gid=3&amp;range=B3:B6&lt;/span&gt;", "r");&lt;br /&gt;&lt;br /&gt;if(($results = readCSV($handle, 1000)) == FALSE) break;&lt;br /&gt;$settings_recipients = str_replace("\n", "", $results[0]);&lt;br /&gt;&lt;br /&gt;if(($results = readCSV($handle, 1000)) == FALSE) break;&lt;br /&gt;$settings_failures = str_replace("\n", "", $results[0]);&lt;br /&gt;&lt;br /&gt;if(($results = readCSV($handle, 1000)) == FALSE) break;&lt;br /&gt;$settings_retry_minutes = str_replace("\n", "", $results[0]);&lt;br /&gt;&lt;br /&gt;if(($results = readCSV($handle, 1000)) == FALSE) break;&lt;br /&gt;$settings_archive_days = str_replace("\n", "", $results[0]);&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;// create a set of 2 dimensional arrays holding lag/down time for all sensors on all archivable dates&lt;br /&gt;// (grouped by sensorID and date)&lt;br /&gt;$compressed_sensor_readings = array();&lt;br /&gt;$compressed_cummulativelag = array(); // Culumative lag time&lt;br /&gt;$compressed_downtime = array();&lt;br /&gt;&lt;br /&gt;$first_bad = array();&lt;br /&gt;$last_code = array();&lt;br /&gt;&lt;br /&gt;$sql = "SELECT sensorID, lag, status_code, created_date, DATE(created_date) AS group_date FROM sensor_log WHERE created_date &amp;lt; DATE_SUB(CURRENT_DATE(), INTERVAL $settings_archive_days DAY) ORDER BY sensorID, created_date";&lt;br /&gt;$result = mysql_query($sql, $database_connection) or die(mysql_error());&lt;br /&gt;$result_totalrows = mysql_num_rows($result);&lt;br /&gt;&lt;br /&gt;for($i=0 ; $i&amp;lt;$result_totalrows ; $i++) {&lt;br /&gt;&amp;nbsp;$row = mysql_fetch_assoc($result);&lt;br /&gt;&amp;nbsp;$compressed_sensor_readings[$row['sensorID']][$row['group_date']]++;&lt;br /&gt;&amp;nbsp;$compressed_cummulativelag[$row['sensorID']][$row['group_date']]+=$row['lag'];&lt;br /&gt;&lt;br /&gt;&amp;nbsp;// check to see if up or down; and calculate downtime&lt;br /&gt;&amp;nbsp;if($row['status_code'] == 200) {&lt;br /&gt;&amp;nbsp;&amp;nbsp;$compressed_downtime[$row['sensorID']][$row['group_date']] += (isset($last_code[$row['sensorID']][$row['group_date']]) &amp;&amp; $last_code[$row['sensorID']][$row['group_date']] != 200) ? (strtotime($row['created_date']) - $first_bad[$row['sensorID']][$row['group_date']]) : 0;&lt;br /&gt;&amp;nbsp;&amp;nbsp;$last_code[$row['sensorID']][$row['group_date']] = 200;&lt;br /&gt;&amp;nbsp;&amp;nbsp;$first_bad[$row['sensorID']][$row['group_date']] = null;&lt;br /&gt;&amp;nbsp;} else {&lt;br /&gt;&amp;nbsp;&amp;nbsp;$first_bad[$row['sensorID']][$row['group_date']] = (!isset($last_code[$row['sensorID']][$row['group_date']]) ) ? strtotime($row['created_date']) : $first_bad[$row['sensorID']][$row['group_date']];&lt;br /&gt;&amp;nbsp;&amp;nbsp;$first_bad[$row['sensorID']][$row['group_date']] = ($last_code[$row['sensorID']][$row['group_date']] == 200) ? strtotime($row['created_date']) : $first_bad[$row['sensorID']][$row['group_date']];&lt;br /&gt;&amp;nbsp;&amp;nbsp;$last_code[$row['sensorID']][$row['group_date']] = 0;&lt;br /&gt;&amp;nbsp;}&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;mysql_free_result($result);&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;// Insert archived data into archive table&lt;br /&gt;while (list($current_sensorID, $value) = each($compressed_sensor_readings)) {&lt;br /&gt;&amp;nbsp;while (list($current_sensor_date, $value2) = each($value)) {&lt;br /&gt;&amp;nbsp;&amp;nbsp;$sql = "INSERT INTO sensor_log_archive (sensorID, average_lag, downtime, sensor_date) VALUES ($current_sensorID," . intval($compressed_cummulativelag[$current_sensorID][$current_sensor_date]/$compressed_sensor_readings[$current_sensorID][$current_sensor_date]) . ",{$compressed_downtime[$current_sensorID][$current_sensor_date]},'$current_sensor_date');\n";&lt;br /&gt;&amp;nbsp;&amp;nbsp;$result = mysql_query($sql, $database_connection) or die(mysql_error());&lt;br /&gt;&amp;nbsp;&amp;nbsp;mysql_free_result($result);&lt;br /&gt;&amp;nbsp;}&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;// Delete pre-archived data from log&lt;br /&gt;$sql = "DELETE FROM sensor_log WHERE created_date &amp;lt; DATE_SUB(CURRENT_DATE(), INTERVAL $settings_archive_days DAY);";&lt;br /&gt;$result = mysql_query($sql, $database_connection) or die(mysql_error());&lt;br /&gt;&lt;br /&gt;mysql_free_result($result);&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;function percentage_to_color($p){&lt;br /&gt;&amp;nbsp;$red = $p&amp;lt;50 ? 255 : round(256 - ($p-50)*5.12);&lt;br /&gt;&amp;nbsp;$green = $p&amp;gt;50 ? 255 : round(($p)*5.12);&lt;br /&gt;&amp;nbsp;return sprintf("%02X%02X00", $red, $green);&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;function lag_to_color($l){&lt;br /&gt;&amp;nbsp;$p = 100 - ($l/12.5);&lt;br /&gt;&amp;nbsp;$p = ($p&amp;lt;0) ? 0 : $p;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;return percentage_to_color($p);&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;// We get the list of sensors from the spreadsheet&lt;br /&gt;$spreadsheet_sensorID = array();&lt;br /&gt;$spreadsheet_name = array();&lt;br /&gt;$spreadsheet_url = array();&lt;br /&gt;&lt;br /&gt;$handle = fopen("http://spreadsheets.google.com/pub?&lt;span class="editme"&gt;key=XXXXXXXXXXXXXXXXXX&amp;output=csv&amp;gid=0&amp;range=C3:E99&lt;/span&gt;", "r");&lt;br /&gt;&lt;br /&gt;while(($results = readCSV($handle, 1000)) != FALSE) {&lt;br /&gt;&lt;br /&gt;&amp;nbsp;array_push($spreadsheet_sensorID, intval($results[0]));&lt;br /&gt;&amp;nbsp;array_push($spreadsheet_name, str_replace("\n", "", $results[1]));&lt;br /&gt;&amp;nbsp;array_push($spreadsheet_url, str_replace("\n", "", $results[2]));&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;// Summarize the data from yesterday&lt;br /&gt;&lt;br /&gt;$summary_sensor_readings = array();&lt;br /&gt;$summary_lag = array();&lt;br /&gt;$summary_good_codes = array();&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;$sql = "SELECT sensorID, lag, status_code FROM sensor_log WHERE DAY(created_date) = DAY(DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY)) ORDER BY sensorID, created_date;";&lt;br /&gt;&lt;br /&gt;$result = mysql_query($sql, $database_connection) or die(mysql_error());&lt;br /&gt;$result_totalrows = mysql_num_rows($result);&lt;br /&gt;&lt;br /&gt;for($i=0 ; $i&amp;lt;$result_totalrows ; $i++) {&lt;br /&gt;&amp;nbsp;$row = mysql_fetch_assoc($result);&lt;br /&gt;&amp;nbsp;if(intval($row['sensorID']) &amp;gt; 0) {&lt;br /&gt;&amp;nbsp;&amp;nbsp;$summary_sensor_readings[intval($row['sensorID'])]++;&lt;br /&gt;&amp;nbsp;&amp;nbsp;$summary_lag[intval($row['sensorID'])] += $row['lag'];&lt;br /&gt;&amp;nbsp;&amp;nbsp;if($row['status_code'] == 200) $summary_good_codes[intval($row['sensorID'])]++;&lt;br /&gt;&amp;nbsp;}&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;mysql_free_result($result);&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;// create the daily email report.&lt;br /&gt;&lt;br /&gt;$emailcontent = "";&lt;br /&gt;&lt;br /&gt;$yesterday=date('l jS \of F, Y', time()-86400);&lt;br /&gt;&lt;br /&gt;$emailcontent .= "&amp;lt;p&amp;gt;Monitoring Report for: $yesterday&amp;lt;/p&amp;gt;";&lt;br /&gt;&lt;br /&gt;$emailcontent .= "&amp;lt;table&amp;gt;";&lt;br /&gt;$emailcontent .= "&amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;Sensor Tested&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Average Lag&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Uptime&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;";&lt;br /&gt;&lt;br /&gt;for($i=0 ; $i&amp;lt;count($spreadsheet_sensorID) ; $i++) {&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;$average_lag = intval($summary_lag[$spreadsheet_sensorID[$i]] / $summary_sensor_readings[$spreadsheet_sensorID[$i]]);&lt;br /&gt;&amp;nbsp;&amp;nbsp;$percentage_up = sprintf("%.2d", ($summary_good_codes[$spreadsheet_sensorID[$i]] / $summary_sensor_readings[$spreadsheet_sensorID[$i]])*100 );&lt;br /&gt;&amp;nbsp;&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;$emailcontent .= "&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;&amp;lt;a href=\"{$spreadsheet_url[$i]}\"&amp;gt;{$spreadsheet_name[$i]} ({$spreadsheet_sensorID[$i]})&amp;lt;/a&amp;gt;&amp;lt;/td&amp;gt;&amp;lt;td style=\"background: #" . lag_to_color(intval($average_lag)) . ";\"&amp;gt;{$average_lag}&amp;lt;/td&amp;gt;&amp;lt;td style=\"background: #" . percentage_to_color(intval($percentage_up)) . ";\"&amp;gt;{$percentage_up}%&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;\n";&lt;br /&gt;}&lt;br /&gt;$emailcontent .= "&amp;lt;/table&amp;gt;";&lt;br /&gt;&lt;br /&gt;$email = "&amp;lt;html&amp;gt;&amp;lt;body&amp;gt;\n" . $emailcontent . "\n&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;";&lt;br /&gt;&lt;br /&gt;ini_set('sendmail_from', '&lt;span class="editme"&gt;nopreply@yourdomain.com&lt;/span&gt;');&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br /&gt;$mailheaders = "From: Server Monitor &amp;lt;&lt;span class="editme"&gt;noreply@yourdomain.com&lt;/span&gt;&amp;gt; \n";&lt;br /&gt;$mailheaders .= 'Content-type: text/html; charset=iso-8859-1\n';&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br /&gt;mail($settings_recipients, "Server Monitor Report", $email, $mailheaders);&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;?&amp;gt;&lt;/pre&gt;Remember now, to add new sensors all you have to do is upload a sensor script to the server you are monitoring and add 1 line to the sensor list.  The application will see the published list and start calling the sensor.I have several little extra features I've added to my spreadsheet (e.g. a call that tells the user the next unused sensorID) that I'd be happy to share with people (I just don't want to turn this article into a &lt;i&gt;book&lt;/i&gt; - grin).&lt;h2&gt;Happy Days are Here Again!&lt;/h2&gt;Oh, Happy Customers!  Now you are the &lt;i&gt;first&lt;/i&gt; to know when a website goes down.  What's more, you've got charts and graphs to show your customers the great service they are getting and demonstrate the diligence you show on their behalf.  &lt;i&gt;And&lt;/i&gt;, you have historical data that you can compare and will give you are better idea of how well your servers are performing, as well as providing you with data that you can use when working with your providers to help diagnose issues, identify bottlenecks and improve service [where needed].If you like this post, and you'd like to see some more projects along these lines, drop me a line (especially if you have ideas you'd like to contribute).  If there is enough interest I will consider doing a series on using Google Spreadsheets as front ends to other types of reports and monitoring webapps.&lt;hr /&gt;&lt;ol&gt;&lt;li&gt;&lt;a name="FOOTNOTE-1"&gt;&lt;/a&gt;By "live data" I refer to data that is connected to an external data source, and contains the most current information available.&lt;/li&gt;&lt;li&gt;&lt;a name="FOOTNOTE-2"&gt;&lt;/a&gt;A worksheet is like a page within your spreadsheet.  You can select between, and create, worksheets using the tabs at the bottom of your spreadsheet.&lt;/li&gt;&lt;/ol&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8927077563876229110-2556448186097702381?l=stephenakins.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://stephenakins.blogspot.com/feeds/2556448186097702381/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stephenakins.blogspot.com/2009/04/google-docs-server-monitoring_8546.html#comment-form' title='6 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8927077563876229110/posts/default/2556448186097702381'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8927077563876229110/posts/default/2556448186097702381'/><link rel='alternate' type='text/html' href='http://stephenakins.blogspot.com/2009/04/google-docs-server-monitoring_8546.html' title='Google Spreadsheet Server Monitoring'/><author><name>Stephen Akins</name><uri>https://profiles.google.com/106963705487836408694</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//lh6.googleusercontent.com/-OBjWBqY9XUM/AAAAAAAAAAI/AAAAAAAABww/ozuFLWf-lsE/s512-c/photo.jpg'/></author><thr:total>6</thr:total></entry></feed>
