boulder-observer (#5315)
Add configuration driven Prometheus black box metric exporter
This commit is contained in:
parent
1e5d89e6c8
commit
97e393d2e7
|
@ -0,0 +1,216 @@
|
||||||
|
# boulder-observer
|
||||||
|
|
||||||
|
A modular configuration driven approach to black box monitoring with
|
||||||
|
Prometheus.
|
||||||
|
|
||||||
|
* [boulder-observer](#boulder-observer)
|
||||||
|
* [Usage](#usage)
|
||||||
|
* [Options](#options)
|
||||||
|
* [Starting the boulder-observer
|
||||||
|
daemon](#starting-the-boulder-observer-daemon)
|
||||||
|
* [Configuration](#configuration)
|
||||||
|
* [Root](#root)
|
||||||
|
* [Schema](#schema)
|
||||||
|
* [Example](#example)
|
||||||
|
* [Monitors](#monitors)
|
||||||
|
* [Schema](#schema-1)
|
||||||
|
* [Example](#example-1)
|
||||||
|
* [Probers](#probers)
|
||||||
|
* [DNS](#dns)
|
||||||
|
* [Schema](#schema-2)
|
||||||
|
* [Example](#example-2)
|
||||||
|
* [HTTP](#http)
|
||||||
|
* [Schema](#schema-3)
|
||||||
|
* [Example](#example-3)
|
||||||
|
* [Metrics](#metrics)
|
||||||
|
* [obs_monitors](#obs_monitors)
|
||||||
|
* [obs_observations](#obs_observations)
|
||||||
|
* [Development](#development)
|
||||||
|
* [Starting Prometheus locally](#starting-prometheus-locally)
|
||||||
|
* [Viewing metrics locally](#viewing-metrics-locally)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ./boulder-observer -help
|
||||||
|
-config string
|
||||||
|
Path to boulder-observer configuration file (default "config.yml")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Starting the boulder-observer daemon
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ./boulder-observer -config test/config-next/observer.yml
|
||||||
|
I152525 boulder-observer _KzylQI Versions: main=(Unspecified Unspecified) Golang=(go1.16.2) BuildHost=(Unspecified)
|
||||||
|
I152525 boulder-observer q_D84gk Initializing boulder-observer daemon from config: test/config-next/observer.yml
|
||||||
|
I152525 boulder-observer 7aq68AQ all monitors passed validation
|
||||||
|
I152527 boulder-observer yaefiAw kind=[HTTP] success=[true] duration=[0.130097] name=[https://letsencrypt.org-[200]]
|
||||||
|
I152527 boulder-observer 65CuDAA kind=[HTTP] success=[true] duration=[0.148633] name=[http://letsencrypt.org/foo-[200 404]]
|
||||||
|
I152530 boulder-observer idi4rwE kind=[DNS] success=[false] duration=[0.000093] name=[[2606:4700:4700::1111]:53-udp-A-google.com-recurse]
|
||||||
|
I152530 boulder-observer prOnrw8 kind=[DNS] success=[false] duration=[0.000242] name=[[2606:4700:4700::1111]:53-tcp-A-google.com-recurse]
|
||||||
|
I152530 boulder-observer 6uXugQw kind=[DNS] success=[true] duration=[0.022962] name=[1.1.1.1:53-udp-A-google.com-recurse]
|
||||||
|
I152530 boulder-observer to7h-wo kind=[DNS] success=[true] duration=[0.029860] name=[owen.ns.cloudflare.com:53-udp-A-letsencrypt.org-no-recurse]
|
||||||
|
I152530 boulder-observer ovDorAY kind=[DNS] success=[true] duration=[0.033820] name=[owen.ns.cloudflare.com:53-tcp-A-letsencrypt.org-no-recurse]
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Configuration is provided via a YAML file.
|
||||||
|
|
||||||
|
### Root
|
||||||
|
|
||||||
|
#### Schema
|
||||||
|
|
||||||
|
`debugaddr`: The Prometheus scrape port prefixed with a single colon
|
||||||
|
(e.g. `:8040`).
|
||||||
|
|
||||||
|
`buckets`: List of floats representing Prometheus histogram buckets (e.g
|
||||||
|
`[.001, .002, .005, .01, .02, .05, .1, .2, .5, 1, 2, 5, 10]`)
|
||||||
|
|
||||||
|
`syslog`: Map of log levels, see schema below.
|
||||||
|
|
||||||
|
- `stdoutlevel`: Log level for stdout, see legend below.
|
||||||
|
- `sysloglevel`:Log level for stdout, see legend below.
|
||||||
|
|
||||||
|
`0`: *EMERG* `1`: *ALERT* `2`: *CRIT* `3`: *ERR* `4`: *WARN* `5`:
|
||||||
|
*NOTICE* `6`: *INFO* `7`: *DEBUG*
|
||||||
|
|
||||||
|
`monitors`: List of monitors, see [monitors](#monitors) for schema.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
debugaddr: :8040
|
||||||
|
buckets: [.001, .002, .005, .01, .02, .05, .1, .2, .5, 1, 2, 5, 10]
|
||||||
|
syslog:
|
||||||
|
stdoutlevel: 6
|
||||||
|
sysloglevel: 6
|
||||||
|
-
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monitors
|
||||||
|
|
||||||
|
#### Schema
|
||||||
|
|
||||||
|
`period`: Interval between probing attempts (e.g. `1s` `1m` `1h`).
|
||||||
|
|
||||||
|
`kind`: Kind of prober to use, see [probers](#probers) for schema.
|
||||||
|
|
||||||
|
`settings`: Map of prober settings, see [probers](#probers) for schema.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
monitors:
|
||||||
|
-
|
||||||
|
period: 5s
|
||||||
|
kind: DNS
|
||||||
|
settings:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Probers
|
||||||
|
|
||||||
|
#### DNS
|
||||||
|
|
||||||
|
##### Schema
|
||||||
|
|
||||||
|
`protocol`: Protocol to use, options are: `udp` or `tcp`.
|
||||||
|
|
||||||
|
`server`: Hostname, IPv4 address, or IPv6 address surrounded with
|
||||||
|
brackets + port of the DNS server to send the query to (e.g.
|
||||||
|
`example.com:53`, `1.1.1.1:53`, or `[2606:4700:4700::1111]:53`).
|
||||||
|
|
||||||
|
`recurse`: Bool indicating if recursive resolution is desired.
|
||||||
|
|
||||||
|
`query_name`: Name to query (e.g. `example.com`).
|
||||||
|
|
||||||
|
`query_type`: Record type to query, options are: `A`, `AAAA`, `TXT`, or
|
||||||
|
`CAA`.
|
||||||
|
|
||||||
|
##### Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
monitors:
|
||||||
|
-
|
||||||
|
period: 5s
|
||||||
|
kind: DNS
|
||||||
|
settings:
|
||||||
|
protocol: tcp
|
||||||
|
server: [2606:4700:4700::1111]:53
|
||||||
|
recurse: false
|
||||||
|
query_name: letsencrypt.org
|
||||||
|
query_type: A
|
||||||
|
```
|
||||||
|
|
||||||
|
#### HTTP
|
||||||
|
|
||||||
|
##### Schema
|
||||||
|
|
||||||
|
`url`: Scheme + Hostname to send a request to (e.g.
|
||||||
|
`https://example.com`).
|
||||||
|
|
||||||
|
`rcodes`: List of expected HTTP response codes.
|
||||||
|
|
||||||
|
##### Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
monitors:
|
||||||
|
-
|
||||||
|
period: 2s
|
||||||
|
kind: HTTP
|
||||||
|
settings:
|
||||||
|
url: http://letsencrypt.org/FOO
|
||||||
|
rcodes: [200, 404]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
Observer provides the following metrics.
|
||||||
|
|
||||||
|
### obs_monitors
|
||||||
|
|
||||||
|
Count of configured monitors.
|
||||||
|
|
||||||
|
**Labels:**
|
||||||
|
|
||||||
|
`kind`: Kind of Prober the monitor is configured to use.
|
||||||
|
|
||||||
|
`valid`: Bool indicating whether settings provided could be validated
|
||||||
|
for the `kind` of Prober specified.
|
||||||
|
|
||||||
|
### obs_observations
|
||||||
|
|
||||||
|
**Labels:**
|
||||||
|
|
||||||
|
`name`: Name of the monitor.
|
||||||
|
|
||||||
|
`kind`: Kind of prober the monitor is configured to use.
|
||||||
|
|
||||||
|
`duration`: Duration of the probing in seconds.
|
||||||
|
|
||||||
|
`success`: Bool indicating whether the result of the probe attempt was
|
||||||
|
successful.
|
||||||
|
|
||||||
|
**Bucketed response times:**
|
||||||
|
|
||||||
|
This is configurable, see `buckets` under [root/schema](#schema).
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Starting Prometheus locally
|
||||||
|
|
||||||
|
Please note, this assumes you've installed a local Prometheus binary.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
prometheus --config.file=boulder/test/prometheus/prometheus.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Viewing metrics locally
|
||||||
|
|
||||||
|
When developing with a local Prometheus instance you can use this link
|
||||||
|
to view metrics: [link](http://0.0.0.0:9090)
|
|
@ -0,0 +1,35 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/letsencrypt/boulder/cmd"
|
||||||
|
"github.com/letsencrypt/boulder/observer"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
configPath := flag.String(
|
||||||
|
"config", "config.yml", "Path to boulder-observer configuration file")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
configYAML, err := ioutil.ReadFile(*configPath)
|
||||||
|
cmd.FailOnError(err, "failed to read config file")
|
||||||
|
|
||||||
|
// Parse the YAML config file.
|
||||||
|
var config observer.ObsConf
|
||||||
|
err = yaml.Unmarshal(configYAML, &config)
|
||||||
|
if err != nil {
|
||||||
|
cmd.FailOnError(err, "failed to parse YAML config")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make an `Observer` object.
|
||||||
|
observer, err := config.MakeObserver()
|
||||||
|
if err != nil {
|
||||||
|
cmd.FailOnError(err, "config failed validation")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the `Observer` daemon.
|
||||||
|
observer.Start()
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package observer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/letsencrypt/boulder/cmd"
|
||||||
|
"github.com/letsencrypt/boulder/observer/probers"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MonConf is exported to receive YAML configuration in `ObsConf`.
|
||||||
|
type MonConf struct {
|
||||||
|
Period cmd.ConfigDuration `yaml:"period"`
|
||||||
|
Kind string `yaml:"kind"`
|
||||||
|
Settings probers.Settings `yaml:"settings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// validatePeriod ensures the received `Period` field is at least 1µs.
|
||||||
|
func (c *MonConf) validatePeriod() error {
|
||||||
|
if c.Period.Duration < 1*time.Microsecond {
|
||||||
|
return errors.New("period must be at least 1µs")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshalConfigurer constructs a `Configurer` by marshaling the
|
||||||
|
// value of the `Settings` field back to bytes, then passing it to the
|
||||||
|
// `UnmarshalSettings` method of the `Configurer` type specified by the
|
||||||
|
// `Kind` field.
|
||||||
|
func (c MonConf) unmarshalConfigurer() (probers.Configurer, error) {
|
||||||
|
kind := strings.Trim(strings.ToLower(c.Kind), " ")
|
||||||
|
configurer, err := probers.GetConfigurer(kind)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
settings, _ := yaml.Marshal(c.Settings)
|
||||||
|
configurer, err = configurer.UnmarshalSettings(settings)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return configurer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeMonitor constructs a `monitor` object from the contents of the
|
||||||
|
// bound `MonConf`. If the `MonConf` cannot be validated, an error
|
||||||
|
// appropriate for end-user consumption is returned instead.
|
||||||
|
func (c MonConf) makeMonitor() (*monitor, error) {
|
||||||
|
err := c.validatePeriod()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
probeConf, err := c.unmarshalConfigurer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
prober, err := probeConf.MakeProber()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &monitor{c.Period.Duration, prober}, nil
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package observer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/letsencrypt/boulder/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMonConf_validatePeriod(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
Period cmd.ConfigDuration
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"valid", fields{cmd.ConfigDuration{Duration: 1 * time.Microsecond}}, false},
|
||||||
|
{"1 nanosecond", fields{cmd.ConfigDuration{Duration: 1 * time.Nanosecond}}, true},
|
||||||
|
{"none supplied", fields{cmd.ConfigDuration{}}, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := &MonConf{
|
||||||
|
Period: tt.fields.Period,
|
||||||
|
}
|
||||||
|
if err := c.validatePeriod(); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("MonConf.validatePeriod() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package observer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
blog "github.com/letsencrypt/boulder/log"
|
||||||
|
"github.com/letsencrypt/boulder/observer/probers"
|
||||||
|
)
|
||||||
|
|
||||||
|
type monitor struct {
|
||||||
|
period time.Duration
|
||||||
|
prober probers.Prober
|
||||||
|
}
|
||||||
|
|
||||||
|
// start spins off a 'Prober' goroutine on an interval of `m.period`
|
||||||
|
// with a timeout of half `m.period`
|
||||||
|
func (m monitor) start(logger blog.Logger) {
|
||||||
|
ticker := time.NewTicker(m.period)
|
||||||
|
timeout := m.period / 2
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
// Attempt to probe the configured target.
|
||||||
|
success, dur := m.prober.Probe(timeout)
|
||||||
|
|
||||||
|
// Produce metrics to be scraped by Prometheus.
|
||||||
|
histObservations.WithLabelValues(
|
||||||
|
m.prober.Name(), m.prober.Kind(), strconv.FormatBool(success),
|
||||||
|
).Observe(dur.Seconds())
|
||||||
|
|
||||||
|
// Log the outcome of the probe attempt.
|
||||||
|
logger.Infof(
|
||||||
|
"kind=[%s] success=[%v] duration=[%f] name=[%s]",
|
||||||
|
m.prober.Kind(), success, dur.Seconds(), m.prober.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
|
@ -0,0 +1,137 @@
|
||||||
|
package observer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/letsencrypt/boulder/cmd"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
countMonitors = prometheus.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "obs_monitors",
|
||||||
|
Help: "details of each configured monitor",
|
||||||
|
},
|
||||||
|
[]string{"kind", "valid"},
|
||||||
|
)
|
||||||
|
histObservations *prometheus.HistogramVec
|
||||||
|
)
|
||||||
|
|
||||||
|
// ObsConf is exported to receive YAML configuration.
|
||||||
|
type ObsConf struct {
|
||||||
|
DebugAddr string `yaml:"debugaddr"`
|
||||||
|
Buckets []float64 `yaml:"buckets"`
|
||||||
|
Syslog cmd.SyslogConfig `yaml:"syslog"`
|
||||||
|
MonConfs []*MonConf `yaml:"monitors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateSyslog ensures the the `Syslog` field received by `ObsConf`
|
||||||
|
// contains valid log levels.
|
||||||
|
func (c *ObsConf) validateSyslog() error {
|
||||||
|
syslog, stdout := c.Syslog.SyslogLevel, c.Syslog.StdoutLevel
|
||||||
|
if stdout < 0 || stdout > 7 || syslog < 0 || syslog > 7 {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"invalid 'syslog', '%+v', valid log levels are 0-7", c.Syslog)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateDebugAddr ensures the `debugAddr` received by `ObsConf` is
|
||||||
|
// properly formatted and a valid port.
|
||||||
|
func (c *ObsConf) validateDebugAddr() error {
|
||||||
|
_, p, err := net.SplitHostPort(c.DebugAddr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"invalid 'debugaddr', %q, not expected format", c.DebugAddr)
|
||||||
|
}
|
||||||
|
port, _ := strconv.Atoi(p)
|
||||||
|
if port <= 0 || port > 65535 {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"invalid 'debugaddr','%d' is not a valid port", port)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ObsConf) makeMonitors() ([]*monitor, []error, error) {
|
||||||
|
var errs []error
|
||||||
|
var monitors []*monitor
|
||||||
|
for e, m := range c.MonConfs {
|
||||||
|
entry := strconv.Itoa(e + 1)
|
||||||
|
monitor, err := m.makeMonitor()
|
||||||
|
if err != nil {
|
||||||
|
// append validation error to errs
|
||||||
|
errs = append(
|
||||||
|
errs, fmt.Errorf(
|
||||||
|
"'monitors' entry #%s couldn't be validated: %v", entry, err))
|
||||||
|
|
||||||
|
// increment metrics
|
||||||
|
countMonitors.WithLabelValues(m.Kind, "false").Inc()
|
||||||
|
} else {
|
||||||
|
// append monitor to monitors
|
||||||
|
monitors = append(monitors, monitor)
|
||||||
|
|
||||||
|
// increment metrics
|
||||||
|
countMonitors.WithLabelValues(m.Kind, "true").Inc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(c.MonConfs) == len(errs) {
|
||||||
|
return nil, errs, errors.New("no valid monitors, cannot continue")
|
||||||
|
}
|
||||||
|
return monitors, errs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeObserver constructs an `Observer` object from the contents of the
|
||||||
|
// bound `ObsConf`. If the `ObsConf` cannot be validated, an error
|
||||||
|
// appropriate for end-user consumption is returned instead.
|
||||||
|
func (c *ObsConf) MakeObserver() (*Observer, error) {
|
||||||
|
err := c.validateSyslog()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.validateDebugAddr()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.MonConfs) == 0 {
|
||||||
|
return nil, errors.New("no monitors provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Buckets) == 0 {
|
||||||
|
return nil, errors.New("no histogram buckets provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start monitoring and logging.
|
||||||
|
metrics, logger := cmd.StatsAndLogging(c.Syslog, c.DebugAddr)
|
||||||
|
histObservations = prometheus.NewHistogramVec(
|
||||||
|
prometheus.HistogramOpts{
|
||||||
|
Name: "obs_observations",
|
||||||
|
Help: "details of each probe attempt",
|
||||||
|
Buckets: c.Buckets,
|
||||||
|
}, []string{"name", "kind", "success"})
|
||||||
|
metrics.MustRegister(countMonitors)
|
||||||
|
metrics.MustRegister(histObservations)
|
||||||
|
defer logger.AuditPanic()
|
||||||
|
logger.Info(cmd.VersionString())
|
||||||
|
logger.Infof("Initializing boulder-observer daemon")
|
||||||
|
logger.Debugf("Using config: %+v", c)
|
||||||
|
|
||||||
|
monitors, errs, err := c.makeMonitors()
|
||||||
|
if len(errs) != 0 {
|
||||||
|
logger.Errf("%d of %d monitors failed validation", len(errs), len(c.MonConfs))
|
||||||
|
for _, err := range errs {
|
||||||
|
logger.Errf("%s", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.Info("all monitors passed validation")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Observer{logger, monitors}, nil
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
package observer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/letsencrypt/boulder/cmd"
|
||||||
|
"github.com/letsencrypt/boulder/observer/probers"
|
||||||
|
_ "github.com/letsencrypt/boulder/observer/probers/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
debugAddr = ":8040"
|
||||||
|
errDBZMsg = "over 9000"
|
||||||
|
mockConf = "MockConf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestObsConf_makeMonitors(t *testing.T) {
|
||||||
|
var errDBZ = errors.New(errDBZMsg)
|
||||||
|
var cfgSyslog = cmd.SyslogConfig{StdoutLevel: 6, SyslogLevel: 6}
|
||||||
|
var cfgDur = cmd.ConfigDuration{Duration: time.Second * 5}
|
||||||
|
var cfgBuckets = []float64{.001}
|
||||||
|
var validMonConf = &MonConf{
|
||||||
|
cfgDur, mockConf, probers.Settings{"valid": true, "pname": "foo", "pkind": "bar"}}
|
||||||
|
var invalidMonConf = &MonConf{
|
||||||
|
cfgDur, mockConf, probers.Settings{"valid": false, "errmsg": errDBZMsg, "pname": "foo", "pkind": "bar"}}
|
||||||
|
type fields struct {
|
||||||
|
Syslog cmd.SyslogConfig
|
||||||
|
Buckets []float64
|
||||||
|
DebugAddr string
|
||||||
|
MonConfs []*MonConf
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
errs []error
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// valid
|
||||||
|
{"1 valid", fields{cfgSyslog, cfgBuckets, debugAddr, []*MonConf{validMonConf}}, nil, false},
|
||||||
|
{"2 valid", fields{
|
||||||
|
cfgSyslog, cfgBuckets, debugAddr, []*MonConf{validMonConf, validMonConf}}, nil, false},
|
||||||
|
{"1 valid, 1 invalid", fields{
|
||||||
|
cfgSyslog, cfgBuckets, debugAddr, []*MonConf{validMonConf, invalidMonConf}}, []error{errDBZ}, false},
|
||||||
|
{"1 valid, 2 invalid", fields{
|
||||||
|
cfgSyslog, cfgBuckets, debugAddr, []*MonConf{invalidMonConf, validMonConf, invalidMonConf}}, []error{errDBZ, errDBZ}, false},
|
||||||
|
// invalid
|
||||||
|
{"1 invalid", fields{cfgSyslog, cfgBuckets, debugAddr, []*MonConf{invalidMonConf}}, []error{errDBZ}, true},
|
||||||
|
{"0", fields{cfgSyslog, cfgBuckets, debugAddr, []*MonConf{}}, nil, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := &ObsConf{
|
||||||
|
Syslog: tt.fields.Syslog,
|
||||||
|
Buckets: tt.fields.Buckets,
|
||||||
|
DebugAddr: tt.fields.DebugAddr,
|
||||||
|
MonConfs: tt.fields.MonConfs,
|
||||||
|
}
|
||||||
|
_, errs, err := c.makeMonitors()
|
||||||
|
if len(errs) != len(tt.errs) {
|
||||||
|
t.Errorf("ObsConf.validateMonConfs() errs = %d, want %d", len(errs), len(tt.errs))
|
||||||
|
t.Logf("%v", errs)
|
||||||
|
}
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("ObsConf.validateMonConfs() err = %v, want %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestObsConf_ValidateDebugAddr(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
DebugAddr string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// valid
|
||||||
|
{"max len and range", fields{":65535"}, false},
|
||||||
|
{"min len and range", fields{":1"}, false},
|
||||||
|
{"2 digits", fields{":80"}, false},
|
||||||
|
// invalid
|
||||||
|
{"out of range high", fields{":65536"}, true},
|
||||||
|
{"out of range low", fields{":0"}, true},
|
||||||
|
{"not even a port", fields{":foo"}, true},
|
||||||
|
{"missing :", fields{"foo"}, true},
|
||||||
|
{"missing port", fields{"foo:"}, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := &ObsConf{
|
||||||
|
DebugAddr: tt.fields.DebugAddr,
|
||||||
|
}
|
||||||
|
if err := c.validateDebugAddr(); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("ObsConf.ValidateDebugAddr() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestObsConf_validateSyslog(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
Syslog cmd.SyslogConfig
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// valid
|
||||||
|
{"valid", fields{cmd.SyslogConfig{StdoutLevel: 6, SyslogLevel: 6}}, false},
|
||||||
|
// invalid
|
||||||
|
{"both too high", fields{cmd.SyslogConfig{StdoutLevel: 9, SyslogLevel: 9}}, true},
|
||||||
|
{"stdout too high", fields{cmd.SyslogConfig{StdoutLevel: 9, SyslogLevel: 6}}, true},
|
||||||
|
{"syslog too high", fields{cmd.SyslogConfig{StdoutLevel: 6, SyslogLevel: 9}}, true},
|
||||||
|
{"both too low", fields{cmd.SyslogConfig{StdoutLevel: -1, SyslogLevel: -1}}, true},
|
||||||
|
{"stdout too low", fields{cmd.SyslogConfig{StdoutLevel: -1, SyslogLevel: 6}}, true},
|
||||||
|
{"syslog too low", fields{cmd.SyslogConfig{StdoutLevel: 6, SyslogLevel: -1}}, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := &ObsConf{
|
||||||
|
Syslog: tt.fields.Syslog,
|
||||||
|
}
|
||||||
|
if err := c.validateSyslog(); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("ObsConf.validateSyslog() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package observer
|
||||||
|
|
||||||
|
import (
|
||||||
|
blog "github.com/letsencrypt/boulder/log"
|
||||||
|
_ "github.com/letsencrypt/boulder/observer/probers/dns"
|
||||||
|
_ "github.com/letsencrypt/boulder/observer/probers/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Observer is the steward of goroutines started for each `monitor`.
|
||||||
|
type Observer struct {
|
||||||
|
logger blog.Logger
|
||||||
|
monitors []*monitor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start spins off a goroutine for each monitor and then runs forever.
|
||||||
|
func (o Observer) Start() {
|
||||||
|
for _, mon := range o.monitors {
|
||||||
|
go mon.start(o.logger)
|
||||||
|
}
|
||||||
|
select {}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package probers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNSProbe is the exported 'Prober' object for monitors configured to
|
||||||
|
// perform DNS requests.
|
||||||
|
type DNSProbe struct {
|
||||||
|
proto string
|
||||||
|
server string
|
||||||
|
recurse bool
|
||||||
|
qname string
|
||||||
|
qtype uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns a string that uniquely identifies the monitor.
|
||||||
|
func (p DNSProbe) Name() string {
|
||||||
|
recursion := func() string {
|
||||||
|
if p.recurse {
|
||||||
|
return "recurse"
|
||||||
|
}
|
||||||
|
return "no-recurse"
|
||||||
|
}()
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"%s-%s-%s-%s-%s", p.server, p.proto, recursion, dns.TypeToString[p.qtype], p.qname)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kind returns a name that uniquely identifies the `Kind` of `Prober`.
|
||||||
|
func (p DNSProbe) Kind() string {
|
||||||
|
return "DNS"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Probe performs the configured DNS query.
|
||||||
|
func (p DNSProbe) Probe(timeout time.Duration) (bool, time.Duration) {
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion(dns.Fqdn(p.qname), p.qtype)
|
||||||
|
m.RecursionDesired = p.recurse
|
||||||
|
c := dns.Client{Timeout: timeout, Net: p.proto}
|
||||||
|
start := time.Now()
|
||||||
|
r, _, err := c.Exchange(m, p.server)
|
||||||
|
if err != nil {
|
||||||
|
return false, time.Since(start)
|
||||||
|
}
|
||||||
|
if r == nil {
|
||||||
|
return false, time.Since(start)
|
||||||
|
}
|
||||||
|
if r.Rcode != dns.RcodeSuccess {
|
||||||
|
return false, time.Since(start)
|
||||||
|
}
|
||||||
|
return true, time.Since(start)
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
package probers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/letsencrypt/boulder/observer/probers"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
validQTypes = map[string]uint16{"A": 1, "TXT": 16, "AAAA": 28, "CAA": 257}
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNSConf is exported to receive YAML configuration
|
||||||
|
type DNSConf struct {
|
||||||
|
Proto string `yaml:"protocol"`
|
||||||
|
Server string `yaml:"server"`
|
||||||
|
Recurse bool `yaml:"recurse"`
|
||||||
|
QName string `yaml:"query_name"`
|
||||||
|
QType string `yaml:"query_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalSettings constructs a DNSConf object from YAML as bytes.
|
||||||
|
func (c DNSConf) UnmarshalSettings(settings []byte) (probers.Configurer, error) {
|
||||||
|
var conf DNSConf
|
||||||
|
err := yaml.Unmarshal(settings, &conf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return conf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c DNSConf) validateServer() error {
|
||||||
|
server := strings.Trim(strings.ToLower(c.Server), " ")
|
||||||
|
// Ensure `server` contains a port.
|
||||||
|
host, port, err := net.SplitHostPort(server)
|
||||||
|
if err != nil || port == "" {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"invalid `server`, %q, could not be split: %s", c.Server, err)
|
||||||
|
}
|
||||||
|
// Ensure `server` port is valid.
|
||||||
|
portNum, err := strconv.Atoi(port)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"invalid `server`, %q, port must be a number", c.Server)
|
||||||
|
}
|
||||||
|
if portNum <= 0 || portNum > 65535 {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"invalid `server`, %q, port number must be one in [1-65535]", c.Server)
|
||||||
|
}
|
||||||
|
// Ensure `server` is a valid FQDN or IPv4 / IPv6 address.
|
||||||
|
IPv6 := net.ParseIP(host).To16()
|
||||||
|
IPv4 := net.ParseIP(host).To4()
|
||||||
|
FQDN := dns.IsFqdn(dns.Fqdn(host))
|
||||||
|
if IPv6 == nil && IPv4 == nil && !FQDN {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"invalid `server`, %q, is not an FQDN or IPv4 / IPv6 address", c.Server)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c DNSConf) validateProto() error {
|
||||||
|
validProtos := []string{"udp", "tcp"}
|
||||||
|
proto := strings.Trim(strings.ToLower(c.Proto), " ")
|
||||||
|
for _, i := range validProtos {
|
||||||
|
if proto == i {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf(
|
||||||
|
"invalid `protocol`, got: %q, expected one in: %s", c.Proto, validProtos)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c DNSConf) validateQType() error {
|
||||||
|
validQTypes = map[string]uint16{"A": 1, "TXT": 16, "AAAA": 28, "CAA": 257}
|
||||||
|
qtype := strings.Trim(strings.ToUpper(c.QType), " ")
|
||||||
|
q := make([]string, 0, len(validQTypes))
|
||||||
|
for i := range validQTypes {
|
||||||
|
q = append(q, i)
|
||||||
|
if qtype == i {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf(
|
||||||
|
"invalid `query_type`, got: %q, expected one in %s", c.QType, q)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeProber constructs a `DNSProbe` object from the contents of the
|
||||||
|
// bound `DNSConf` object. If the `DNSConf` cannot be validated, an
|
||||||
|
// error appropriate for end-user consumption is returned instead.
|
||||||
|
func (c DNSConf) MakeProber() (probers.Prober, error) {
|
||||||
|
// validate `query_name`
|
||||||
|
if !dns.IsFqdn(dns.Fqdn(c.QName)) {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"invalid `query_name`, %q is not an fqdn", c.QName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate `server`
|
||||||
|
err := c.validateServer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate `protocol`
|
||||||
|
err = c.validateProto()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate `query_type`
|
||||||
|
err = c.validateQType()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return DNSProbe{
|
||||||
|
proto: strings.Trim(strings.ToLower(c.Proto), " "),
|
||||||
|
recurse: c.Recurse,
|
||||||
|
qname: c.QName,
|
||||||
|
server: c.Server,
|
||||||
|
qtype: validQTypes[strings.Trim(strings.ToUpper(c.QType), " ")],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// init is called at runtime and registers `DNSConf`, a `Prober`
|
||||||
|
// `Configurer` type, as "DNS".
|
||||||
|
func init() {
|
||||||
|
probers.Register("DNS", DNSConf{})
|
||||||
|
}
|
|
@ -0,0 +1,195 @@
|
||||||
|
package probers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/letsencrypt/boulder/observer/probers"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDNSConf_validateServer(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
Server string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// ipv4 cases
|
||||||
|
{"ipv4 with port", fields{"1.1.1.1:53"}, false},
|
||||||
|
{"ipv4 without port", fields{"1.1.1.1"}, true},
|
||||||
|
{"ipv4 port num missing", fields{"1.1.1.1:"}, true},
|
||||||
|
{"ipv4 string for port", fields{"1.1.1.1:foo"}, true},
|
||||||
|
{"ipv4 port out of range high", fields{"1.1.1.1:65536"}, true},
|
||||||
|
{"ipv4 port out of range low", fields{"1.1.1.1:0"}, true},
|
||||||
|
|
||||||
|
// ipv6 cases
|
||||||
|
{"ipv6 with port", fields{"[2606:4700:4700::1111]:53"}, false},
|
||||||
|
{"ipv6 without port", fields{"[2606:4700:4700::1111]"}, true},
|
||||||
|
{"ipv6 port num missing", fields{"[2606:4700:4700::1111]:"}, true},
|
||||||
|
{"ipv6 string for port", fields{"[2606:4700:4700::1111]:foo"}, true},
|
||||||
|
{"ipv6 port out of range high", fields{"[2606:4700:4700::1111]:65536"}, true},
|
||||||
|
{"ipv6 port out of range low", fields{"[2606:4700:4700::1111]:0"}, true},
|
||||||
|
|
||||||
|
// hostname cases
|
||||||
|
{"hostname with port", fields{"foo:53"}, false},
|
||||||
|
{"hostname without port", fields{"foo"}, true},
|
||||||
|
{"hostname port num missing", fields{"foo:"}, true},
|
||||||
|
{"hostname string for port", fields{"foo:bar"}, true},
|
||||||
|
{"hostname port out of range high", fields{"foo:65536"}, true},
|
||||||
|
{"hostname port out of range low", fields{"foo:0"}, true},
|
||||||
|
|
||||||
|
// fqdn cases
|
||||||
|
{"fqdn with port", fields{"bar.foo.baz:53"}, false},
|
||||||
|
{"fqdn without port", fields{"bar.foo.baz"}, true},
|
||||||
|
{"fqdn port num missing", fields{"bar.foo.baz:"}, true},
|
||||||
|
{"fqdn string for port", fields{"bar.foo.baz:bar"}, true},
|
||||||
|
{"fqdn port out of range high", fields{"bar.foo.baz:65536"}, true},
|
||||||
|
{"fqdn port out of range low", fields{"bar.foo.baz:0"}, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := DNSConf{
|
||||||
|
Server: tt.fields.Server,
|
||||||
|
}
|
||||||
|
if err := c.validateServer(); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("DNSConf.validateServer() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDNSConf_validateQType(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
QType string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// valid
|
||||||
|
{"A", fields{"A"}, false},
|
||||||
|
{"AAAA", fields{"AAAA"}, false},
|
||||||
|
{"TXT", fields{"TXT"}, false},
|
||||||
|
// invalid
|
||||||
|
{"AAA", fields{"AAA"}, true},
|
||||||
|
{"TXTT", fields{"TXTT"}, true},
|
||||||
|
{"D", fields{"D"}, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := DNSConf{
|
||||||
|
QType: tt.fields.QType,
|
||||||
|
}
|
||||||
|
if err := c.validateQType(); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("DNSConf.validateQType() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDNSConf_validateProto(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
Proto string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// valid
|
||||||
|
{"tcp", fields{"tcp"}, false},
|
||||||
|
{"udp", fields{"udp"}, false},
|
||||||
|
// invalid
|
||||||
|
{"foo", fields{"foo"}, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := DNSConf{
|
||||||
|
Proto: tt.fields.Proto,
|
||||||
|
}
|
||||||
|
if err := c.validateProto(); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("DNSConf.validateProto() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDNSConf_MakeProber(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
Proto string
|
||||||
|
Server string
|
||||||
|
Recurse bool
|
||||||
|
QName string
|
||||||
|
QType string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// valid
|
||||||
|
{"valid", fields{"udp", "1.1.1.1:53", true, "google.com", "A"}, false},
|
||||||
|
// invalid
|
||||||
|
{"bad proto", fields{"can with string", "1.1.1.1:53", true, "google.com", "A"}, true},
|
||||||
|
{"bad server", fields{"udp", "1.1.1.1:9000000", true, "google.com", "A"}, true},
|
||||||
|
{"bad qtype", fields{"udp", "1.1.1.1:9000000", true, "google.com", "BAZ"}, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := DNSConf{
|
||||||
|
Proto: tt.fields.Proto,
|
||||||
|
Server: tt.fields.Server,
|
||||||
|
Recurse: tt.fields.Recurse,
|
||||||
|
QName: tt.fields.QName,
|
||||||
|
QType: tt.fields.QType,
|
||||||
|
}
|
||||||
|
if _, err := c.MakeProber(); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("HTTPConf.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDNSConf_UnmarshalSettings(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
protocol interface{}
|
||||||
|
server interface{}
|
||||||
|
recurse interface{}
|
||||||
|
query_name interface{}
|
||||||
|
query_type interface{}
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
want probers.Configurer
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"valid", fields{"udp", "1.1.1.1:53", true, "google.com", "A"}, DNSConf{"udp", "1.1.1.1:53", true, "google.com", "A"}, false},
|
||||||
|
{"invalid", fields{42, 42, 42, 42, 42}, nil, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
settings := probers.Settings{
|
||||||
|
"protocol": tt.fields.protocol,
|
||||||
|
"server": tt.fields.server,
|
||||||
|
"recurse": tt.fields.recurse,
|
||||||
|
"query_name": tt.fields.query_name,
|
||||||
|
"query_type": tt.fields.query_type,
|
||||||
|
}
|
||||||
|
settingsBytes, _ := yaml.Marshal(settings)
|
||||||
|
c := DNSConf{}
|
||||||
|
got, err := c.UnmarshalSettings(settingsBytes)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("DNSConf.UnmarshalSettings() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("DNSConf.UnmarshalSettings() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package probers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTPProbe is the exported 'Prober' object for monitors configured to
|
||||||
|
// perform HTTP requests.
|
||||||
|
type HTTPProbe struct {
|
||||||
|
url string
|
||||||
|
rcodes []int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns a string that uniquely identifies the monitor.
|
||||||
|
func (p HTTPProbe) Name() string {
|
||||||
|
return fmt.Sprintf("%s-%d", p.url, p.rcodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kind returns a name that uniquely identifies the `Kind` of `Prober`.
|
||||||
|
func (p HTTPProbe) Kind() string {
|
||||||
|
return "HTTP"
|
||||||
|
}
|
||||||
|
|
||||||
|
// isExpected ensures that the received HTTP response code matches one
|
||||||
|
// that's expected.
|
||||||
|
func (p HTTPProbe) isExpected(received int) bool {
|
||||||
|
for _, c := range p.rcodes {
|
||||||
|
if received == c {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Probe performs the configured HTTP request.
|
||||||
|
func (p HTTPProbe) Probe(timeout time.Duration) (bool, time.Duration) {
|
||||||
|
client := http.Client{Timeout: timeout}
|
||||||
|
start := time.Now()
|
||||||
|
// TODO(@beautifulentropy): add support for more than HTTP GET
|
||||||
|
resp, err := client.Get(p.url)
|
||||||
|
if err != nil {
|
||||||
|
return false, time.Since(start)
|
||||||
|
}
|
||||||
|
return p.isExpected(resp.StatusCode), time.Since(start)
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package probers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/letsencrypt/boulder/observer/probers"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTPConf is exported to receive YAML configuration.
|
||||||
|
type HTTPConf struct {
|
||||||
|
URL string `yaml:"url"`
|
||||||
|
RCodes []int `yaml:"rcodes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalSettings takes YAML as bytes and unmarshals it to the to an
|
||||||
|
// HTTPConf object.
|
||||||
|
func (c HTTPConf) UnmarshalSettings(settings []byte) (probers.Configurer, error) {
|
||||||
|
var conf HTTPConf
|
||||||
|
err := yaml.Unmarshal(settings, &conf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return conf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c HTTPConf) validateURL() error {
|
||||||
|
url, err := url.Parse(c.URL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"invalid 'url', got: %q, expected a valid url", c.URL)
|
||||||
|
}
|
||||||
|
if url.Scheme == "" {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"invalid 'url', got: %q, missing scheme", c.URL)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c HTTPConf) validateRCodes() error {
|
||||||
|
if len(c.RCodes) == 0 {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"invalid 'rcodes', got: %q, please specify at least one", c.RCodes)
|
||||||
|
}
|
||||||
|
for _, c := range c.RCodes {
|
||||||
|
// ensure rcode entry is in range 100-599
|
||||||
|
if c < 100 || c > 599 {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"'rcodes' contains an invalid HTTP response code, '%d'", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeProber constructs a `HTTPProbe` object from the contents of the
|
||||||
|
// bound `HTTPConf` object. If the `HTTPConf` cannot be validated, an
|
||||||
|
// error appropriate for end-user consumption is returned instead.
|
||||||
|
func (c HTTPConf) MakeProber() (probers.Prober, error) {
|
||||||
|
// validate `url`
|
||||||
|
err := c.validateURL()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate `rcodes`
|
||||||
|
err = c.validateRCodes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return HTTPProbe{c.URL, c.RCodes}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// init is called at runtime and registers `HTTPConf`, a `Prober`
|
||||||
|
// `Configurer` type, as "HTTP".
|
||||||
|
func init() {
|
||||||
|
probers.Register("HTTP", HTTPConf{})
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package probers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/letsencrypt/boulder/observer/probers"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHTTPConf_MakeProber(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
URL string
|
||||||
|
RCodes []int
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// valid
|
||||||
|
{"valid fqdn valid rcode", fields{"http://example.com", []int{200}}, false},
|
||||||
|
{"valid hostname valid rcode", fields{"example", []int{200}}, true},
|
||||||
|
// invalid
|
||||||
|
{"valid fqdn no rcode", fields{"http://example.com", nil}, true},
|
||||||
|
{"valid fqdn invalid rcode", fields{"http://example.com", []int{1000}}, true},
|
||||||
|
{"valid fqdn 1 invalid rcode", fields{"http://example.com", []int{200, 1000}}, true},
|
||||||
|
{"bad fqdn good rcode", fields{":::::", []int{200}}, true},
|
||||||
|
{"missing scheme", fields{"example.com", []int{200}}, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := HTTPConf{
|
||||||
|
URL: tt.fields.URL,
|
||||||
|
RCodes: tt.fields.RCodes,
|
||||||
|
}
|
||||||
|
if _, err := c.MakeProber(); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("HTTPConf.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPConf_UnmarshalSettings(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
url interface{}
|
||||||
|
rcodes interface{}
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
want probers.Configurer
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"valid", fields{"google.com", []int{200}}, HTTPConf{"google.com", []int{200}}, false},
|
||||||
|
{"invalid", fields{42, 42}, nil, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
settings := probers.Settings{
|
||||||
|
"url": tt.fields.url,
|
||||||
|
"rcodes": tt.fields.rcodes,
|
||||||
|
}
|
||||||
|
settingsBytes, _ := yaml.Marshal(settings)
|
||||||
|
c := HTTPConf{}
|
||||||
|
got, err := c.UnmarshalSettings(settingsBytes)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("DNSConf.UnmarshalSettings() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("DNSConf.UnmarshalSettings() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package probers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/letsencrypt/boulder/cmd"
|
||||||
|
"github.com/letsencrypt/boulder/observer/probers"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockConfigurer struct {
|
||||||
|
Valid bool `yaml:"valid"`
|
||||||
|
ErrMsg string `yaml:"errmsg"`
|
||||||
|
PName string `yaml:"pname"`
|
||||||
|
PKind string `yaml:"pkind"`
|
||||||
|
PTook cmd.ConfigDuration `yaml:"ptook"`
|
||||||
|
PSuccess bool `yaml:"psuccess"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c MockConfigurer) UnmarshalSettings(settings []byte) (probers.Configurer, error) {
|
||||||
|
var conf MockConfigurer
|
||||||
|
err := yaml.Unmarshal(settings, &conf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return conf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c MockConfigurer) MakeProber() (probers.Prober, error) {
|
||||||
|
if !c.Valid {
|
||||||
|
return nil, errors.New("could not be validated")
|
||||||
|
}
|
||||||
|
return MockProber{c.PName, c.PKind, c.PTook, c.PSuccess}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
probers.Register("MockConf", MockConfigurer{})
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package probers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/letsencrypt/boulder/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockProber struct {
|
||||||
|
name string
|
||||||
|
kind string
|
||||||
|
took cmd.ConfigDuration
|
||||||
|
success bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p MockProber) Name() string {
|
||||||
|
return p.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p MockProber) Kind() string {
|
||||||
|
return p.kind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p MockProber) Probe(timeout time.Duration) (bool, time.Duration) {
|
||||||
|
return p.success, p.took.Duration
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package probers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/letsencrypt/boulder/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Registry is the global mapping of all `Configurer` types. Types
|
||||||
|
// are added to this mapping on import by including a call to
|
||||||
|
// `Register` in their `init` function.
|
||||||
|
Registry = make(map[string]Configurer)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Prober is the interface for `Prober` types.
|
||||||
|
type Prober interface {
|
||||||
|
// Name returns a name that uniquely identifies the monitor that
|
||||||
|
// configured this `Prober`.
|
||||||
|
Name() string
|
||||||
|
|
||||||
|
// Kind returns a name that uniquely identifies the `Kind` of
|
||||||
|
// `Prober`.
|
||||||
|
Kind() string
|
||||||
|
|
||||||
|
// Probe attempts the configured request or query, Each `Prober`
|
||||||
|
// must treat the duration passed to it as a timeout.
|
||||||
|
Probe(time.Duration) (bool, time.Duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configurer is the interface for `Configurer` types.
|
||||||
|
type Configurer interface {
|
||||||
|
// UnmarshalSettings unmarshals YAML as bytes to a `Configurer`
|
||||||
|
// object.
|
||||||
|
UnmarshalSettings([]byte) (Configurer, error)
|
||||||
|
|
||||||
|
// MakeProber constructs a `Prober` object from the contents of the
|
||||||
|
// bound `Configurer` object. If the `Configurer` cannot be
|
||||||
|
// validated, an error appropriate for end-user consumption is
|
||||||
|
// returned instead.
|
||||||
|
MakeProber() (Prober, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings is exported as a temporary receiver for the `settings` field
|
||||||
|
// of `MonConf`. `Settings` is always marshaled back to bytes and then
|
||||||
|
// unmarshalled into the `Configurer` specified by the `Kind` field of
|
||||||
|
// the `MonConf`.
|
||||||
|
type Settings map[string]interface{}
|
||||||
|
|
||||||
|
// GetConfigurer returns the probe configurer specified by name from
|
||||||
|
// `Registry`.
|
||||||
|
func GetConfigurer(kind string) (Configurer, error) {
|
||||||
|
// normalize
|
||||||
|
name := strings.Trim(strings.ToLower(kind), " ")
|
||||||
|
// check if exists
|
||||||
|
if _, ok := Registry[name]; ok {
|
||||||
|
return Registry[name], nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("%s is not a registered Prober type", kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register is called by the `init` function of every `Configurer` to
|
||||||
|
// add the caller to the global `Registry` map. If the caller attempts
|
||||||
|
// to add a `Configurer` to the registry using the same name as a prior
|
||||||
|
// `Configurer` Observer will exit after logging an error.
|
||||||
|
func Register(kind string, c Configurer) {
|
||||||
|
// normalize
|
||||||
|
name := strings.Trim(strings.ToLower(kind), " ")
|
||||||
|
// check for name collision
|
||||||
|
if _, exists := Registry[name]; exists {
|
||||||
|
cmd.Fail(fmt.Sprintf(
|
||||||
|
"problem registering configurer %s: name collision", kind))
|
||||||
|
}
|
||||||
|
Registry[name] = c
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
---
|
||||||
|
debugaddr: :8040
|
||||||
|
buckets: [.001, .002, .005, .01, .02, .05, .1, .2, .5, 1, 2, 5, 10]
|
||||||
|
syslog:
|
||||||
|
stdoutlevel: 6
|
||||||
|
sysloglevel: 6
|
||||||
|
monitors:
|
||||||
|
-
|
||||||
|
period: 5s
|
||||||
|
kind: DNS
|
||||||
|
settings:
|
||||||
|
protocol: udp
|
||||||
|
server: owen.ns.cloudflare.com:53
|
||||||
|
recurse: false
|
||||||
|
query_name: letsencrypt.org
|
||||||
|
query_type: A
|
||||||
|
-
|
||||||
|
period: 5s
|
||||||
|
kind: DNS
|
||||||
|
settings:
|
||||||
|
protocol: udp
|
||||||
|
server: 1.1.1.1:53
|
||||||
|
recurse: true
|
||||||
|
query_name: google.com
|
||||||
|
query_type: A
|
||||||
|
-
|
||||||
|
period: 10s
|
||||||
|
kind: DNS
|
||||||
|
settings:
|
||||||
|
protocol: tcp
|
||||||
|
server: 8.8.8.8:53
|
||||||
|
recurse: true
|
||||||
|
query_name: google.com
|
||||||
|
query_type: A
|
||||||
|
-
|
||||||
|
period: 2s
|
||||||
|
kind: HTTP
|
||||||
|
settings:
|
||||||
|
url: https://letsencrypt.org
|
||||||
|
rcodes: [200]
|
||||||
|
-
|
||||||
|
period: 5s
|
||||||
|
kind: DNS
|
||||||
|
settings:
|
||||||
|
protocol: tcp
|
||||||
|
server: owen.ns.cloudflare.com:53
|
||||||
|
recurse: false
|
||||||
|
query_name: letsencrypt.org
|
||||||
|
query_type: A
|
||||||
|
-
|
||||||
|
period: 5s
|
||||||
|
kind: DNS
|
||||||
|
settings:
|
||||||
|
protocol: tcp
|
||||||
|
server: 1.1.1.1:53
|
||||||
|
recurse: true
|
||||||
|
query_name: google.com
|
||||||
|
query_type: A
|
||||||
|
-
|
||||||
|
period: 10s
|
||||||
|
kind: DNS
|
||||||
|
settings:
|
||||||
|
protocol: udp
|
||||||
|
server: 8.8.8.8:53
|
||||||
|
recurse: true
|
||||||
|
query_name: google.com
|
||||||
|
query_type: A
|
||||||
|
-
|
||||||
|
period: 5s
|
||||||
|
kind: DNS
|
||||||
|
settings:
|
||||||
|
protocol: tcp
|
||||||
|
server: "[2606:4700:4700::1111]:53"
|
||||||
|
recurse: true
|
||||||
|
query_name: google.com
|
||||||
|
query_type: A
|
||||||
|
-
|
||||||
|
period: 5s
|
||||||
|
kind: DNS
|
||||||
|
settings:
|
||||||
|
protocol: udp
|
||||||
|
server: "[2606:4700:4700::1111]:53"
|
||||||
|
recurse: true
|
||||||
|
query_name: google.com
|
||||||
|
query_type: A
|
||||||
|
-
|
||||||
|
period: 2s
|
||||||
|
kind: HTTP
|
||||||
|
settings:
|
||||||
|
url: http://letsencrypt.org/foo
|
||||||
|
rcodes: [200, 404]
|
|
@ -16,3 +16,4 @@ scrape_configs:
|
||||||
- boulder:8008
|
- boulder:8008
|
||||||
- boulder:8009
|
- boulder:8009
|
||||||
- boulder:8010
|
- boulder:8010
|
||||||
|
- boulder:8040
|
||||||
|
|
Loading…
Reference in New Issue