diff --git a/internal/adapters/metrics.go b/internal/adapters/metrics.go index db90796..20fdd56 100644 --- a/internal/adapters/metrics.go +++ b/internal/adapters/metrics.go @@ -136,3 +136,21 @@ func (m *MetricsServer) UpdatePeerMetrics(peer *domain.Peer, status domain.PeerS m.peerSendBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesTransmitted)) m.peerIsConnected.WithLabelValues(labels...).Set(internal.BoolToFloat64(status.IsConnected)) } + +// DeletePeerMetrics removes all Prometheus series for a peer that was deleted +// from the WireGuard portal. GaugeVecs retain label values until explicitly +// deleted, so leaving these behind makes a deleted peer indistinguishable from +// a retained peer that is currently offline. +func (m *MetricsServer) DeletePeerMetrics(peer domain.Peer) { + labels := []string{ + string(peer.InterfaceIdentifier), + peer.Interface.AddressStr(), + string(peer.Identifier), + peer.DisplayName, + string(peer.UserIdentifier), + } + m.peerLastHandshakeSeconds.DeleteLabelValues(labels...) + m.peerReceivedBytesTotal.DeleteLabelValues(labels...) + m.peerSendBytesTotal.DeleteLabelValues(labels...) + m.peerIsConnected.DeleteLabelValues(labels...) +} diff --git a/internal/adapters/metrics_test.go b/internal/adapters/metrics_test.go new file mode 100644 index 0000000..1bdcdbe --- /dev/null +++ b/internal/adapters/metrics_test.go @@ -0,0 +1,77 @@ +package adapters + +import ( + "testing" + + "github.com/h44z/wg-portal/internal/domain" + "github.com/prometheus/client_golang/prometheus" +) + +func TestDeletePeerMetricsRemovesAllPeerSeries(t *testing.T) { + labels := []string{"interface", "addresses", "id", "name", "user"} + m := &MetricsServer{ + peerIsConnected: prometheus.NewGaugeVec(prometheus.GaugeOpts{Name: "peer_up_test"}, labels), + peerLastHandshakeSeconds: prometheus.NewGaugeVec(prometheus.GaugeOpts{Name: "peer_handshake_test"}, labels), + peerReceivedBytesTotal: prometheus.NewGaugeVec(prometheus.GaugeOpts{Name: "peer_received_test"}, labels), + peerSendBytesTotal: prometheus.NewGaugeVec(prometheus.GaugeOpts{Name: "peer_sent_test"}, labels), + } + addr, err := domain.CidrFromString("10.0.0.2/32") + if err != nil { + t.Fatal(err) + } + peer := &domain.Peer{ + Identifier: "peer-id", + InterfaceIdentifier: "wg0", + DisplayName: "peer", + UserIdentifier: "user", + Interface: domain.PeerInterfaceConfig{Addresses: []domain.Cidr{addr}}, + } + m.UpdatePeerMetrics(peer, domain.PeerStatus{PeerId: peer.Identifier, IsConnected: true}) + m.DeletePeerMetrics(*peer) + + for name, collector := range map[string]prometheus.Collector{ + "up": m.peerIsConnected, "handshake": m.peerLastHandshakeSeconds, + "received": m.peerReceivedBytesTotal, "sent": m.peerSendBytesTotal, + } { + ch := make(chan prometheus.Metric, 1) + collector.Collect(ch) + close(ch) + if _, ok := <-ch; ok { + t.Errorf("%s metric remained after peer deletion", name) + } + } +} + +func TestDeletePeerMetricsLeavesRetainedPeerSeries(t *testing.T) { + labels := []string{"interface", "addresses", "id", "name", "user"} + m := &MetricsServer{ + peerIsConnected: prometheus.NewGaugeVec(prometheus.GaugeOpts{Name: "peer_up_retained_test"}, labels), + peerLastHandshakeSeconds: prometheus.NewGaugeVec(prometheus.GaugeOpts{Name: "peer_handshake_retained_test"}, labels), + peerReceivedBytesTotal: prometheus.NewGaugeVec(prometheus.GaugeOpts{Name: "peer_received_retained_test"}, labels), + peerSendBytesTotal: prometheus.NewGaugeVec(prometheus.GaugeOpts{Name: "peer_sent_retained_test"}, labels), + } + deletedAddr, err := domain.CidrFromString("10.0.0.2/32") + if err != nil { + t.Fatal(err) + } + retainedAddr, err := domain.CidrFromString("10.0.0.3/32") + if err != nil { + t.Fatal(err) + } + deleted := &domain.Peer{Identifier: "deleted", InterfaceIdentifier: "wg0", DisplayName: "deleted", UserIdentifier: "user", Interface: domain.PeerInterfaceConfig{Addresses: []domain.Cidr{deletedAddr}}} + retained := &domain.Peer{Identifier: "retained", InterfaceIdentifier: "wg0", DisplayName: "retained", UserIdentifier: "user", Interface: domain.PeerInterfaceConfig{Addresses: []domain.Cidr{retainedAddr}}} + m.UpdatePeerMetrics(deleted, domain.PeerStatus{PeerId: deleted.Identifier, IsConnected: false}) + m.UpdatePeerMetrics(retained, domain.PeerStatus{PeerId: retained.Identifier, IsConnected: false}) + m.DeletePeerMetrics(*deleted) + + ch := make(chan prometheus.Metric, 2) + m.peerIsConnected.Collect(ch) + close(ch) + count := 0 + for range ch { + count++ + } + if count != 1 { + t.Fatalf("expected one retained peer series, got %d", count) + } +} diff --git a/internal/app/wireguard/statistics.go b/internal/app/wireguard/statistics.go index 819bbf1..e6f6d69 100644 --- a/internal/app/wireguard/statistics.go +++ b/internal/app/wireguard/statistics.go @@ -31,6 +31,7 @@ type StatisticsDatabaseRepo interface { type StatisticsMetricsServer interface { UpdateInterfaceMetrics(status domain.InterfaceStatus) UpdatePeerMetrics(peer *domain.Peer, status domain.PeerStatus) + DeletePeerMetrics(peer domain.Peer) } type StatisticsEventBus interface { @@ -433,6 +434,11 @@ func (c *StatisticsCollector) updatePeerMetrics(ctx context.Context, status doma func (c *StatisticsCollector) connectToMessageBus() { _ = c.bus.Subscribe(app.TopicPeerIdentifierUpdated, c.handlePeerIdentifierChangeEvent) + _ = c.bus.Subscribe(app.TopicPeerDeleted, c.handlePeerDeletedEvent) +} + +func (c *StatisticsCollector) handlePeerDeletedEvent(peer domain.Peer) { + c.ms.DeletePeerMetrics(peer) } func (c *StatisticsCollector) handlePeerIdentifierChangeEvent(oldIdentifier, newIdentifier domain.PeerIdentifier) {