Build a Status Page powered by Github and Uptime Robot

Roger Stringer

Roger Stringer / March 7, 2017

8 min read

We built our own status page for Flybase, you can see it here.

It's built as a static HTML page on Github Pages. It uses Github Issues to report any incidents and Uptime Robot to monitor our sites.

This idea is based loosely on the statuspage repo created by @pyupio, but simplified greatly, as I wanted this to be pretty much automated, plus I already use Uptime Robot for monitoring, so combining Uptime Robot with Github Issues works great.

To get started, you'll want two things:

  1. An Uptime Robot account
  2. A github repo where you can throw your site up and use the issues system.

Create a branch in your repo called gh-pages, this is where your files will sit.

Ok, let's build our status page:

1. create index.html

First, create our index.html file:

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="Service status">
    <meta name="robots" content="index, follow">
    <title>Status</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <link rel="stylesheet" href="style.css?v=20170223">
</head>
<body>
<div class="container">
    <header>
        <h1>Status Page</h1>
    </header>
    <div class="panel" id="panel">
        <div class="panel-heading">
            <h3 class="panel-title" id="paneltitle"></h3>
        </div>
    </div>
    <h4 class="page-header">Systems Status</h4>
    <div class="list-group" id="services"></div>
    <h4 class="page-header">Incidents</h4>
    <div class="timeline-centered" id="incidents"></div>
    <script src="//code.jquery.com/jquery.min.js"></script>
    <script src="script.js?v=20170223"></script>
</body>
</html>

2. Create script.js

Next, we'll create script.js, this is the file that talks to our services.

Replace the following variables with actual lines:

YOUR-UPTIME-ROBOT-API-KEY-1 and YOUR-UPTIME-ROBOT-API-KEY-2: Uptime Robot's default API key is universal, but is also read and write. We want this monitor to be read-only so we have to create an API Key for each site we are creating. This is an array of keys.

To add more sites, just add a new line and add a new monitor key.

YOUR-GITHUB-USERNAME: Your Github username where the repo you created lives.

YOUR-GITHUB-REPO: The Github repo you created to use.

$(document).ready(function() {
    var config = {
        uptimerobot: {
            api_keys: [
                "YOUR-UPTIME-ROBOT-API-KEY-1",
                "YOUR-UPTIME-ROBOT-API-KEY-2"
            ],
            logs: 1
        },
        github: {
            org: 'YOUR-GITHUB-USERNAME',
            repo: 'YOUR-GITHUB-REPO'
        }
    };

    var status_text = {
        'operational': 'operational',
        'investigating': 'investigating',
        'major outage': 'outage',
        'degraded performance': 'degraded',
    };

    var monitors = config.uptimerobot.api_keys;
    for( var i in monitors ){
        var api_key = monitors[i];
        $.post('https://api.uptimerobot.com/v2/getMonitors', {
            "api_key": api_key,
            "format": "json",
            "logs": config.uptimerobot.logs,
        }, function(response) {
            status( response );
        }, 'json');
    }

    function status(data) {
        data.monitors = data.monitors.map(function(check) {
            check.class = check.status === 2 ? 'label-success' : 'label-danger';
            check.text = check.status === 2 ? 'operational' : 'major outage';
            if( check.status !== 2 && !check.lasterrortime ){
                check.lasterrortime = Date.now();
            }
            if (check.status === 2 && Date.now() - (check.lasterrortime * 1000) <= 86400000) {
                check.class = 'label-warning';
                check.text = 'degraded performance';
            }
            return check;
        });

        var status = data.monitors.reduce(function(status, check) {
            return check.status !== 2 ? 'danger' : 'operational';
        }, 'operational');

        if (!$('#panel').data('incident')) {
            $('#panel').attr('class', (status === 'operational' ? 'panel-success' : 'panel-warning') );
            $('#paneltitle').html(status === 'operational' ? 'All systems are operational.' : 'One or more systems inoperative');
        }
        data.monitors.forEach(function(item) {
            var name = item.friendly_name;
            var clas = item.class;
            var text = item.text;
            $('#services').append('<div class="list-group-item">'+
                '<span class="badge '+ clas + '">' + text + '</span>' +
                '<h4 class="list-group-item-heading">' + name + '</h4>' +
                '</div>');
        });
    };

    $.getJSON( 'https://api.github.com/repos/' + config.github.org + '/' + config.github.repo + '/issues?state=all' ).done(message);

    function message(issues) {
        issues.forEach(function(issue) {
            var status = issue.labels.reduce(function(status, label) {
                if (/^status:/.test(label.name)) {
                    return label.name.replace('status:', '');
                } else {
                    return status;
                }
            }, 'operational');

            var systems = issue.labels.filter(function(label) {
                return /^system:/.test(label.name);
            }).map(function(label) {
                return label.name.replace('system:', '')
            });

            if (issue.state === 'open') {
                $('#panel').data('incident', 'true');
                $('#panel').attr('class', (status === 'operational' ? 'panel-success' : 'panel-warn') );
                $('#paneltitle').html('<a href="#incidents">' + issue.title + '</a>');
            }

            var html = '<article class="timeline-entry">\n';
            html += '<div class="timeline-entry-inner">\n';

            if (issue.state === 'closed') {
                html += '<div class="timeline-icon bg-success"><i class="entypo-feather"></i></div>';
            } else {
                html += '<div class="timeline-icon bg-secondary"><i class="entypo-feather"></i></div>';
            }

            html += '<div class="timeline-label">\n';
            html += '<span class="date">' + datetime(issue.created_at) + '</span>\n';

            if (issue.state === 'closed') {
                html += '<span class="badge label-success pull-right">closed</span>';
            } else {
                html += '<span class="badge ' + (status === 'operational' ? 'label-success' : 'label-warn') + ' pull-right">open</span>\n';
            }

            for (var i = 0; i < systems.length; i++) {
                html += '<span class="badge system pull-right">' + systems[i] + '</span>';
            }

            html += '<h2>' + issue.title + '</h2>\n';
            html += '<hr>\n';
            html += '<p>' + issue.body + '</p>\n';

            if (issue.state === 'closed') {
                html += '<p><em>Updated ' + datetime(issue.closed_at) + '<br/>';
                html += 'The system is back in normal operation.</p>';
            }
            html += '</div>';
            html += '</div>';
            html += '</article>';
            $('#incidents').append(html);
        });

        function datetime(string) {
            var datetime = string.split('T');
            var date = datetime[0];
            var time = datetime[1].replace('Z', '');
            return date + ' ' + time;
        };
    };
});

