Skip to content

Commit

Permalink
Merge pull request #54 from SuperQ/config
Browse files Browse the repository at this point in the history
Add support for targets from a config file
  • Loading branch information
SuperQ authored Dec 19, 2021
2 parents be00e1e + 1ca4385 commit 1b9381e
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 15 deletions.
22 changes: 17 additions & 5 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
version: 2.1

orbs:
prometheus: prometheus/prometheus@0.14.0
prometheus: prometheus/prometheus@0.15.0

executors:
# This must match .promu.yml.
Expand All @@ -18,7 +18,7 @@ jobs:
- checkout
- run: go mod download
- run: make promu
- run: make style unused test build
- run: make style lint unused test build
- run: rm -v smokeping_prober

workflows:
Expand All @@ -31,15 +31,27 @@ workflows:
only: /.*/
- prometheus/build:
name: build
parallelism: 3
promu_opts: "-p linux/amd64 -p windows/amd64 -p linux/arm64 -p darwin/amd64 -p darwin/arm64 -p linux/386"
filters:
tags:
only: /.*/
ignore: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/
branches:
ignore: /^(master|release-.*|.*build-all.*)$/
- prometheus/build:
name: build_all
parallelism: 3
filters:
branches:
only: /^(master|release-.*|.*build-all.*)$/
tags:
only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/
- prometheus/publish_master:
docker_hub_organization: superque
quay_io_organization: superq
requires:
- test
- build
- build_all
filters:
branches:
only: master
Expand All @@ -48,7 +60,7 @@ workflows:
quay_io_organization: superq
requires:
- test
- build
- build_all
filters:
tags:
only: /^v.*/
Expand Down
4 changes: 4 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
linters-settings:
errcheck:
exclude: scripts/errcheck_excludes.txt
36 changes: 31 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@ Prometheus style "smokeping" prober.
This prober sends a series of ICMP (or UDP) pings to a target and records the responses in Prometheus histogram metrics.

```
usage: smokeping_prober [<flags>] <hosts>...
usage: smokeping_prober [<flags>] [<hosts>...]
Flags:
-h, --help Show context-sensitive help (also try --help-long and --help-man).
--config.file="smokeping_prober.yml"
Optional smokeping_prober configuration file.
--web.listen-address=":9374"
Address on which to expose metrics and web interface.
--web.telemetry-path="/metrics"
--web.telemetry-path="/metrics"
Path under which to expose metrics.
--buckets=BUCKETS A comma delimited list of buckets to use
--buckets="5e-05,0.0001,0.0002,0.0004,0.0008,0.0016,0.0032,0.0064,0.0128,0.0256,0.0512,0.1024,0.2048,0.4096,0.8192,1.6384,3.2768,6.5536,13.1072,26.2144"
A comma delimited list of buckets to use
-i, --ping.interval=1s Ping interval duration
--privileged Run in privileged ICMP mode
--log.level="info" Only log messages with the given severity or above. Valid levels: [debug, info, warn,
Expand All @@ -27,12 +30,35 @@ Flags:
--version Show application version.
Args:
<hosts> List of hosts to ping
[<hosts>] List of hosts to ping
```

## Configuration

The prober can take a list of targets and parameters from the command line or from a yaml config file.

Example config:

```yaml
---
targets:
- hosts:
- host1
- host2
interval: 1s # Duration, Default 1s.
network: ip # One of ip, ip4, ip6. Default: ip (automatic IPv4/IPv6)
protocol: icmp # One of icmp, udp. Default: icmp (Requires privileged operation)
```
In each host group the `interval`, `network`, and `protocol` are optional.

