ping-monitoring

A prometheus exporter to monitor matrix federation with using the existing pingbot infra.
git clone git://archive.git.mtrnord.blog/MTRNord/ping-monitoring.git
Log | Files | Refs | LICENSE

ping_manager.go (5823B)


      1 package main
      2 
      3 import (
      4 	"context"
      5 	"encoding/json"
      6 	"net/http"
      7 	"net/url"
      8 	"sync"
      9 	"time"
     10 
     11 	"github.com/prometheus/client_golang/prometheus"
     12 	log "github.com/sirupsen/logrus"
     13 	"maunium.net/go/mautrix"
     14 	"maunium.net/go/mautrix/id"
     15 )
     16 
     17 type PingCollector struct {
     18 	Config        *Config
     19 	Mean          *prometheus.GaugeVec
     20 	Median        *prometheus.GaugeVec
     21 	GMean         *prometheus.GaugeVec
     22 	Failures      *prometheus.CounterVec
     23 	LastCollected map[string]time.Time
     24 }
     25 
     26 func (c *PingCollector) Describe(ch chan<- *prometheus.Desc) {
     27 	c.Mean.Describe(ch)
     28 	c.Median.Describe(ch)
     29 	c.GMean.Describe(ch)
     30 	c.Failures.Describe(ch)
     31 	log.Infof("Registered metrics")
     32 }
     33 
     34 func (c *PingCollector) Collect(ch chan<- prometheus.Metric) {
     35 	c.Mean.Collect(ch)
     36 	c.Median.Collect(ch)
     37 	c.GMean.Collect(ch)
     38 	c.Failures.Collect(ch)
     39 }
     40 
     41 // For collection of matrix metrics we first need to ping.
     42 // To do that we do 2 things:
     43 // 1. Write `!ping` in the ping room as a message from our own homeserver
     44 // 2. Send `!ping` from all remote homeservers to our ping room
     45 // We then track our event_ids and make sure that at least one event is reaching our own homeserver
     46 // And at least one event id (can be a different one) reaches the other homeservers.
     47 // We also do react on our own `!ping` message and send a `!pong` back to the ping room based on the maubot echobot logic.
     48 func (c *PingCollector) UpdateData() {
     49 	log.Infoln("Starting Collecting metrics")
     50 	var wg sync.WaitGroup
     51 	wg.Add(1)
     52 
     53 	// Send ping from our own homeserver
     54 	go c.SendPing(context.Background(), c.Config.OwnHomeserver.Client, &wg)
     55 
     56 	// Send ping from all remote homeservers
     57 	for _, homeserver := range c.Config.RemoteHomeservers {
     58 		wg.Add(1)
     59 		go c.SendPing(context.Background(), homeserver.Client, &wg)
     60 	}
     61 
     62 	wg.Wait()
     63 }
     64 
     65 // This sends a ping into the ping room
     66 // It takes a homeserver config as an argument to know where to send the ping
     67 func (c *PingCollector) SendPing(ctx context.Context, client *mautrix.Client, wg *sync.WaitGroup) {
     68 	defer wg.Done()
     69 	if c.LastCollected[client.UserID.Homeserver()].Add(time.Duration(c.Config.PingRateSeconds) * time.Second).After(time.Now()) {
     70 		log.Infof("Not sending ping as we sent one less than %d seconds ago", c.Config.PingRateSeconds)
     71 		return
     72 	}
     73 	if c.LastCollected == nil {
     74 		c.LastCollected = make(map[string]time.Time)
     75 	}
     76 	c.LastCollected[client.UserID.Homeserver()] = time.Now()
     77 	log.Infof("Sending ping as %s", client.UserID)
     78 	if c.Config.PingRoomID == "" {
     79 		log.Errorf("No ping room ID found")
     80 		return
     81 	}
     82 
     83 	// Send the ping
     84 	resp, err := client.SendText(ctx, c.Config.PingRoomID, "!ping")
     85 	if err != nil {
     86 		log.Errorf("Failed to send ping: %s", err)
     87 	}
     88 	log.Infof("Sent ping as %s with event_id %s", client.UserID, resp.EventID)
     89 	eventID := resp.EventID
     90 
     91 	// Poll the json to check if any pongs have been received for this ping
     92 	// Otherwise timeout after PingThresholdSeconds
     93 	var currentData Data
     94 	var gotKnownPong bool = false
     95 	var pingTime time.Time = time.Now()
     96 	u, err := url.Parse(c.Config.OwnHomeserver.Homeserver)
     97 	if err != nil {
     98 		log.Errorf("Failed to parse homeserver url: %s", err)
     99 		return
    100 	}
    101 	direction := "outgoing"
    102 	if client.HomeserverURL.Host != u.Host {
    103 		direction = "incoming"
    104 	}
    105 
    106 outer:
    107 	for time.Now().Before(pingTime.Add(time.Duration(c.Config.PingThresholdSeconds) * time.Second)) {
    108 		resp, err := http.Get(c.Config.PingJsonURL)
    109 		if err != nil {
    110 			log.Errorf("Failed to get ping json: %s", err)
    111 		}
    112 
    113 		// Parse the json
    114 		// If we have a pong for this ping, we can break the loop
    115 		// Otherwise we sleep for 1 second and try again
    116 		defer resp.Body.Close()
    117 		json.NewDecoder(resp.Body).Decode(&currentData)
    118 
    119 		// Check if we have a pong for this ping
    120 		log.Infof("Checking for pong for ping as %s", client.UserID.Homeserver())
    121 		if ping, ok := currentData.Pings[client.UserID.Homeserver()]; ok {
    122 			hs_parsed := id.UserID(c.Config.OwnHomeserver.Username).Homeserver()
    123 			if _, ok := ping.Pongs[hs_parsed].Diffs[eventID.String()]; ok {
    124 				log.Infof("Got pong for ping as %s", client.UserID)
    125 				gotKnownPong = true
    126 				break outer
    127 			}
    128 			// Check if its from a known remote homeserver in ping.Pongs
    129 			for _, homeserver := range c.Config.RemoteHomeservers {
    130 				hs_parsed := id.UserID(homeserver.Username).Homeserver()
    131 				if _, ok := ping.Pongs[hs_parsed].Diffs[eventID.String()]; ok {
    132 					log.Infof("Got pong for ping as %s", client.UserID)
    133 					gotKnownPong = true
    134 					break outer
    135 				}
    136 			}
    137 		} else {
    138 			log.Warnf("No pong for ping as %s", client.UserID)
    139 		}
    140 
    141 		// Sleep for 1 second
    142 		time.Sleep(1 * time.Second)
    143 	}
    144 
    145 	// If we have not received a pong for this ping, we should log it
    146 	if !gotKnownPong {
    147 		log.Errorf("Failed to get pong for ping as %s", client.UserID)
    148 		c.Failures.WithLabelValues(client.UserID.Homeserver(), direction).Inc()
    149 	}
    150 
    151 	// Wait 5s before we collect the final data
    152 	time.Sleep(5 * time.Second)
    153 	pingresp, err := http.Get(c.Config.PingJsonURL)
    154 	if err != nil {
    155 		log.Errorf("Failed to get ping json: %s", err)
    156 	}
    157 
    158 	// Parse the json
    159 	// If we have a pong for this ping, we can break the loop
    160 	// Otherwise we sleep for 1 second and try again
    161 	defer pingresp.Body.Close()
    162 	json.NewDecoder(pingresp.Body).Decode(&currentData)
    163 
    164 	// Update mean, median and gmean metrics
    165 	// All of these are per homeserver we received a pong from
    166 	// They are all of type Gauge (Should they be a historgram?)
    167 	for homeserver, ping := range currentData.Pings[client.UserID.Homeserver()].Pongs {
    168 		direction := "outgoing"
    169 		if client.HomeserverURL.Host != u.Host {
    170 			direction = "incoming"
    171 		}
    172 		c.Mean.WithLabelValues(homeserver, client.UserID.Homeserver(), direction).Set(ping.Mean)
    173 		c.Median.WithLabelValues(homeserver, client.UserID.Homeserver(), direction).Set(ping.Median)
    174 		c.GMean.WithLabelValues(homeserver, client.UserID.Homeserver(), direction).Set(ping.GMean)
    175 	}
    176 }