From f87f7d8d9b65eeebb0409744269e0ab532b12dab Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Fri, 17 Apr 2026 03:26:27 +0300 Subject: [PATCH 1/8] feat(logging): always capture debug entries to the per-run log file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The logrus file hook no longer filters by user-selected verbosity. JAR subprocess output (logged at Debug) now lands in the log file in every mode, so the per-run log is a reliable record of analyzer and autobuilder execution. SetUpLogs drops its level parameter — the console-side filtering moves to a later commit. --- cli/cmd/root.go | 2 +- cli/internal/utils/log/set_up_logs.go | 20 +++--------- cli/internal/utils/log/set_up_logs_test.go | 36 ++++++++++++++++++++++ 3 files changed, 42 insertions(+), 16 deletions(-) create mode 100644 cli/internal/utils/log/set_up_logs_test.go diff --git a/cli/cmd/root.go b/cli/cmd/root.go index aa63dc34..70f17712 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -40,7 +40,7 @@ var rootCmd = &cobra.Command{ } globals.Config.Log.Verbosity = verbosity - if err := log.SetUpLogs(globals.Config.Log.Verbosity); err != nil { + if err := log.SetUpLogs(); err != nil { return fmt.Errorf("failed to set up logging: %w", err) } diff --git a/cli/internal/utils/log/set_up_logs.go b/cli/internal/utils/log/set_up_logs.go index ccfe2159..ef2e03e1 100644 --- a/cli/internal/utils/log/set_up_logs.go +++ b/cli/internal/utils/log/set_up_logs.go @@ -99,20 +99,10 @@ func (f *blockTextFormatter) Format(entry *logrus.Entry) ([]byte, error) { } // SetUpLogs configures logging using the global SwitchableWriter (io.Discard by default). -// Console output is no longer handled by logrus — it's handled by the output.Printer. -// Logrus is used exclusively for structured file logging. -func SetUpLogs(level string) error { - normalizedLevel := strings.ToLower(strings.TrimSpace(level)) - var fileLevel logrus.Level - switch normalizedLevel { - case "", "info": - fileLevel = logrus.InfoLevel - case "debug": - fileLevel = logrus.DebugLevel - default: - return fmt.Errorf("invalid verbosity %q: expected one of info, debug", level) - } - +// The file hook always captures Debug-and-above entries so the per-run log file is +// the reliable record of analyzer/autobuilder output regardless of console verbosity. +// Console output is handled by the output.Printer, not by logrus. +func SetUpLogs() error { fileFormatter := &blockTextFormatter{ TimestampFormat: "2006-01-02 15:04:05", Indent: " ", @@ -124,7 +114,7 @@ func SetUpLogs(level string) error { logrus.AddHook(&writerHook{ Writer: LogWriter(), Formatter: fileFormatter, - LogLevels: allowedLevels(fileLevel), + LogLevels: allowedLevels(logrus.DebugLevel), }) logrus.AddHook(&writerHook{ diff --git a/cli/internal/utils/log/set_up_logs_test.go b/cli/internal/utils/log/set_up_logs_test.go new file mode 100644 index 00000000..18a97bae --- /dev/null +++ b/cli/internal/utils/log/set_up_logs_test.go @@ -0,0 +1,36 @@ +package log + +import ( + "bytes" + "strings" + "testing" + + "github.com/sirupsen/logrus" +) + +// SetUpLogs must wire the file hook so that Debug-level entries are +// captured no matter what verbosity argument is passed in. JAR output is +// emitted at Debug; it must land in the log file even in default mode. +func TestSetUpLogsFileHookAlwaysCapturesDebug(t *testing.T) { + var buf bytes.Buffer + LogWriter().Swap(&buf) + t.Cleanup(func() { LogWriter().Swap(nopWriter{}) }) + + // Reset hooks between cases. + logrus.StandardLogger().ReplaceHooks(make(logrus.LevelHooks)) + + if err := SetUpLogs(); err != nil { + t.Fatalf("SetUpLogs returned error: %v", err) + } + + logrus.Debug("captured-debug-line") + + got := buf.String() + if !strings.Contains(got, "captured-debug-line") { + t.Fatalf("expected debug line in log file output even with info verbosity, got %q", got) + } +} + +type nopWriter struct{} + +func (nopWriter) Write(p []byte) (int, error) { return len(p), nil } From e7a4f63bd9856792875911ea9d06eac6557a4fc7 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Fri, 17 Apr 2026 03:35:06 +0300 Subject: [PATCH 2/8] feat(cli): replace --verbosity with --debug, move output settings under output.* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops the --verbosity info|debug enum in favor of a boolean --debug/-d flag. Moves debug, color, quiet under a single output.* config namespace in YAML and env (OPENTAINT_OUTPUT_DEBUG, etc.); the old log.* block and top-level quiet are removed. Decouples JAR streaming from --quiet — quiet is now truly quiet, no spinner and no JAR mirroring. Debug mode is the only thing that streams JAR output to stderr. Removes the WithStreamOutput builder method on JavaRunner since the decision collapses to a single boolean read. The JAR's own --verbosity flag is still passed through (mapped from the new bool) — that's the external Java tool's CLI, not opentaint's. Breaking change: --verbosity, log.verbosity, log.color, top-level quiet in YAML, and OPENTAINT_LOG_VERBOSITY/_COLOR/OPENTAINT_QUIET env vars all stop working. --- cli/cmd/command_builder.go | 17 ++++---- cli/cmd/compile.go | 3 +- cli/cmd/project.go | 1 - cli/cmd/root.go | 42 ++++++------------- cli/cmd/scan.go | 4 +- cli/internal/globals/global.go | 12 +++--- .../load_trace/rule_statistics_tree.go | 2 +- cli/internal/output/output.go | 35 ++++++++-------- cli/internal/output/output_test.go | 12 +++--- cli/internal/utils/java/runner.go | 14 +------ cli/internal/utils/java/runner_test.go | 36 ---------------- 11 files changed, 54 insertions(+), 124 deletions(-) diff --git a/cli/cmd/command_builder.go b/cli/cmd/command_builder.go index bbb250b9..772957de 100644 --- a/cli/cmd/command_builder.go +++ b/cli/cmd/command_builder.go @@ -9,24 +9,23 @@ import ( ) type BaseCommandBuilder struct { - verbosity string + debug bool } +// appendVerbosityFlag passes the JAR's own --verbosity flag based on opentaint's +// debug bool. The Java tool's CLI surface is independent of opentaint's; we just +// translate. info ↔ false, debug ↔ true. func (b *BaseCommandBuilder) appendVerbosityFlag(flags []string) []string { - switch b.verbosity { - case "info": - return append(flags, "--verbosity=info") - case "debug": + if b.debug { return append(flags, "--verbosity=debug") - default: - return flags } + return append(flags, "--verbosity=info") } func NewAnalyzerBuilder() *AnalyzerBuilder { return &AnalyzerBuilder{ BaseCommandBuilder: &BaseCommandBuilder{ - verbosity: globals.Config.Log.Verbosity, + debug: globals.Config.Output.Debug, }, maxMemory: "-Xmx8G", } @@ -35,7 +34,7 @@ func NewAnalyzerBuilder() *AnalyzerBuilder { func NewAutobuilderBuilder() *AutobuilderBuilder { return &AutobuilderBuilder{ BaseCommandBuilder: &BaseCommandBuilder{ - verbosity: globals.Config.Log.Verbosity, + debug: globals.Config.Output.Debug, }, maxMemory: "-Xmx1G", } diff --git a/cli/cmd/compile.go b/cli/cmd/compile.go index 25e8fd91..4dc97fba 100644 --- a/cli/cmd/compile.go +++ b/cli/cmd/compile.go @@ -67,7 +67,7 @@ Arguments: sb := out.Section("OpenTaint Compile") addConfigFields(cmd, sb) - if globals.Config.Log.Verbosity == "debug" { + if globals.Config.Output.Debug { sb.Line() } sb.Field("Project", absProjectRoot). @@ -88,7 +88,6 @@ Arguments: compileJavaRunner := java.NewJavaRunner(). WithSkipVerify(globals.Config.SkipVerify). - WithStreamOutput(globals.Config.Quiet). WithDebugOutput(out.DebugStream("Autobuilder")). TrySystem(). TrySpecificVersion(globals.Config.Java.Version) diff --git a/cli/cmd/project.go b/cli/cmd/project.go index 9cb45582..216ad36d 100644 --- a/cli/cmd/project.go +++ b/cli/cmd/project.go @@ -151,7 +151,6 @@ func (c *JavaAutobuilderConfig) runAutobuilder() error { javaRunner := java.NewJavaRunner(). WithSkipVerify(globals.Config.SkipVerify). - WithStreamOutput(globals.Config.Quiet). WithDebugOutput(out.DebugStream("Autobuilder")). WithImageType(java.AdoptiumImageJRE). TrySpecificVersion(globals.DefaultJavaVersion) diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 70f17712..2bc7749b 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -34,25 +34,19 @@ var rootCmd = &cobra.Command{ SilenceUsage: true, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - verbosity, err := normalizeVerbosity(globals.Config.Log.Verbosity) - if err != nil { - return err - } - globals.Config.Log.Verbosity = verbosity - if err := log.SetUpLogs(); err != nil { return fmt.Errorf("failed to set up logging: %w", err) } - // Configure the output printer (color mode, quiet mode) - out.Configure(globals.Config.Log.Color, globals.Config.Quiet) - out.SetVerbosity(globals.Config.Log.Verbosity) + // Configure the output printer. + out.Configure(globals.Config.Output.Color, globals.Config.Output.Quiet) + out.SetDebug(globals.Config.Output.Debug) // Reconcile install-tier version marker if needed (lightweight: a few Stat calls). utils.ReconcileInstallMarker() // Start async update check (non-blocking, at most once per day) - if !globals.Config.Quiet { + if !globals.Config.Output.Quiet { go checkForUpdateAsync() } @@ -98,14 +92,14 @@ func init() { rootCmd.Flags().BoolVarP(&toolVersion, "version", "v", false, "Print the version information") - rootCmd.PersistentFlags().StringVar(&globals.Config.Log.Verbosity, "verbosity", "info", "Verbosity level (info, debug)") - _ = viper.BindPFlag("log.verbosity", rootCmd.PersistentFlags().Lookup("verbosity")) + rootCmd.PersistentFlags().BoolVarP(&globals.Config.Output.Debug, "debug", "d", false, "Enable debug output (stream JAR subprocess output, show debug-only fields)") + _ = viper.BindPFlag("output.debug", rootCmd.PersistentFlags().Lookup("debug")) - rootCmd.PersistentFlags().StringVar(&globals.Config.Log.Color, "color", "auto", "Color mode (auto, always, never)") - _ = viper.BindPFlag("log.color", rootCmd.PersistentFlags().Lookup("color")) + rootCmd.PersistentFlags().StringVar(&globals.Config.Output.Color, "color", "auto", "Color mode (auto, always, never)") + _ = viper.BindPFlag("output.color", rootCmd.PersistentFlags().Lookup("color")) - rootCmd.PersistentFlags().BoolVarP(&globals.Config.Quiet, "quiet", "q", false, "Suppress interactive UI elements (spinners, progress bars)") - _ = viper.BindPFlag("quiet", rootCmd.PersistentFlags().Lookup("quiet")) + rootCmd.PersistentFlags().BoolVarP(&globals.Config.Output.Quiet, "quiet", "q", false, "Suppress interactive UI elements (spinners, progress bars) and JAR streaming") + _ = viper.BindPFlag("output.quiet", rootCmd.PersistentFlags().Lookup("quiet")) rootCmd.PersistentFlags().StringVar(&globals.Config.Analyzer.Version, "analyzer-version", globals.AnalyzerBindVersion, "Version of opentaint analyzer") _ = rootCmd.PersistentFlags().MarkHidden("analyzer-version") @@ -153,23 +147,11 @@ func initConfig() { _ = viper.Unmarshal(&globals.Config) } -func normalizeVerbosity(level string) (string, error) { - value := strings.ToLower(strings.TrimSpace(level)) - switch value { - case "", "info": - return "info", nil - case "debug": - return "debug", nil - default: - return "info", nil - } -} - // addConfigFields appends config fields to a SectionBuilder if PrintConfig annotation is set. func addConfigFields(cmd *cobra.Command, sb *output.SectionBuilder) { if cmd.Annotations != nil && cmd.Annotations["PrintConfig"] == "true" { - if globals.Config.Log.Verbosity == "debug" { - sb.Field("Log level", globals.Config.Log.Verbosity) + if globals.Config.Output.Debug { + sb.Field("Log level", "debug") if viper.ConfigFileUsed() != "" { sb.Field("Config file", viper.ConfigFileUsed()) } diff --git a/cli/cmd/scan.go b/cli/cmd/scan.go index f6e8b6f4..0be61474 100644 --- a/cli/cmd/scan.go +++ b/cli/cmd/scan.go @@ -263,7 +263,6 @@ func scan(cmd *cobra.Command) { compileJavaRunner := java.NewJavaRunner(). WithSkipVerify(globals.Config.SkipVerify). - WithStreamOutput(globals.Config.Quiet). WithDebugOutput(out.DebugStream("Autobuilder")). TrySystem(). TrySpecificVersion(globals.Config.Java.Version) @@ -344,7 +343,6 @@ func scan(cmd *cobra.Command) { analyzerJavaRunner := java.NewJavaRunner(). WithSkipVerify(globals.Config.SkipVerify). - WithStreamOutput(globals.Config.Quiet). WithDebugOutput(out.DebugStream("Analyzer")). WithImageType(java.AdoptiumImageJRE). TrySpecificVersion(globals.DefaultJavaVersion) @@ -467,7 +465,7 @@ func resolveScanConfig(absUserProjectRoot string) scanConfig { func printScanInfo(cmd *cobra.Command, cfg scanConfig, absSemgrepRuleLoadTracePath string, absUserProjectRoot string, absRuleSetPaths []RulesetType) { sb := out.Section(cfg.mode.String()) addConfigFields(cmd, sb) - if globals.Config.Log.Verbosity == "debug" { + if globals.Config.Output.Debug { sb.Field("Rule load trace", absSemgrepRuleLoadTracePath) sb.Line() } diff --git a/cli/internal/globals/global.go b/cli/internal/globals/global.go index d14d2ace..8af14008 100644 --- a/cli/internal/globals/global.go +++ b/cli/internal/globals/global.go @@ -46,9 +46,10 @@ type Scan struct { CodeFlowLimit int64 `mapstructure:"code_flow_limit"` } -type Log struct { - Verbosity string `mapstructure:"verbosity"` - Color string `mapstructure:"color"` +type Output struct { + Debug bool `mapstructure:"debug"` + Color string `mapstructure:"color"` + Quiet bool `mapstructure:"quiet"` } type Github struct { @@ -72,8 +73,8 @@ type Java struct { } type ConfigType struct { - Scan Scan `mapstructure:"scan"` - Log Log `mapstructure:"log"` + Scan Scan `mapstructure:"scan"` + Output Output `mapstructure:"output"` Github Github `mapstructure:"github"` Analyzer Analyzer `mapstructure:"analyzer"` @@ -82,7 +83,6 @@ type ConfigType struct { Java Java `mapstructure:"java"` Owner string `mapstructure:"owner"` Repo string `mapstructure:"repo"` - Quiet bool `mapstructure:"quiet"` SkipVerify bool `mapstructure:"skip-verify"` } diff --git a/cli/internal/load_trace/rule_statistics_tree.go b/cli/internal/load_trace/rule_statistics_tree.go index 48f5f82e..e3acf4a5 100644 --- a/cli/internal/load_trace/rule_statistics_tree.go +++ b/cli/internal/load_trace/rule_statistics_tree.go @@ -29,7 +29,7 @@ func buildRuleParsingIssuesNode(out *output.Printer, result *RuleLoadErrorsResul } s := result.Summary - isDebug := globals.Config.Log.Verbosity == "debug" + isDebug := globals.Config.Output.Debug var children []any diff --git a/cli/internal/output/output.go b/cli/internal/output/output.go index 34f64c65..687dd7c3 100644 --- a/cli/internal/output/output.go +++ b/cli/internal/output/output.go @@ -16,15 +16,15 @@ import ( // through a Printer instance. It holds the active theme, the output writer, // and knows whether it's writing to a TTY. type Printer struct { - baseW io.Writer - w io.Writer - debugW io.Writer - logW io.Writer - verbosity string - theme *Theme - isTTY bool - quiet bool - profile colorprofile.Profile + baseW io.Writer + w io.Writer + debugW io.Writer + logW io.Writer + debug bool + theme *Theme + isTTY bool + quiet bool + profile colorprofile.Profile } var ansiEscapePattern = regexp.MustCompile(`\x1b\[[0-9;?]*[ -/]*[@-~]`) @@ -119,21 +119,22 @@ func (p *Printer) SetLogWriter(w io.Writer) { p.logW = w } -// SetVerbosity stores the configured log verbosity to control -// interactive UI elements like spinners and progress bars. -func (p *Printer) SetVerbosity(level string) { - p.verbosity = strings.ToLower(strings.TrimSpace(level)) +// SetDebug enables debug-mode console output: JAR streaming, debug-only +// fields in info sections, and suppression of spinners (which would conflict +// with streamed JAR output). +func (p *Printer) SetDebug(debug bool) { + p.debug = debug } -// IsDebugVerbosity returns true for debug-like verbosity modes. -func (p *Printer) IsDebugVerbosity() bool { - return p.verbosity == "debug" +// IsDebug returns true when debug-mode console output is enabled. +func (p *Printer) IsDebug() bool { + return p.debug } // IsInteractiveUI returns true when interactive UI components // (spinners/progress bars) should be rendered. func (p *Printer) IsInteractiveUI() bool { - return p.IsInteractive() && !p.IsDebugVerbosity() && !p.quiet + return p.IsInteractive() && !p.IsDebug() && !p.quiet } func (p *Printer) writeMirroredLine(line string) { diff --git a/cli/internal/output/output_test.go b/cli/internal/output/output_test.go index ade6f074..da0277c5 100644 --- a/cli/internal/output/output_test.go +++ b/cli/internal/output/output_test.go @@ -245,26 +245,26 @@ func TestFileLinkNonTTY(t *testing.T) { } } -func TestIsInteractiveUIDisabledOnDebugVerbosity(t *testing.T) { +func TestIsInteractiveUIDisabledOnDebug(t *testing.T) { var buf bytes.Buffer p := NewWithWriter(&buf) p.Configure("never", false) p.isTTY = true - p.SetVerbosity("debug") + p.SetDebug(true) if p.IsInteractiveUI() { - t.Fatal("expected interactive UI to be disabled on debug verbosity") + t.Fatal("expected interactive UI to be disabled when debug is enabled") } } -func TestIsInteractiveUIEnabledOnInfoVerbosity(t *testing.T) { +func TestIsInteractiveUIEnabledByDefault(t *testing.T) { var buf bytes.Buffer p := NewWithWriter(&buf) p.Configure("never", false) p.isTTY = true - p.SetVerbosity("info") + p.SetDebug(false) if !p.IsInteractiveUI() { - t.Fatal("expected interactive UI to be enabled on info verbosity") + t.Fatal("expected interactive UI to be enabled in default (non-debug) mode") } } diff --git a/cli/internal/utils/java/runner.go b/cli/internal/utils/java/runner.go index 904456b5..1641e301 100644 --- a/cli/internal/utils/java/runner.go +++ b/cli/internal/utils/java/runner.go @@ -30,7 +30,6 @@ type JavaRunner interface { WithImageType(imageType AdoptiumImageType) JavaRunner WithSkipVerify(skipVerify bool) JavaRunner WithDebugOutput(writer DebugLineWriter) JavaRunner - WithStreamOutput(stream bool) JavaRunner GetJavaResolutions() []JavaResolution // EnsureJava resolves and downloads Java if needed, returning the path. // Call this before wrapping ExecuteJavaCommand in a spinner to avoid @@ -48,7 +47,6 @@ type javaRunner struct { specificStrategy *int imageType AdoptiumImageType skipVerify bool - streamOutput bool resolvedJavaPath string debugOutput DebugLineWriter } @@ -204,7 +202,7 @@ func (j *javaRunner) executeWithJava(javaPath string, strategy ResolutionStrateg return fmt.Errorf("failed to start Java command: %w", err) } - streamToTerminal := shouldStreamJavaOutput(globals.Config.Log.Verbosity, j.streamOutput) + streamToTerminal := globals.Config.Output.Debug // Function to read from a reader and log each line logOutput := func(pipe io.Reader) { @@ -248,11 +246,6 @@ func (j *javaRunner) executeWithJava(javaPath string, strategy ResolutionStrateg return fmt.Errorf("java command failed") } -func shouldStreamJavaOutput(verbosity string, forceStream bool) bool { - level := strings.ToLower(strings.TrimSpace(verbosity)) - return level == "debug" || forceStream -} - func (j *javaRunner) TrySpecificVersion(version int) JavaRunner { j.specificStrategy = &version return j @@ -278,11 +271,6 @@ func (j *javaRunner) WithDebugOutput(writer DebugLineWriter) JavaRunner { return j } -func (j *javaRunner) WithStreamOutput(stream bool) JavaRunner { - j.streamOutput = stream - return j -} - // unsetJavaEnvironmentVariables unsets Java-related environment variables // to ensure a clean environment when using specific Java versions func unsetJavaEnvironmentVariables() { diff --git a/cli/internal/utils/java/runner_test.go b/cli/internal/utils/java/runner_test.go index fce4e411..0da44e4c 100644 --- a/cli/internal/utils/java/runner_test.go +++ b/cli/internal/utils/java/runner_test.go @@ -390,39 +390,3 @@ func TestEnvironmentVariableList(t *testing.T) { _ = os.Unsetenv("NON_JAVA") } -func TestShouldStreamJavaOutput(t *testing.T) { - tests := []struct { - verbosity string - want bool - }{ - {verbosity: "debug", want: true}, - {verbosity: "DEBUG", want: true}, - {verbosity: "info", want: false}, - {verbosity: "warn", want: false}, - {verbosity: "", want: false}, - } - - for _, tt := range tests { - if got := shouldStreamJavaOutput(tt.verbosity, false); got != tt.want { - t.Fatalf("shouldStreamJavaOutput(%q, false) = %t, want %t", tt.verbosity, got, tt.want) - } - } -} - -func TestShouldStreamJavaOutput_ForceStream(t *testing.T) { - tests := []struct { - verbosity string - want bool - }{ - {verbosity: "debug", want: true}, - {verbosity: "info", want: true}, - {verbosity: "warn", want: true}, - {verbosity: "", want: true}, - } - - for _, tt := range tests { - if got := shouldStreamJavaOutput(tt.verbosity, true); got != tt.want { - t.Fatalf("shouldStreamJavaOutput(%q, true) = %t, want %t", tt.verbosity, got, tt.want) - } - } -} From 3673b819a64b6c51ea4b7b1a11eafb5f5fb1c490 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Fri, 17 Apr 2026 03:41:50 +0300 Subject: [PATCH 3/8] ci: switch opentaint invocations from --verbosity debug to --debug Updates the CLI Dockerfile bootstrap and ci-cli.yaml runs to use the new --debug flag. JAR-direct workflows (analyzer-owasp, autobuilder, rules) keep --verbosity debug because that flag belongs to the Java tool, not to opentaint. --- .github/workflows/ci-cli.yaml | 10 +++++----- cli/Dockerfile | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-cli.yaml b/.github/workflows/ci-cli.yaml index 01e4d97d..8e6324ce 100644 --- a/.github/workflows/ci-cli.yaml +++ b/.github/workflows/ci-cli.yaml @@ -105,7 +105,7 @@ jobs: - name: Run opentaint scan in native environment when user can build project but has no java 17+ working-directory: cli run: | - ./opentaint scan ${{ steps.github-token.outputs.arg }} --output report.sarif ../project-root-java-11 --verbosity debug + ./opentaint scan --debug ${{ steps.github-token.outputs.arg }} --output report.sarif ../project-root-java-11 run-on-petclinic: runs-on: ubuntu-latest @@ -140,8 +140,8 @@ jobs: - name: Run opentaint separate compile and scan working-directory: cli run: | - ./opentaint compile --quiet ${{ steps.github-token.outputs.arg }} --output portable-project ../project-root --verbosity debug - ./opentaint scan --quiet ${{ steps.github-token.outputs.arg }} --output report.sarif --project-model portable-project --verbosity debug + ./opentaint compile --quiet --debug ${{ steps.github-token.outputs.arg }} --output portable-project ../project-root + ./opentaint scan --quiet --debug ${{ steps.github-token.outputs.arg }} --output report.sarif --project-model portable-project - name: Run opentaint scan with explicit path and output working-directory: cli @@ -265,8 +265,8 @@ jobs: - name: Run opentaint separate compile and scan working-directory: cli run: | - ./opentaint compile --quiet ${{ steps.github-token.outputs.arg }} --output portable-project ../project-root --verbosity debug - ./opentaint scan --quiet ${{ steps.github-token.outputs.arg }} --output report.sarif --project-model portable-project --verbosity debug + ./opentaint compile --quiet --debug ${{ steps.github-token.outputs.arg }} --output portable-project ../project-root + ./opentaint scan --quiet --debug ${{ steps.github-token.outputs.arg }} --output report.sarif --project-model portable-project - name: Run opentaint scan working-directory: cli diff --git a/cli/Dockerfile b/cli/Dockerfile index cb4904d0..4671a942 100644 --- a/cli/Dockerfile +++ b/cli/Dockerfile @@ -23,6 +23,6 @@ COPY --from=builder /app/opentaint /usr/local/lib/opentaint/opentaint RUN ln -sf /usr/local/lib/opentaint/opentaint /usr/local/bin/opentaint RUN --mount=type=secret,id=github_token \ - opentaint pull --github-token=$(cat /run/secrets/github_token) --verbosity debug + opentaint pull --github-token=$(cat /run/secrets/github_token) --debug CMD ["opentaint", "--help"] From 8b8386149276dddf186aa6be8a028516be01555c Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Fri, 17 Apr 2026 03:45:15 +0300 Subject: [PATCH 4/8] docs: rename --verbosity to --debug, move log.* settings under output.* Updates user-facing docs (usage, configuration, troubleshooting, docker, README) for the new flag name, the new output.* config namespace, and the new env vars. Adds a note that the per-run log file always captures full JAR output regardless of the new flags. --- docs/README.md | 8 ++++---- docs/configuration.md | 27 +++++++++++++++------------ docs/docker.md | 2 +- docs/troubleshooting.md | 2 +- docs/usage.md | 4 ++-- 5 files changed, 23 insertions(+), 20 deletions(-) diff --git a/docs/README.md b/docs/README.md index 43c9f798..4b95648f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -146,9 +146,9 @@ For detailed usage, see [Usage Guide](usage.md). scan: timeout: 15m max_memory: 16G -log: - verbosity: info # info, debug - color: auto # auto, always, never +output: + debug: false # true to enable debug output + color: auto # auto, always, never ``` Or use environment variables: `OPENTAINT_SCAN_TIMEOUT=30m`, `OPENTAINT_SCAN_MAX_MEMORY=16G` @@ -173,7 +173,7 @@ For detailed configuration, see [Configuration Guide](configuration.md). | Timeout | Use `--timeout 20m` | | Re-download deps | `opentaint prune --yes && opentaint pull` | | Stale cached model | Use `--recompile` to force recompilation | -| Debug | Use `--verbosity debug` | +| Debug | Use `--debug` | For detailed troubleshooting, see [Troubleshooting Guide](troubleshooting.md). diff --git a/docs/configuration.md b/docs/configuration.md index 6c94fda6..09381d8b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -18,17 +18,15 @@ scan: timeout: 15m max_memory: 16G -# Logging -log: - verbosity: info # info, debug - color: auto # auto, always, never +# Output (terminal-side controls) +output: + debug: false # true streams JAR output to stderr and shows debug-only fields + color: auto # auto, always, never + quiet: false # suppress spinners, progress bars, JAR streaming # Java runtime settings java: version: 23 - -# Suppress interactive output -quiet: false ``` ### Available Options @@ -37,10 +35,14 @@ quiet: false |---------|-------------|---------| | `scan.timeout` | Analysis timeout duration | `15m` | | `scan.max_memory` | Maximum memory for analyzer (e.g., `8G`, `1024m`) | `8G` | -| `log.verbosity` | Verbosity level: `info`, `debug` | `info` | -| `log.color` | Color mode: `auto`, `always`, `never` | `auto` | +| `output.debug` | Enable debug output (stream JAR subprocess output, show debug fields) | `false` | +| `output.color` | Color mode: `auto`, `always`, `never` | `auto` | +| `output.quiet` | Suppress interactive console output (spinners, progress bars, JAR streaming) | `false` | | `java.version` | Java version for running the analyzer | `23` | -| `quiet` | Suppress interactive console output | `false` | + +The per-run log file (`~/.opentaint/logs//.log`) always +captures full JAR subprocess output regardless of these flags. They control +only what is shown on the terminal. ## Environment Variables @@ -49,8 +51,9 @@ All configuration options can also be set via environment variables with the `OP ```bash export OPENTAINT_SCAN_TIMEOUT=30m export OPENTAINT_SCAN_MAX_MEMORY=16G -export OPENTAINT_LOG_VERBOSITY=debug -export OPENTAINT_LOG_COLOR=always +export OPENTAINT_OUTPUT_DEBUG=true +export OPENTAINT_OUTPUT_COLOR=always +export OPENTAINT_OUTPUT_QUIET=false export OPENTAINT_JAVA_VERSION=23 opentaint scan /path/to/project diff --git a/docs/docker.md b/docs/docker.md index 8916118e..e7889e7f 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -208,7 +208,7 @@ docker run --rm \ -v /path/to/your/project:/project \ -v /path/to/output:/output \ ghcr.io/seqra/opentaint:latest \ - opentaint scan --verbosity debug --output /output/results.sarif /project + opentaint scan --debug --output /output/results.sarif /project ``` ### View Available Commands diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index f02f9347..613097a7 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -114,7 +114,7 @@ opentaint prune ### Enable Verbose Logging ```bash -opentaint scan --verbosity debug /path/to/project +opentaint scan --debug /path/to/project ``` ### Common Log Locations diff --git a/docs/usage.md b/docs/usage.md index 91934747..abf74f2f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -165,8 +165,8 @@ These options apply to all commands: - `--config string` — Path to configuration file - `--java-version int` — Java version for analyzer (default: 21) -- `--quiet` — Suppress interactive output -- `--verbosity string` — Verbosity level (`info`, `debug`) +- `--quiet` / `-q` — Suppress interactive output (spinners, progress bars, JAR streaming) +- `--debug` / `-d` — Enable debug output (stream JAR subprocess output, show debug fields) - `--color string` — Color mode (`auto`, `always`, `never`); defaults to `auto` (detects terminal) For persistent configuration using files or environment variables, see the [Configuration](configuration.md) documentation. From 7c573dacc7e6578044bcfdd69abe2279c8172103 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Fri, 17 Apr 2026 03:53:12 +0300 Subject: [PATCH 5/8] chore: clean up review nits from verbosity rename Two cosmetic fixes flagged in the final code review: - gofmt: trailing blank line in runner_test.go after deleting the shouldStreamJavaOutput tests - stale comments in set_up_logs_test.go that still mentioned the old verbosity argument SetUpLogs no longer takes --- cli/internal/utils/java/runner_test.go | 1 - cli/internal/utils/log/set_up_logs_test.go | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cli/internal/utils/java/runner_test.go b/cli/internal/utils/java/runner_test.go index 0da44e4c..a04b19cd 100644 --- a/cli/internal/utils/java/runner_test.go +++ b/cli/internal/utils/java/runner_test.go @@ -389,4 +389,3 @@ func TestEnvironmentVariableList(t *testing.T) { } _ = os.Unsetenv("NON_JAVA") } - diff --git a/cli/internal/utils/log/set_up_logs_test.go b/cli/internal/utils/log/set_up_logs_test.go index 18a97bae..b3f605ec 100644 --- a/cli/internal/utils/log/set_up_logs_test.go +++ b/cli/internal/utils/log/set_up_logs_test.go @@ -9,8 +9,8 @@ import ( ) // SetUpLogs must wire the file hook so that Debug-level entries are -// captured no matter what verbosity argument is passed in. JAR output is -// emitted at Debug; it must land in the log file even in default mode. +// always captured. JAR output is emitted at Debug; it must land in the +// log file regardless of any user-facing console mode. func TestSetUpLogsFileHookAlwaysCapturesDebug(t *testing.T) { var buf bytes.Buffer LogWriter().Swap(&buf) @@ -27,7 +27,7 @@ func TestSetUpLogsFileHookAlwaysCapturesDebug(t *testing.T) { got := buf.String() if !strings.Contains(got, "captured-debug-line") { - t.Fatalf("expected debug line in log file output even with info verbosity, got %q", got) + t.Fatalf("expected debug line in log file output in default mode, got %q", got) } } From 73cae7e8f688e0ac9c7be7e5030966fe9e16ef80 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Fri, 17 Apr 2026 11:07:18 +0300 Subject: [PATCH 6/8] test(cmd): add table-driven coverage for appendVerbosityFlag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pins the opentaint→JAR translation contract (debug bool ↔ --verbosity=info|debug) so an accidental branch flip during a future refactor wouldn't silently invert what the analyzer subprocess sees. Seeds the first test under cli/cmd/. --- cli/cmd/command_builder_test.go | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 cli/cmd/command_builder_test.go diff --git a/cli/cmd/command_builder_test.go b/cli/cmd/command_builder_test.go new file mode 100644 index 00000000..9abbf617 --- /dev/null +++ b/cli/cmd/command_builder_test.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "reflect" + "testing" +) + +func TestAppendVerbosityFlag(t *testing.T) { + tests := []struct { + name string + debug bool + want []string + }{ + {name: "debug off emits info", debug: false, want: []string{"--verbosity=info"}}, + {name: "debug on emits debug", debug: true, want: []string{"--verbosity=debug"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := &BaseCommandBuilder{debug: tt.debug} + got := b.appendVerbosityFlag(nil) + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("appendVerbosityFlag(debug=%t) = %v, want %v", tt.debug, got, tt.want) + } + }) + } +} + +func TestAppendVerbosityFlagPreservesExistingFlags(t *testing.T) { + b := &BaseCommandBuilder{debug: true} + got := b.appendVerbosityFlag([]string{"--project", "foo.yaml"}) + want := []string{"--project", "foo.yaml", "--verbosity=debug"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("appendVerbosityFlag did not preserve prior flags: got %v, want %v", got, want) + } +} From d3f627337b4d3a15021b121a5881ee46423e2b53 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Fri, 17 Apr 2026 11:09:38 +0300 Subject: [PATCH 7/8] feat(cli): warn on legacy log.* / quiet config keys and env vars Viper accepts unknown YAML keys silently, which would let an old ~/.opentaint/config.yaml drift quietly after the verbosity rename; likewise OPENTAINT_LOG_VERBOSITY, OPENTAINT_LOG_COLOR, and OPENTAINT_QUIET env vars would stop applying with no signal. Emit a one-line warning at startup pointing the user at the new output.* namespace, for both sources. Extends the optional Task 8 from the plan by also covering env vars (not just YAML) since the drift concern is equivalent on both paths. Includes table-driven tests for the hasNestedKey helper. --- cli/cmd/command_builder_test.go | 29 ++++++++++++++++++++++ cli/cmd/root.go | 43 +++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/cli/cmd/command_builder_test.go b/cli/cmd/command_builder_test.go index 9abbf617..a3400f4d 100644 --- a/cli/cmd/command_builder_test.go +++ b/cli/cmd/command_builder_test.go @@ -5,6 +5,35 @@ import ( "testing" ) +func TestHasNestedKey(t *testing.T) { + settings := map[string]any{ + "log": map[string]any{ + "verbosity": "debug", + "color": "auto", + }, + "quiet": true, + } + tests := []struct { + name string + path []string + want bool + }{ + {name: "top-level key present", path: []string{"quiet"}, want: true}, + {name: "nested key present", path: []string{"log", "verbosity"}, want: true}, + {name: "nested key missing", path: []string{"log", "missing"}, want: false}, + {name: "parent exists but not a map", path: []string{"quiet", "sub"}, want: false}, + {name: "empty path", path: []string{}, want: false}, + {name: "missing top-level", path: []string{"other"}, want: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := hasNestedKey(settings, tt.path); got != tt.want { + t.Fatalf("hasNestedKey(%v) = %t, want %t", tt.path, got, tt.want) + } + }) + } +} + func TestAppendVerbosityFlag(t *testing.T) { tests := []struct { name string diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 2bc7749b..e3469167 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -42,6 +42,8 @@ var rootCmd = &cobra.Command{ out.Configure(globals.Config.Output.Color, globals.Config.Output.Quiet) out.SetDebug(globals.Config.Output.Debug) + warnLegacyConfigKeys() + // Reconcile install-tier version marker if needed (lightweight: a few Stat calls). utils.ReconcileInstallMarker() @@ -147,6 +149,47 @@ func initConfig() { _ = viper.Unmarshal(&globals.Config) } +// warnLegacyConfigKeys emits a one-line warning for each legacy config key or +// env var that was renamed in the verbosity-options redesign. Viper accepts +// unknown YAML keys silently and env vars only take effect through bindings, +// so without this nudge a user whose ~/.opentaint/config.yaml still contains +// the old log.* block or top-level quiet — or whose shell still exports the +// old OPENTAINT_LOG_* / OPENTAINT_QUIET env vars — would see no warning at +// all and their settings would drift silently. +func warnLegacyConfigKeys() { + settings := viper.AllSettings() + for _, key := range []string{"log.verbosity", "log.color", "quiet"} { + if hasNestedKey(settings, strings.Split(key, ".")) { + out.Warnf("Config key %q is no longer recognized. Use the output.* namespace instead (output.debug, output.color, output.quiet).", key) + } + } + for _, env := range []string{"OPENTAINT_LOG_VERBOSITY", "OPENTAINT_LOG_COLOR", "OPENTAINT_QUIET"} { + if os.Getenv(env) != "" { + out.Warnf("Environment variable %s is no longer recognized. Use OPENTAINT_OUTPUT_DEBUG / OPENTAINT_OUTPUT_COLOR / OPENTAINT_OUTPUT_QUIET instead.", env) + } + } +} + +// hasNestedKey reports whether a dotted key path is present in a viper settings map. +// Each path segment must resolve to a non-nil value; intermediate segments must be maps. +func hasNestedKey(m map[string]any, parts []string) bool { + if len(parts) == 0 { + return false + } + v, ok := m[parts[0]] + if !ok { + return false + } + if len(parts) == 1 { + return true + } + nested, ok := v.(map[string]any) + if !ok { + return false + } + return hasNestedKey(nested, parts[1:]) +} + // addConfigFields appends config fields to a SectionBuilder if PrintConfig annotation is set. func addConfigFields(cmd *cobra.Command, sb *output.SectionBuilder) { if cmd.Annotations != nil && cmd.Annotations["PrintConfig"] == "true" { From c29f4ca40eb4823385938955c214fe1f1db38904 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Fri, 17 Apr 2026 18:33:56 +0300 Subject: [PATCH 8/8] chore(cli): Remove legacy config key warning --- cli/cmd/root.go | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/cli/cmd/root.go b/cli/cmd/root.go index e3469167..fea11a5a 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -42,8 +42,6 @@ var rootCmd = &cobra.Command{ out.Configure(globals.Config.Output.Color, globals.Config.Output.Quiet) out.SetDebug(globals.Config.Output.Debug) - warnLegacyConfigKeys() - // Reconcile install-tier version marker if needed (lightweight: a few Stat calls). utils.ReconcileInstallMarker() @@ -149,27 +147,6 @@ func initConfig() { _ = viper.Unmarshal(&globals.Config) } -// warnLegacyConfigKeys emits a one-line warning for each legacy config key or -// env var that was renamed in the verbosity-options redesign. Viper accepts -// unknown YAML keys silently and env vars only take effect through bindings, -// so without this nudge a user whose ~/.opentaint/config.yaml still contains -// the old log.* block or top-level quiet — or whose shell still exports the -// old OPENTAINT_LOG_* / OPENTAINT_QUIET env vars — would see no warning at -// all and their settings would drift silently. -func warnLegacyConfigKeys() { - settings := viper.AllSettings() - for _, key := range []string{"log.verbosity", "log.color", "quiet"} { - if hasNestedKey(settings, strings.Split(key, ".")) { - out.Warnf("Config key %q is no longer recognized. Use the output.* namespace instead (output.debug, output.color, output.quiet).", key) - } - } - for _, env := range []string{"OPENTAINT_LOG_VERBOSITY", "OPENTAINT_LOG_COLOR", "OPENTAINT_QUIET"} { - if os.Getenv(env) != "" { - out.Warnf("Environment variable %s is no longer recognized. Use OPENTAINT_OUTPUT_DEBUG / OPENTAINT_OUTPUT_COLOR / OPENTAINT_OUTPUT_QUIET instead.", env) - } - } -} - // hasNestedKey reports whether a dotted key path is present in a viper settings map. // Each path segment must resolve to a non-nil value; intermediate segments must be maps. func hasNestedKey(m map[string]any, parts []string) bool {