The interval Duration is in [Go time.ParseDuration()](https://golang.org/pkg/time/#ParseDuration) syntax.

NOTE: The config is only read on startup, SIGHUP is not supported (yet).

## Building and running

Requires Go >= 1.11
Requires Go >= 1.14

```console
go get github.com/superq/smokeping_prober
Expand Down
2 changes: 1 addition & 1 deletion collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func newPingResponseHistogram(buckets []float64) *prometheus.HistogramVec {
Namespace: namespace,
Name: "response_duration_seconds",
Help: "A histogram of latencies for ping responses.",
Buckets: buckets,
Buckets: prometheus.ExponentialBuckets(0.00025, 2, 16),
},
labelNames,
)
Expand Down
116 changes: 116 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2021 Ben Kochie <superq@gmail.com>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package config

import (
"fmt"
"os"
"sync"
"time"

yaml "gopkg.in/yaml.v2"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)

const namespace = "smokeping_prober"

var (
configReloadSuccess = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Name: "config_last_reload_successful",
Help: "Blackbox exporter config loaded successfully.",
})

configReloadSeconds = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Name: "config_last_reload_success_timestamp_seconds",
Help: "Timestamp of the last successful configuration reload.",
})

// DefaultTargetGroup sets the default configuration for the TargetGroup
DefaultTargetGroup = TargetGroup{
Interval: time.Second,
Network: "ip",
Protocol: "icmp",
}
)

type Config struct {
Targets []TargetGroup `yaml:"targets"`
}

type SafeConfig struct {
sync.RWMutex
C *Config
}

func (sc *SafeConfig) ReloadConfig(confFile string) (err error) {
var c = &Config{}
defer func() {
if err != nil {
configReloadSuccess.Set(0)
} else {
configReloadSuccess.Set(1)
configReloadSeconds.SetToCurrentTime()
}
}()

yamlReader, err := os.Open(confFile)
if err != nil {
return fmt.Errorf("error reading config file: %w", err)
}
defer yamlReader.Close()
decoder := yaml.NewDecoder(yamlReader)

if err = decoder.Decode(c); err != nil {
return fmt.Errorf("error parsing config file: %w", err)
}

sc.Lock()
sc.C = c
sc.Unlock()

return nil
}

type TargetGroup struct {
Hosts []string `yaml:"hosts"`
Interval time.Duration `yaml:"interval,omitempty"`
Network string `yaml:"network,omitempty"`
Protocol string `yaml:"protocol,omitempty"`
// TODO: Needs work to fix MetricFamily consistency.
// Labels map[string]string `yaml:"labels,omitempty"`
}

// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (s *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
type plain Config
if err := unmarshal((*plain)(s)); err != nil {
return err
}
return nil
}

// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (s *TargetGroup) UnmarshalYAML(unmarshal func(interface{}) error) error {
*s = DefaultTargetGroup
type plain TargetGroup
if err := unmarshal((*plain)(s)); err != nil {
return err
}
return nil
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ require (
github.com/go-ping/ping v0.0.0-20211130115550-779d1e919534
github.com/prometheus/client_golang v1.11.0
github.com/prometheus/common v0.32.1
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/yaml.v2 v2.4.0
)
62 changes: 58 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package main

import (
"errors"
"fmt"
"math"
"net/http"
Expand All @@ -25,6 +26,7 @@ import (
"time"

"github.com/go-ping/ping"
"github.com/superq/smokeping_prober/config"

"github.com/go-kit/log"
"github.com/go-kit/log/level"
Expand All @@ -33,6 +35,7 @@ import (
"github.com/prometheus/common/promlog"
"github.com/prometheus/common/promlog/flag"
"github.com/prometheus/common/version"
"golang.org/x/sync/errgroup"
"gopkg.in/alecthomas/kingpin.v2"
)

Expand All @@ -41,6 +44,10 @@ var (
defaultBuckets = "5e-05,0.0001,0.0002,0.0004,0.0008,0.0016,0.0032,0.0064,0.0128,0.0256,0.0512,0.1024,0.2048,0.4096,0.8192,1.6384,3.2768,6.5536,13.1072,26.2144"

logger log.Logger

sc = &config.SafeConfig{
C: &config.Config{},
}
)

type hostList []string
Expand Down Expand Up @@ -87,13 +94,14 @@ func parseBuckets(buckets string) ([]float64, error) {

func main() {
var (
configFile = kingpin.Flag("config.file", "Optional smokeping_prober configuration yaml file.").String()
listenAddress = kingpin.Flag("web.listen-address", "Address on which to expose metrics and web interface.").Default(":9374").String()
metricsPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").String()

buckets = kingpin.Flag("buckets", "A comma delimited list of buckets to use").Default(defaultBuckets).String()
interval = kingpin.Flag("ping.interval", "Ping interval duration").Short('i').Default("1s").Duration()
privileged = kingpin.Flag("privileged", "Run in privileged ICMP mode").Default("true").Bool()
hosts = HostList(kingpin.Arg("hosts", "List of hosts to ping").Required())
hosts = HostList(kingpin.Arg("hosts", "List of hosts to ping"))
)

promlogConfig := &promlog.Config{}
Expand All @@ -105,6 +113,16 @@ func main() {

level.Info(logger).Log("msg", "Starting smokeping_prober", "version", version.Info())
level.Info(logger).Log("msg", "Build context", "build_context", version.BuildContext())

if err := sc.ReloadConfig(*configFile); err != nil {
if errors.Is(err, os.ErrNotExist) {
level.Info(logger).Log("msg", "ignoring missing config file", "filename", *configFile)
} else {
level.Error(logger).Log("msg", "error loading config", "filename", err.Error())
return
}
}

bucketlist, err := parseBuckets(*buckets)
if err != nil {
level.Error(logger).Log("msg", "Failed to parse buckets", "err", err)
Expand All @@ -114,8 +132,10 @@ func main() {
prometheus.MustRegister(pingResponseSeconds)

pingers := make([]*ping.Pinger, len(*hosts))
var pinger *ping.Pinger
var host string
for i, host := range *hosts {
pinger := ping.New(host)
pinger = ping.New(host)

err := pinger.Resolve()
if err != nil {
Expand All @@ -133,11 +153,41 @@ func main() {
pingers[i] = pinger
}

maxInterval := *interval

sc.Lock()
for _, targetGroup := range sc.C.Targets {
if targetGroup.Interval > maxInterval {
maxInterval = targetGroup.Interval
}
for _, host = range targetGroup.Hosts {
pinger = ping.New(host)
pinger.Interval = targetGroup.Interval
pinger.SetNetwork(targetGroup.Network)
if targetGroup.Protocol == "icmp" {
pinger.SetPrivileged(true)
}
err := pinger.Resolve()
if err != nil {
level.Error(logger).Log("msg", "failed to resolve pinger", "error", err.Error())
return
}
pingers = append(pingers, pinger)
}
}
sc.Unlock()

if len(pingers) == 0 {
level.Error(logger).Log("msg", "no targets specified on command line or in config file")
return
}

splay := time.Duration(interval.Nanoseconds() / int64(len(pingers)))
level.Info(logger).Log("msg", fmt.Sprintf("Waiting %s between starting pingers", splay))
g := new(errgroup.Group)
for _, pinger := range pingers {
level.Info(logger).Log("msg", "Starting prober", "addr", pinger.Addr())
go pinger.Run()
level.Info(logger).Log("msg", "Starting prober", "address", pinger.Addr(), "interval", pinger.Interval)
g.Go(pinger.Run)
time.Sleep(splay)
}

Expand All @@ -163,4 +213,8 @@ func main() {
for _, pinger := range pingers {
pinger.Stop()
}
if err = g.Wait(); err != nil {
level.Error(logger).Log("msg", "pingers failed", "error", err)
os.Exit(1)
}
}
5 changes: 5 additions & 0 deletions scripts/errcheck_excludes.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Used in HTTP handlers, any error is handled by the server itself.
(net/http.ResponseWriter).Write

// Never check for logger errors.
(github.com/go-kit/log.Logger).Log
Loading

0 comments on commit 1b9381e

Please sign in to comment.