package metrics import ( "errors" "fmt" "github.com/prometheus/client_golang/prometheus" outcomeDomain "proxy-pool/internal/domain/outcome" proxyDomain "proxy-pool/internal/domain/proxy" ) // GatewayCollector exposes fixed-cardinality request-path outcome metrics. // Proxy, route, destination, client and credential values are never labels. type GatewayCollector struct { outcomes *prometheus.CounterVec dropped prometheus.Counter invariants *prometheus.CounterVec httpRequests prometheus.Counter connectRequests prometheus.Counter httpInFlight prometheus.Gauge connectInFlight prometheus.Gauge activeTunnels prometheus.Gauge } var ( _ outcomeDomain.MetricsObserver = (*GatewayCollector)(nil) _ proxyDomain.CapacityInvariantObserver = (*GatewayCollector)(nil) ) func NewGatewayCollector(registerer prometheus.Registerer) (*GatewayCollector, error) { if registerer == nil { return nil, ErrInvalidDependencies } outcomes, err := registerCounterVec(registerer, prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: "proxy_pool", Subsystem: "gateway", Name: "outcomes_total", Help: "Number of Gateway proxy attempts by furthest completed stage and result.", }, []string{"stage", "result"})) if err != nil { return nil, err } dropped, err := registerCounter(registerer, prometheus.NewCounter(prometheus.CounterOpts{ Namespace: "proxy_pool", Subsystem: "gateway", Name: "outcome_queue_dropped_total", Help: "Number of Gateway outcome observations dropped after the local queue was full.", })) if err != nil { return nil, err } invariants, err := registerCounterVec(registerer, prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: "proxy_pool", Subsystem: "gateway", Name: "capacity_invariant_violations_total", Help: "Number of invalid Gateway local proxy capacity lifecycle transitions.", }, []string{"operation"})) if err != nil { return nil, err } requests, err := registerCounterVec(registerer, prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: "proxy_pool", Subsystem: "gateway", Name: "requests_total", Help: "Number of accepted Gateway requests by fixed proxy protocol.", }, []string{"protocol"})) if err != nil { return nil, err } inFlight, err := registerGaugeVec(registerer, prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: "proxy_pool", Subsystem: "gateway", Name: "requests_in_flight", Help: "Number of accepted Gateway requests currently executing by fixed proxy protocol.", }, []string{"protocol"})) if err != nil { return nil, err } activeTunnels, err := registerGauge(registerer, prometheus.NewGauge(prometheus.GaugeOpts{ Namespace: "proxy_pool", Subsystem: "gateway", Name: "active_tunnels", Help: "Number of established CONNECT tunnels currently relaying through this Gateway Worker.", })) if err != nil { return nil, err } return &GatewayCollector{ outcomes: outcomes, dropped: dropped, invariants: invariants, httpRequests: requests.WithLabelValues("HTTP"), connectRequests: requests.WithLabelValues("CONNECT"), httpInFlight: inFlight.WithLabelValues("HTTP"), connectInFlight: inFlight.WithLabelValues("CONNECT"), activeTunnels: activeTunnels, }, nil } func (collector *GatewayCollector) Observe(event outcomeDomain.Event) { if collector == nil || collector.outcomes == nil || !validGatewayStage(event.Stage) { return } result := "failure" if event.Success { result = "success" } collector.outcomes.WithLabelValues(gatewayStageLabel(event.Stage), result).Inc() } func (collector *GatewayCollector) ObserveDropped() { if collector == nil || collector.dropped == nil { return } collector.dropped.Inc() } func (collector *GatewayCollector) ObserveCapacityInvariant(event proxyDomain.CapacityInvariant) { if collector == nil || collector.invariants == nil || !validCapacityInvariantViolation(event.Violation) { return } collector.invariants.WithLabelValues(string(event.Violation)).Inc() } func (collector *GatewayCollector) ObserveRequestStarted(protocol string) { if collector == nil { return } switch protocol { case "HTTP": if collector.httpRequests != nil { collector.httpRequests.Inc() } if collector.httpInFlight != nil { collector.httpInFlight.Inc() } case "CONNECT": if collector.connectRequests != nil { collector.connectRequests.Inc() } if collector.connectInFlight != nil { collector.connectInFlight.Inc() } } } func (collector *GatewayCollector) ObserveRequestFinished(protocol string) { if collector == nil { return } switch protocol { case "HTTP": if collector.httpInFlight != nil { collector.httpInFlight.Dec() } case "CONNECT": if collector.connectInFlight != nil { collector.connectInFlight.Dec() } } } func (collector *GatewayCollector) ObserveTunnelOpened() { if collector != nil && collector.activeTunnels != nil { collector.activeTunnels.Inc() } } func (collector *GatewayCollector) ObserveTunnelClosed() { if collector != nil && collector.activeTunnels != nil { collector.activeTunnels.Dec() } } func registerCounter(registerer prometheus.Registerer, candidate prometheus.Counter) (prometheus.Counter, error) { if err := registerer.Register(candidate); err == nil { return candidate, nil } else { var registered prometheus.AlreadyRegisteredError if !errors.As(err, ®istered) { return nil, fmt.Errorf("register counter: %w", err) } existing, ok := registered.ExistingCollector.(prometheus.Counter) if !ok { return nil, fmt.Errorf("register counter: existing collector has unexpected type") } return existing, nil } } func registerGauge(registerer prometheus.Registerer, candidate prometheus.Gauge) (prometheus.Gauge, error) { if err := registerer.Register(candidate); err == nil { return candidate, nil } else { var registered prometheus.AlreadyRegisteredError if !errors.As(err, ®istered) { return nil, fmt.Errorf("register gauge: %w", err) } existing, ok := registered.ExistingCollector.(prometheus.Gauge) if !ok { return nil, fmt.Errorf("register gauge: existing collector has unexpected type") } return existing, nil } } func registerGaugeVec(registerer prometheus.Registerer, candidate *prometheus.GaugeVec) (*prometheus.GaugeVec, error) { if err := registerer.Register(candidate); err == nil { return candidate, nil } else { var registered prometheus.AlreadyRegisteredError if !errors.As(err, ®istered) { return nil, fmt.Errorf("register gauge vector: %w", err) } existing, ok := registered.ExistingCollector.(*prometheus.GaugeVec) if !ok { return nil, fmt.Errorf("register gauge vector: existing collector has unexpected type") } return existing, nil } } func validGatewayStage(stage outcomeDomain.Stage) bool { switch stage { case outcomeDomain.StageDial, outcomeDomain.StageProxyHandshake, outcomeDomain.StageResponseHeaders, outcomeDomain.StageTunnel: return true default: return false } } func gatewayStageLabel(stage outcomeDomain.Stage) string { switch stage { case outcomeDomain.StageDial: return "DIAL" case outcomeDomain.StageProxyHandshake: return "PROXY_HANDSHAKE" case outcomeDomain.StageResponseHeaders: return "RESPONSE_HEADERS" case outcomeDomain.StageTunnel: return "TUNNEL" default: return "" } } func validCapacityInvariantViolation(violation proxyDomain.CapacityInvariantViolation) bool { switch violation { case proxyDomain.CapacityInvariantCommitAlreadyCommitted, proxyDomain.CapacityInvariantCommitFinished, proxyDomain.CapacityInvariantCancelFinished, proxyDomain.CapacityInvariantReleaseBeforeCommit, proxyDomain.CapacityInvariantReleaseFinished: return true default: return false } }