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.
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)
}