fix(metrics): add doc comments, fix labels mutation and nil returns, add idempotent Close, add nil guards, add tests

- Fix promMetrics.Gauge/Counter/Observer mutating caller's labels map by
  using maps.Copy instead of direct assignment
- Fix Gauge/Counter/Observer returning nil for unknown metric names
  (now return noop implementations)
- Fix metricService.Close() race with sync.Once for safe concurrent calls
- Fix SetDSCP returning nil instead of errUnsupport on unsupported ops
- Add nil guards to WrapConn, WrapPacketConn, WrapUDPConn, WrapListener
- Add doc comments to all exported symbols
- Add 45 unit tests (metrics, noop, prom, service, wrapper)
This commit is contained in:
ginuerzh
2026-05-24 22:50:41 +08:00
parent 64f9a048f8
commit bc3d12ec2c
12 changed files with 950 additions and 23 deletions
+11 -1
View File
@@ -26,6 +26,8 @@ type serverConn struct {
clientIP string
}
// WrapConn wraps a net.Conn with metrics tracking. Bytes read/written are
// counted as service transfer input/output. If c is nil, nil is returned.
func WrapConn(service string, c net.Conn) net.Conn {
if c == nil {
return c
@@ -101,6 +103,8 @@ type packetConn struct {
service string
}
// WrapPacketConn wraps a net.PacketConn with metrics tracking. Bytes read/written
// are counted as service transfer input/output. If pc is nil, nil is returned.
func WrapPacketConn(service string, pc net.PacketConn) net.PacketConn {
if pc == nil {
return pc
@@ -161,7 +165,13 @@ type udpConn struct {
service string
}
// WrapUDPConn wraps a net.PacketConn as a udp.Conn with metrics tracking. Bytes
// read/written are counted as service transfer input/output. If pc is nil, nil
// is returned.
func WrapUDPConn(service string, pc net.PacketConn) udp.Conn {
if pc == nil {
return nil
}
return &udpConn{
PacketConn: pc,
service: service,
@@ -377,7 +387,7 @@ func (c *udpConn) SetDSCP(n int) error {
if nc, ok := c.PacketConn.(xnet.SetDSCP); ok {
return nc.SetDSCP(n)
}
return nil
return errUnsupport
}
func (c *udpConn) Context() context.Context {
+230
View File
@@ -0,0 +1,230 @@
package wrapper
import (
"io"
"net"
"sync"
"testing"
"time"
xio "github.com/go-gost/x/internal/io"
)
func TestWrapConnNil(t *testing.T) {
if c := WrapConn("svc", nil); c != nil {
t.Error("WrapConn(nil) should return nil")
}
}
func TestWrapPacketConnNil(t *testing.T) {
if pc := WrapPacketConn("svc", nil); pc != nil {
t.Error("WrapPacketConn(nil) should return nil")
}
}
func TestWrapUDPConnNil(t *testing.T) {
if uc := WrapUDPConn("svc", nil); uc != nil {
t.Error("WrapUDPConn(nil) should return nil")
}
}
func TestWrapConnReadWrite(t *testing.T) {
client, server := net.Pipe()
defer client.Close()
defer server.Close()
wrapped := WrapConn("test-svc", server)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
client.Write([]byte("hello"))
}()
buf := make([]byte, 1024)
n, err := wrapped.Read(buf)
wg.Wait()
if err != nil {
t.Fatalf("Read error: %v", err)
}
if n != 5 {
t.Errorf("expected 5 bytes, got %d", n)
}
if string(buf[:n]) != "hello" {
t.Errorf("expected 'hello', got %q", string(buf[:n]))
}
// Write through the wrapped conn.
go func() {
buf := make([]byte, 1024)
n, err := client.Read(buf)
if err != nil {
t.Errorf("client Read error: %v", err)
return
}
if n != 5 || string(buf[:n]) != "world" {
t.Errorf("expected 'world', got %q", string(buf[:n]))
}
}()
nw, err := wrapped.Write([]byte("world"))
if err != nil {
t.Fatalf("Write error: %v", err)
}
if nw != 5 {
t.Errorf("expected 5 bytes written, got %d", nw)
}
}
func TestWrapConnSyscallConn(t *testing.T) {
client, server := net.Pipe()
defer client.Close()
defer server.Close()
wrapped := WrapConn("test-svc", server)
// SyscallConn should not panic. The return values depend on the underlying
// connection type.
_, _ = wrapped.(*serverConn).SyscallConn()
}
func TestWrapConnCloseReadCloseWrite(t *testing.T) {
client, server := net.Pipe()
defer client.Close()
defer server.Close()
wrapped := WrapConn("test-svc", server)
// net.Pipe does not implement CloseRead/CloseWrite, so these should
// return ErrUnsupported.
err := wrapped.(*serverConn).CloseRead()
if err != xio.ErrUnsupported {
t.Errorf("CloseRead: expected ErrUnsupported, got %v", err)
}
err = wrapped.(*serverConn).CloseWrite()
if err != xio.ErrUnsupported {
t.Errorf("CloseWrite: expected ErrUnsupported, got %v", err)
}
}
func TestWrapConnContext(t *testing.T) {
client, server := net.Pipe()
defer client.Close()
defer server.Close()
wrapped := WrapConn("test-svc", server)
// net.Pipe does not implement ctx.Context, so Context() returns nil.
ctx := wrapped.(*serverConn).Context()
if ctx != nil {
t.Errorf("expected nil context, got %v", ctx)
}
}
// mockPacketConn implements net.PacketConn for testing.
type mockPacketConn struct {
readFrom func([]byte) (int, net.Addr, error)
writeTo func([]byte, net.Addr) (int, error)
}
func (m *mockPacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
return m.readFrom(p)
}
func (m *mockPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) {
return m.writeTo(p, addr)
}
func (m *mockPacketConn) Close() error { return nil }
func (m *mockPacketConn) LocalAddr() net.Addr { return &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0} }
func (m *mockPacketConn) SetDeadline(t time.Time) error { return nil }
func (m *mockPacketConn) SetReadDeadline(t time.Time) error { return nil }
func (m *mockPacketConn) SetWriteDeadline(t time.Time) error { return nil }
func TestWrapPacketConnReadFromWriteTo(t *testing.T) {
testAddr := &net.UDPAddr{IP: net.IPv4(10, 0, 0, 1), Port: 1234}
mock := &mockPacketConn{
readFrom: func(p []byte) (int, net.Addr, error) {
copy(p, []byte("data"))
return 4, testAddr, nil
},
writeTo: func(p []byte, addr net.Addr) (int, error) {
return len(p), nil
},
}
wrapped := WrapPacketConn("test-svc", mock)
// ReadFrom
buf := make([]byte, 1024)
n, addr, err := wrapped.ReadFrom(buf)
if err != nil {
t.Fatalf("ReadFrom error: %v", err)
}
if n != 4 {
t.Errorf("expected 4 bytes, got %d", n)
}
if addr.String() != testAddr.String() {
t.Errorf("expected addr %s, got %s", testAddr, addr)
}
// WriteTo
nw, err := wrapped.WriteTo([]byte("reply"), testAddr)
if err != nil {
t.Fatalf("WriteTo error: %v", err)
}
if nw != 5 {
t.Errorf("expected 5 bytes written, got %d", nw)
}
}
func TestWrapPacketConnContext(t *testing.T) {
mock := &mockPacketConn{
readFrom: func(p []byte) (int, net.Addr, error) { return 0, nil, io.EOF },
writeTo: func(p []byte, addr net.Addr) (int, error) { return len(p), nil },
}
wrapped := WrapPacketConn("test-svc", mock)
// mockPacketConn does not implement ctx.Context
ctx := wrapped.(*packetConn).Context()
if ctx != nil {
t.Errorf("expected nil context, got %v", ctx)
}
}
func TestWrapConnRemoteAddr(t *testing.T) {
client, server := net.Pipe()
defer client.Close()
defer server.Close()
wrapped := WrapConn("test-svc", server)
// The wrapped conn should expose the underlying remote address.
if wrapped.RemoteAddr() == nil {
t.Error("RemoteAddr should not be nil")
}
}
func TestWrapConnNilLabels(t *testing.T) {
// When the remote address has no port, SplitHostPort may fail.
// WrapConn should still succeed and the host may be empty.
client, server := net.Pipe()
defer client.Close()
defer server.Close()
wrapped := WrapConn("test-svc", server)
// Read should not panic even with potentially malformed addresses.
// net.Pipe addresses are well-formed so this is a smoke test.
buf := make([]byte, 1)
go client.Write([]byte{0x01})
_, err := wrapped.Read(buf)
if err != nil {
t.Fatalf("Read error: %v", err)
}
}
+6
View File
@@ -9,7 +9,13 @@ type listener struct {
net.Listener
}
// WrapListener wraps a net.Listener so that all accepted connections are
// automatically wrapped with WrapConn for metrics tracking. If ln is nil,
// nil is returned.
func WrapListener(service string, ln net.Listener) net.Listener {
if ln == nil {
return ln
}
return &listener{
service: service,
Listener: ln,
+81
View File
@@ -0,0 +1,81 @@
package wrapper
import (
"errors"
"net"
"testing"
)
func TestWrapListenerNil(t *testing.T) {
if ln := WrapListener("svc", nil); ln != nil {
t.Error("WrapListener(nil) should return nil")
}
}
// mockListener implements net.Listener for testing.
type mockListener struct {
accept func() (net.Conn, error)
}
func (l *mockListener) Accept() (net.Conn, error) { return l.accept() }
func (l *mockListener) Close() error { return nil }
func (l *mockListener) Addr() net.Addr { return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8080} }
func TestWrapListenerAccept(t *testing.T) {
client, server := net.Pipe()
ln := &mockListener{
accept: func() (net.Conn, error) {
return server, nil
},
}
wrapped := WrapListener("test-svc", ln)
c, err := wrapped.Accept()
if err != nil {
t.Fatalf("Accept error: %v", err)
}
if c == nil {
t.Fatal("accepted connection should not be nil")
}
// The accepted connection should be a *serverConn (wrapped by WrapConn).
if _, ok := c.(*serverConn); !ok {
t.Errorf("accepted connection should be *serverConn, got %T", c)
}
client.Close()
c.Close()
}
func TestWrapListenerAcceptError(t *testing.T) {
testErr := errors.New("accept failed")
ln := &mockListener{
accept: func() (net.Conn, error) {
return nil, testErr
},
}
wrapped := WrapListener("test-svc", ln)
c, err := wrapped.Accept()
if err != testErr {
t.Errorf("expected error %v, got %v", testErr, err)
}
if c != nil {
t.Error("connection should be nil on error")
}
}
func TestWrapListenerAddr(t *testing.T) {
ln := &mockListener{
accept: func() (net.Conn, error) { return nil, errors.New("not used") },
}
wrapped := WrapListener("test-svc", ln)
if wrapped.Addr() == nil {
t.Error("Addr should not be nil")
}
}