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.