From f11355f2fb048d1d27ea6ddb775026a37faf1507 Mon Sep 17 00:00:00 2001 From: Eno Compton Date: Thu, 7 Jul 2022 14:27:15 -0600 Subject: [PATCH] feat: add support for structured logs (#1246) --- cloudsql/cloudsql.go | 10 ++++ cmd/root.go | 97 ++++++++++++++++++++++------------- cmd/root_test.go | 7 +++ internal/log/log.go | 98 ++++++++++++++++++++++++++++++++++++ internal/proxy/proxy.go | 39 +++++++------- internal/proxy/proxy_test.go | 48 +++++++++++------- testsV2/common_test.go | 8 +-- 7 files changed, 231 insertions(+), 76 deletions(-) create mode 100644 internal/log/log.go diff --git a/cloudsql/cloudsql.go b/cloudsql/cloudsql.go index 4e4f5371a..d4c1522b2 100644 --- a/cloudsql/cloudsql.go +++ b/cloudsql/cloudsql.go @@ -32,3 +32,13 @@ type Dialer interface { io.Closer } + +// Logger is the interface used throughout the project for logging. +type Logger interface { + // Debugf is for reporting additional information about internal operations. + Debugf(format string, args ...interface{}) + // Infof is for reporting informational messages. + Infof(format string, args ...interface{}) + // Errorf is for reporting errors. + Errorf(format string, args ...interface{}) +} diff --git a/cmd/root.go b/cmd/root.go index 773cba0c3..ea7b0ec94 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -32,6 +32,7 @@ import ( "contrib.go.opencensus.io/exporter/prometheus" "contrib.go.opencensus.io/exporter/stackdriver" "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/cloudsql" + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/internal/log" "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/internal/proxy" "github.com/spf13/cobra" "go.opencensus.io/trace" @@ -64,8 +65,11 @@ func Execute() { // Command represents an invocation of the Cloud SQL Auth Proxy. type Command struct { *cobra.Command - conf *proxy.Config + conf *proxy.Config + logger cloudsql.Logger + dialer cloudsql.Dialer + cleanup func() error disableTraces bool telemetryTracingSampleRate int disableMetrics bool @@ -76,25 +80,25 @@ type Command struct { } // Option is a function that configures a Command. -type Option func(*proxy.Config) +type Option func(*Command) + +// WithLogger overrides the default logger. +func WithLogger(l cloudsql.Logger) Option { + return func(c *Command) { + c.logger = l + } +} // WithDialer configures the Command to use the provided dialer to connect to // Cloud SQL instances. func WithDialer(d cloudsql.Dialer) Option { - return func(c *proxy.Config) { - c.Dialer = d + return func(c *Command) { + c.dialer = d } } // NewCommand returns a Command object representing an invocation of the proxy. func NewCommand(opts ...Option) *Command { - c := &Command{ - conf: &proxy.Config{}, - } - for _, o := range opts { - o(c.conf) - } - cmd := &cobra.Command{ Use: "cloud_sql_proxy instance_connection_name...", Version: versionString, @@ -103,19 +107,38 @@ func NewCommand(opts ...Option) *Command { connecting to Cloud SQL instances. It listens on a local port and forwards connections to your instance's IP address, providing a secure connection without having to manage any client SSL certificates.`, - Args: func(cmd *cobra.Command, args []string) error { - err := parseConfig(cmd, c.conf, args) - if err != nil { - return err - } - // The arguments are parsed. Usage is no longer needed. - cmd.SilenceUsage = true - return nil - }, - RunE: func(*cobra.Command, []string) error { - return runSignalWrapper(c) + } + + logger := log.NewStdLogger(os.Stdout, os.Stderr) + c := &Command{ + Command: cmd, + logger: logger, + cleanup: func() error { return nil }, + conf: &proxy.Config{ + UserAgent: userAgent, }, } + for _, o := range opts { + o(c) + } + + cmd.Args = func(cmd *cobra.Command, args []string) error { + // Handle logger separately from config + if c.conf.StructuredLogs { + c.logger, c.cleanup = log.NewStructuredLogger() + } + err := parseConfig(c, c.conf, args) + if err != nil { + return err + } + // The arguments are parsed. Usage is no longer needed. + cmd.SilenceUsage = true + // Errors will be handled by logging from here on. + cmd.SilenceErrors = true + return nil + } + + cmd.RunE = func(*cobra.Command, []string) error { return runSignalWrapper(c) } // Global-only flags cmd.PersistentFlags().StringVarP(&c.conf.Token, "token", "t", "", @@ -124,6 +147,8 @@ any client SSL certificates.`, "Path to a service account key to use for authentication.") cmd.PersistentFlags().BoolVarP(&c.conf.GcloudAuth, "gcloud-auth", "g", false, "Use gcloud's user configuration to retrieve a token for authentication.") + cmd.PersistentFlags().BoolVarP(&c.conf.StructuredLogs, "structured-logs", "l", false, + "Enable structured logs using the LogEntry format") cmd.PersistentFlags().Uint64Var(&c.conf.MaxConnections, "max-connections", 0, `Limits the number of connections by refusing any additional connections. When this flag is not set, there is no limit.`) @@ -162,18 +187,15 @@ the maximum time has passed. Defaults to 0s.`) cmd.PersistentFlags().BoolVar(&c.conf.PrivateIP, "private-ip", false, "Connect to the private ip address for all instances") - c.Command = cmd return c } -func parseConfig(cmd *cobra.Command, conf *proxy.Config, args []string) error { +func parseConfig(cmd *Command, conf *proxy.Config, args []string) error { // If no instance connection names were provided, error. if len(args) == 0 { return newBadCommandError("missing instance_connection_name (e.g., project:region:instance)") } - conf.UserAgent = userAgent - userHasSet := func(f string) bool { return cmd.PersistentFlags().Lookup(f).Changed } @@ -203,13 +225,13 @@ func parseConfig(cmd *cobra.Command, conf *proxy.Config, args []string) error { } if !userHasSet("telemetry-project") && userHasSet("telemetry-prefix") { - cmd.Println("Ignoring telementry-prefix as telemetry-project was not set") + cmd.logger.Infof("Ignoring telementry-prefix as telemetry-project was not set") } if !userHasSet("telemetry-project") && userHasSet("disable-metrics") { - cmd.Println("Ignoring disable-metrics as telemetry-project was not set") + cmd.logger.Infof("Ignoring disable-metrics as telemetry-project was not set") } if !userHasSet("telemetry-project") && userHasSet("disable-traces") { - cmd.Println("Ignoring disable-traces as telemetry-project was not set") + cmd.logger.Infof("Ignoring disable-traces as telemetry-project was not set") } if userHasSet("sqladmin-api-endpoint") && conf.ApiEndpointUrl != "" { @@ -333,6 +355,7 @@ func parseBoolOpt(q url.Values, name string) (*bool, error) { // runSignalWrapper watches for SIGTERM and SIGINT and interupts execution if necessary. func runSignalWrapper(cmd *Command) error { + defer cmd.cleanup() ctx, cancel := context.WithCancel(cmd.Context()) defer cancel() @@ -385,7 +408,7 @@ func runSignalWrapper(cmd *Command) error { // Give the HTTP server a second to shutdown cleanly. ctx2, _ := context.WithTimeout(context.Background(), time.Second) if err := server.Shutdown(ctx2); err != nil { - cmd.Printf("failed to shutdown Prometheus HTTP server: %v\n", err) + cmd.logger.Errorf("failed to shutdown Prometheus HTTP server: %v\n", err) } } }() @@ -423,7 +446,7 @@ func runSignalWrapper(cmd *Command) error { startCh := make(chan *proxy.Client) go func() { defer close(startCh) - p, err := proxy.NewClient(ctx, cmd.Command, cmd.conf) + p, err := proxy.NewClient(ctx, cmd.dialer, cmd.logger, cmd.conf) if err != nil { shutdownCh <- fmt.Errorf("unable to start: %v", err) return @@ -434,13 +457,15 @@ func runSignalWrapper(cmd *Command) error { var p *proxy.Client select { case err := <-shutdownCh: + cmd.logger.Errorf("The proxy has encountered a terminal error: %v", err) return err case p = <-startCh: } - cmd.Println("The proxy has started successfully and is ready for new connections!") + cmd.logger.Infof("The proxy has started successfully and is ready for new connections!") + defer p.Close() defer func() { if cErr := p.Close(); cErr != nil { - cmd.PrintErrf("The proxy failed to close cleanly: %v\n", cErr) + cmd.logger.Errorf("error during shutdown: %v", cErr) } }() @@ -451,11 +476,11 @@ func runSignalWrapper(cmd *Command) error { err := <-shutdownCh switch { case errors.Is(err, errSigInt): - cmd.PrintErrln("SIGINT signal received. Shutting down...") + cmd.logger.Errorf("SIGINT signal received. Shutting down...") case errors.Is(err, errSigTerm): - cmd.PrintErrln("SIGTERM signal received. Shutting down...") + cmd.logger.Errorf("SIGTERM signal received. Shutting down...") default: - cmd.PrintErrf("The proxy has encountered a terminal error: %v\n", err) + cmd.logger.Errorf("The proxy has encountered a terminal error: %v", err) } return err } diff --git a/cmd/root_test.go b/cmd/root_test.go index b679ccd07..f6bd9ed0b 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -214,6 +214,13 @@ func TestNewCommandArguments(t *testing.T) { }}, }), }, + { + desc: "enabling structured logging", + args: []string{"--structured-logs", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + StructuredLogs: true, + }), + }, { desc: "using the max connections flag", args: []string{"--max-connections", "1", "proj:region:inst"}, diff --git a/internal/log/log.go b/internal/log/log.go new file mode 100644 index 000000000..a2a11b1a8 --- /dev/null +++ b/internal/log/log.go @@ -0,0 +1,98 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log + +import ( + "io" + llog "log" + "os" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/cloudsql" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// StdLogger is the standard logger that distinguishes between info and error +// logs. +type StdLogger struct { + infoLog *llog.Logger + errLog *llog.Logger +} + +// NewStdLogger create a Logger that uses out and err for informational and +// error messages. +func NewStdLogger(out, err io.Writer) cloudsql.Logger { + return &StdLogger{ + infoLog: llog.New(out, "", llog.LstdFlags), + errLog: llog.New(err, "", llog.LstdFlags), + } +} + +func (l *StdLogger) Infof(format string, v ...interface{}) { + l.infoLog.Printf(format, v...) +} + +func (l *StdLogger) Errorf(format string, v ...interface{}) { + l.errLog.Printf(format, v...) +} + +func (l *StdLogger) Debugf(format string, v ...interface{}) { + l.infoLog.Printf(format, v...) +} + +// StructuredLogger writes log messages in JSON. +type StructuredLogger struct { + logger *zap.SugaredLogger +} + +func (l *StructuredLogger) Infof(format string, v ...interface{}) { + l.logger.Infof(format, v...) +} + +func (l *StructuredLogger) Errorf(format string, v ...interface{}) { + l.logger.Errorf(format, v...) +} + +func (l *StructuredLogger) Debugf(format string, v ...interface{}) { + l.logger.Infof(format, v...) +} + +// NewStructuredLogger creates a Logger that logs messages using JSON. +func NewStructuredLogger() (cloudsql.Logger, func() error) { + // Configure structured logs to adhere to LogEntry format + // https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry + c := zap.NewProductionEncoderConfig() + c.LevelKey = "severity" + c.MessageKey = "message" + c.TimeKey = "timestamp" + c.EncodeLevel = zapcore.CapitalLevelEncoder + c.EncodeTime = zapcore.ISO8601TimeEncoder + + enc := zapcore.NewJSONEncoder(c) + core := zapcore.NewTee( + zapcore.NewCore(enc, zapcore.Lock(os.Stdout), zap.LevelEnablerFunc(func(l zapcore.Level) bool { + // Anything below error, goes to the info log. + return l < zapcore.ErrorLevel + })), + zapcore.NewCore(enc, zapcore.Lock(os.Stderr), zap.LevelEnablerFunc(func(l zapcore.Level) bool { + // Anything at error or higher goes to the error log. + return l >= zapcore.ErrorLevel + })), + ) + l := &StructuredLogger{ + logger: zap.New(core).Sugar(), + } + return l, l.logger.Sync +} diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index c9bdca9af..08b4a66ee 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -28,7 +28,6 @@ import ( "cloud.google.com/go/cloudsqlconn" "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/cloudsql" "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/internal/gcloud" - "github.com/spf13/cobra" "golang.org/x/oauth2" ) @@ -106,9 +105,9 @@ type Config struct { // configuration takes precedence over global configuration. Instances []InstanceConnConfig - // Dialer specifies the dialer to use when connecting to Cloud SQL - // instances. - Dialer cloudsql.Dialer + // StructuredLogs sets all output to use JSON in the LogEntry format. + // See https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry + StructuredLogs bool } // DialOptions interprets appropriate dial options for a particular instance @@ -131,26 +130,30 @@ func (c *Config) DialOptions(i InstanceConnConfig) []cloudsqlconn.DialOption { // DialerOptions builds appropriate list of options from the Config // values for use by cloudsqlconn.NewClient() -func (c *Config) DialerOptions() ([]cloudsqlconn.Option, error) { +func (c *Config) DialerOptions(l cloudsql.Logger) ([]cloudsqlconn.Option, error) { opts := []cloudsqlconn.Option{ cloudsqlconn.WithUserAgent(c.UserAgent), } switch { case c.Token != "": + l.Infof("Authorizing with the -token flag") opts = append(opts, cloudsqlconn.WithTokenSource( oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.Token}), )) case c.CredentialsFile != "": + l.Infof("Authorizing with the credentials file at %q", c.CredentialsFile) opts = append(opts, cloudsqlconn.WithCredentialsFile( c.CredentialsFile, )) case c.GcloudAuth: + l.Infof("Authorizing with gcloud user credentials") ts, err := gcloud.TokenSource() if err != nil { return nil, err } opts = append(opts, cloudsqlconn.WithTokenSource(ts)) default: + l.Infof("Authorizing with Application Default Credentials") } if c.ApiEndpointUrl != "" { @@ -218,7 +221,6 @@ type Client struct { // connCount. If not set, there is no limit. maxConns uint64 - cmd *cobra.Command dialer cloudsql.Dialer // mnts is a list of all mounted sockets for this client @@ -227,16 +229,17 @@ type Client struct { // waitOnClose is the maximum duration to wait for open connections to close // when shutting down. waitOnClose time.Duration + + logger cloudsql.Logger } // NewClient completes the initial setup required to get the proxy to a "steady" state. -func NewClient(ctx context.Context, cmd *cobra.Command, conf *Config) (*Client, error) { +func NewClient(ctx context.Context, d cloudsql.Dialer, l cloudsql.Logger, conf *Config) (*Client, error) { // Check if the caller has configured a dialer. // Otherwise, initialize a new one. - d := conf.Dialer if d == nil { var err error - dialerOpts, err := conf.DialerOptions() + dialerOpts, err := conf.DialerOptions(l) if err != nil { return nil, fmt.Errorf("error initializing dialer: %v", err) } @@ -264,18 +267,18 @@ func NewClient(ctx context.Context, cmd *cobra.Command, conf *Config) (*Client, for _, m := range mnts { mErr := m.Close() if mErr != nil { - cmd.PrintErrf("failed to close mount: %v", mErr) + l.Errorf("failed to close mount: %v", mErr) } } return nil, fmt.Errorf("[%v] Unable to mount socket: %v", inst.Name, err) } - cmd.Printf("[%s] Listening on %s\n", inst.Name, m.Addr()) + l.Infof("[%s] Listening on %s", inst.Name, m.Addr()) mnts = append(mnts, m) } c := &Client{ mnts: mnts, - cmd: cmd, + logger: l, dialer: d, maxConns: conf.MaxConnections, waitOnClose: conf.WaitOnClose, @@ -375,7 +378,7 @@ func (c *Client) serveSocketMount(ctx context.Context, s *socketMount) error { cConn, err := s.Accept() if err != nil { if nerr, ok := err.(net.Error); ok && nerr.Temporary() { - c.cmd.PrintErrf("[%s] Error accepting connection: %v\n", s.inst, err) + c.logger.Errorf("[%s] Error accepting connection: %v", s.inst, err) // For transient errors, wait a small amount of time to see if it resolves itself time.Sleep(10 * time.Millisecond) continue @@ -384,7 +387,7 @@ func (c *Client) serveSocketMount(ctx context.Context, s *socketMount) error { } // handle the connection in a separate goroutine go func() { - c.cmd.Printf("[%s] accepted connection from %s\n", s.inst, cConn.RemoteAddr()) + c.logger.Errorf("[%s] accepted connection from %s", s.inst, cConn.RemoteAddr()) // A client has established a connection to the local socket. Before // we initiate a connection to the Cloud SQL backend, increment the @@ -394,7 +397,7 @@ func (c *Client) serveSocketMount(ctx context.Context, s *socketMount) error { defer atomic.AddUint64(&c.connCount, ^uint64(0)) if c.maxConns > 0 && count > c.maxConns { - c.cmd.Printf("max connections (%v) exceeded, refusing new connection\n", c.maxConns) + c.logger.Infof("max connections (%v) exceeded, refusing new connection", c.maxConns) _ = cConn.Close() return } @@ -405,7 +408,7 @@ func (c *Client) serveSocketMount(ctx context.Context, s *socketMount) error { sConn, err := c.dialer.Dial(ctx, s.inst, s.dialOpts...) if err != nil { - c.cmd.Printf("[%s] failed to connect to instance: %v\n", s.inst, err) + c.logger.Infof("[%s] failed to connect to instance: %v", s.inst, err) cConn.Close() return } @@ -515,9 +518,9 @@ func (c *Client) proxyConn(inst string, client, server net.Conn) { client.Close() server.Close() if isErr { - c.cmd.PrintErrln(errDesc) + c.logger.Errorf(errDesc) } else { - c.cmd.Println(errDesc) + c.logger.Infof(errDesc) } }) } diff --git a/internal/proxy/proxy_test.go b/internal/proxy/proxy_test.go index 2013e5846..9fbd2020c 100644 --- a/internal/proxy/proxy_test.go +++ b/internal/proxy/proxy_test.go @@ -28,8 +28,8 @@ import ( "time" "cloud.google.com/go/cloudsqlconn" + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/internal/log" "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/internal/proxy" - "github.com/spf13/cobra" ) type fakeDialer struct { @@ -238,8 +238,8 @@ func TestClientInitialization(t *testing.T) { for _, tc := range tcs { t.Run(tc.desc, func(t *testing.T) { - tc.in.Dialer = &fakeDialer{} - c, err := proxy.NewClient(ctx, &cobra.Command{}, tc.in) + logger := log.NewStdLogger(os.Stdout, os.Stdout) + c, err := proxy.NewClient(ctx, &fakeDialer{}, logger, tc.in) if err != nil { t.Fatalf("want error = nil, got = %v", err) } @@ -278,9 +278,9 @@ func TestClientLimitsMaxConnections(t *testing.T) { {Name: "proj:region:pg"}, }, MaxConnections: 1, - Dialer: d, } - c, err := proxy.NewClient(context.Background(), &cobra.Command{}, in) + logger := log.NewStdLogger(os.Stdout, os.Stdout) + c, err := proxy.NewClient(context.Background(), d, logger, in) if err != nil { t.Fatalf("proxy.NewClient error: %v", err) } @@ -303,11 +303,21 @@ func TestClientLimitsMaxConnections(t *testing.T) { // wait only a second for the result (since nothing is writing to the // socket) conn2.SetReadDeadline(time.Now().Add(time.Second)) - _, rErr := conn2.Read(make([]byte, 1)) - if rErr != io.EOF { - t.Fatalf("conn.Read should return io.EOF, got = %v", rErr) + + wantEOF := func(t *testing.T, c net.Conn) { + var got error + for i := 0; i < 10; i++ { + _, got = c.Read(make([]byte, 1)) + if got == io.EOF { + return + } + time.Sleep(100 * time.Millisecond) + } + t.Fatalf("conn.Read should return io.EOF, got = %v", got) } + wantEOF(t, conn2) + want := 1 if got := d.dialAttempts(); got != want { t.Fatalf("dial attempts did not match expected, want = %v, got = %v", want, got) @@ -334,15 +344,16 @@ func tryTCPDial(t *testing.T, addr string) net.Conn { } func TestClientCloseWaitsForActiveConnections(t *testing.T) { + logger := log.NewStdLogger(os.Stdout, os.Stdout) in := &proxy.Config{ Addr: "127.0.0.1", Port: 5000, Instances: []proxy.InstanceConnConfig{ {Name: "proj:region:pg"}, }, - Dialer: &fakeDialer{}, } - c, err := proxy.NewClient(context.Background(), &cobra.Command{}, in) + + c, err := proxy.NewClient(context.Background(), &fakeDialer{}, logger, in) if err != nil { t.Fatalf("proxy.NewClient error: %v", err) } @@ -357,7 +368,7 @@ func TestClientCloseWaitsForActiveConnections(t *testing.T) { in.WaitOnClose = time.Second in.Port = 5001 - c, err = proxy.NewClient(context.Background(), &cobra.Command{}, in) + c, err = proxy.NewClient(context.Background(), &fakeDialer{}, logger, in) if err != nil { t.Fatalf("proxy.NewClient error: %v", err) } @@ -386,9 +397,9 @@ func TestClientClosesCleanly(t *testing.T) { Instances: []proxy.InstanceConnConfig{ {Name: "proj:reg:inst"}, }, - Dialer: &fakeDialer{}, } - c, err := proxy.NewClient(context.Background(), &cobra.Command{}, in) + logger := log.NewStdLogger(os.Stdout, os.Stdout) + c, err := proxy.NewClient(context.Background(), &fakeDialer{}, logger, in) if err != nil { t.Fatalf("proxy.NewClient error want = nil, got = %v", err) } @@ -409,9 +420,9 @@ func TestClosesWithError(t *testing.T) { Instances: []proxy.InstanceConnConfig{ {Name: "proj:reg:inst"}, }, - Dialer: &errorDialer{}, } - c, err := proxy.NewClient(context.Background(), &cobra.Command{}, in) + logger := log.NewStdLogger(os.Stdout, os.Stdout) + c, err := proxy.NewClient(context.Background(), &errorDialer{}, logger, in) if err != nil { t.Fatalf("proxy.NewClient error want = nil, got = %v", err) } @@ -465,15 +476,16 @@ func TestClientInitializationWorksRepeatedly(t *testing.T) { Instances: []proxy.InstanceConnConfig{ {Name: "proj:region:pg"}, }, - Dialer: &fakeDialer{}, } - c, err := proxy.NewClient(ctx, &cobra.Command{}, in) + + logger := log.NewStdLogger(os.Stdout, os.Stdout) + c, err := proxy.NewClient(ctx, &fakeDialer{}, logger, in) if err != nil { t.Fatalf("want error = nil, got = %v", err) } c.Close() - c, err = proxy.NewClient(ctx, &cobra.Command{}, in) + c, err = proxy.NewClient(ctx, &fakeDialer{}, logger, in) if err != nil { t.Fatalf("want error = nil, got = %v", err) } diff --git a/testsV2/common_test.go b/testsV2/common_test.go index f9ffa2b56..ac83ede47 100644 --- a/testsV2/common_test.go +++ b/testsV2/common_test.go @@ -29,6 +29,7 @@ import ( "strings" "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/cmd" + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/internal/log" ) // proxyExec represents an execution of the Cloud SQL proxy. @@ -45,16 +46,15 @@ type proxyExec struct { // StartProxy returns a proxyExec representing a running instance of the proxy. func StartProxy(ctx context.Context, args ...string) (*proxyExec, error) { ctx, cancel := context.WithCancel(ctx) - cmd := cmd.NewCommand() - cmd.SetArgs(args) - // Open a pipe for tracking the output from the cmd pr, pw, err := os.Pipe() if err != nil { cancel() return nil, fmt.Errorf("unable to open stdout pipe: %w", err) } - // defer pw.Close() + + cmd := cmd.NewCommand(cmd.WithLogger(log.NewStdLogger(pw, pw))) + cmd.SetArgs(args) cmd.SetOut(pw) cmd.SetErr(pw)