Browse Source

initial commit

Signed-off-by: Erik Hollensbe <github@hollensbe.org>
master
Erik Hollensbe 2 years ago
commit
ad55800bd1
  1. 106
      agent.go
  2. 67
      agent_test.go
  3. 1
      example.yaml
  4. 17
      example.yml
  5. 70
      main.go
  6. 115
      monitor.go
  7. 90
      monitor_test.go
  8. 17
      testdata/configs/good.yml

106
agent.go

@ -0,0 +1,106 @@
package main
import (
"context"
"io/ioutil"
"net"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
)
var monitorMap = map[string]monitor{
"ping": &pingMonitor{},
}
type agent struct {
config agentConfig
debug bool
}
type agentConfig struct {
Listen string `yaml:"listen"`
Monitors []monitorConfig `yaml:"monitors"`
Alerters []alerterConfig `yaml:"alerters"`
}
type baseConfig struct {
Name string `yaml:"name"`
Type string `yaml:"type"`
Params map[string]interface{} `yaml:"parameters"`
}
type monitorConfig struct {
baseConfig `yaml:",inline"`
Interval time.Duration `yaml:"interval"`
}
type alerterConfig struct {
baseConfig `yaml:",inline"`
MinInterval time.Duration `yaml:"min_interval"`
ThrottleAfter uint `yaml:"throttle_after"`
ThrottleDeadline time.Duration `yaml:"throttle_deadline"`
}
func (a *agent) parseConfig(filename string) error {
content, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
return yaml.Unmarshal(content, &a.config)
}
func (ac *agentConfig) validate() error {
// assumes the config has already been parsed
parts := strings.SplitN(strings.TrimSpace(ac.Listen), ":", 2)
if len(parts) != 2 {
return errors.Errorf("missing optional host, or mandatory port in listen directive: %q", ac.Listen)
}
if parts[0] != "" {
ip := net.ParseIP(parts[0])
if ip == nil || len(ip) < 4 {
return errors.Errorf("IP address %q does not parse as an IPv4 or IPv6 address", parts[0])
}
}
if _, err := strconv.ParseUint(parts[1], 10, 32); err != nil {
return errors.Wrap(err, "invalid port in listen directive")
}
return nil
}
func (a *agent) boot(ctx context.Context) error {
<-ctx.Done()
return ctx.Err()
}
func (ac *agentConfig) assembleMonitors() ([]monitor, error) {
monitors := []monitor{}
for _, mc := range ac.Monitors {
if mc.baseConfig.Name == "" || mc.baseConfig.Type == "" {
return nil, errors.New("invalid configuration file: missing a type and/or name")
}
res, ok := monitorMap[mc.baseConfig.Type]
if !ok {
return nil, errors.Errorf("invalid monitor type %q for name %q", mc.baseConfig.Type, mc.baseConfig.Name)
}
monitor := res.New()
if err := monitor.FromConfig(mc); err != nil {
return nil, err
}
monitors = append(monitors, monitor)
}
return monitors, nil
}

67
agent_test.go

