diff --git a/README.md b/README.md index 06195e4..dfbfc4a 100644 --- a/README.md +++ b/README.md @@ -21,34 +21,48 @@ is a generic gRPC reverse proxy handler. ## Proxy Handler The package [`proxy`](proxy/) contains a generic gRPC reverse proxy handler that allows a gRPC server to -not know about registered handlers or their data types. Please consult the docs, here's an exaple usage. +not know about registered handlers or their data types. Please consult the docs, here's an example usage. Defining a `StreamDirector` that decides where (if at all) to send the request ```go director = func(ctx context.Context, fullMethodName string) (*grpc.ClientConn, error) { // Make sure we never forward internal services. if strings.HasPrefix(fullMethodName, "/com.example.internal.") { - return nil, grpc.Errorf(codes.Unimplemented, "Unknown method") + return nil, status.Errorf(codes.Unimplemented, "Unknown method") } md, ok := metadata.FromContext(ctx) if ok { // Decide on which backend to dial if val, exists := md[":authority"]; exists && val[0] == "staging.api.example.com" { // Make sure we use DialContext so the dialing can be cancelled/time out together with the context. - return grpc.DialContext(ctx, "api-service.staging.svc.local", grpc.WithCodec(proxy.Codec())) + conn, err := grpc.DialContext( + ctx, + "api-service.staging.svc.local", + grpc.WithDefaultCallOptions(grpc.CallContentSubtype((&codec.Proxy{}).Name())), + ) + return outCtx, conn, err } else if val, exists := md[":authority"]; exists && val[0] == "api.example.com" { - return grpc.DialContext(ctx, "api-service.prod.svc.local", grpc.WithCodec(proxy.Codec())) + conn, err := grpc.DialContext( + ctx, + "api-service.prod.svc.local", + grpc.WithDefaultCallOptions(grpc.CallContentSubtype((&codec.Proxy{}).Name())), + ) + return outCtx, conn, err } } - return nil, grpc.Errorf(codes.Unimplemented, "Unknown method") + return nil, status.Errorf(codes.Unimplemented, "Unknown method") } ``` -Then you need to register it with a `grpc.Server`. The server may have other handlers that will be served +Then you need to register it with a `grpc.Server`. The proxy codec is automatically registered by importing the codec package. The server may have other handlers that will be served locally: ```go + +import codec "github.com/mwitkow/grpc-proxy/proxy/codec" + +... + server := grpc.NewServer( - grpc.CustomCodec(proxy.Codec()), grpc.UnknownServiceHandler(proxy.TransparentHandler(director))) pb_test.RegisterTestServiceServer(server, &testImpl{}) ``` diff --git a/proxy/codec.go b/proxy/codec.go deleted file mode 100644 index 846b9c4..0000000 --- a/proxy/codec.go +++ /dev/null @@ -1,70 +0,0 @@ -package proxy - -import ( - "fmt" - - "github.com/golang/protobuf/proto" - "google.golang.org/grpc" -) - -// Codec returns a proxying grpc.Codec with the default protobuf codec as parent. -// -// See CodecWithParent. -func Codec() grpc.Codec { - return CodecWithParent(&protoCodec{}) -} - -// CodecWithParent returns a proxying grpc.Codec with a user provided codec as parent. -// -// This codec is *crucial* to the functioning of the proxy. It allows the proxy server to be oblivious -// to the schema of the forwarded messages. It basically treats a gRPC message frame as raw bytes. -// However, if the server handler, or the client caller are not proxy-internal functions it will fall back -// to trying to decode the message using a fallback codec. -func CodecWithParent(fallback grpc.Codec) grpc.Codec { - return &rawCodec{fallback} -} - -type rawCodec struct { - parentCodec grpc.Codec -} - -type frame struct { - payload []byte -} - -func (c *rawCodec) Marshal(v interface{}) ([]byte, error) { - out, ok := v.(*frame) - if !ok { - return c.parentCodec.Marshal(v) - } - return out.payload, nil - -} - -func (c *rawCodec) Unmarshal(data []byte, v interface{}) error { - dst, ok := v.(*frame) - if !ok { - return c.parentCodec.Unmarshal(data, v) - } - dst.payload = data - return nil -} - -func (c *rawCodec) String() string { - return fmt.Sprintf("proxy>%s", c.parentCodec.String()) -} - -// protoCodec is a Codec implementation with protobuf. It is the default rawCodec for gRPC. -type protoCodec struct{} - -func (protoCodec) Marshal(v interface{}) ([]byte, error) { - return proto.Marshal(v.(proto.Message)) -} - -func (protoCodec) Unmarshal(data []byte, v interface{}) error { - return proto.Unmarshal(data, v.(proto.Message)) -} - -func (protoCodec) String() string { - return "proto" -} diff --git a/proxy/codec/codec.go b/proxy/codec/codec.go new file mode 100644 index 0000000..4e6a0a0 --- /dev/null +++ b/proxy/codec/codec.go @@ -0,0 +1,89 @@ +package codec + +import ( + "github.com/golang/protobuf/proto" + "google.golang.org/grpc/encoding" +) + +// Name is the name by which the proxy codec is registered in the encoding codec registry +// We have to say that we are the "proto" codec otherwise marshaling will fail! +const Name = "proto" + +func init() { + Register() +} + +// Register manually registers the codec +func Register() { + encoding.RegisterCodec(codec()) +} + +// codec returns a proxying grpc.codec with the default protobuf codec as parent. +// +// See CodecWithParent. +func codec() encoding.Codec { + // since we have registered the default codec by importing it, + // we can fetch it from the registry and use it as our parent + // and overwrite the existing codec in the registry + return codecWithParent(&protoCodec{}) +} + +// CodecWithParent returns a proxying grpc.Codec with a user provided codec as parent. +// +// This codec is *crucial* to the functioning of the proxy. It allows the proxy server to be oblivious +// to the schema of the forwarded messages. It basically treats a gRPC message frame as raw bytes. +// However, if the server handler, or the client caller are not proxy-internal functions it will fall back +// to trying to decode the message using a fallback codec. +func codecWithParent(fallback encoding.Codec) encoding.Codec { + return &Proxy{parentCodec: fallback} +} + +// Proxy satisfies the encoding.Codec interface +type Proxy struct { + parentCodec encoding.Codec +} + +// Frame holds the proxy transported data +type Frame struct { + payload []byte +} + +// Marshal implents the encoding.Codec interface method +func (p *Proxy) Marshal(v interface{}) ([]byte, error) { + out, ok := v.(*Frame) + if !ok { + return p.parentCodec.Marshal(v) + } + return out.payload, nil + +} + +// Unmarshal implents the encoding.Codec interface method +func (p *Proxy) Unmarshal(data []byte, v interface{}) error { + dst, ok := v.(*Frame) + if !ok { + return p.parentCodec.Unmarshal(data, v) + } + dst.payload = data + return nil +} + +// Name implents the encoding.Codec interface method +func (*Proxy) Name() string { + return Name +} + +// protoCodec is a Codec implementation with protobuf. It is the default rawCodec for gRPC. +type protoCodec struct{} + +func (*protoCodec) Marshal(v interface{}) ([]byte, error) { + return proto.Marshal(v.(proto.Message)) +} + +func (*protoCodec) Unmarshal(data []byte, v interface{}) error { + return proto.Unmarshal(data, v.(proto.Message)) +} + +func (*protoCodec) Name() string { + return "proxy>proto" +} diff --git a/proxy/codec/codec_test.go b/proxy/codec/codec_test.go new file mode 100644 index 0000000..67e8672 --- /dev/null +++ b/proxy/codec/codec_test.go @@ -0,0 +1,52 @@ +package codec_test + +import ( + "testing" + + _ "github.com/gogo/protobuf/proto" + codec "github.com/mwitkow/grpc-proxy/proxy/codec" + pb "github.com/mwitkow/grpc-proxy/testservice" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/encoding" +) + +func TestCodec_ReadYourWrites(t *testing.T) { + framePtr := &codec.Frame{} + data := []byte{0xDE, 0xAD, 0xBE, 0xEF} + codec.Register() + codec := encoding.GetCodec((&codec.Proxy{}).Name()) + require.NotNil(t, codec, "codec must be registered") + require.NoError(t, codec.Unmarshal(data, framePtr), "unmarshalling must go ok") + out, err := codec.Marshal(framePtr) + require.NoError(t, err, "no marshal error") + require.Equal(t, data, out, "output and data must be the same") + + // reuse + require.NoError(t, codec.Unmarshal([]byte{0x55}, framePtr), "unmarshalling must go ok") + out, err = codec.Marshal(framePtr) + require.NoError(t, err, "no marshal error") + require.Equal(t, []byte{0x55}, out, "output and data must be the same") + +} + +func TestProtoCodec_ReadYourWrites(t *testing.T) { + p1 := &pb.PingRequest{ + Value: "test-ping", + } + proxyCd := encoding.GetCodec((&codec.Proxy{}).Name()) + + require.NotNil(t, proxyCd, "proxy codec must not be nil") + + out1p1, err := proxyCd.Marshal(p1) + require.NoError(t, err, "marshalling must go ok") + out2p1, err := proxyCd.Marshal(p1) + require.NoError(t, err, "marshalling must go ok") + + p2 := &pb.PingRequest{} + err = proxyCd.Unmarshal(out1p1, p2) + require.NoError(t, err, "unmarshalling must go ok") + err = proxyCd.Unmarshal(out2p1, p2) + require.NoError(t, err, "unmarshalling must go ok") + + require.Equal(t, *p1, *p2) +} diff --git a/proxy/codec_test.go b/proxy/codec_test.go deleted file mode 100644 index dd3893c..0000000 --- a/proxy/codec_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package proxy - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestCodec_ReadYourWrites(t *testing.T) { - framePtr := &frame{} - data := []byte{0xDE, 0xAD, 0xBE, 0xEF} - codec := rawCodec{} - require.NoError(t, codec.Unmarshal(data, framePtr), "unmarshalling must go ok") - out, err := codec.Marshal(framePtr) - require.NoError(t, err, "no marshal error") - require.Equal(t, data, out, "output and data must be the same") - - // reuse - require.NoError(t, codec.Unmarshal([]byte{0x55}, framePtr), "unmarshalling must go ok") - out, err = codec.Marshal(framePtr) - require.NoError(t, err, "no marshal error") - require.Equal(t, []byte{0x55}, out, "output and data must be the same") - -} diff --git a/proxy/examples_test.go b/proxy/examples_test.go index bef4ce3..fa3c066 100644 --- a/proxy/examples_test.go +++ b/proxy/examples_test.go @@ -7,10 +7,12 @@ import ( "strings" "github.com/mwitkow/grpc-proxy/proxy" + codec "github.com/mwitkow/grpc-proxy/proxy/codec" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" ) var ( @@ -19,7 +21,7 @@ var ( func ExampleRegisterService() { // A gRPC server with the proxying codec enabled. - server := grpc.NewServer(grpc.CustomCodec(proxy.Codec())) + server := grpc.NewServer() // Register a TestService with 4 of its methods explicitly. proxy.RegisterService(server, director, "mwitkow.testproto.TestService", @@ -28,7 +30,6 @@ func ExampleRegisterService() { func ExampleTransparentHandler() { grpc.NewServer( - grpc.CustomCodec(proxy.Codec()), grpc.UnknownServiceHandler(proxy.TransparentHandler(director))) } @@ -38,7 +39,7 @@ func ExampleStreamDirector() { director = func(ctx context.Context, fullMethodName string) (context.Context, *grpc.ClientConn, error) { // Make sure we never forward internal services. if strings.HasPrefix(fullMethodName, "/com.example.internal.") { - return nil, nil, grpc.Errorf(codes.Unimplemented, "Unknown method") + return nil, nil, status.Errorf(codes.Unimplemented, "Unknown method") } md, ok := metadata.FromIncomingContext(ctx) // Copy the inbound metadata explicitly. @@ -48,13 +49,13 @@ func ExampleStreamDirector() { // Decide on which backend to dial if val, exists := md[":authority"]; exists && val[0] == "staging.api.example.com" { // Make sure we use DialContext so the dialing can be cancelled/time out together with the context. - conn, err := grpc.DialContext(ctx, "api-service.staging.svc.local", grpc.WithCodec(proxy.Codec())) + conn, err := grpc.DialContext(ctx, "api-service.staging.svc.local", grpc.WithDefaultCallOptions(grpc.CallContentSubtype((&codec.Proxy{}).Name()))) return outCtx, conn, err } else if val, exists := md[":authority"]; exists && val[0] == "api.example.com" { - conn, err := grpc.DialContext(ctx, "api-service.prod.svc.local", grpc.WithCodec(proxy.Codec())) + conn, err := grpc.DialContext(ctx, "api-service.prod.svc.local", grpc.WithDefaultCallOptions(grpc.CallContentSubtype((&codec.Proxy{}).Name()))) return outCtx, conn, err } } - return nil, nil, grpc.Errorf(codes.Unimplemented, "Unknown method") + return nil, nil, status.Errorf(codes.Unimplemented, "Unknown method") } } diff --git a/proxy/handler.go b/proxy/handler.go index 752f892..951e371 100644 --- a/proxy/handler.go +++ b/proxy/handler.go @@ -6,9 +6,11 @@ package proxy import ( "io" + "github.com/mwitkow/grpc-proxy/proxy/codec" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) var ( @@ -61,7 +63,7 @@ func (s *handler) handler(srv interface{}, serverStream grpc.ServerStream) error // little bit of gRPC internals never hurt anyone fullMethodName, ok := grpc.MethodFromServerStream(serverStream) if !ok { - return grpc.Errorf(codes.Internal, "lowLevelServerStream not exists in context") + return status.Errorf(codes.Internal, "lowLevelServerStream not exists in context") } // We require that the director's returned context inherits from the serverStream.Context(). outgoingCtx, backendConn, err := s.director(serverStream.Context(), fullMethodName) @@ -71,7 +73,7 @@ func (s *handler) handler(srv interface{}, serverStream grpc.ServerStream) error clientCtx, clientCancel := context.WithCancel(outgoingCtx) // TODO(mwitkow): Add a `forwarded` header to metadata, https://en.wikipedia.org/wiki/X-Forwarded-For. - clientStream, err := grpc.NewClientStream(clientCtx, clientStreamDescForProxying, backendConn, fullMethodName) + clientStream, err := grpc.NewClientStream(clientCtx, clientStreamDescForProxying, backendConn, fullMethodName, grpc.CallContentSubtype((&codec.Proxy{}).Name())) if err != nil { return err } @@ -88,13 +90,13 @@ func (s *handler) handler(srv interface{}, serverStream grpc.ServerStream) error // this is the happy case where the sender has encountered io.EOF, and won't be sending anymore./ // the clientStream>serverStream may continue pumping though. clientStream.CloseSend() - break + continue } else { // however, we may have gotten a receive error (stream disconnected, a read error etc) in which case we need // to cancel the clientStream to the backend, let all of its goroutines be freed up by the CancelFunc and // exit with an error to the stack clientCancel() - return grpc.Errorf(codes.Internal, "failed proxying s2c: %v", s2cErr) + return status.Errorf(codes.Internal, "failed proxying s2c: %v", s2cErr) } case c2sErr := <-c2sErrChan: // This happens when the clientStream has nothing else to offer (io.EOF), returned a gRPC error. In those two @@ -108,13 +110,13 @@ func (s *handler) handler(srv interface{}, serverStream grpc.ServerStream) error return nil } } - return grpc.Errorf(codes.Internal, "gRPC proxying should never reach this stage.") + return status.Errorf(codes.Internal, "gRPC proxying should never reach this stage.") } func (s *handler) forwardClientToServer(src grpc.ClientStream, dst grpc.ServerStream) chan error { ret := make(chan error, 1) go func() { - f := &frame{} + f := &codec.Frame{} for i := 0; ; i++ { if err := src.RecvMsg(f); err != nil { ret <- err // this can be io.EOF which is happy case @@ -146,7 +148,7 @@ func (s *handler) forwardClientToServer(src grpc.ClientStream, dst grpc.ServerSt func (s *handler) forwardServerToClient(src grpc.ServerStream, dst grpc.ClientStream) chan error { ret := make(chan error, 1) go func() { - f := &frame{} + f := &codec.Frame{} for i := 0; ; i++ { if err := src.RecvMsg(f); err != nil { ret <- err // this can be io.EOF which is happy case diff --git a/proxy/handler_test.go b/proxy/handler_test.go index c811408..434db23 100644 --- a/proxy/handler_test.go +++ b/proxy/handler_test.go @@ -4,27 +4,26 @@ package proxy_test import ( + "fmt" "io" - "log" "net" - "os" "strings" "testing" "time" "github.com/mwitkow/grpc-proxy/proxy" + codec "github.com/mwitkow/grpc-proxy/proxy/codec" + pb "github.com/mwitkow/grpc-proxy/testservice" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/codes" + "google.golang.org/grpc/encoding" "google.golang.org/grpc/grpclog" "google.golang.org/grpc/metadata" - - "fmt" - - pb "github.com/mwitkow/grpc-proxy/testservice" + "google.golang.org/grpc/status" ) const ( @@ -40,8 +39,7 @@ const ( // asserting service is implemented on the server side and serves as a handler for stuff type assertingService struct { - logger *grpclog.Logger - t *testing.T + t *testing.T } func (s *assertingService) PingEmpty(ctx context.Context, _ *pb.Empty) (*pb.PingResponse, error) { @@ -61,7 +59,7 @@ func (s *assertingService) Ping(ctx context.Context, ping *pb.PingRequest) (*pb. } func (s *assertingService) PingError(ctx context.Context, ping *pb.PingRequest) (*pb.Empty, error) { - return nil, grpc.Errorf(codes.FailedPrecondition, "Userspace error.") + return nil, status.Errorf(codes.FailedPrecondition, "Userspace error.") } func (s *assertingService) PingList(ping *pb.PingRequest, stream pb.TestService_PingListServer) error { @@ -116,6 +114,7 @@ func (s *ProxyHappySuite) ctx() context.Context { } func (s *ProxyHappySuite) TestPingEmptyCarriesClientMetadata() { + // s.T().Skip() ctx := metadata.NewOutgoingContext(s.ctx(), metadata.Pairs(clientMdKey, "true")) out, err := s.testClient.PingEmpty(ctx, &pb.Empty{}) require.NoError(s.T(), err, "PingEmpty should succeed without errors") @@ -129,6 +128,7 @@ func (s *ProxyHappySuite) TestPingEmpty_StressTest() { } func (s *ProxyHappySuite) TestPingCarriesServerHeadersAndTrailers() { + // s.T().Skip() headerMd := make(metadata.MD) trailerMd := make(metadata.MD) // This is an awkward calling convention... but meh. @@ -142,8 +142,10 @@ func (s *ProxyHappySuite) TestPingCarriesServerHeadersAndTrailers() { func (s *ProxyHappySuite) TestPingErrorPropagatesAppError() { _, err := s.testClient.PingError(s.ctx(), &pb.PingRequest{Value: "foo"}) require.Error(s.T(), err, "PingError should never succeed") - assert.Equal(s.T(), codes.FailedPrecondition, grpc.Code(err)) - assert.Equal(s.T(), "Userspace error.", grpc.ErrorDesc(err)) + st, ok := status.FromError(err) + require.True(s.T(), ok, "must get status from error") + assert.Equal(s.T(), codes.FailedPrecondition, st.Code()) + assert.Equal(s.T(), "Userspace error.", st.Message()) } func (s *ProxyHappySuite) TestDirectorErrorIsPropagated() { @@ -151,8 +153,10 @@ func (s *ProxyHappySuite) TestDirectorErrorIsPropagated() { ctx := metadata.NewOutgoingContext(s.ctx(), metadata.Pairs(rejectingMdKey, "true")) _, err := s.testClient.Ping(ctx, &pb.PingRequest{Value: "foo"}) require.Error(s.T(), err, "Director should reject this RPC") - assert.Equal(s.T(), codes.PermissionDenied, grpc.Code(err)) - assert.Equal(s.T(), "testing rejection", grpc.ErrorDesc(err)) + st, ok := status.FromError(err) + require.True(s.T(), ok, "must get status from error") + assert.Equal(s.T(), codes.PermissionDenied, st.Code()) + assert.Equal(s.T(), "testing rejection", st.Message()) } func (s *ProxyHappySuite) TestPingStream_FullDuplexWorks() { @@ -172,6 +176,7 @@ func (s *ProxyHappySuite) TestPingStream_FullDuplexWorks() { require.NoError(s.T(), err, "PingStream headers should not error.") assert.Contains(s.T(), headerMd, serverHeaderMdKey, "PingStream response headers user contain metadata") } + require.NotNil(s.T(), resp, "resp must not be nil") assert.EqualValues(s.T(), i, resp.Counter, "ping roundtrip must succeed with the correct id") } require.NoError(s.T(), stream.CloseSend(), "no error on close send") @@ -191,24 +196,33 @@ func (s *ProxyHappySuite) TestPingStream_StressTest() { func (s *ProxyHappySuite) SetupSuite() { var err error + pc := encoding.GetCodec((&codec.Proxy{}).Name()) + dc := encoding.GetCodec("proto") + require.NotNil(s.T(), pc, "proxy codec must be registered") + require.NotNil(s.T(), dc, "default codec must be registered") + s.proxyListener, err = net.Listen("tcp", "127.0.0.1:0") require.NoError(s.T(), err, "must be able to allocate a port for proxyListener") s.serverListener, err = net.Listen("tcp", "127.0.0.1:0") require.NoError(s.T(), err, "must be able to allocate a port for serverListener") - grpclog.SetLogger(log.New(os.Stderr, "grpc: ", log.LstdFlags)) + grpclog.SetLogger(testingLog{s.T()}) s.server = grpc.NewServer() pb.RegisterTestServiceServer(s.server, &assertingService{t: s.T()}) // Setup of the proxy's Director. - s.serverClientConn, err = grpc.Dial(s.serverListener.Addr().String(), grpc.WithInsecure(), grpc.WithCodec(proxy.Codec())) + s.serverClientConn, err = grpc.Dial( + s.serverListener.Addr().String(), + grpc.WithInsecure(), + grpc.WithDefaultCallOptions(grpc.CallContentSubtype((&codec.Proxy{}).Name())), + ) require.NoError(s.T(), err, "must not error on deferred client Dial") director := func(ctx context.Context, fullName string) (context.Context, *grpc.ClientConn, error) { md, ok := metadata.FromIncomingContext(ctx) if ok { if _, exists := md[rejectingMdKey]; exists { - return ctx, nil, grpc.Errorf(codes.PermissionDenied, "testing rejection") + return ctx, nil, status.Errorf(codes.PermissionDenied, "testing rejection") } } // Explicitly copy the metadata, otherwise the tests will fail. @@ -217,7 +231,6 @@ func (s *ProxyHappySuite) SetupSuite() { return outCtx, s.serverClientConn, nil } s.proxy = grpc.NewServer( - grpc.CustomCodec(proxy.Codec()), grpc.UnknownServiceHandler(proxy.TransparentHandler(director)), ) // Ping handler is handled as an explicit registration and not as a TransparentHandler. @@ -235,9 +248,17 @@ func (s *ProxyHappySuite) SetupSuite() { s.proxy.Serve(s.proxyListener) }() - clientConn, err := grpc.Dial(strings.Replace(s.proxyListener.Addr().String(), "127.0.0.1", "localhost", 1), grpc.WithInsecure(), grpc.WithTimeout(1*time.Second)) + time.Sleep(time.Second) + + clientConn, err := grpc.Dial( + strings.Replace(s.proxyListener.Addr().String(), "127.0.0.1", "localhost", 1), + grpc.WithInsecure(), + grpc.WithTimeout(1*time.Second), + grpc.WithDefaultCallOptions(grpc.CallContentSubtype((&codec.Proxy{}).Name())), + ) require.NoError(s.T(), err, "must not error on deferred client Dial") s.testClient = pb.NewTestServiceClient(clientConn) + } func (s *ProxyHappySuite) TearDownSuite() { @@ -265,21 +286,29 @@ func TestProxyHappySuite(t *testing.T) { // Abstraction that allows us to pass the *testing.T as a grpclogger. type testingLog struct { - testing.T + T *testing.T +} + +func (t testingLog) Fatal(args ...interface{}) { + t.T.Fatal(args...) } -func (t *testingLog) Fatalln(args ...interface{}) { +func (t testingLog) Fatalln(args ...interface{}) { t.T.Fatal(args...) } -func (t *testingLog) Print(args ...interface{}) { +func (t testingLog) Fatalf(format string, args ...interface{}) { + t.T.Fatalf(format, args...) +} + +func (t testingLog) Print(args ...interface{}) { t.T.Log(args...) } -func (t *testingLog) Printf(format string, args ...interface{}) { +func (t testingLog) Printf(format string, args ...interface{}) { t.T.Logf(format, args...) } -func (t *testingLog) Println(args ...interface{}) { +func (t testingLog) Println(args ...interface{}) { t.T.Log(args...) }