diff --git a/README.md b/README.md index 2c25a91..faebaee 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,10 @@ Flags: also accepted --param-file=STRING YAML or JSON file of query parameters (name to type/literal string) - --log-grpc="off" gRPC logging mode: off, metadata, or payload - (payload may include request and response - payloads in logs) + --log-grpc gRPC logging: --log-grpc means payload; + use --log-grpc=off|metadata|payload to select + a mode (payload may include request and + response payloads) --experimental-trace-project=STRING Export traces to Cloud Trace in the given project. @@ -327,6 +328,9 @@ $ execspansql $DATABASE_ID --query-mode=PROFILE --sql 'SELECT 1' --experimental- ``` Note: `--experimental-trace-stdout` writes to **stderr**, not stdout. + +A bare `--log-grpc` retains its previous meaning: payload logging. Logging is off when the flag is omitted. Use `--log-grpc=off`, `--log-grpc=metadata`, or `--log-grpc=payload` to select a mode; an explicit value must use `=` so the next database argument is not consumed. Legacy boolean values such as `--log-grpc=true` and `--log-grpc=false` remain accepted. + Note: `--log-grpc=payload` can log request and response payloads (including bound parameters and row values) and should only be used in trusted environments. ![trace.png](docs/trace.png) diff --git a/log_grpc_flag.go b/log_grpc_flag.go new file mode 100644 index 0000000..0baca37 --- /dev/null +++ b/log_grpc_flag.go @@ -0,0 +1,35 @@ +package main + +import ( + "strings" + + "github.com/alecthomas/kong" +) + +// logGrpcFlag retains the old boolean flag syntax while accepting explicit +// logging modes. As with boolean flags, explicit values use "=" so a bare +// flag never consumes the following database argument. +type logGrpcFlag string + +func (*logGrpcFlag) IsBool() bool { return true } + +func (m *logGrpcFlag) Decode(ctx *kong.DecodeContext) error { + if ctx.Scan.Peek().Type != kong.FlagValueToken { + *m = logGrpcModePayload + return nil + } + var value string + if err := ctx.Scan.PopValueInto("logging mode", &value); err != nil { + return err + } + // Preserve Kong's previous boolean spellings as well as the bare flag. + // The enum tag validates explicit mode names after this normalization. + switch strings.ToLower(value) { + case "true", "1", "yes": + value = logGrpcModePayload + case "false", "0", "no": + value = logGrpcModeOff + } + *m = logGrpcFlag(value) + return nil +} diff --git a/log_grpc_flag_test.go b/log_grpc_flag_test.go new file mode 100644 index 0000000..139d8b2 --- /dev/null +++ b/log_grpc_flag_test.go @@ -0,0 +1,56 @@ +package main + +import ( + "strings" + "testing" + + "github.com/alecthomas/kong" +) + +func TestLogGrpcFlag(t *testing.T) { + for _, tt := range []struct { + name string + args []string + want string + wantDB string + wantErr bool + }{ + {name: "omitted", args: []string{"db"}, want: "off", wantDB: "db"}, + {name: "bare_before_database", args: []string{"--log-grpc", "db"}, want: "payload", wantDB: "db"}, + {name: "bare_after_database", args: []string{"db", "--log-grpc"}, want: "payload", wantDB: "db"}, + {name: "mode_named_database", args: []string{"--log-grpc", "metadata"}, want: "payload", wantDB: "metadata"}, + {name: "off", args: []string{"db", "--log-grpc=off"}, want: "off", wantDB: "db"}, + {name: "metadata", args: []string{"db", "--log-grpc=metadata"}, want: "metadata", wantDB: "db"}, + {name: "payload", args: []string{"db", "--log-grpc=payload"}, want: "payload", wantDB: "db"}, + {name: "legacy_true", args: []string{"db", "--log-grpc=true"}, want: "payload", wantDB: "db"}, + {name: "legacy_yes", args: []string{"db", "--log-grpc=YES"}, want: "payload", wantDB: "db"}, + {name: "legacy_one", args: []string{"db", "--log-grpc=1"}, want: "payload", wantDB: "db"}, + {name: "legacy_false", args: []string{"db", "--log-grpc=false"}, want: "off", wantDB: "db"}, + {name: "legacy_no", args: []string{"db", "--log-grpc=NO"}, want: "off", wantDB: "db"}, + {name: "legacy_zero", args: []string{"db", "--log-grpc=0"}, want: "off", wantDB: "db"}, + {name: "invalid", args: []string{"db", "--log-grpc=invalid"}, wantErr: true}, + {name: "empty", args: []string{"db", "--log-grpc="}, wantErr: true}, + } { + t.Run(tt.name, func(t *testing.T) { + var o opts + parser, err := kong.New(&o) + if err != nil { + t.Fatal(err) + } + args := append(append([]string{}, tt.args...), "--project=p", "--instance=i", "--sql=SELECT 1") + _, err = parser.Parse(args) + if tt.wantErr { + if err == nil || !strings.Contains(err.Error(), "--log-grpc") { + t.Fatalf("error = %v, want invalid log mode", err) + } + return + } + if err != nil { + t.Fatal(err) + } + if string(o.LogGrpc) != tt.want || o.Database != tt.wantDB { + t.Fatalf("mode=%q database=%q, want %q %q", o.LogGrpc, o.Database, tt.want, tt.wantDB) + } + }) + } +} diff --git a/main.go b/main.go index 5e250dc..b7caa4f 100644 --- a/main.go +++ b/main.go @@ -65,7 +65,7 @@ type opts struct { JqInputMode string `name:"jq-input-mode" enum:"eager,lazy" default:"eager" help:"How query rows are passed to jq (json/yaml only): eager (full ResultSet), lazy (JQValue root)."` ParamFlags []string `name:"param" help:"[name]=[type or literal]; legacy [name]:[...] also accepted"` ParamFile string `name:"param-file" help:"YAML or JSON file of query parameters (name to type/literal string)"` - LogGrpc string `name:"log-grpc" enum:"off,metadata,payload" default:"off" help:"gRPC logging mode: off, metadata, or payload (payload may include request and response payloads in logs)"` + LogGrpc logGrpcFlag `name:"log-grpc" enum:"off,metadata,payload" default:"off" help:"gRPC logging: --log-grpc means payload; use --log-grpc=off|metadata|payload to select a mode (payload may include request and response payloads)"` TraceProject string `name:"experimental-trace-project" xor:"trace" help:"Export traces to Cloud Trace in the given project."` TraceStdout bool `name:"experimental-trace-stdout" xor:"trace" help:"Export spans to stderr as pretty JSON (local debugging)."` TraceOTLP bool `name:"experimental-trace-otlp" xor:"trace" help:"Export spans via OTLP/gRPC to a local OpenTelemetry collector."` @@ -474,7 +474,7 @@ func runCLI(clientOptions ...option.ClientOption) error { }() } - client, err := newClient(ctx, o.Project, o.Instance, o.Database, o.DatabaseRole, o.LogGrpc, tracingEnabled(o), clientOptions...) + client, err := newClient(ctx, o.Project, o.Instance, o.Database, o.DatabaseRole, string(o.LogGrpc), tracingEnabled(o), clientOptions...) if err != nil { return err }