There are plenty of ways to gain insights on website availability and performance, from setting up complex monitoring agents to browsing through real-time logs. Few services are as straightforward and robust as SolarWinds® Pingdom®. Pingdom lets you set up checks for your website including uptime, page speed, and user interactions. It then collates the results in a dashboard for you to visualize. But aside from its point-and-click interface, Pingdom also has a powerful API, which you can use to automate the collection of all sorts of data.
In this post, we’ll explore some of the capabilities of the Pingdom API. We’ll build a quick headless CMS in Ruby, then automate some common tasks such as listing all the available routes, fetching their statuses, and testing all this logic before implementation.
Setting Up Your Environment
All the sample code for this project can be found on GitHub, if you’d like to simply clone that project to get started. Make sure you have a modern version of Ruby installed (greater than 2.5).
If you’re starting from scratch, create your Gemfile
and paste the following lines into it:
# frozen_string_literal: true
source "https://rubygems.org"
gem 'httparty', '~> 0.18'
gem 'minitest', '~> 5.14'
gem 'rack-test', '~> 1.1'
gem 'sinatra', '~> 2.1'
gem 'webmock', '~> 3.12'
Create a new file called app.rb
and paste these lines:
# frozen_string_literal: true
require 'sinatra'
get '/' do
'Hello, world!'
end
Run bundle exec ruby app.rb
. Navigate to localhost:4567. There, you should see the bare-bones “Hello, world!” response.
Your final task will be to obtain an API token from Pingdom. Once you’ve done so through the Pingdom API, create the following test request to ensure you have access:
curl -X GET https://api.pingdom.com/api/3.1/checks -H 'Authorization:Bearer $PINGDOM_API_TOKEN
If you get back a JSON response without any errors, you’re all set.
Building the API
Our headless CMS will be very simple. All our content will be stored as a text file, and there will just be three routes:
GET /posts
returns all of the postsGET /post/:id
returns a single post with the :id identifierPOST /post
takes in a JSON payload and creates a new post
Our complete app might look something like this:
# frozen_string_literal: true
require 'sinatra'
require 'json'
get '/' do
'Hello, world!'
end
get '/posts' do
posts = Dir.entries('content').each_with_object([]) do |filename, arr|
next if filename == '.' || filename == '..'
arr << { id: File.basename(filename, '.md'), body: File.read("content/#{filename}") }
end
posts.to_json
end
get '/post/:id' do
filename = params[:id]
{ id: filename, body: File.read("content/#{filename}.md") }.to_json
end
post '/posts' do
request.body.rewind
data = JSON.parse(request.body.read)
File.open("content/#{data['id']}.md", 'w') { |f| f.write(data['body']) }
redirect '/posts'
end
Given a directory called content
, with a bunch of Markdown files in it, the GET /posts
route will simply read all the files and return the information in a JSON format. The GET /post/:id
route will look up a file based on its filename and return it in a JSON format. And the POST /posts
will take a JSON request body and create a new file from it. In fact, you can test the POST
route by creating a request similar to the following:
curl -X POST http://localhost:4567/posts -d '{"id":"new-post", "body":"This is a new post!"}'
At this point, our API is woefully incomplete. Among other things, we should have some authentication for creating and deleting posts (to ensure not everyone can simply change content), and we should have some error checking (to 404 in case the wrong :id
is provided for a nonexistent file). But, for the purposes of our Pingdom sample, it’s good enough.
Testing the API
As with any modern application, adding a test suite guarantees your app’s behavior and functionality remains consistent through any future changes. For our demo, we’ll be relying on native Ruby test frameworks, like Minitest, to simulate user behavior and assert that the responses are what we’d expect:
ENV['RACK_ENV'] = 'test'
require_relative 'app.rb'
require 'minitest/autorun'
require 'rack/test'
class MiniTest::Spec
include Rack::Test::Methods
def app
Sinatra::Application
end
end
describe 'My App' do
it 'should get posts' do
get '/posts'
assert last_response, :ok?
response = JSON.parse(last_response.body)
refute_predicate response.count, :zero?
end
it 'should get post' do
get '/post/first-post'
assert last_response, :ok?
response = JSON.parse(last_response.body)
assert_equal response['id'], 'first-post'
end
it 'should post a new post' do
FileUtils.rm('content/a-new-post.md') if File.exist?('content/a-new-post.md')
post '/posts', {
id: 'a-new-post',
body: 'Look at all this lovely content'
}.to_json, { 'CONTENT_TYPE' => 'application/json' }
follow_redirect!
assert last_response, :ok?
response = JSON.parse(last_response.body)
assert(response.any? { |post| post['id'] == 'a-new-post' })
end
it 'should delete a post' do
unless File.exist?('content/some-test-post.md')
File.open('content/some-test-post.md', 'w') { |f| f.write('Words words.') }
end
delete '/post/some-test-post'
follow_redirect!
assert last_response, :ok?
response = JSON.parse(last_response.body)
refute(response.any? { |post| post['id'] == 'some-test-post' })
end
end
Even if Ruby isn’t your strongest language, the test DSL should make it easy to understand what’s happening. For example, consider the following method:
it 'should get post' do
get '/post/first-post'
assert last_response, :ok?
response = JSON.parse(last_response.body)
assert_equal response['id'], 'first-post'
end
We’re testing the behavior of GET /post/:id
here. We pass `first-post` as an ID, and assert that the last_response
of that API call does in fact return the post we expect.
Automated Monitoring
So far, we’ve only completed half of our original goal. We created a headless CMS in Ruby which can list posts, as well as create and delete them. We also added a test suite to verify our app behaves as we expect it to. Now, suppose we’ve hosted this application on a platform like DigitalOcean at a domain called our-great-cms.app.
We’ll now use some features of the Pingdom API to ensure our service is functional.
One quick API feature we can try is having a single probe check our app’s availability. Given a domain (and an optional path), a random Pingdom server from around the world will attempt to access your site. The HTTP request to make this call looks something like this:
curl "https://api.pingdom.com/api/3.1/single?type=http&host=our-great-cms.app" -H 'Authorization:Bearer $PINGDOM_API_TOKEN'
Now let’s do something a little more interesting. Follow this tutorial on setting up an uptime check through the Pingdom UI. With that established, verify the check was created with this API call:
curl "https://api.pingdom.com/api/3.1/checks" -H 'Authorization:Bearer $PINGDOM_API_TOKEN'
You should get back a checks
array. Take note of the id
of your newly created check, as we’ll be using it throughout our API calls.
With this check created, we can now perform a variety of actions through the API, like getting the status results of this check:
curl "https://api.pingdom.com/api/3.1/results/$check_id" -H 'Authorization:Bearer $PINGDOM_API_TOKEN'
Or, we can get a summary of the average uptime:
curl "https://api.pingdom.com/api/3.1/summary.average/$check_id" -H 'Authorization:Bearer $PINGDOM_API_TOKEN'
We can then integrate all this capability straight into our application itself. Add the following lines of code to the end of app.rb:
require 'httparty'
get '/status/:password' do
return 404 if params[:password].nil? || params[:password] != 'supersecret'
url = 'https://api.pingdom.com/api/3.1/results/$check_id'
headers = {
Authorization: 'Bearer $PINGDOM_API_TOKEN'
}
response = HTTParty.get(url, headers: headers)
JSON.parse(response.body)
end
Essentially, what we’re trying to do here is create some sort of admin status page. If a user navigates to /status
, they’ll be denied entry if they haven’t provided the right password (in this case, it’s supersecret
). If the password was given, then the page will make a request to the Pingdom API and return the response back.
It cannot be stressed enough: in a real application, DO NOT paste your credentials directly into the file! Instead, following the Twelve-Factor App recommendations, you should store your sensitive information in environment config variables.
With this route newly established, we can test it as well:
require 'webmock/minitest'
it 'should require password for status' do
get '/status/notreal'
assert last_response.status, 404
end
it 'should make a call out to pingdom ' do
stub_request(:get, "https://api.pingdom.com/api/3.1/results/$check_id")
.with(
headers: {
'Authorization'=>'Bearer $PINGDOM_API_TOKEN',
})
.to_return(status: 200, body: '{
"activeprobes":[257],
"results":[
{"probeid":261,
"time":1617657276,
"status":"up",
"responsetime":1186,
"statusdesc":"OK",
"statusdesclong":"OK"
}]
}')
get '/status/supersecret'
assert last_response.status, 200
end
Here, the ever-useful webmock gem simulates a response to the Pingdom API. We don’t actually make a call; however, in our test, we tell webmock what we expect the request to look like, and when we navigate to get ‘/status/supersecret’, webmock asserts that the request is actually being made. We’re also asserting that a user without the right password gets a 404 error response.
Conclusion
We’ve only scratched the surface of what can be done with the Pingdom API. For example, you could also set up maintenance windows in the event of some serious downtime. Or, you could simulate user behavior using TMS checks. All these can be integrated in places where HTTP requests can be made, whether it’s the command line, a Slack bot, or even in an app itself.
The full source code for this demo can be found on GitHub. For more tutorials, check out the Pingdom guided tour.
Be sure to check out the 30-day free trial of Pingdom and experience how it helps you gain insights on website availability and performance.