From d21ccb5b9f2ccfcf304c5470185a13696b60bd8f Mon Sep 17 00:00:00 2001 From: Samuel McBroom Date: Tue, 22 Apr 2025 05:37:25 -0700 Subject: [PATCH] Add option to emit metrics and logs when players connect to the router (#391) --- README.md | 2 ++ cmd/mc-router/main.go | 3 ++- cmd/mc-router/metrics.go | 16 ++++++++++++++++ mcproto/read.go | 3 ++- mcproto/types.go | 4 ++++ server/connector.go | 34 +++++++++++++++++++++++++++++++++- 6 files changed, 59 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ccd1b04..d582e57 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,8 @@ Routes Minecraft client connections to backend servers based upon the requested Indicates if the webhook will only be called if a user is connecting rather than just server list/ping (env WEBHOOK_REQUIRE_USER) -webhook-url string If set, a POST request that contains connection status notifications will be sent to this HTTP address (env WEBHOOK_URL) + -record-logins + Log and generate metrics on player logins. Metrics only supported with influxdb or prometheus backend ``` ## Docker Multi-Architecture Image diff --git a/cmd/mc-router/main.go b/cmd/mc-router/main.go index d4b6a68..a3daa1c 100644 --- a/cmd/mc-router/main.go +++ b/cmd/mc-router/main.go @@ -54,6 +54,7 @@ type Config struct { UseProxyProtocol bool `default:"false" usage:"Send PROXY protocol to backend servers"` ReceiveProxyProtocol bool `default:"false" usage:"Receive PROXY protocol from backend servers, by default trusts every proxy header that it receives, combine with -trusted-proxies to specify a list of trusted proxies"` TrustedProxies []string `usage:"Comma delimited list of CIDR notation IP blocks to trust when receiving PROXY protocol"` + RecordLogins bool `default:"false" usage:"Log and generate metrics on player logins. Metrics only supported with influxdb or prometheus backend"` MetricsBackendConfig MetricsBackendConfig RoutesConfig string `usage:"Name or full path to routes config file"` NgrokToken string `usage:"If set, an ngrok tunnel will be established. It is HIGHLY recommended to pass as an environment variable."` @@ -142,7 +143,7 @@ func main() { trustedIpNets = append(trustedIpNets, ipNet) } - connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), config.UseProxyProtocol, config.ReceiveProxyProtocol, trustedIpNets) + connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), config.UseProxyProtocol, config.ReceiveProxyProtocol, trustedIpNets, config.RecordLogins) clientFilter, err := server.NewClientFilter(config.ClientsToAllow, config.ClientsToDeny) if err != nil { diff --git a/cmd/mc-router/metrics.go b/cmd/mc-router/metrics.go index d63dbf4..8552517 100644 --- a/cmd/mc-router/metrics.go +++ b/cmd/mc-router/metrics.go @@ -65,6 +65,8 @@ func (b expvarMetricsBuilder) BuildConnectorMetrics() *server.ConnectorMetrics { ConnectionsFrontend: c, ConnectionsBackend: c, ActiveConnections: expvarMetrics.NewGauge("active_connections"), + ServerActivePlayer: expvarMetrics.NewGauge("server_active_player"), + ServerLogins: expvarMetrics.NewCounter("server_logins"), } } @@ -83,6 +85,8 @@ func (b discardMetricsBuilder) BuildConnectorMetrics() *server.ConnectorMetrics ConnectionsFrontend: discardMetrics.NewCounter(), ConnectionsBackend: discardMetrics.NewCounter(), ActiveConnections: discardMetrics.NewGauge(), + ServerActivePlayer: discardMetrics.NewGauge(), + ServerLogins: discardMetrics.NewCounter(), } } @@ -132,6 +136,8 @@ func (b *influxMetricsBuilder) BuildConnectorMetrics() *server.ConnectorMetrics ConnectionsFrontend: c.With("side", "frontend"), ConnectionsBackend: c.With("side", "backend"), ActiveConnections: metrics.NewGauge("mc_router_connections_active"), + ServerActivePlayer: metrics.NewGauge("mc_router_server_player_active"), + ServerLogins: metrics.NewCounter("mc_router_server_logins"), } } @@ -178,5 +184,15 @@ func (b prometheusMetricsBuilder) BuildConnectorMetrics() *server.ConnectorMetri Name: "active_connections", Help: "The number of active connections", }, nil)), + ServerActivePlayer: prometheusMetrics.NewGauge(promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "mc_router", + Name: "server_active_player", + Help: "Player is active on server", + }, []string{"player_name", "player_uuid", "server_address"})), + ServerLogins: prometheusMetrics.NewCounter(promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: "mc_router", + Name: "server_logins", + Help: "The total number of player logins", + }, []string{"player_name", "player_uuid", "server_address"})), } } diff --git a/mcproto/read.go b/mcproto/read.go index d410af0..13b5fac 100644 --- a/mcproto/read.go +++ b/mcproto/read.go @@ -45,7 +45,8 @@ func ReadPacket(reader *bufio.Reader, addr net.Addr, state State) (*Packet, erro return nil, err } - packet := &Packet{Length: frame.Length} + // Packet length is frame length (bytes for packetID and data) plus bytes used to store the frame length data + packet := &Packet{Length: frame.Length + PacketLengthFieldBytes} remainder := bytes.NewBuffer(frame.Payload) diff --git a/mcproto/types.go b/mcproto/types.go index f9339a8..9ff0f2f 100644 --- a/mcproto/types.go +++ b/mcproto/types.go @@ -80,3 +80,7 @@ type LegacyServerListPing struct { type ByteReader interface { ReadByte() (byte, error) } + +const ( + PacketLengthFieldBytes = 1 +) diff --git a/server/connector.go b/server/connector.go index ce296b5..388ac4d 100644 --- a/server/connector.go +++ b/server/connector.go @@ -34,6 +34,8 @@ type ConnectorMetrics struct { ConnectionsFrontend metrics.Counter ConnectionsBackend metrics.Counter ActiveConnections metrics.Gauge + ServerActivePlayer metrics.Gauge + ServerLogins metrics.Counter } type ClientInfo struct { @@ -57,13 +59,14 @@ type PlayerInfo struct { Uuid uuid.UUID `json:"uuid"` } -func NewConnector(metrics *ConnectorMetrics, sendProxyProto bool, receiveProxyProto bool, trustedProxyNets []*net.IPNet) *Connector { +func NewConnector(metrics *ConnectorMetrics, sendProxyProto bool, receiveProxyProto bool, trustedProxyNets []*net.IPNet, recordLogins bool) *Connector { return &Connector{ metrics: metrics, sendProxyProto: sendProxyProto, connectionsCond: sync.NewCond(&sync.Mutex{}), receiveProxyProto: receiveProxyProto, trustedProxyNets: trustedProxyNets, + recordLogins: recordLogins, } } @@ -72,6 +75,7 @@ type Connector struct { metrics *ConnectorMetrics sendProxyProto bool receiveProxyProto bool + recordLogins bool trustedProxyNets []*net.IPNet activeConnections int32 @@ -383,9 +387,37 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net. c.metrics.ActiveConnections.Set(float64( atomic.AddInt32(&c.activeConnections, 1))) + if c.recordLogins && userInfo != nil { + logrus. + WithField("client", clientAddr). + WithField("playerName", userInfo.Name). + WithField("playerUUID", userInfo.Uuid). + WithField("serverAddress", serverAddress). + Info("Player attempted to login to server") + + c.metrics.ServerActivePlayer. + With("player_name", userInfo.Name). + With("player_uuid", userInfo.Uuid.String()). + With("server_address", serverAddress). + Set(1) + + c.metrics.ServerLogins. + With("player_name", userInfo.Name). + With("player_uuid", userInfo.Uuid.String()). + With("server_address", serverAddress). + Add(1) + } + defer func() { c.metrics.ActiveConnections.Set(float64( atomic.AddInt32(&c.activeConnections, -1))) + if c.recordLogins && userInfo != nil { + c.metrics.ServerActivePlayer. + With("player_name", userInfo.Name). + With("player_uuid", userInfo.Uuid.String()). + With("server_address", serverAddress). + Set(0) + } c.connectionsCond.Signal() }()