Add Prometheus metrics for connections, firewall, L7, and bytes transferred
Instrument mc-proxy with prometheus/client_golang. New internal/metrics/ package defines counters, gauges, and histograms for connection totals, active connections, firewall blocks by reason, backend dial latency, bytes transferred, L7 HTTP status codes, and L7 policy blocks. Optional [metrics] config section starts a scrape endpoint. Firewall gains BlockedWithReason() to report block cause. L7 handler wraps ResponseWriter to record status codes per hostname. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"git.wntrmute.dev/kyle/mc-proxy/internal/config"
|
||||
"git.wntrmute.dev/kyle/mc-proxy/internal/firewall"
|
||||
"git.wntrmute.dev/kyle/mc-proxy/internal/l7"
|
||||
"git.wntrmute.dev/kyle/mc-proxy/internal/metrics"
|
||||
"git.wntrmute.dev/kyle/mc-proxy/internal/proxy"
|
||||
"git.wntrmute.dev/kyle/mc-proxy/internal/proxyproto"
|
||||
"git.wntrmute.dev/kyle/mc-proxy/internal/sni"
|
||||
@@ -41,7 +42,7 @@ type ListenerState struct {
|
||||
ID int64 // database primary key
|
||||
Addr string
|
||||
ProxyProtocol bool
|
||||
MaxConnections int64 // 0 = unlimited
|
||||
MaxConnections int64 // 0 = unlimited
|
||||
routes map[string]RouteInfo // lowercase hostname → route info
|
||||
mu sync.RWMutex
|
||||
ActiveConnections atomic.Int64
|
||||
@@ -204,6 +205,17 @@ func (s *Server) Version() string {
|
||||
return s.version
|
||||
}
|
||||
|
||||
// listenerAddrForRoute finds the listener address that owns the given hostname.
|
||||
func (s *Server) listenerAddrForRoute(hostname string) string {
|
||||
key := strings.ToLower(hostname)
|
||||
for _, ls := range s.listeners {
|
||||
if _, ok := ls.lookupRoute(key); ok {
|
||||
return ls.Addr
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// TotalConnections returns the total number of active connections.
|
||||
func (s *Server) TotalConnections() int64 {
|
||||
var total int64
|
||||
@@ -289,6 +301,7 @@ func (s *Server) serve(ctx context.Context, ln net.Listener, ls *ListenerState)
|
||||
|
||||
s.wg.Add(1)
|
||||
ls.ActiveConnections.Add(1)
|
||||
metrics.ConnectionsActive.WithLabelValues(ls.Addr).Inc()
|
||||
go s.handleConn(ctx, conn, ls)
|
||||
}
|
||||
}
|
||||
@@ -307,6 +320,7 @@ func (s *Server) forceCloseAll() {
|
||||
func (s *Server) handleConn(ctx context.Context, conn net.Conn, ls *ListenerState) {
|
||||
defer s.wg.Done()
|
||||
defer ls.ActiveConnections.Add(-1)
|
||||
defer metrics.ConnectionsActive.WithLabelValues(ls.Addr).Dec()
|
||||
defer conn.Close()
|
||||
|
||||
ls.connMu.Lock()
|
||||
@@ -340,8 +354,9 @@ func (s *Server) handleConn(ctx context.Context, conn net.Conn, ls *ListenerStat
|
||||
}
|
||||
}
|
||||
|
||||
if s.fw.Blocked(addr) {
|
||||
s.logger.Debug("blocked by firewall", "addr", addr)
|
||||
if blocked, reason := s.fw.BlockedWithReason(addr); blocked {
|
||||
metrics.FirewallBlockedTotal.WithLabelValues(reason).Inc()
|
||||
s.logger.Debug("blocked by firewall", "addr", addr, "reason", reason)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -368,7 +383,11 @@ func (s *Server) handleConn(ctx context.Context, conn net.Conn, ls *ListenerStat
|
||||
|
||||
// handleL4 handles an L4 (passthrough) connection.
|
||||
func (s *Server) handleL4(ctx context.Context, conn net.Conn, addr netip.Addr, clientAddrPort netip.AddrPort, hostname string, route RouteInfo, peeked []byte) {
|
||||
metrics.ConnectionsTotal.WithLabelValues(s.listenerAddrForRoute(hostname), "l4").Inc()
|
||||
|
||||
dialStart := time.Now()
|
||||
backendConn, err := net.DialTimeout("tcp", route.Backend, s.cfg.Proxy.ConnectTimeout.Duration)
|
||||
metrics.BackendDialDuration.WithLabelValues(route.Backend).Observe(time.Since(dialStart).Seconds())
|
||||
if err != nil {
|
||||
s.logger.Error("backend dial failed", "hostname", hostname, "backend", route.Backend, "error", err)
|
||||
return
|
||||
@@ -391,6 +410,9 @@ func (s *Server) handleL4(ctx context.Context, conn net.Conn, addr netip.Addr, c
|
||||
s.logger.Debug("relay ended", "hostname", hostname, "error", err)
|
||||
}
|
||||
|
||||
metrics.TransferredBytesTotal.WithLabelValues("client_to_backend", hostname).Add(float64(result.ClientBytes))
|
||||
metrics.TransferredBytesTotal.WithLabelValues("backend_to_client", hostname).Add(float64(result.BackendBytes))
|
||||
|
||||
s.logger.Info("connection closed",
|
||||
"addr", addr,
|
||||
"hostname", hostname,
|
||||
@@ -401,6 +423,8 @@ func (s *Server) handleL4(ctx context.Context, conn net.Conn, addr netip.Addr, c
|
||||
|
||||
// handleL7 handles an L7 (TLS-terminating) connection.
|
||||
func (s *Server) handleL7(ctx context.Context, conn net.Conn, addr netip.Addr, clientAddrPort netip.AddrPort, hostname string, route RouteInfo, peeked []byte) {
|
||||
metrics.ConnectionsTotal.WithLabelValues(s.listenerAddrForRoute(hostname), "l7").Inc()
|
||||
|
||||
s.logger.Debug("L7 proxying", "addr", addr, "hostname", hostname, "backend", route.Backend)
|
||||
|
||||
var policies []l7.PolicyRule
|
||||
@@ -409,6 +433,7 @@ func (s *Server) handleL7(ctx context.Context, conn net.Conn, addr netip.Addr, c
|
||||
}
|
||||
|
||||
rc := l7.RouteConfig{
|
||||
Hostname: hostname,
|
||||
Backend: route.Backend,
|
||||
TLSCert: route.TLSCert,
|
||||
TLSKey: route.TLSKey,
|
||||
|
||||
Reference in New Issue
Block a user