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 } 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 } return &GatewayCollector{outcomes: outcomes, dropped: dropped, invariants: invariants}, 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 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 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 } }