@ -0,0 +1,67 @@
package main
import (
"context"
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
func TestAgentConfig(t *testing.T) {
a := &agent{}
assert.Nil(t, a.parseConfig("testdata/configs/good.yml"))
a = &agent{}
assert.NotNil(t, a.parseConfig("invalidfile"))
}
func TestAgentConfigValidate(t *testing.T) {
listeners := map[string]bool{
"0.0.0.0:1234": true,
"127.0.0.1:1234": true,
":1234": true,
" 0.0.0.0:1234 ": true,
" :1234 ": true,
":abcdef": false,
"xyzzy:1234": false,
"127.0.0.1: 1234": false,
"1:1234": false,
}
for addr, pass := range listeners {
a := agentConfig{Listen: addr}
if pass {
assert.Nil(t, a.validate(), addr)
} else {
assert.NotNil(t, a.validate(), addr)
}
}
}
func TestAgentAssembleMonitors(t *testing.T) {
a := &agent{}
assert.Nil(t, a.parseConfig("testdata/configs/good.yml"))
monitors, err := a.config.assembleMonitors()
assert.Nil(t, err)
assert.Equal(t, len(monitors), 2)
}
func TestAgentBoot(t *testing.T) {
a := &agent{}
ctx, cancel := context.WithCancel(context.Background())
// this muckery just ensures we hit the cancel function *after* the service boots.
mutex := &sync.Mutex{}
errChan := make(chan error, 1)
mutex.Lock()
go func() {
mutex.Unlock()
errChan <- a.boot(ctx)
}()
mutex.Lock()
cancel()
assert.Equal(t, <-errChan, context.Canceled)
}

1
example.yaml

@ -0,0 +1 @@
---

17
example.yml

@ -0,0 +1,17 @@
---
listen: "127.0.0.1:1234"
monitors:
- name: "ping1"
type: "ping"
parameters:
host: "127.0.0.1"
count: 1
timeout: 1s
interval: 1m
- name: "ping2"
type: "ping"
parameters:
host: "10.0.0.1"
count: 10
timeout: 1s
interval: 2m

70
main.go

@ -0,0 +1,70 @@
package main
import (
"context"
"fmt"
"os"
"os/signal"
"github.com/pkg/errors"
"github.com/urfave/cli"
"golang.org/x/sys/unix"
)
const appDescription = `
`
// AppVersion is the version number of the application.
const AppVersion = "dirty" // to be compiled in
func main() {
app := cli.NewApp()
app.Name = "achtung"
app.Usage = "The simplest of host monitoring solutions"
app.Version = AppVersion
app.Author = "Erik Hollensbe <erik+github@hollensbe.org>"
app.ArgsUsage = "[flags] [configuration filename]"
app.Description = appDescription
app.Flags = []cli.Flag{
cli.BoolFlag{
Name: "debug, d",
Usage: "Enable debug logging",
},
}
app.Action = run
if err := app.Run(os.Args); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
}
func handleTerminationSignals(cancel context.CancelFunc) {
sigChan := make(chan os.Signal, 2)
signal.Notify(sigChan, unix.SIGTERM, unix.SIGINT)
for range sigChan {
cancel()
}
}
func run(ctx *cli.Context) error {
if len(ctx.Args()) != 1 {
return errors.New("invalid arguments; seek --help")
}
a := &agent{
debug: ctx.Bool("debug"),
}
if err := a.parseConfig(ctx.Args()[0]); err != nil {
return errors.Wrap(err, "during the parse of the configuration file")
}
goCtx, cancel := context.WithCancel(context.Background())
go handleTerminationSignals(cancel)
return a.boot(goCtx)
}

115
monitor.go

@ -0,0 +1,115 @@
package main
import (
"math/rand"
"net"
"time"
"github.com/erikh/ping"
"github.com/pkg/errors"
)
func init() {
rand.Seed(time.Now().Unix())
}
type monitor interface {
New() monitor
Name() string
Type() string
Interval() time.Duration
Query() error
FromConfig(monitorConfig) error
}
type pingMonitor struct {
name string
host string
count uint
timeout time.Duration
pollInterval time.Duration
}
func (pm *pingMonitor) New() monitor {
return &pingMonitor{}
}
func (pm *pingMonitor) Name() string {
return pm.name
}
func (pm *pingMonitor) Type() string {
return "ping"
}
func (pm *pingMonitor) Interval() time.Duration {
return pm.pollInterval
}
func (pm *pingMonitor) Query() error {
addrs, err := net.LookupHost(pm.host)
if err != nil {
return err
}
errChan := make(chan error, len(addrs))
doneChan := make(chan struct{}, len(addrs))
for i := uint(0); i < pm.count; i++ {
for _, addr := range addrs {
go func(addr string) {
defer func() { doneChan <- struct{}{} }()
ip := net.ParseIP(addr)
if err := ping.Pinger(&net.IPAddr{IP: ip}, pm.timeout); err != nil {
errChan <- err
}
}(addr)
}
for i := 0; i < len(addrs); i++ {
select {
case err := <-errChan:
return err
case <-doneChan:
}
}
time.Sleep(10 * time.Millisecond)
}
return nil
}
func (pm *pingMonitor) FromConfig(mc monitorConfig) error {
pm.name = mc.baseConfig.Name
if host, ok := mc.Params["host"].(string); ok {
pm.host = host
} else {
return errors.Errorf("missing host in %q ping monitor", pm.name)
}
if count, ok := mc.Params["count"].(int); ok {
pm.count = uint(count)
} else {
return errors.Errorf("missing count in %q ping monitor", pm.name)
}
if timeout, ok := mc.Params["timeout"].(string); ok {
var err error
pm.timeout, err = time.ParseDuration(timeout)
if err != nil {
return errors.Wrapf(err, "invalid timeout in %q ping monitor", pm.name)
}
} else {
return errors.Errorf("missing timeout in %q ping monitor", pm.name)
}
if mc.Interval == 0 {
return errors.Errorf("missing interval in %q ping monitor", pm.name)
}
pm.pollInterval = mc.Interval
return nil
}

90
monitor_test.go

@ -0,0 +1,90 @@
package main
import (
"math/rand"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func init() {
rand.Seed(time.Now().Unix())
}
func randString() string {
retval := ""
count := 'z' - 'a'
x := rand.Intn(10) + 3
for i := 0; i < x; i++ {
retval = string(append([]byte(retval), byte(int('a')+rand.Intn(int(count)))))
}
return retval
}
func TestPingMonitorFromConfig(t *testing.T) {
// name and type will be filled in automatically
failing := map[string]monitorConfig{
"blank": {},
"missing_interval": {baseConfig: baseConfig{
Params: map[string]interface{}{
"host": "127.0.0.1",
"count": 1,
"timeout": "1s",
},
}},
"missing_host": {baseConfig: baseConfig{
Params: map[string]interface{}{
"count": 1,
"timeout": "1s",
},
},
Interval: 30 * time.Second,
},
"missing_count": {baseConfig: baseConfig{
Params: map[string]interface{}{
"host": "127.0.0.1",
"timeout": "1s",
},
},
Interval: 30 * time.Second,
},
"missing_timeout": {baseConfig: baseConfig{
Params: map[string]interface{}{
"host": "127.0.0.1",
"count": 1,
},
},
Interval: 30 * time.Second,
},
}
for name, target := range failing {
target.Name = randString()
target.Type = "ping"
pm := &pingMonitor{}
assert.NotNil(t, pm.FromConfig(target), name)
}
passing := monitorConfig{baseConfig: baseConfig{
Name: "passing",
Type: "ping",
Params: map[string]interface{}{
"host": "127.0.0.1",
"count": 100,
"timeout": "1s",
},
},
Interval: 30 * time.Second,
}
pm := &pingMonitor{}
assert.Nil(t, pm.FromConfig(passing), "passing")
assert.Equal(t, pm.name, passing.Name)
assert.Equal(t, pm.host, passing.Params["host"])
assert.Equal(t, pm.count, uint(passing.Params["count"].(int)))
assert.Equal(t, pm.timeout, time.Second)
assert.Equal(t, pm.pollInterval, 30*time.Second)
assert.Equal(t, pm.Type(), "ping")
}

17
testdata/configs/good.yml

@ -0,0 +1,17 @@
---
listen: "127.0.0.1:1234"
monitors:
- name: "ping1"
type: "ping"
parameters:
host: "127.0.0.1"
count: 1
timeout: 1s
interval: 1m
- name: "ping2"
type: "ping"
parameters:
host: "10.0.0.1"
count: 10
timeout: 1s
interval: 2m
Loading…
Cancel
Save