Skip to content

Centralize lets log prefix and colorize debug output#319

Open
kindermax wants to merge 1 commit intomasterfrom
codex/update-logging-prefix-and-color
Open

Centralize lets log prefix and colorize debug output#319
kindermax wants to merge 1 commit intomasterfrom
codex/update-logging-prefix-and-color

Conversation

@kindermax
Copy link
Collaborator

@kindermax kindermax commented Mar 23, 2026

Summary

  • move all lets: prefixes into the shared formatter and adjust call sites/formatter tests accordingly
  • ensure debug messages are rendered in blue while keeping exec logger prefixes colorized without redundant lets:
  • update changelog to note the logging changes

Testing

  • Not run (not requested)

Summary by Sourcery

Centralize the lets: log prefix in the shared log formatter, simplify call sites and exec logger prefixes, and ensure debug output is consistently colored while updating documentation accordingly.

Enhancements:

  • Route all log messages through a formatter that injects a unified lets: prefix and applies blue coloring to debug-level messages.
  • Adjust the ExecLogger to rely on the global formatter for the lets: prefix while keeping per-command prefixes colorized without duplication.
  • Clean up log message strings across CLI, config, and upgrade code to remove hard-coded lets: prefixes in favor of the centralized formatter.

Documentation:

  • Document the logging behavior change and debug message coloring in the changelog.

Tests:

  • Extend logging tests to cover the centralized prefix behavior and verify that debug messages are rendered in blue.

@sourcery-ai
Copy link

sourcery-ai bot commented Mar 23, 2026

Reviewer's Guide

Centralizes the lets: log prefix in the shared logging formatter, updates tests and exec logging to rely on the formatter for prefixing, and ensures debug messages (including formatter output) are colorized in blue while cleaning up call sites and messages that previously hard-coded lets:.

Sequence diagram for centralized lets prefix and debug coloring

sequenceDiagram
    participant Caller as Caller
    participant Logrus as Logrus_Logger
    participant Formatter as Formatter
    participant Color as FatihColor

    Caller->>Logrus: Debugf("message")
    activate Logrus
    Logrus->>Formatter: Format(entry level=Debug message="message")
    activate Formatter
    Formatter->>Color: BlueString("lets:")
    Color-->>Formatter: blueLetsPrefix
    Formatter->>Formatter: writeData(entry.Data)
    Formatter->>Formatter: formatMessage(entry)
    Formatter->>Color: BlueString("message")
    Color-->>Formatter: blueMessage
    Formatter-->>Logrus: "blueLetsPrefix [data] blueMessage\n"
    deactivate Formatter
    Logrus-->>Caller: write to output
    deactivate Logrus

    Caller->>Logrus: Infof("message")
    activate Logrus
    Logrus->>Formatter: Format(entry level=Info message="message")
    activate Formatter
    Formatter->>Color: BlueString("lets:")
    Color-->>Formatter: blueLetsPrefix
    Formatter->>Formatter: writeData(entry.Data)
    Formatter->>Formatter: formatMessage(entry)
    Formatter-->>Logrus: "blueLetsPrefix [data] message\n"
    deactivate Formatter
    Logrus-->>Caller: write to output
    deactivate Logrus
Loading

Sequence diagram for ExecLogger using shared formatter prefix

sequenceDiagram
    participant Caller as ExecutorCode
    participant Exec as ExecLogger
    participant Logrus as Logrus_Logger
    participant Formatter as Formatter

    Caller->>Exec: Child("build")
    activate Exec
    Exec->>Exec: create child with prefix="[build]"
    Exec-->>Caller: childExec
    deactivate Exec

    Caller->>Exec: Info("running %s", cmd)
    activate Exec
    Exec->>Exec: if prefix != "" then prepend prefix
    Exec->>Logrus: Logf(Info, "[build] running %s", cmd)
    deactivate Exec
    activate Logrus
    Logrus->>Formatter: Format(entry message="[build] running make")
    activate Formatter
    Formatter->>Formatter: writeData(entry.Data)
    Formatter->>Formatter: formatMessage(entry level=Info)
    Formatter-->>Logrus: "lets: [build] running make\n"
    deactivate Formatter
    Logrus-->>Caller: write to output
    deactivate Logrus
Loading

Updated class diagram for logging formatter and ExecLogger

