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.