github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/prober/traceroute/traceroute.go (about) 1 package traceroute 2 3 import ( 4 "context" 5 "errors" 6 "hash/fnv" 7 "maps" 8 "slices" 9 "sort" 10 "strings" 11 "time" 12 13 "github.com/google/uuid" 14 "github.com/grafana/synthetic-monitoring-agent/internal/model" 15 "github.com/grafana/synthetic-monitoring-agent/internal/prober/logger" 16 sm "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring" 17 "github.com/prometheus/client_golang/prometheus" 18 "github.com/rs/zerolog" 19 "github.com/tonobo/mtr/pkg/mtr" 20 ) 21 22 var errUnsupportedCheck = errors.New("unsupported check") 23 24 type Module struct { 25 count int 26 timeout time.Duration 27 hopTimeout time.Duration 28 interval time.Duration 29 hopSleep time.Duration 30 maxHops int 31 maxUnknownHops int 32 ptrLookup bool 33 srcAddr string 34 ringBufferSize int 35 } 36 37 type Prober struct { 38 config Module 39 logger zerolog.Logger 40 } 41 42 func NewProber(check model.Check, logger zerolog.Logger) (Prober, error) { 43 if check.Settings.Traceroute == nil { 44 return Prober{}, errUnsupportedCheck 45 } 46 47 c := settingsToModule(check.Settings.Traceroute) 48 49 return Prober{ 50 config: c, 51 logger: logger, 52 }, nil 53 } 54 55 func (p Prober) Name() string { 56 return "traceroute" 57 } 58 59 func (p Prober) Probe(ctx context.Context, target string, registry *prometheus.Registry, logger logger.Logger, _ string) (bool, float64) { 60 m, ch, err := mtr.NewMTR( 61 target, 62 p.config.srcAddr, 63 p.config.hopTimeout, 64 p.config.interval, 65 p.config.hopSleep, 66 p.config.maxHops, 67 p.config.maxUnknownHops, 68 p.config.ringBufferSize, 69 p.config.ptrLookup, 70 ) 71 if err != nil { 72 logErr := logger.Log(err) 73 if logErr != nil { 74 p.logger.Error().Err(logErr).Msg("logging error") 75 return false, 0 76 } 77 78 return false, 0 79 } 80 81 go func(ch <-chan struct{}) { 82 for { 83 _, isOpen := <-ch 84 if !isOpen { 85 return 86 } 87 } 88 }(ch) 89 90 success := true 91 92 err = m.RunWithContext(ctx, p.config.count) 93 if err != nil { 94 err = logger.Log("Level", "error", "msg", err.Error()) 95 if err != nil { 96 p.logger.Err(err).Msg("logging error") 97 } 98 99 success = false 100 } 101 102 tracerouteID := uuid.New() 103 totalPacketsLost := float64(0) 104 totalPacketsSent := float64(0) 105 hosts := make(map[int]string) 106 107 ttls := slices.Collect(maps.Keys(m.Statistic)) 108 slices.Sort(ttls) 109 110 for _, ttl := range ttls { 111 hop := m.Statistic[ttl] 112 113 totalPacketsLost += float64(hop.Lost) 114 totalPacketsSent += float64(hop.Sent) 115 avgElapsedTime := time.Duration(hop.Avg()) * time.Millisecond 116 sort.Strings(hop.Targets) 117 118 targets := make([]string, 0) 119 120 for target := range hop.Targets { 121 host := hop.LookupAddr(p.config.ptrLookup, target) 122 targets = append(targets, host) 123 } 124 125 t := strings.Join(targets, ",") 126 hosts[ttl] = t 127 128 err := logger.Log("Level", "info", "Destination", m.Address, "Hosts", t, "TTL", hop.TTL, "ElapsedTime", avgElapsedTime, "LossPercent", hop.Loss(), "Sent", hop.Sent, "TracerouteID", tracerouteID) 129 if err != nil { 130 p.logger.Error().Err(err).Msg("logging error") 131 continue 132 } 133 } 134 135 hostsKeys := make([]int, 0, len(hosts)) 136 for ttl := range hosts { 137 hostsKeys = append(hostsKeys, ttl) 138 } 139 140 sort.Ints(hostsKeys) 141 142 hostsString := "" 143 for _, ttl := range hostsKeys { 144 hostsString += hosts[ttl] 145 } 146 147 traceHash := fnv.New32() 148 149 _, err = traceHash.Write([]byte(hostsString)) 150 if err != nil { 151 p.logger.Error().Err(err).Msg("computing trace hash") 152 return false, 0 153 } 154 155 traceHashGauge := prometheus.NewGauge(prometheus.GaugeOpts{ 156 Name: "probe_traceroute_route_hash", 157 Help: "Hash of all the hosts in a traceroute path. Used to determine route volatility.", 158 }) 159 160 totalHopsGauge := prometheus.NewGauge(prometheus.GaugeOpts{ 161 Name: "probe_traceroute_total_hops", 162 Help: "Total hops to reach a traceroute destination", 163 }) 164 165 overallPacketLossGauge := prometheus.NewGauge(prometheus.GaugeOpts{ 166 Name: "probe_traceroute_packet_loss_percent", 167 Help: "Overall percentage of packet loss during the traceroute", 168 }) 169 170 // It shouldn't be possible for these registrations to fail 171 registry.MustRegister(traceHashGauge) 172 registry.MustRegister(totalHopsGauge) 173 registry.MustRegister(overallPacketLossGauge) 174 175 traceHashGauge.Set(float64(traceHash.Sum32())) 176 totalHopsGauge.Set(float64((len(m.Statistic)))) 177 178 overallPacketLoss := totalPacketsLost / totalPacketsSent 179 overallPacketLossGauge.Set(overallPacketLoss) 180 181 return success, 0 182 } 183 184 func settingsToModule(settings *sm.TracerouteSettings) Module { 185 m := Module{ 186 count: 5, 187 timeout: 30 * time.Second, 188 hopTimeout: 500 * time.Millisecond, 189 interval: time.Nanosecond, 190 hopSleep: time.Nanosecond, 191 maxHops: 64, 192 maxUnknownHops: 15, 193 ptrLookup: settings.PtrLookup, 194 ringBufferSize: 50, 195 srcAddr: "", 196 } 197 198 if settings.MaxHops > 0 { 199 m.maxUnknownHops = int(settings.MaxHops) 200 } 201 202 if settings.MaxUnknownHops > 1 { 203 m.maxUnknownHops = int(settings.MaxUnknownHops) 204 } 205 206 if settings.HopTimeout > 0 { 207 m.hopTimeout = time.Duration(settings.HopTimeout) 208 } 209 210 return m 211 }