classDiagram
    class Formatter {
        +Format(entry *log.Entry) []byte
        +formatMessage(entry *log.Entry) string
        +writeData(fields log.Fields) string
    }

    class ExecLogger {
        -log *log.Logger
        -name string
        -prefix string
        -cache map[string]*ExecLogger
        +NewExecLogger() *ExecLogger
        +Child(name string) *ExecLogger
        +Info(format string, a ...any)
        +Debug(format string, a ...any)
    }

    class Logrus_Logger {
        +Logf(level log.Level, format string, a ...any)
        +StandardLogger() *Logrus_Logger
    }

    class Color {
        +BlueString(format string, a ...any) string
        +GreenString(format string, a ...any) string
        +YellowString(format string, a ...any) string
        +RedString(format string, a ...any) string
    }

    ExecLogger --> Logrus_Logger : uses
    Formatter --> Color : uses
    Logrus_Logger --> Formatter : uses as Formatter
    ExecLogger --> ExecLogger : cache children by name
Loading

File-Level Changes

Change Details Files
Centralize the lets: prefix in the logging formatter and colorize debug messages
  • Update Formatter.Format to always prepend a blue lets: prefix, optionally include structured fields, and delegate message styling to a new helper
  • Introduce formatMessage to render debug-level messages in blue while keeping other levels unstyled
  • Simplify writeData so it just joins key=value pairs without adding a trailing space token
internal/logging/formatter.go
Align logging tests with the new formatter behavior and add coverage for colorized debug output
  • Adjust TestLoggingToStd expectations so both stdout and stderr now include the lets: prefix produced by the formatter
  • Use color.NoColor toggling in tests to make formatter output deterministic
  • Add TestFormatterColorsDebugMessages to verify that debug messages and the prefix are rendered in blue with the new formatter
internal/logging/log_test.go
Update ExecLogger to rely on centralized prefixing and avoid redundant lets: decorations
  • Remove lets: from ExecLogger comments and prefixes so only command-chain prefixes (e.g. [foo=>bar]) remain
  • Stop colorizing the ExecLogger format string itself and instead prepend the prefix only when present
  • Ensure Info and Debug methods no longer hard-code blue coloring, deferring styling to the global formatter
internal/logging/log.go
Remove hard-coded lets: prefixes from CLI, config, and upgrade log messages to avoid duplication
  • Strip lets: from various log messages in CLI startup and error paths, including traversal, flags, config, and signal handling
  • Simplify update-check debug messages by removing the inline lets: prefix
  • Update config discovery and env_file error messages to avoid embedding the prefix directly in formatted strings
  • Adjust update notifier messages so the human-facing text no longer includes a baked-in lets: label
internal/cli/cli.go
internal/upgrade/notifier.go
internal/config/find.go
internal/config/config/command.go
Document the logging behavior change in the changelog
  • Add a changelog entry describing the centralized lets: prefix and blue debug rendering
docs/docs/changelog.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The new TestFormatterColorsDebugMessages mutates the global color.NoColor flag and could race with other tests that rely on fatih/color; consider scoping this via a helper that serializes such tests or avoiding global mutation altogether.
  • In formatMessage, only log.DebugLevel messages are colorized; if you intend consistent treatment for other verbose levels (e.g., TraceLevel), consider broadening the condition or documenting that only debug is special-cased.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `TestFormatterColorsDebugMessages` mutates the global `color.NoColor` flag and could race with other tests that rely on `fatih/color`; consider scoping this via a helper that serializes such tests or avoiding global mutation altogether.
- In `formatMessage`, only `log.DebugLevel` messages are colorized; if you intend consistent treatment for other verbose levels (e.g., `TraceLevel`), consider broadening the condition or documenting that only debug is special-cased.

## Individual Comments

### Comment 1
<location path="internal/logging/log_test.go" line_range="42-51" />
<code_context>
 	})
 }