2. Create style.css

Finally, we want to create our style.css file. Just copy this entire block into the file.

@import url("https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,700italic,400,300,700");

.timeline-centered .timeline-entry .timeline-entry-inner:after,
.timeline-centered .timeline-entry:after,
.timeline-centered:after {
    clear: both
}

img {
    vertical-align: middle
}

.img-responsive {
    display: block;
    height: auto;
    max-width: 100%
}

.img-rounded {
    border-radius: 3px
}

.img-thumbnail {
    background-color: #fff;
    border: 1px solid #ededf0;
    border-radius: 3px;
    display: inline-block;
    height: auto;
    line-height: 1.428571429;
    max-width: 100%;
    moz-transition: all .2s ease-in-out;
    o-transition: all .2s ease-in-out;
    padding: 2px;
    transition: all .2s ease-in-out;
    webkit-transition: all .2s ease-in-out
}

.img-circle {
    border-radius: 50%
}

.timeline-centered {
    position: relative;
    margin-bottom: 30px
}

.timeline-centered:after,
.timeline-centered:before {
    content: " ";
    display: table
}

.timeline-centered:before {
    content: '';
    position: absolute;
    display: block;
    width: 4px;
    background: #f5f5f6;
    top: 20px;
    bottom: 20px;
    margin-left: 30px
}

.timeline-centered .timeline-entry .timeline-entry-inner:after,
.timeline-centered .timeline-entry .timeline-entry-inner:before,
.timeline-centered .timeline-entry:after,
.timeline-centered .timeline-entry:before {
    content: " ";
    display: table
}

.timeline-centered .timeline-entry {
    position: relative;
    margin-top: 5px;
    margin-left: 30px;
    margin-bottom: 10px;
    clear: both
}

.timeline-centered .timeline-entry.begin {
    margin-bottom: 0
}

.timeline-centered .timeline-entry.left-aligned {
    float: left
}

.timeline-centered .timeline-entry.left-aligned .timeline-entry-inner {
    margin-left: 0;
    margin-right: -18px
}

.timeline-centered .timeline-entry.left-aligned .timeline-entry-inner .timeline-time {
    left: auto;
    right: -100px;
    text-align: left
}

.timeline-centered .timeline-entry.left-aligned .timeline-entry-inner .timeline-icon {
    float: right
}

.timeline-centered .timeline-entry.left-aligned .timeline-entry-inner .timeline-label {
    margin-left: 0;
    margin-right: 70px
}

.timeline-centered .timeline-entry.left-aligned .timeline-entry-inner .timeline-label:after {
    left: auto;
    right: 0;
    margin-left: 0;
    margin-right: -9px;
    -moz-transform: rotate(180deg);
    -o-transform: rotate(180deg);
    -webkit-transform: rotate(180deg);
    -ms-transform: rotate(180deg);
    transform: rotate(180deg)
}

.timeline-centered .timeline-entry .timeline-entry-inner {
    position: relative;
    margin-left: -20px
}

.timeline-centered .timeline-entry .timeline-entry-inner .timeline-time {
    position: absolute;
    left: -100px;
    text-align: right;
    padding: 10px;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box
}

