Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ import (
"github.com/spf13/pflag"
"github.com/spf13/viper"
"go.opencensus.io/trace"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

var (
Expand Down Expand Up @@ -1073,6 +1076,9 @@ func runSignalWrapper(cmd *Command) (err error) {
sd, err := stackdriver.NewExporter(stackdriver.Options{
ProjectID: cmd.conf.TelemetryProject,
MetricPrefix: cmd.conf.TelemetryPrefix,
OnError: func(err error) {
cmd.logger.Errorf("Failed to export to Stackdriver: %v", formatStackdriverError(err))
},
})
if err != nil {
return err
Expand Down Expand Up @@ -1302,3 +1308,58 @@ func startHTTPServer(ctx context.Context, l cloudsql.Logger, addr string, mux *h
l.Errorf("failed to shutdown HTTP server: %v\n", err)
}
}

func formatStackdriverError(err error) string {
st, ok := status.FromError(err)
if !ok {
return err.Error()
}

// Default message: code and description
msg := fmt.Sprintf("rpc error: code = %s desc = %s", st.Code(), st.Message())

// Try to extract details
details := st.Details()
if len(details) > 0 {
var detailStrings []string
for _, detail := range details {
switch d := detail.(type) {
case *errdetails.ErrorInfo:
detailStrings = append(detailStrings, fmt.Sprintf("ErrorInfo: Reason=%s, Domain=%s, Metadata=%v", d.Reason, d.Domain, d.Metadata))
case *errdetails.Help:
for _, link := range d.Links {
detailStrings = append(detailStrings, fmt.Sprintf("Help: %s (%s)", link.Description, link.Url))
}
default:
detailStrings = append(detailStrings, fmt.Sprintf("%T: %v", d, d))
}
}
if len(detailStrings) > 0 {
msg = fmt.Sprintf("%s (details: %s)", msg, strings.Join(detailStrings, "; "))
}
}

// Add a general hint if we suspect permission issues based on code, message or details
isPermissionIssue := st.Code() == codes.PermissionDenied ||
strings.Contains(strings.ToLower(st.Message()), "permission") ||
strings.Contains(strings.ToLower(st.Message()), "denied")

if !isPermissionIssue {
// Also check details for permission-like reasons
for _, detail := range details {
if ei, ok := detail.(*errdetails.ErrorInfo); ok {
if ei.Reason == "ACCESS_TOKEN_SCOPE_INSUFFICIENT" ||
strings.Contains(strings.ToLower(ei.Reason), "permission") {
isPermissionIssue = true
break
}
}
}
}

if isPermissionIssue {
msg = fmt.Sprintf("%s - Hint: The Service Account may be missing required IAM permissions. Please ensure it has 'Monitoring Metric Writer' (roles/monitoring.metricWriter) and 'Cloud Trace Agent' (roles/cloudtrace.agent) roles.", msg)
}

return msg
}
76 changes: 76 additions & 0 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ import (
"github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/proxy"
"github.com/google/go-cmp/cmp"
"github.com/spf13/cobra"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

func withDefaults(c *proxy.Config) *proxy.Config {
Expand Down Expand Up @@ -1583,3 +1586,76 @@ func TestQuitQuitQuitWithErrors(t *testing.T) {
t.Fatalf("want = %v, got = %v", errCloseFailed, got)
}
}

func TestFormatStackdriverError(t *testing.T) {
tcs := []struct {
desc string
err error
wantSub string
wantHint bool
}{
{
desc: "generic error",
err: errors.New("some random error"),
wantSub: "some random error",
wantHint: false,
},
{
desc: "gRPC error without details",
err: status.Error(codes.Internal, "Internal error encountered"),
wantSub: "rpc error: code = Internal desc = Internal error encountered",
wantHint: false,
},
{
desc: "gRPC PermissionDenied error",
err: status.Error(codes.PermissionDenied, "Permission denied"),
wantSub: "rpc error: code = PermissionDenied desc = Permission denied",
wantHint: true,
},
{
desc: "gRPC error with ErrorInfo permission reason",
err: func() error {
st, _ := status.New(codes.Internal, "Internal error encountered").WithDetails(
&errdetails.ErrorInfo{
Reason: "ACCESS_TOKEN_SCOPE_INSUFFICIENT",
Domain: "googleapis.com",
},
)
return st.Err()
}(),
wantSub: "rpc error: code = Internal desc = Internal error encountered",
wantHint: true,
},
{
desc: "gRPC error with Help link",
err: func() error {
st, _ := status.New(codes.Aborted, "Aborted").WithDetails(
&errdetails.Help{
Links: []*errdetails.Help_Link{
{
Description: "See docs",
Url: "http://docs",
},
},
},
)
return st.Err()
}(),
wantSub: "Help: See docs (http://docs)",
wantHint: false,
},
}

for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
got := formatStackdriverError(tc.err)
if !strings.Contains(got, tc.wantSub) {
t.Errorf("formatStackdriverError(%v) = %q, want to contain %q", tc.err, got, tc.wantSub)
}
hasHint := strings.Contains(got, "Hint: The Service Account may be missing required IAM permissions")
if hasHint != tc.wantHint {
t.Errorf("formatStackdriverError(%v) = %q, hasHint = %v, wantHint = %v", tc.err, got, hasHint, tc.wantHint)
}
})
}
}
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ require (
golang.org/x/oauth2 v0.36.0
golang.org/x/sys v0.47.0
google.golang.org/api v0.289.0
google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a
google.golang.org/grpc v1.82.1
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)

Expand Down Expand Up @@ -79,9 +81,7 @@ require (
golang.org/x/sync v0.22.0 // indirect
golang.org/x/text v0.40.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto v0.0.0-20260720211330-0afa2a65878a // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a // indirect
google.golang.org/grpc v1.82.1 // indirect
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -640,10 +640,10 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20260720211330-0afa2a65878a h1:MVNwR9RFj7qfpMtNK71pq97FgrLG0lVHZh+VbM2LZeI=
google.golang.org/genproto v0.0.0-20260720211330-0afa2a65878a/go.mod h1:0qnvndM9dUhat9AtF1jqYN6WZ+tMxEAFImo3WNvUX7w=
google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a h1:97PfJ4tCxY5C7NzzgGqQEMZmXbISdvSArNNEOoUGKBg=
google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a/go.mod h1:1brfde68Npq6+WA75c1EHWPijZEG1kMus61ygPZfn4A=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 h1:jQ9p21COKWjP3VwuFrNRiiOTMh3mPpN45R7SLrH/HUU=
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7/go.mod h1:KqHwBx2upmfa1XSi1WuRvC+2VGCLtooKkfmyvRbUmqA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a h1:qI/YMH1ep2qQtqcp00gMQyoU7mjvbhg88GJKCvfoLj0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
Expand Down
Loading