A WAN monitor running on Google AppEngine written in Go language

As I stated in my earlier post, I have two WAN connections and of course, there’s a need to monitor them. The monitoring logic is pretty simple, it will send me a message on Telegram every time there’s a state change – UP or DOWN.

Initially this monitoring logic was built as OpenWrt hotplug script which used to trigger on interface UP / DOWN events as described in this article. But then I got a mini PC box and it runs Ubuntu and a pfsense virtual machine. While I could build the same logic by discovering hooks in the pfsense code, but it’s too complex and moreover it doesn’t really make sense to monitor the connection of a device using the same connections!

Perfect, time for a new small project. I was trying to learn Go language, what can be a better way to learn a new programming language other than solving a problem? I build my solution using Google AppEngine in Go.

Why AppEngine? Well, yes I could use any random monitoring service out there but I doubt any such service exists which sends alerts on Telegram. Also, AppEngine is included in the Google Cloud Free Tier. So it makes a lot of sense here. My monitoring program runs off Google’s epic infrastructure and I don’t have to pay anything for it!

If you’ve looked at Go examples, it’s pretty easy to spin up a web server. AppEngine makes running your own Go based app even easier, though with a bit of restrictions which is documented nicely by Google in their docs. The restrictions are mostly about outgoing connections and file modifications. While I don’t need to read/write any files, but I need to make outgoing connections, for which I used the required libraries.

AppEngine app always consists of a file app.yaml which describes the runtime, and url endpoints. So here’s mine:

runtime: go
api_version: go1.8

handlers:
  - url: /checkisps
    script: _go_app
    login: admin
  - url: /
    script: _go_app

Now the main code which will handle the requests:

package main

import (
	"fmt"
	"ispinfo"
	"net/http"
	"time"

	"google.golang.org/appengine"
)

var isps = [2]ispinfo.ISPInfo{
	ispinfo.ISPInfo{
		Name:       "ISP1",
		IPAddress:  "x.x.x.x",
		PortNumber: 80,
	},
	ispinfo.ISPInfo{
		Name:       "ISP2",
		IPAddress:  "y.y.y.y",
		PortNumber: 443,
	},
}

func main() {
	http.HandleFunc("/", handle)
	http.HandleFunc("/checkisps", checkisps)

	for index := range isps {
		isps[index].State = false
		isps[index].LastCheck = time.Now()
	}

	appengine.Main()
}

func handle(w http.ResponseWriter, r *http.Request) {
	location := time.FixedZone("IST", 19800)
	w.Header().Add("Content-Type", "text/plain")

	for _, isp := range isps {
		fmt.Fprintln(w, "ISP", isp.Name, "is", isp.Status(), ". Last Checked at", isp.LastCheck.In(location).Format(time.RFC822))
	}
}

func checkisps(w http.ResponseWriter, r *http.Request) {
	for idx := range isps {
		oldstate := isps[idx].State
		isps[idx].Check(r)

		if oldstate != isps[idx].State {
			defer isps[idx].SendAlert(r, w)
		}
	}
}

I separated the code into two packages to keep it clean, so here’s the ispinfo package:

package ispinfo

import (
	"fmt"
	"net/http"
	"net/url"
	"time"

	"google.golang.org/appengine"
	"google.golang.org/appengine/socket"
	"google.golang.org/appengine/urlfetch"
)

type ISPInfo struct {
	Name       string
	IPAddress  string
	PortNumber uint16
	State      bool
	LastCheck  time.Time
}

var telegram_bot_token := "<<< bot token >>>"
var telegram_chat_id := "<<< chat id >>>"

func (i *ISPInfo) Check(r *http.Request) {
	ctx := appengine.NewContext(r)

	host := fmt.Sprintf("%s:%d", i.IPAddress, i.PortNumber)
	timeout, _ := time.ParseDuration("5s")
	conn, err := socket.DialTimeout(ctx, "tcp", host, timeout)
	i.LastCheck = time.Now()

	if err == nil {
		i.State = true
	} else {
		i.State = false
	}

	conn.Close()
}

func (i *ISPInfo) SendAlert(r *http.Request, w http.ResponseWriter) {
	ctx := appengine.NewContext(r)
	client := urlfetch.Client(ctx)

	message := fmt.Sprintf("%s is %s", i.Name, i.Status())
	params := url.Values{}
	url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", telegram_bot_token)

	params.Add("chat_id", telegram_chat_id)
	params.Add("text", message)

	response, err := client.PostForm(url, params)

	if err != nil {
		fmt.Fprintln(w, "Error sending message ", err.Error())
		return
	}

	response.Body.Close()
}

func (i *ISPInfo) Status() string {
	if i.State {
		return "UP"
	}

	return "DOWN"
}

Since the connection status needs to be monitored periodically define a cron job for it, in cron.yaml:

cron:
  - description: "check connection status"
    url: /checkisps
    schedule: every 5 mins
 

gcloud app deploy app.yaml cron.yaml  in the directory and the app is ready!

This is a small monitoring service that managed to build in a couple of hours while learning Go language and the AppEngine API. It should take hardly an hour for a pro. Also I didn’t really follow the correct packaging principles – the ispinfo package exposes pretty much all fields. This could have been better.

The code is available in my github repository in case you’re interested in it.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: