Real-time information webhook using Go

Webhook is one of quickest way to receive notification when event occurred. For example, notification when receiving new SMS or payment can be made known by sending request to webhooks. In short, it is an automated messages sent from apps when something happens.

Using Go programming language we can build a simple and useful webhook program to alert us when something happen. For this tutorial, we will be writing a program to monitor recording file movement.

Test scenario

Let say you are running a program to transfer file from local directory to cloud such as Azure, Amazon S3 or Digital Ocean, now this directory must be emptied after it has been transferred to the cloud location and any existing file in local directory means something is not right with the program and requires investigation. One of the example of this program is recording system from call center. Once calling session is finished, the recording file will be kept in cloud storage and local recording files will be deleted to save storage.

Solution

To receive an alert whenever completed file still exist in local server. Investigation must be made as to why this file is not uploaded to cloud storage. We can write a Go program to satisfy this need. The program can be integrated in availability check application such as Pingdom, Uptime Robot and Alertbot. We will returning status 500 when there is file exist in local directory and status 200 if everything is ok.

The code

Let’s start with package and import part in main.go file. We will be using mostly standard package. For routing, gorilla/mux package will be implemented so make sure you have it installed in your Go environment.

package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"time"

	"github.com/gorilla/mux"
)

To kick off the function part, we need a function that can identify our target file. This function will have path as it’s parameter. Using regexp package, we can fulfill this requirement by looking for a file with mp3 extension as first requirement for our check.

func isTargetFile(path string) bool {
	targetFileExt := ".mp3"
	matched, err := regexp.MatchString(fmt.Sprintf("^%s$", targetFileExt), filepath.Ext(path))
	if err != nil {
		log.Printf("Error %s occured while comparing file extension for %s\n", err, path)
	}
	return matched
}

Next, we need to define file’s grace period. This is important because file in our local server might still being uploaded to cloud when the check is running. Having grace period will eliminate the possibility of false error. For this example, we will set 1 second as grace period. This might not reflect real life situation so please change it to 1 hour if you were to run it in production as it’s more logical upload time to cloud. Time package with its ParseDuration feature can be use here. Error handling also will be added in case there is error while passing the grace period.

func uploadGracePeriod(file os.FileInfo) bool {
	gracePeriod := "1s"
	gracePeriodDuration, err := time.ParseDuration(gracePeriod)
	if err != nil {
		log.Printf("Error %s occurred while passing grace period: %s\n", err, gracePeriod)
	}
	return time.Since(file.ModTime()) > gracePeriodDuration
}

In order to get an accurate result while doing the check, we need a function that can walk through the target directory and satisfy the requirements that we set earlier (mp3 file and passed 1 hour grace period). This specific function will do that job using filepath.Walk feature.

func isIn(dir string) error {
	err := filepath.Walk(dir, func(path string, file os.FileInfo, er error) error {
		if er != nil {
			return er
		}
		if !file.IsDir() && isTargetFile(path) && uploadGracePeriod(file) && exec.Command("lsof", path).Run() != nil {
			log.Printf("Warning!! %s is completed but not uploaded to cloud\n", file.Name())
			return fmt.Errorf("Warning!! %s is completed but not uploaded to cloud", file.Name())
		}
		return nil
	})

	return err
}

Notice that we are using exec.Command feature, this is done to run lsof command to our target file to make sure it’s not an active recording.

Now our main requirements is complete, let’s write a function to set our target directory which where isIn function will run its scan. We are using /etc/call/recording for this purpose, your case might be different.

func monitorUploadHandler(w http.ResponseWriter, r *http.Request) {
	err := isIn(filepath.FromSlash("/etc/call/recording"))

	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusOK)
}

When there is a file that satisfy our requirements exist in local directory, this webhook will return StatusInternalServerError, otherwise it will return StatusOK.

Lastly, let’s create our main function which is to server the request and setting up listening port. We will be using /monitor as its route and port 8000 as listening port.

func main() {
	r := mux.NewRouter()
	r.HandleFunc("/monitor", monitorUploadHandler)
	fmt.Printf("Starting server...\n")
	log.Fatal(http.ListenAndServe(":8000", r))
}

To test the program, let’s create test file by issuing this command

touch test.mp3

We will have test.mp3 file in our target directory. Now run the program by using this command.

go run main.go

If everything is running correctly, you will see this result from browser or postman request.

This showing that our app manage to identify target file and return such notification when it’s being called.

Posted Under: Go