Skip to content

Commit

Permalink
feat: target hosts can now specify a name to use for Prometheus metri…
Browse files Browse the repository at this point in the history
…cs (#89)

* feat: host configuration can now include names

* feat: host configuration can now include names

* feat: host configuration can now include names

* feat: host configuration can now include names

* build: move to common workflows

* build: move to common workflows
  • Loading branch information
clambin authored Feb 6, 2023
1 parent 6fa2686 commit 761fc35
Show file tree
Hide file tree
Showing 16 changed files with 524 additions and 141 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches:
- master
- slog
- labels

jobs:
test:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches-ignore:
- master
- slog
- labels
pull_request_target:
branches:
- master
Expand Down
49 changes: 38 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,61 @@
Born on a rainy Sunday afternoon, when my ISP was being its unreliable self again. Measures the latency and packet loss to one of more hosts and reports the data to Prometheus.

## Getting started

### Command line arguments:

The following command line arguments can be passed:

```
usage: pinger [<flags>] [<hosts>...]
pinger
Usage:
pinger [flags] [ <host> ... ]
Flags:
-h, --help Show context-sensitive help (also try --help-long and --help-man).
-v, --version Show application version.
--port=8080 Metrics listener port
--debug Log debug messages
--addr string Metrics listener address (default ":8080")
--config string Configuration file
--debug Log debug messages
-h, --help help for pinger
--port int Metrics listener port (obsolete)
-v, --version version for pinger
```

### Configuration file
The configuration file option specifies a yaml-formatted configuration file::

```
# Log debug messages
debug: true
# Metrics listener address (default ":8080")
addr: :8080
# Targets to ping
targets:
- host: 127.0.0.1 # Host IP address of hostname (mandatory)
name: localhost # Name to use for prometheus metrics (optional; pinger uses host if name is not specified)
```

Args:
[<hosts>] hosts to ping
If the filename is not specified on the command line, pinger will look for a file `config.yaml` in the following directories:

```
/etc/pinger
$HOME/.pinger
.
```

Any value in the configuration file may be overriden by setting an environment variable with a prefix `PINGER_`.


The target hosts can also be provided by exporting an environment variable 'HOSTS', e.g.

```
export HOSTS="127.0.0.1 192.168.0.1 192.168.0.200"
```

If both are provided, the environment variable takes precedence.
Pinger will consider provided hosts in the following order:

- HOSTS environment variable
- command-line arguments
- configuration file

NOTE: support for the HOSTS environment variable and command-line arguments will be removed in a future release.

### Docker

Expand Down
92 changes: 62 additions & 30 deletions cmd/pinger/pinger.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,59 +4,58 @@ import (
"context"
"fmt"
"github.com/clambin/pinger/collector"
"github.com/clambin/pinger/configuration"
"github.com/clambin/pinger/version"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/xonvanetta/shutdown/pkg/shutdown"
"golang.org/x/exp/slog"
"gopkg.in/alecthomas/kingpin.v2"
"net/http"
"os"
"path/filepath"
"strings"
)

func main() {
var cfg struct {
port int
debug bool
hosts []string
var (
configFilename string
cmd = cobra.Command{
Use: "pinger [flags] [ <host> ... ]",
Short: "Pings a set of hosts and exports latency & packet loss as Prometheus metrics",
Run: Main,
}
)

a := kingpin.New(filepath.Base(os.Args[0]), "collector")
a.Version(version.BuildVersion)
a.HelpFlag.Short('h')
a.VersionFlag.Short('v')
a.Flag("port", "Metrics listener port").Default("8080").IntVar(&cfg.port)
a.Flag("debug", "Log debug messages").BoolVar(&cfg.debug)
a.Arg("hosts", "hosts to collector").StringsVar(&cfg.hosts)

_, err := a.Parse(os.Args[1:])
if err != nil {
a.Usage(os.Args[1:])
os.Exit(2)
func main() {
if err := cmd.Execute(); err != nil {
slog.Error("failed to start", err)
os.Exit(1)
}
}

func Main(_ *cobra.Command, args []string) {
var opts slog.HandlerOptions
if cfg.debug {
if viper.GetBool("debug") {
opts.Level = slog.LevelDebug
opts.AddSource = true
//opts.AddSource = true
}
slog.SetDefault(slog.New(opts.NewTextHandler(os.Stdout)))
slog.SetDefault(slog.New(opts.NewTextHandler(os.Stderr)))

if value, ok := os.LookupEnv("HOSTS"); ok {
cfg.hosts = strings.Fields(value)
}

slog.Info("collector started", "hosts", cfg.hosts, "version", version.BuildVersion)
targets := configuration.GetTargets(viper.GetViper(), args)
slog.Info("pinger started", "targets", targets, "version", version.BuildVersion)

p := collector.New(cfg.hosts)
p := collector.New(targets)
prometheus.MustRegister(p)
go p.Run(context.Background())

http.Handle("/metrics", promhttp.Handler())
go func() {
if err2 := http.ListenAndServe(fmt.Sprintf(":%d", cfg.port), nil); err2 != http.ErrServerClosed {
var addr string
if port := viper.GetInt("port"); port > 0 {
addr = fmt.Sprintf(":%d", port)
} else {
addr = viper.GetString("addr")
}
if err2 := http.ListenAndServe(addr, nil); err2 != http.ErrServerClosed {
slog.Error("failed to start http server", err2)
}
}()
Expand All @@ -65,3 +64,36 @@ func main() {

slog.Info("collector stopped")
}

func init() {
cobra.OnInitialize(initConfig)
cmd.Version = version.BuildVersion
cmd.Flags().StringVar(&configFilename, "config", "", "Configuration file")
cmd.Flags().Bool("debug", false, "Log debug messages")
_ = viper.BindPFlag("debug", cmd.Flags().Lookup("debug"))
cmd.Flags().Int("port", 0, "Metrics listener port (obsolete)")
_ = viper.BindPFlag("port", cmd.Flags().Lookup("port"))
cmd.Flags().String("addr", ":8080", "Metrics listener address")
_ = viper.BindPFlag("addr", cmd.Flags().Lookup("addr"))
}

func initConfig() {
if configFilename != "" {
viper.SetConfigFile(configFilename)
} else {
viper.AddConfigPath("/etc/pinger/")
viper.AddConfigPath("$HOME/.pinger")
viper.AddConfigPath(".")
viper.SetConfigName("config")
}

viper.SetDefault("debug", false)
viper.SetDefault("addr", ":8080")

viper.SetEnvPrefix("PINGER")
viper.AutomaticEnv()

if err := viper.ReadInConfig(); err != nil {
slog.Warn("failed to read config file", "error", err)
}
}
23 changes: 12 additions & 11 deletions collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"github.com/clambin/pinger/collector/pinger"
"github.com/clambin/pinger/collector/tracker"
"github.com/clambin/pinger/configuration"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/exp/slog"
"time"
Expand All @@ -12,21 +13,21 @@ import (
// Collector pings a number of hosts and measures latency & packet loss
type Collector struct {
Pinger *pinger.Pinger
Trackers map[string]*tracker.Tracker
Trackers map[configuration.Target]*tracker.Tracker
Packets chan pinger.Response
}

// New creates a Collector for the specified hosts
func New(hosts []string) (monitor *Collector) {
func New(targets configuration.Targets) (monitor *Collector) {
ch := make(chan pinger.Response)
monitor = &Collector{
Pinger: pinger.MustNew(ch, hosts...),
Trackers: make(map[string]*tracker.Tracker),
Pinger: pinger.MustNew(ch, targets),
Trackers: make(map[configuration.Target]*tracker.Tracker),
Packets: ch,
}

for _, host := range hosts {
monitor.Trackers[host] = tracker.New()
for _, target := range targets {
monitor.Trackers[target] = tracker.New()
}

return
Expand All @@ -41,7 +42,7 @@ func (c *Collector) Run(ctx context.Context) {
case <-ctx.Done():
running = false
case packet := <-c.Packets:
c.Trackers[packet.Host].Track(packet.SequenceNr, packet.Latency)
c.Trackers[packet.Target].Track(packet.SequenceNr, packet.Latency)
}
}
}
Expand Down Expand Up @@ -80,11 +81,11 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric) {
count, loss, latency := t.Calculate()

if count > 0 {
slog.Debug("stats", "host", host, "count", count, "loss", loss, "latency", latency)
slog.Debug("stats", "host", host.GetName(), "count", count, "loss", loss, "latency", latency)
}

ch <- prometheus.MustNewConstMetric(packetsMetric, prometheus.GaugeValue, float64(count), host)
ch <- prometheus.MustNewConstMetric(lossMetric, prometheus.GaugeValue, float64(loss), host)
ch <- prometheus.MustNewConstMetric(latencyMetric, prometheus.GaugeValue, latency.Seconds(), host)
ch <- prometheus.MustNewConstMetric(packetsMetric, prometheus.GaugeValue, float64(count), host.GetName())
ch <- prometheus.MustNewConstMetric(lossMetric, prometheus.GaugeValue, float64(loss), host.GetName())
ch <- prometheus.MustNewConstMetric(latencyMetric, prometheus.GaugeValue, latency.Seconds(), host.GetName())
}
}
47 changes: 29 additions & 18 deletions collector/collector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@ import (
"bytes"
"context"
"github.com/clambin/pinger/collector"
"github.com/clambin/pinger/configuration"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
io_prometheus_client "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/exp/slog"
"os"
"testing"
"time"
)

func TestPinger_Collect(t *testing.T) {
p := collector.New([]string{"localhost"})
target := configuration.Target{Host: "127.0.0.1", Name: "localhost"}
p := collector.New([]configuration.Target{target})

p.Trackers["localhost"].Track(0, 150*time.Millisecond)
p.Trackers["localhost"].Track(1, 50*time.Millisecond)
p.Trackers[target].Track(0, 150*time.Millisecond)
p.Trackers[target].Track(1, 50*time.Millisecond)

r := prometheus.NewPedanticRegistry()
r.MustRegister(p)
Expand All @@ -36,7 +36,7 @@ pinger_packet_loss_count{host="localhost"} 0
`))
require.NoError(t, err)

p.Trackers["localhost"].Track(3, 100*time.Millisecond)
p.Trackers[target].Track(3, 100*time.Millisecond)
err = testutil.GatherAndCompare(r, bytes.NewBufferString(`# HELP pinger_latency_seconds Average latency in seconds
# TYPE pinger_latency_seconds gauge
pinger_latency_seconds{host="localhost"} 0.1
Expand All @@ -51,9 +51,14 @@ pinger_packet_loss_count{host="localhost"} 1
}

func TestPinger_Run(t *testing.T) {
ops := slog.HandlerOptions{Level: slog.LevelDebug}
slog.SetDefault(slog.New(ops.NewTextHandler(os.Stdout)))
p := collector.New([]string{"127.0.0.1"})
//ops := slog.HandlerOptions{Level: slog.LevelDebug}
//slog.SetDefault(slog.New(ops.NewTextHandler(os.Stdout)))

p := collector.New([]configuration.Target{
{Host: "127.0.0.1", Name: "localhost1"},
{Host: "localhost", Name: "localhost2"},
})

r := prometheus.NewPedanticRegistry()
r.MustRegister(p)

Expand All @@ -76,16 +81,22 @@ func TestPinger_Run(t *testing.T) {
return false
}, 5*time.Second, 10*time.Millisecond)

var entries int
for _, metric := range metrics {
switch metric.GetName() {
case "pinger_packet_count":
assert.NotZero(t, metric.Metric[0].GetGauge().GetValue())
case "pinger_latency_seconds":
assert.NotZero(t, metric.Metric[0].GetGauge().GetValue())
case "pinger_packet_loss_count":
assert.Zero(t, metric.Metric[0].GetGauge().GetValue())
default:
t.Fatal(metric.GetName())
for _, entry := range metric.Metric {
entries++
switch metric.GetName() {
case "pinger_packet_count":
assert.NotZero(t, entry.GetGauge().GetValue())
case "pinger_latency_seconds":
assert.NotZero(t, entry.GetGauge().GetValue())
case "pinger_packet_loss_count":
assert.Zero(t, entry.GetGauge().GetValue())
default:
t.Fatal(metric.GetName())
}
}
}
// check two targets for same IP address are both counted: 3 metrics for 2 targets = 6 entries
assert.Equal(t, 6, entries)
}
Loading

0 comments on commit 761fc35

Please sign in to comment.