.timeline-centered .timeline-entry .timeline-entry-inner .timeline-time>span {
    display: block
}

.timeline-centered .timeline-entry .timeline-entry-inner .timeline-time>span:first-child {
    font-size: 15px;
    font-weight: 700
}

.timeline-centered .timeline-entry .timeline-entry-inner .timeline-time>span:last-child {
    font-size: 12px
}

.timeline-centered .timeline-entry .timeline-entry-inner .timeline-icon {
    background: #fff;
    color: #737881;
    display: block;
    width: 40px;
    height: 40px;
    -webkit-background-clip: padding-box;
    -moz-background-clip: padding;
    background-clip: padding-box;
    -webkit-border-radius: 20px;
    -moz-border-radius: 20px;
    border-radius: 20px;
    text-align: center;
    -moz-box-shadow: 0 0 0 5px #f5f5f6;
    -webkit-box-shadow: 0 0 0 5px #f5f5f6;
    box-shadow: 0 0 0 5px #f5f5f6;
    line-height: 40px;
    font-size: 15px;
    float: left
}

.timeline-centered .timeline-entry .timeline-entry-inner .timeline-icon.bg-primary {
    background-color: #303641;
    color: #fff
}

.timeline-centered .timeline-entry .timeline-entry-inner .timeline-icon.bg-secondary {
    background-color: #ee4749;
    color: #fff
}

.timeline-centered .timeline-entry .timeline-entry-inner .timeline-icon.bg-success {
    background-color: #00a651;
    color: #fff
}

.timeline-centered .timeline-entry .timeline-entry-inner .timeline-icon.bg-info {
    background-color: #21a9e1;
    color: #fff
}

.timeline-centered .timeline-entry .timeline-entry-inner .timeline-icon.bg-warning {
    background-color: #fad839;
    color: #fff
}

.timeline-centered .timeline-entry .timeline-entry-inner .timeline-icon.bg-danger {
    background-color: #cc2424;
    color: #fff
}

.timeline-centered .timeline-entry .timeline-entry-inner .timeline-label {
    position: relative;
    background: #f5f5f6;
    padding: 1em;
    margin-left: 60px;
    -webkit-background-clip: padding-box;
    -moz-background-clip: padding;
    background-clip: padding-box;
    -webkit-border-radius: 3px;
    -moz-border-radius: 3px;
    border-radius: 3px
}

.timeline-centered .timeline-entry .timeline-entry-inner .timeline-label:after {
    content: '';
    display: block;
    position: absolute;
    width: 0;
    height: 0;
    border-style: solid;
    border-width: 9px 9px 9px 0;
    border-color: transparent #f5f5f6 transparent transparent;
    left: 0;
    top: 10px;
    margin-left: -9px
}

.timeline-centered .timeline-entry .timeline-entry-inner .timeline-label h2,
.timeline-centered .timeline-entry .timeline-entry-inner .timeline-label p {
    color: #737881;
    font-family: "Noto Sans", sans-serif;
    font-size: 12px;
    margin: 0;
    line-height: 1.428571429
}

.timeline-centered .timeline-entry .timeline-entry-inner .timeline-label p+p {
    margin-top: 15px
}

.timeline-centered .timeline-entry .timeline-entry-inner .timeline-label h2 {
    font-size: 16px;
    margin-bottom: 10px
}

.timeline-centered .timeline-entry .timeline-entry-inner .timeline-label h2 a {
    color: #303641
}

.timeline-centered .timeline-entry .timeline-entry-inner .timeline-label h2 span {
    -webkit-opacity: .6;
    -moz-opacity: .6;
    opacity: .6;
    -ms-filter: alpha(opacity=60);
    filter: alpha(opacity=60)
}

4. Git it Going

Once you've created your files, you want to put them on your repo, you can either create them directly from the github.com interface, or you can create them locally and commit them.

If you want to add this to a specific domain, then create a file called CNAME and store your domain or subdomain in there.

Finally, create a file called .nojekyll which tells Github Pages that this is a strictly static site.

To customize Github Issues, we set up labels to identify issues:

  • operational means all systems good.
  • investigating means under investigation.
  • outage to identify an outage.
  • degraded to identify an issue causing degraded performance.

On top of that, you can add labels that start with system: and they will show what system the issue is related to. For example system:blog would show an issue with our blog.

Labeling an issue with any of these tags will reflect on the status page.

//

This status page works pretty well, and was useful last week with the AWS outage that happened. It showed the status of our various services, and let us push updates via Github Issues that showed up below.

I do plan on making an update at some point to take into account comments inside issues.

This is a basic status page, but it helps show people what is happening with your sites, and keep everything nice and transparent.

This post has been cross posted on the Flybase Blog as well. Flybase helps you build real-time, collaborative apps in a fraction of the time with our API. Flybase lets you create fully interactive apps with just frontend code.

Do you like my content?

Sponsor Me On Github