+
+func TestFormatterColorsDebugMessages(t *testing.T) {
+	prevNoColor := color.NoColor
+	color.NoColor = false
+	defer func() {
+		color.NoColor = prevNoColor
+	}()
+
+	line, err := (&Formatter{}).Format(&log.Entry{
+		Level:   log.DebugLevel,
+		Message: "debug message",
+	})
+	if err != nil {
+		t.Fatalf("Format() error = %v", err)
+	}
+
+	expected := color.BlueString("lets:") + " " + color.BlueString("debug message") + "\n"
+	if string(line) != expected {
+		t.Fatalf("unexpected debug line: %q", string(line))
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests to cover non-debug levels and messages with structured fields to fully exercise the new formatter behavior.

Since we only test the debug case without structured fields, please add table-driven tests that cover: (1) info/warn/error levels (non-colorized message), (2) entries with `Data` set (one or more fields) to verify spacing and ordering around `lets:`, fields, and the message, and (3) an empty `Data` case to ensure no extra spaces. This will better validate the new centralized prefixing/spacing logic.

Suggested implementation:

```golang
func TestFormatterColorsDebugMessages(t *testing.T) {
	prevNoColor := color.NoColor
	color.NoColor = false
	defer func() {
		color.NoColor = prevNoColor
	}()

	line, err := (&Formatter{}).Format(&log.Entry{
		Level:   log.DebugLevel,
		Message: "debug message",
	})
	if err != nil {
		t.Fatalf("Format() error = %v", err)
	}

	expected := color.BlueString("lets:") + " " + color.BlueString("debug message") + "\n"
	if string(line) != expected {
		t.Fatalf("unexpected debug line: %q", string(line))
	}
}

func TestFormatterFormatsLevelsAndFields(t *testing.T) {
	tests := []struct {
		name  string
		entry *log.Entry
	}{
		{
			name: "info_no_data",
			entry: &log.Entry{
				Level:   log.InfoLevel,
				Message: "info message",
				Data:    log.Fields{},
			},
		},
		{
			name: "warn_with_single_field",
			entry: &log.Entry{
				Level:   log.WarnLevel,
				Message: "warn message",
				Data: log.Fields{
					"foo": "bar",
				},
			},
		},
		{
			name: "error_with_multiple_fields",
			entry: &log.Entry{
				Level:   log.ErrorLevel,
				Message: "error message",
				Data: log.Fields{
					"alpha": "one",
					"beta":  "two",
				},
			},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			lineBytes, err := (&Formatter{}).Format(tt.entry)
			if err != nil {
				t.Fatalf("Format() error = %v", err)
			}
			line := string(lineBytes)

			// All levels should be prefixed consistently.
			if !strings.HasPrefix(line, "lets: ") {
				t.Fatalf("line does not start with expected prefix: %q", line)
			}

			// Non-debug messages should not be colorized by the formatter under default settings.
			if strings.Contains(line, "\x1b[") {
				t.Fatalf("expected non-colorized output for non-debug levels, got: %q", line)
			}

			// Empty Data: ensure there are no extra spaces (only one space after the prefix).
			if len(tt.entry.Data) == 0 {
				expected := "lets: " + tt.entry.Message + "\n"
				if line != expected {
					t.Fatalf("unexpected formatted line for empty Data.\nexpected: %q\ngot:      %q", expected, line)
				}
				return
			}

			// With Data: verify ordering and spacing around prefix, fields, and message.
			prefixIdx := strings.Index(line, "lets:")
			if prefixIdx != 0 {
				t.Fatalf("prefix not at beginning of line: %q", line)
			}

			msgIdx := strings.LastIndex(line, tt.entry.Message)
			if msgIdx == -1 {
				t.Fatalf("message %q not found in line: %q", tt.entry.Message, line)
			}

			if msgIdx <= prefixIdx {
				t.Fatalf("message appears before prefix in line: %q", line)
			}

			// Ensure no double spaces in the formatted output.
			if strings.Contains(line, "  ") {
				t.Fatalf("unexpected double spaces in line: %q", line)
			}

			// Each field should appear between the prefix and the message.
			for key, rawVal := range tt.entry.Data {
				val, ok := rawVal.(string)
				if !ok {
					t.Fatalf("test setup error: expected string value for key %q, got %T", key, rawVal)
				}
				fieldStr := key + "=" + val

				fieldIdx := strings.Index(line, fieldStr)
				if fieldIdx == -1 {
					t.Fatalf("field %q not found in line: %q", fieldStr, line)
				}
				if !(fieldIdx > prefixIdx && fieldIdx < msgIdx) {
					t.Fatalf("field %q not positioned between prefix and message in line: %q", fieldStr, line)
				}
			}
		})
	}
}

```

To compile these tests, update the imports in `internal/logging/log_test.go`:

1. Add the `strings` package to the import list:
   - `import "strings"`

Make sure no existing imports are removed, and keep the import block sorted/grouped according to your current conventions.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +42 to +51
func TestFormatterColorsDebugMessages(t *testing.T) {
prevNoColor := color.NoColor
color.NoColor = false
defer func() {
color.NoColor = prevNoColor
}()

line, err := (&Formatter{}).Format(&log.Entry{
Level: log.DebugLevel,
Message: "debug message",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add tests to cover non-debug levels and messages with structured fields to fully exercise the new formatter behavior.

Since we only test the debug case without structured fields, please add table-driven tests that cover: (1) info/warn/error levels (non-colorized message), (2) entries with Data set (one or more fields) to verify spacing and ordering around lets:, fields, and the message, and (3) an empty Data case to ensure no extra spaces. This will better validate the new centralized prefixing/spacing logic.

Suggested implementation:

func TestFormatterColorsDebugMessages(t *testing.T) {
	prevNoColor := color.NoColor
	color.NoColor = false
	defer func() {
		color.NoColor = prevNoColor
	}()

	line, err := (&Formatter{}).Format(&log.Entry{
		Level:   log.DebugLevel,
		Message: "debug message",
	})
	if err != nil {
		t.Fatalf("Format() error = %v", err)
	}

	expected := color.BlueString("lets:") + " " + color.BlueString("debug message") + "\n"
	if string(line) != expected {
		t.Fatalf("unexpected debug line: %q", string(line))
	}
}

func TestFormatterFormatsLevelsAndFields(t *testing.T) {
	tests := []struct {
		name  string
		entry *log.Entry
	}{
		{
			name: "info_no_data",
			entry: &log.Entry{
				Level:   log.InfoLevel,
				Message: "info message",
				Data:    log.Fields{},
			},
		},
		{
			name: "warn_with_single_field",
			entry: &log.Entry{
				Level:   log.WarnLevel,
				Message: "warn message",
				Data: log.Fields{
					"foo": "bar",
				},
			},
		},
		{
			name: "error_with_multiple_fields",
			entry: &log.Entry{
				Level:   log.ErrorLevel,
				Message: "error message",
				Data: log.Fields{
					"alpha": "one",
					"beta":  "two",
				},
			},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			lineBytes, err := (&Formatter{}).Format(tt.entry)
			if err != nil {
				t.Fatalf("Format() error = %v", err)
			}
			line := string(lineBytes)

			// All levels should be prefixed consistently.
			if !strings.HasPrefix(line, "lets: ") {
				t.Fatalf("line does not start with expected prefix: %q", line)
			}

			// Non-debug messages should not be colorized by the formatter under default settings.
			if strings.Contains(line, "\x1b[") {
				t.Fatalf("expected non-colorized output for non-debug levels, got: %q", line)
			}

			// Empty Data: ensure there are no extra spaces (only one space after the prefix).
			if len(tt.entry.Data) == 0 {
				expected := "lets: " + tt.entry.Message + "\n"
				if line != expected {
					t.Fatalf("unexpected formatted line for empty Data.\nexpected: %q\ngot:      %q", expected, line)
				}
				return
			}

			// With Data: verify ordering and spacing around prefix, fields, and message.
			prefixIdx := strings.Index(line, "lets:")
			if prefixIdx != 0 {
				t.Fatalf("prefix not at beginning of line: %q", line)
			}

			msgIdx := strings.LastIndex(line, tt.entry.Message)
			if msgIdx == -1 {
				t.Fatalf("message %q not found in line: %q", tt.entry.Message, line)
			}

			if msgIdx <= prefixIdx {
				t.Fatalf("message appears before prefix in line: %q", line)
			}

			// Ensure no double spaces in the formatted output.
			if strings.Contains(line, "  ") {
				t.Fatalf("unexpected double spaces in line: %q", line)
			}

			// Each field should appear between the prefix and the message.
			for key, rawVal := range tt.entry.Data {
				val, ok := rawVal.(string)
				if !ok {
					t.Fatalf("test setup error: expected string value for key %q, got %T", key, rawVal)
				}
				fieldStr := key + "=" + val

				fieldIdx := strings.Index(line, fieldStr)
				if fieldIdx == -1 {
					t.Fatalf("field %q not found in line: %q", fieldStr, line)
				}
				if !(fieldIdx > prefixIdx && fieldIdx < msgIdx) {
					t.Fatalf("field %q not positioned between prefix and message in line: %q", fieldStr, line)
				}
			}
		})
	}
}

To compile these tests, update the imports in internal/logging/log_test.go:

  1. Add the strings package to the import list:
    • import "strings"

Make sure no existing imports are removed, and keep the import block sorted/grouped according to your current conventions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant