mirror of
https://github.com/fosrl/gerbil.git
synced 2026-05-13 03:39:56 +00:00
263 lines
7.8 KiB
Go
263 lines
7.8 KiB
Go
package metrics_test
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/fosrl/gerbil/internal/metrics"
|
|
"github.com/fosrl/gerbil/internal/observability"
|
|
)
|
|
|
|
const exampleHostname = "example.com"
|
|
|
|
func initPrometheus(t *testing.T) http.Handler {
|
|
t.Helper()
|
|
cfg := metrics.DefaultConfig()
|
|
cfg.Enabled = true
|
|
cfg.Backend = "prometheus"
|
|
cfg.Prometheus.Path = "/metrics"
|
|
|
|
h, err := metrics.Initialize(cfg)
|
|
if err != nil {
|
|
t.Fatalf("Initialize failed: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
metrics.Shutdown(context.Background()) //nolint:errcheck
|
|
})
|
|
return h
|
|
}
|
|
|
|
func initNoop(t *testing.T) {
|
|
t.Helper()
|
|
cfg := metrics.DefaultConfig()
|
|
cfg.Enabled = false
|
|
_, err := metrics.Initialize(cfg)
|
|
if err != nil {
|
|
t.Fatalf("Initialize noop failed: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
metrics.Shutdown(context.Background()) //nolint:errcheck
|
|
})
|
|
}
|
|
|
|
func scrape(t *testing.T, h http.Handler) string {
|
|
t.Helper()
|
|
req := httptest.NewRequest(http.MethodGet, "/metrics", http.NoBody)
|
|
rr := httptest.NewRecorder()
|
|
h.ServeHTTP(rr, req)
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("scrape returned %d", rr.Code)
|
|
}
|
|
b, _ := io.ReadAll(rr.Body)
|
|
return string(b)
|
|
}
|
|
|
|
func assertContains(t *testing.T, body, substr string) {
|
|
t.Helper()
|
|
if !strings.Contains(body, substr) {
|
|
t.Errorf("expected %q in output\nbody:\n%s", substr, body)
|
|
}
|
|
}
|
|
|
|
// --- Tests ---
|
|
|
|
func TestInitializePrometheus(t *testing.T) {
|
|
h := initPrometheus(t)
|
|
if h == nil {
|
|
t.Error("expected non-nil HTTP handler for prometheus backend")
|
|
}
|
|
}
|
|
|
|
func TestInitializeNoop(t *testing.T) {
|
|
initNoop(t)
|
|
// All Record* functions must not panic when noop backend is active.
|
|
metrics.RecordRestart()
|
|
metrics.RecordHTTPRequest("/test", "GET", "200")
|
|
metrics.RecordSNIConnection("accepted")
|
|
metrics.RecordPeersTotal("wg0", 1)
|
|
}
|
|
|
|
func TestDefaultConfig(t *testing.T) {
|
|
cfg := metrics.DefaultConfig()
|
|
if cfg.Backend != "prometheus" {
|
|
t.Errorf("expected prometheus default backend, got %q", cfg.Backend)
|
|
}
|
|
}
|
|
|
|
func TestShutdownNoInit(t *testing.T) {
|
|
// Ensure a known clean global state before testing no-init shutdown behavior.
|
|
_ = metrics.Shutdown(context.Background())
|
|
|
|
// Shutdown without Initialize should not panic or error.
|
|
if err := metrics.Shutdown(context.Background()); err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRecordHTTPRequest(t *testing.T) {
|
|
h := initPrometheus(t)
|
|
metrics.RecordHTTPRequest("/peers", "POST", "201")
|
|
body := scrape(t, h)
|
|
assertContains(t, body, "gerbil_http_requests_total")
|
|
}
|
|
|
|
func TestRecordHTTPRequestDuration(t *testing.T) {
|
|
h := initPrometheus(t)
|
|
metrics.RecordHTTPRequestDuration("/peers", "POST", 0.05)
|
|
body := scrape(t, h)
|
|
assertContains(t, body, "gerbil_http_request_duration_seconds")
|
|
}
|
|
|
|
func TestRecordInterfaceUp(t *testing.T) {
|
|
h := initPrometheus(t)
|
|
metrics.RecordInterfaceUp("wg0", "host1", true)
|
|
metrics.RecordInterfaceUp("wg0", "host1", false)
|
|
body := scrape(t, h)
|
|
assertContains(t, body, "gerbil_wg_interface_up")
|
|
}
|
|
|
|
func TestRecordPeersTotal(t *testing.T) {
|
|
h := initPrometheus(t)
|
|
metrics.RecordPeersTotal("wg0", 3)
|
|
body := scrape(t, h)
|
|
assertContains(t, body, "gerbil_wg_peers_total")
|
|
}
|
|
|
|
func TestRecordBytesReceivedTransmitted(t *testing.T) {
|
|
h := initPrometheus(t)
|
|
metrics.RecordBytesReceived("wg0", "peer1", 1024)
|
|
metrics.RecordBytesTransmitted("wg0", "peer1", 512)
|
|
body := scrape(t, h)
|
|
assertContains(t, body, "gerbil_wg_bytes_received_total")
|
|
assertContains(t, body, "gerbil_wg_bytes_transmitted_total")
|
|
}
|
|
|
|
func TestRecordSNI(t *testing.T) {
|
|
h := initPrometheus(t)
|
|
metrics.RecordSNIConnection("accepted")
|
|
metrics.RecordSNIActiveConnection(1)
|
|
metrics.RecordSNIConnectionDuration(1.5)
|
|
metrics.RecordSNIRouteCacheHit("hit")
|
|
metrics.RecordSNIRouteAPIRequest("success")
|
|
metrics.RecordSNIRouteAPILatency(0.01)
|
|
metrics.RecordSNILocalOverride("yes")
|
|
metrics.RecordSNITrustedProxyEvent("proxy_protocol_parsed")
|
|
metrics.RecordSNIProxyProtocolParseError()
|
|
metrics.RecordSNIDataBytes("client_to_target", 2048)
|
|
metrics.RecordSNITunnelTermination("eof")
|
|
body := scrape(t, h)
|
|
assertContains(t, body, "gerbil_sni_connections_total")
|
|
assertContains(t, body, "gerbil_sni_active_connections")
|
|
}
|
|
|
|
func TestRecordRelay(t *testing.T) {
|
|
h := initPrometheus(t)
|
|
metrics.RecordUDPPacket("relay", "data", "in")
|
|
metrics.RecordUDPPacketSize("relay", "data", 256)
|
|
metrics.RecordHolePunchEvent("relay", "success")
|
|
metrics.RecordProxyMapping("relay", 1)
|
|
metrics.RecordSession("relay", 1)
|
|
metrics.RecordSessionRebuilt("relay")
|
|
metrics.RecordCommPattern("relay", 1)
|
|
metrics.RecordProxyCleanupRemoved("relay", "session", 2)
|
|
metrics.RecordProxyConnectionError("relay", "dial_udp")
|
|
metrics.RecordProxyInitialMappings("relay", 5)
|
|
metrics.RecordProxyMappingUpdate("relay")
|
|
metrics.RecordProxyIdleCleanupDuration("relay", "conn", 0.1)
|
|
body := scrape(t, h)
|
|
assertContains(t, body, "gerbil_udp_packets_total")
|
|
assertContains(t, body, "gerbil_proxy_mapping_active")
|
|
assertContains(t, body, "gerbil_active_sessions")
|
|
}
|
|
|
|
func TestRecordWireGuard(t *testing.T) {
|
|
h := initPrometheus(t)
|
|
metrics.RecordHandshake("wg0", "peer1", "success")
|
|
metrics.RecordHandshakeLatency("wg0", "peer1", 0.02)
|
|
metrics.RecordPeerRTT("wg0", "peer1", 0.005)
|
|
metrics.RecordPeerConnected("wg0", "peer1", true)
|
|
metrics.RecordAllowedIPsCount("wg0", "peer1", 2)
|
|
metrics.RecordKeyRotation("wg0", "scheduled")
|
|
body := scrape(t, h)
|
|
assertContains(t, body, "gerbil_wg_handshakes_total")
|
|
assertContains(t, body, "gerbil_wg_peer_connected")
|
|
}
|
|
|
|
func TestRecordHousekeeping(t *testing.T) {
|
|
h := initPrometheus(t)
|
|
metrics.RecordRemoteConfigFetch("success")
|
|
metrics.RecordBandwidthReport("success")
|
|
metrics.RecordPeerBandwidthBytes("peer1", "rx", 512)
|
|
metrics.RecordMemorySpike("warning")
|
|
metrics.RecordHeapProfileWritten()
|
|
body := scrape(t, h)
|
|
assertContains(t, body, "gerbil_remote_config_fetches_total")
|
|
assertContains(t, body, "gerbil_memory_spike_total")
|
|
}
|
|
|
|
func TestRecordOperational(t *testing.T) {
|
|
h := initPrometheus(t)
|
|
metrics.RecordConfigReload("success")
|
|
metrics.RecordRestart()
|
|
metrics.RecordAuthFailure("peer1", "bad_key")
|
|
metrics.RecordACLDenied("wg0", "peer1", "default-deny")
|
|
metrics.RecordCertificateExpiry(exampleHostname, "wg0", 90.0)
|
|
body := scrape(t, h)
|
|
assertContains(t, body, "gerbil_config_reloads_total")
|
|
assertContains(t, body, "gerbil_restart_total")
|
|
}
|
|
|
|
func TestRecordNetlink(t *testing.T) {
|
|
h := initPrometheus(t)
|
|
metrics.RecordNetlinkEvent("link_up")
|
|
metrics.RecordNetlinkError("wg", "timeout")
|
|
metrics.RecordSyncDuration("config", 0.1)
|
|
metrics.RecordWorkqueueDepth("main", 3)
|
|
metrics.RecordKernelModuleLoad("success")
|
|
metrics.RecordFirewallRuleApplied("success", "INPUT")
|
|
metrics.RecordActiveSession("wg0", 1)
|
|
metrics.RecordActiveProxyConnection(1)
|
|
metrics.RecordProxyRouteLookup("hit")
|
|
metrics.RecordProxyTLSHandshake(0.05)
|
|
metrics.RecordProxyBytesTransmitted("tx", 1024)
|
|
body := scrape(t, h)
|
|
assertContains(t, body, "gerbil_netlink_events_total")
|
|
assertContains(t, body, "gerbil_active_sessions")
|
|
}
|
|
|
|
func TestRecordPeerOperation(t *testing.T) {
|
|
h := initPrometheus(t)
|
|
metrics.RecordPeerOperation("add", "success")
|
|
metrics.RecordProxyMappingUpdateRequest("success")
|
|
metrics.RecordDestinationsUpdateRequest("success")
|
|
body := scrape(t, h)
|
|
assertContains(t, body, "gerbil_peer_operations_total")
|
|
}
|
|
|
|
func TestInitializeInvalidBackend(t *testing.T) {
|
|
cfg := observability.MetricsConfig{Enabled: true, Backend: "invalid"}
|
|
_, err := metrics.Initialize(cfg)
|
|
if err == nil {
|
|
t.Error("expected error for invalid backend")
|
|
}
|
|
}
|
|
|
|
func TestInitializeBackendNone(t *testing.T) {
|
|
cfg := metrics.DefaultConfig()
|
|
cfg.Backend = "none"
|
|
h, err := metrics.Initialize(cfg)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if h != nil {
|
|
t.Error("none backend should return nil handler")
|
|
}
|
|
// All Record* calls should be noop
|
|
metrics.RecordRestart()
|
|
metrics.Shutdown(context.Background()) //nolint:errcheck
|
|
}
|