diff --git a/.github/workflows/ci-cli.yaml b/.github/workflows/ci-cli.yaml index 01e4d97d7..8e6324ce1 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 cb4904d06..4671a942b 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"] diff --git a/cli/cmd/command_builder.go b/cli/cmd/command_builder.go index bbb250b94..772957de1 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/command_builder_test.go b/cli/cmd/command_builder_test.go new file mode 100644 index 000000000..a3400f4dc --- /dev/null +++ b/cli/cmd/command_builder_test.go @@ -0,0 +1,64 @@ +package cmd + +import ( + "reflect" + "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 + 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) + } +} diff --git a/cli/cmd/compile.go b/cli/cmd/compile.go index 25e8fd914..4dc97fbaa 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 9cb45582d..216ad36d5 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 aa63dc34a..fea11a5a8 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(globals.Config.Log.Verbosity); err != nil { + 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,31 @@ 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 +// 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" { - 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 f6e8b6f4c..0be614740 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 d14d2ace3..8af14008f 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 48f5f82ef..e3acf4a56 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 34f64c65d..687dd7c3e 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 ade6f074d..da0277c53 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 904456b52..1641e301b 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 fce4e411a..a04b19cde 100644 --- a/cli/internal/utils/java/runner_test.go +++ b/cli/internal/utils/java/runner_test.go @@ -389,40 +389,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) - } - } -} diff --git a/cli/internal/utils/log/set_up_logs.go b/cli/internal/utils/log/set_up_logs.go index ccfe2159b..ef2e03e19 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 000000000..b3f605ec4 --- /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 +// 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) + 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 in default mode, got %q", got) + } +} + +type nopWriter struct{} + +func (nopWriter) Write(p []byte) (int, error) { return len(p), nil } diff --git a/docs/README.md b/docs/README.md index 43c9f7989..4b95648fb 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 6c94fda61..09381d8be 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 8916118ec..e7889e7f7 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 f02f93476..613097a7f 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 919347479..abf74f2f7 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.