Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/traceroute #113

Merged
merged 21 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Connect to server",
"type": "go",
"request": "attach",
"mode": "remote",
"remotePath": "${workspaceFolder}",
"port": 2345,
"host": "127.0.0.1"
},
{
"name": "Launch Package",
"type": "go",
Expand Down
8 changes: 7 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ require (
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/aeden/traceroute v0.0.0-20210211061815-03f5f7cb7908
github.com/google/go-cmp v0.6.0
)

require github.com/mitchellh/mapstructure v1.5.0 // indirect

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
Expand All @@ -28,7 +35,6 @@ require (
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/aeden/traceroute v0.0.0-20210211061815-03f5f7cb7908 h1:6suDyKbvZ5r2G/gblQLV9Cdv7rdqNlUxsRXpLOF0rKM=
github.com/aeden/traceroute v0.0.0-20210211061815-03f5f7cb7908/go.mod h1:HPBB/4vaPt7NcN9l72/+IwsmDVQsa6AWM6ZDKJCLB9U=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
Expand Down Expand Up @@ -27,8 +29,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
Expand Down
21 changes: 18 additions & 3 deletions pkg/checks/runtime/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,17 @@ import (
"github.com/caas-team/sparrow/pkg/checks/dns"
"github.com/caas-team/sparrow/pkg/checks/health"
"github.com/caas-team/sparrow/pkg/checks/latency"
"github.com/caas-team/sparrow/pkg/checks/traceroute"
)

// Config holds the runtime configuration
// for the various checks
// the sparrow supports
type Config struct {
Health *health.Config `yaml:"health" json:"health"`
Latency *latency.Config `yaml:"latency" json:"latency"`
Dns *dns.Config `yaml:"dns" json:"dns"`
Health *health.Config `yaml:"health" json:"health"`
Latency *latency.Config `yaml:"latency" json:"latency"`
Dns *dns.Config `yaml:"dns" json:"dns"`
Traceroute *traceroute.Config `yaml:"traceroute" json:"traceroute"`
}

// Empty returns true if no checks are configured
Expand Down Expand Up @@ -63,6 +65,9 @@ func (c Config) Iter() []checks.Runtime {
if c.Dns != nil {
configs = append(configs, c.Dns)
}
if c.Traceroute != nil {
configs = append(configs, c.Traceroute)
}
return configs
}

Expand All @@ -78,6 +83,9 @@ func (c Config) size() int {
if c.HasDNSCheck() {
size++
}
if c.HasTracerouteCheck() {
size++
}
return size
}

Expand All @@ -96,6 +104,11 @@ func (c Config) HasDNSCheck() bool {
return c.Dns != nil
}

// HasTracerouteCheck returns true if the check has a traceroute check configured
func (c Config) HasTracerouteCheck() bool {
return c.Traceroute != nil
}

// HasCheck returns true if the check has a check with the given name configured
func (c Config) HasCheck(name string) bool {
switch name {
Expand All @@ -105,6 +118,8 @@ func (c Config) HasCheck(name string) bool {
return c.HasLatencyCheck()
case dns.CheckName:
return c.HasDNSCheck()
case traceroute.CheckName:
return c.HasTracerouteCheck()
default:
return false
}
lvlcn-t marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
227 changes: 227 additions & 0 deletions pkg/checks/traceroute/traceroute.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
package traceroute

import (
"context"
"fmt"
"net/url"
"sync"
"time"

"github.com/aeden/traceroute"
"github.com/caas-team/sparrow/internal/logger"
"github.com/caas-team/sparrow/pkg/checks"
"github.com/getkin/kin-openapi/openapi3"
"github.com/prometheus/client_golang/prometheus"
)

var (
_ checks.Check = (*Traceroute)(nil)
CheckName = "traceroute"
lvlcn-t marked this conversation as resolved.
Show resolved Hide resolved
)

type Config struct {
Targets []Target `json:"targets" yaml:"targets" mapstructure:"targets"`
Retries int `json:"retries" yaml:"retries" mapstructure:"retries"`
MaxHops int `json:"maxHops" yaml:"maxHops" mapstructure:"maxHops"`
Interval time.Duration `json:"interval" yaml:"interval" mapstructure:"interval"`
Timeout time.Duration `json:"timeout" yaml:"timeout" mapstructure:"timeout"`
}

func (c Config) For() string {
puffitos marked this conversation as resolved.
Show resolved Hide resolved
return CheckName
}

type Target struct {
Addr string `json:"addr" yaml:"addr" mapstructure:"addr"`
Port uint16 `json:"port" yaml:"port" mapstructure:"port"`
}
lvlcn-t marked this conversation as resolved.
Show resolved Hide resolved

func NewCheck() checks.Check {
niklastreml marked this conversation as resolved.
Show resolved Hide resolved
return &Traceroute{
config: Config{},
traceroute: newTraceroute,
CheckBase: checks.CheckBase{
Mu: sync.Mutex{},
DoneChan: make(chan struct{}),
},
}
}

type Traceroute struct {
checks.CheckBase
config Config
traceroute tracerouteFactory
}
lvlcn-t marked this conversation as resolved.
Show resolved Hide resolved

type tracerouteFactory func(dest string, port, timeout, retries, maxHops int) (traceroute.TracerouteResult, error)

func newTraceroute(dest string, port, timeout, retries, maxHops int) (traceroute.TracerouteResult, error) {
opts := &traceroute.TracerouteOptions{}
opts.SetTimeoutMs(timeout)
opts.SetRetries(retries)
opts.SetMaxHops(maxHops)
opts.SetPort(port)
return traceroute.Traceroute(dest, opts)
}

type Result struct {
lvlcn-t marked this conversation as resolved.
Show resolved Hide resolved
// The minimum amount of hops required to reach the target
niklastreml marked this conversation as resolved.
Show resolved Hide resolved
NumHops int
// The path taken to the destination
Hops []Hop
}

type Hop struct {
lvlcn-t marked this conversation as resolved.
Show resolved Hide resolved
Addr string
Latency time.Duration
Success bool
}

func (d *Traceroute) Run(ctx context.Context, cResult chan checks.ResultDTO) error {
ctx, cancel := logger.NewContextWithLogger(ctx)
defer cancel()
log := logger.FromContext(ctx)
log.Info("Starting dns check", "interval", d.config.Interval.String())
lvlcn-t marked this conversation as resolved.
Show resolved Hide resolved

for {
select {
case <-ctx.Done():
log.Error("Context canceled", "err", ctx.Err())
return ctx.Err()
case <-d.DoneChan:
return nil
case <-time.After(d.config.Interval):
res := d.check(ctx)

cResult <- checks.ResultDTO{
Name: d.Name(),
Result: &checks.Result{
Data: res,
Timestamp: time.Now(),
},
}
log.Debug("Successfully finished dns check run")
}
}
}

func (c *Traceroute) GetConfig() checks.Runtime {
c.Mu.Lock()
defer c.Mu.Unlock()
return &c.config
}

func (c *Traceroute) check(ctx context.Context) map[string]Result {
res := make(map[string]Result)
log := logger.FromContext(ctx)

type internalResult struct {
addr string
res Result
}

var wg sync.WaitGroup
cResult := make(chan internalResult, len(c.config.Targets))

for _, t := range c.config.Targets {
wg.Add(1)
go func(t Target) {
defer wg.Done()
log.Debug("Running traceroute", "target", t.Addr)
start := time.Now()
tr, trerr := c.traceroute(t.Addr, int(t.Port), int(c.config.Timeout/time.Millisecond), c.config.Retries, c.config.MaxHops)
duration := time.Since(start)
niklastreml marked this conversation as resolved.
Show resolved Hide resolved
if trerr != nil {
log.Error("Error running traceroute", "err", trerr, "target", t.Addr)
niklastreml marked this conversation as resolved.
Show resolved Hide resolved
}

log.Debug("Ran traceroute", "result", tr, "duration", duration)
niklastreml marked this conversation as resolved.
Show resolved Hide resolved

result := Result{
NumHops: len(tr.Hops),
Hops: []Hop{},
}

for _, h := range tr.Hops {
result.Hops = append(result.Hops, Hop{
Addr: h.Host,
Latency: h.ElapsedTime,
Success: h.Success,
})
}
cResult <- internalResult{addr: t.Addr, res: result}
}(t)
}

log.Debug("Waiting for traceroute checks to finish")

go func() {
wg.Wait()
close(cResult)
}()
niklastreml marked this conversation as resolved.
Show resolved Hide resolved

log.Debug("All traceroute checks finished")

for r := range cResult {
res[r.addr] = r.res
}

log.Debug("Getting errors from traceroute checks")
niklastreml marked this conversation as resolved.
Show resolved Hide resolved

log.Debug("Finished traceroute checks", "result", res)

return res
}

// Shutdown is called once when the check is unregistered or sparrow shuts down
func (c *Traceroute) Shutdown(_ context.Context) error {
c.DoneChan <- struct{}{}
close(c.DoneChan)
return nil
}

// SetConfig is called once when the check is registered
// This is also called while the check is running, if the remote config is updated
// This should return an error if the config is invalid
func (c *Traceroute) SetConfig(cfg checks.Runtime) error {
if cfg, ok := cfg.(*Config); ok {
c.Mu.Lock()
defer c.Mu.Unlock()
c.config = *cfg
return nil
}

return checks.ErrConfigMismatch{
Expected: CheckName,
Current: cfg.For(),
}
}

// Schema returns an openapi3.SchemaRef of the result type returned by the check
func (c *Traceroute) Schema() (*openapi3.SchemaRef, error) {
return checks.OpenapiFromPerfData[Result](Result{})
lvlcn-t marked this conversation as resolved.
Show resolved Hide resolved
}

// GetMetricCollectors allows the check to provide prometheus metric collectors
func (c *Traceroute) GetMetricCollectors() []prometheus.Collector {
return []prometheus.Collector{}
}

func (c *Traceroute) Name() string {
return CheckName
}

func (c *Config) Validate() error {
if c.Timeout <= 0 {
return fmt.Errorf("timeout must be greater than 0")
}
if c.Interval <= 0 {
return fmt.Errorf("interval must be greater than 0")
}
for _, t := range c.Targets {
if _, err := url.Parse(t.Addr); err != nil {
return fmt.Errorf("%s is not a valid url", t.Addr)
lvlcn-t marked this conversation as resolved.
Show resolved Hide resolved
}
}
return nil
}
Loading
Loading