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:
+11
-1
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user