Writing Nagios Plugins in Go

Using Go to write Nagios plugins is a simple, effective way to manage your checks and ensure they are running efficiently.

Although writing a Nagios plugin may vary from language to language in the specifics of it, the basic way in which it works is the same: Nagios will run a check on a host/service and then use the check's return code + its output as the result (OK/Warning/Critical).

This is no different in Go, however there are several advantages to creating your Nagios plugins in Go rather than Python, Perl, Powershell, etc.

Pros:

  • Portability
    • Go is a compiled language, so you'll get a single executable that can run without dependencies on whatever platform you compile for.
  • Security
    • Because it is compiled, the code cannot be easily tampered with by other people who should not be editing checks
    • This helps prevent the multiple times in my environment where some particularly cavalier devs & sysadmins would edit checks to return false values in order to avoid having checks alert.
  • Speed
    • Go is almost always faster than Python & comparable with languages like Java.
    • Go's speed difference with Python (what many of our Nagios checks use) has so far always been orders of magnitude faster (ref: the Conclusion of this post)

Cons:

  • Unfamiliarity
    • Go is only a little over a decade old, so developers aren't as familiar with it as other languages. Although, it is fairly easy to grasp if you have a background in C, C++, or Java.
  • Prototyping speed
    • This ties into the "Unfamiliarity" category, but prototyping speed can initally be higher than, say, a language like Python. So, if a new check needs to be implemented ASAP, it would likely better to just use what is most familiar.

What You'll Need to Get Started

Three packages that you will almost always need to import are os, fmt, and flag.

The os package will allow you to call os.Exit(returncode), which is necessary for Nagios to determine the check result.

The fmt package will allow you to call fmt.Println or fmt.Printf so that you can print the text output that your Nagios check will display.

Finally, the flag package allows you to specify command-line flags, which will allow you to create re-usable checks (e.g. run the same check on 2 machines, but have different thresholds for warning/critical alerts on the two of them).

Conclusion

This article is definitely intended as more of a "here's the tools to get you started" rather than a tutorial, but I've gone ahead and included an example Nagios plugin that I wrote in Go at the end of this article.

The following is a comparison of the execution time between the Go version of said plugin, and the Python version of it.
Execution Time Comparison
This plugin is orders of magnitude faster than the Python version of it that I wrote.

Although the speed difference is still in the milliseconds, that difference quickly becomes significant when executing large checks that normally take several seconds in their Python versions.

Example Check

Below is an example check that I wrote in Go that takes a license file, searches for a matching line (contains the expiration date), and sees how many days away the expiration date is.

package main

import (
	"os"
	"strings"
	"flag"
	"bufio"
	"fmt"
	"time"
)

var (
	regex   = flag.String("regex", "LicenseExpires~dt", "String to match in license file")
	licfile = flag.String("lic", "C:\\Program Files\\XtenderSolutions\\Content Management\\License Server\\license.dat", "License file path")
	warn    = flag.Int("warn", 14, "How many days til expiration constitutes a WARNING?")
	crit    = flag.Int("crit", 7, "How many days til expiration constitutes a CRITICAL alert?")
)

func check(e error) {
	if os.IsNotExist(e){
		NagiosResult(3, 0)
	} else if e != nil {
		panic(e)
	}
}

// Checklic returns a return code (ret) and the unformatted license expiry date from the file (license_date)
func CheckLic(licfile string, regex string) (ret int, license_date string) {
	// Open the file, check for an error in opening, then defer closing file to end of func
	file, err := os.Open(licfile)
	check(err)
	defer file.Close()

	// Create a scanner to scan the file starting @ line 1
	scanner := bufio.NewScanner(file)
	line 		:= 1

	for scanner.Scan() {
		if strings.Contains(scanner.Text(), regex) {
			license_date = scanner.Text()
			ret = 1
			break
		}

		line++
	}

	return
}

// CheckExpiry returns a return code (ret) and time until license expiration (exp)
func CheckExpiry(license_date string, warn int, crit int) (ret int, exp int64) {
	expiration, err := time.Parse("2006/01/02",strings.Split(license_date, "=")[1])
	check(err)
	if time.Now().AddDate(0,0,crit).After(expiration) {
		ret = 2
	} else if time.Now().AddDate(0,0,warn).After(expiration) {
		ret = 1
	}

	exp = int64(time.Until(expiration).Hours() / 24)
	return
}

func NagiosResult(ret int, expiration int64) {
	switch ret {
	case 0:
		fmt.Printf("OK: Time until license expiration - %v days\n", expiration)
		os.Exit(ret)
	case 1:
		fmt.Printf("WARNING: Time until license expiration - %v days\n", expiration)
		os.Exit(ret)
	case 2:
		fmt.Printf("CRITICAL: Time until license expiration - %v days\n", expiration)
		os.Exit(ret)
	case 3:
		fmt.Printf("UNKNOWN: Unable to open the license file (%v)\n",*licfile)
		os.Exit(3)
	case 4:
		fmt.Printf("UNKNOWN: Unable to match the specified regex")
		os.Exit(3)
	default:
		fmt.Printf("UNKNOWN: Unable to determine time until license expiration")
		os.Exit(3)
	}
}

func main() {
	flag.Parse()
	ret, license_date := CheckLic(*licfile, *regex)
	if ret == 0 {
		NagiosResult(4, 0)
	}

	ret, expiration := CheckExpiry(license_date, *warn, *crit)

	NagiosResult(ret, expiration)
}