Skip to content

Conversation

@Lach-dev
Copy link
Contributor

@Lach-dev Lach-dev commented Dec 2, 2025

Description:

Add log redaction support to the logging package ( Issue #44 )

Purpose:

  • Add configurable redaction of sensitive values in logs (string pattern rules and regex based rules) so sensitive data is not written to logs.

Changes:

  1. Implemented redaction support in logging/logging.go:
    • Added RedactionRule type, SetRedactionRules, SetRedactionRegex, and redact usage in log.
  2. Added unit tests and benchmarks in logging/logging_test.go covering:
    • Redaction by simple rules
    • Redaction by regex patterns
    • Existing level filtering tests and benchmarks
  3. Updated documentation examples in logging/EXAMPLES.md.
  4. Updated package documentation in logging/README.md to describe redaction usage.

Checklist:

  • Tests Passing: Verify by running make test.
  • Golint Passing: Confirm by running make lint.

If added a new utility function or updated existing function logic:

  • Updated the utility package EXAMPLES.md
  • Updated the utility package README.md

Summary by CodeRabbit

  • New Features

    • Added sensitive-data redaction for logs with support for prefix-based patterns and regex-based matching.
  • Documentation

    • Added README guidance and examples demonstrating how to configure and use redaction in logging.
  • Tests

    • Added tests covering redaction behavior, invalid regex handling, and a benchmark for redaction performance.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 2, 2025

Walkthrough

Adds a redaction feature to the logging package: two new public APIs for prefix and regex redaction, core redaction logic integrated into the logger, tests (including error path and benchmark), and documentation/examples demonstrating usage.

Changes

Cohort / File(s) Summary
Documentation
logging/EXAMPLES.md, logging/README.md
Adds Redaction section and examples; documents SetRedactionRules(rules map[string]string) and SetRedactionRegex(patterns map[string]string) error usage and behavior; includes example outputs.
Core implementation
logging/logging.go
Adds RedactionRule type, redactionRules storage, SetRedactionRules() and SetRedactionRegex() methods, redact(message string) string helper, and applies redaction in the logging pipeline before color handling; imports regexp.
Tests & benchmarks
logging/logging_test.go
Adds TestLoggerRedaction, TestLoggerRedactionRegex (including invalid-regex error case), and BenchmarkLoggerWithRedaction to validate behavior and measure cost.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Caller
    participant Logger
    participant Redactor
    participant Colorizer
    participant Output

    Caller->>Logger: Log(message)
    Logger->>Redactor: redact(message)  -- apply prefix & regex rules
    Redactor-->>Logger: redactedMessage
    Logger->>Colorizer: applyColors(redactedMessage)
    Colorizer-->>Logger: coloredMessage
    Logger->>Output: write(coloredMessage)
    Output-->>Caller: persisted/logged
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Check placement of redaction in log() (before color handling) and any edge cases with color codes
  • Verify SetRedactionRegex() compiles patterns and returns clear errors for invalid regex
  • Review tests for coverage of replacement correctness and the benchmark for allocation/latency implications

Possibly related PRs

  • Added logging package #85: Adds the foundational logging package that this change extends by introducing redaction APIs, logic, tests, and documentation.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Added log redaction for sensitive information' directly and clearly summarizes the main change—adding redaction functionality to the logging package to prevent sensitive data from appearing in logs.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
logging/README.md (1)

56-69: Clarify overwrite semantics and usage timing for redaction APIs (optional)

The behavioral description matches the implementation, but two subtle points might be worth calling out:

  • Each call to SetRedactionRules or SetRedactionRegex replaces the entire set of redaction rules (they are not additive).
  • These setters are intended to be called during logger setup, not while the logger is being used concurrently.

Adding a short note about “last call wins” semantics and recommended usage at initialization would help avoid surprises for callers who expect to mix rule types or update rules at runtime.

logging/logging_test.go (1)

100-163: Redaction tests look solid; consider one extra assertion in the multi-field case

The table-driven tests for SetRedactionRules cover:

  • Single-field redaction (password, credit card).
  • Multiple rules applied to one message.
  • “No rules” behavior.

In the “success - redact multiple fields” case, you already verify that the email is redacted and the raw email address is absent. Since the message also contains a password, you could optionally assert that the password portion is redacted as well to exercise all configured rules in the same log line, but this is not strictly necessary for correctness.

Otherwise, the structure and expectations look good.

logging/logging.go (2)

93-112: SetRedactionRegex behavior is good; note overwrite & ordering semantics (optional)

SetRedactionRegex correctly:

  • Clears any existing rules.
  • Compiles each pattern and returns a wrapped error on failure.
  • Stores the compiled regexp + replacement for use in redact.

Two subtle behaviors to be aware of:

  • Calling SetRedactionRegex discards any rules previously configured via SetRedactionRules (and vice versa). If callers expect to combine both kinds of rules, they must supply the full set in one call or you’ll need an “append” variant.
  • When multiple regex patterns can match overlapping substrings, their application order is determined by Go’s map iteration order, which is intentionally randomized. That’s usually fine but can be surprising for overlapping rules.

You may want to document these points, or, if deterministic ordering is important, consider taking a []RedactionRule or []struct{Pattern, Replacement string} instead of map[string]string.


114-122: redact helper is straightforward; consider moving the empty-rules check inside

redact implements a simple “apply all rules in sequence” pipeline, which matches the documented behavior.

If you adopt a mutex (as suggested above), it might be cleaner to:

  • Always call redact from log, and
  • Do the “no rules” fast-path inside redact under the read lock.

For example:

func (l *Logger) redact(message string) string {
-	redactedMessage := message
-	for _, rule := range l.redactionRules {
-		redactedMessage = rule.Pattern.ReplaceAllString(redactedMessage, rule.Replacement)
-	}
-	return redactedMessage
+	l.redactionMu.RLock()
+	defer l.redactionMu.RUnlock()
+
+	if len(l.redactionRules) == 0 {
+		return message
+	}
+
+	redactedMessage := message
+	for _, rule := range l.redactionRules {
+		redactedMessage = rule.Pattern.ReplaceAllString(redactedMessage, rule.Replacement)
+	}
+	return redactedMessage
}

Then in log you can simply do message = l.redact(message) unconditionally, which keeps the concurrency story consistent.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 63eaeb1 and 6030a6a.

📒 Files selected for processing (4)
  • logging/EXAMPLES.md (1 hunks)
  • logging/README.md (1 hunks)
  • logging/logging.go (4 hunks)
  • logging/logging_test.go (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
logging/logging_test.go (1)
logging/logging.go (2)
  • NewLogger (62-72)
  • INFO (29-29)
🔇 Additional comments (5)
logging/logging_test.go (2)

165-237: Regex-based redaction tests and invalid-pattern coverage look good

The TestLoggerRedactionRegex cases nicely exercise:

  • Typical PII shapes (email, credit-card number, phone number).
  • That the clear-text value is removed from the output.
  • Error handling when an invalid regex pattern is provided.

The early return when wantErr is true correctly prevents logging when pattern compilation fails, matching the intended contract of SetRedactionRegex.

No issues from a correctness standpoint.


251-260: Benchmark using b.Loop() is appropriate; ensure Go toolchain supports it

BenchmarkLoggerWithRedaction is structured correctly around for b.Loop() { ... } and exercises the redaction path under load, which is exactly what you want here.

Just make sure your CI and consumers are on a Go version that supports testing.B.Loop (Go 1.24+); otherwise this benchmark won’t compile.

logging/logging.go (3)

34-48: RedactionRule and Logger integration are conceptually sound

Embedding compiled regexp.Regexp in RedactionRule and attaching a []RedactionRule to Logger is a clean design. It keeps the logging hot path working with precompiled patterns, which is good for performance, and it’s consistent with the tests and docs.

No issues here.


136-139: Redaction integration point in log is correct

Hooking redaction right before formatting the log line ensures:

  • Only the message body is redacted (prefix, level, and timestamp are untouched).
  • Redaction happens before color-coding and output, matching the docs and tests.

Once concurrency around redactionRules is addressed, this integration point looks good.


74-91: Potential data race when updating redaction rules on a live logger

SetRedactionRules replaces l.redactionRules without synchronization. If log reads from l.redactionRules concurrently (a common pattern for shared loggers), this introduces a data race.

This changes the previous API surface, which exposed no mutating methods after construction, making shared loggers effectively read-only and safe from races.

Consider one of:

  • Guarding access with a sync.RWMutex
  • Using an atomic.Value or copy-on-write pattern to swap the slice safely
  • Explicitly documenting that redaction setters must only be called during initialization, before the logger is shared

Example of a minimal RWMutex-based approach:

-import (
-	"fmt"
-	"io"
-	"os"
-	"regexp"
-	"time"
-)
+import (
+	"fmt"
+	"io"
+	"os"
+	"regexp"
+	"sync"
+	"time"
+)

 type Logger struct {
-	minLevel       LogLevel        // Minimum log level for messages to be logged
-	prefix         string          // Prefix to prepend to all log messages
-	output         io.Writer       // Output destination for log messages (e.g., os.Stdout)
-	disableColors  bool            // Flag to disable color codes (useful for testing or non-ANSI terminals)
-	redactionRules []RedactionRule // Rules for redacting sensitive information
+	minLevel       LogLevel        // Minimum log level for messages to be logged
+	prefix         string          // Prefix to prepend to all log messages
+	output         io.Writer       // Output destination for log messages (e.g., os.Stdout)
+	disableColors  bool            // Flag to disable color codes (useful for testing or non-ANSI terminals)
+	redactionMu    sync.RWMutex    // Protects redactionRules
+	redactionRules []RedactionRule // Rules for redacting sensitive information
 }

 func (l *Logger) SetRedactionRules(rules map[string]string) {
-	l.redactionRules = make([]RedactionRule, 0, len(rules))
+	l.redactionMu.Lock()
+	defer l.redactionMu.Unlock()
+	l.redactionRules = make([]RedactionRule, 0, len(rules))
 	for pattern, replacement := range rules {
 		regex := regexp.MustCompile(regexp.QuoteMeta(pattern) + `[^\s]+`)
 		l.redactionRules = append(l.redactionRules, RedactionRule{
 			Pattern:     regex,
 			Replacement: pattern + replacement,
 		})
 	}
 }

Lock SetRedactionRegex similarly and acquire RLock/RUnlock in redact (removing the len(l.redactionRules) check from log, letting redact early-out when there are no rules).

@kashifkhan0771
Copy link
Owner

Please fix the build failures

@Lach-dev
Copy link
Contributor Author

Lach-dev commented Dec 3, 2025

Created #214 to fix the test, it will always fail in December.

@kashifkhan0771
Copy link
Owner

Gitleaks is reporting a leaked secret at logging/EXAMPLES.md. Please remove that or change it to something that doesn't look like a secret. Also make sure to rewrite history so that it doesn't come up in old commits.

@Lach-dev
Copy link
Contributor Author

Lach-dev commented Dec 6, 2025

Removed from history.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
time/time_test.go (1)

498-506: December overflow handling in age test looks correct

The revised month/year calculation avoids the December failure while keeping the expected age at 24 across all months. The logic is sound; only minor nit is that for December the birthday ends up in January of the next year, so the test name no longer exactly describes the scenario, but behavior is still correct for CalculateAge.

logging/EXAMPLES.md (1)

157-187: Use obvious placeholders instead of secret‑shaped values in examples

The redaction example is functionally correct, but the sample values (mysecretpass123, 1234-5678-9876-5432) look like real credentials and are likely what triggered the Gitleaks alert. To avoid noise from secret scanners and make it clear these are placeholders, consider using more neutral tokens and updating the output accordingly, e.g.:

- // Log messages with sensitive data
- logger.Info("User logged in with password:mysecretpass123")
- logger.Error("Payment failed for credit_card=1234-5678-9876-5432")
+ // Log messages with sensitive data (placeholders for demo purposes)
+ logger.Info("User logged in with password:<USER_PASSWORD>")
+ logger.Error("Payment failed for credit_card=<CARD_NUMBER>")

And:

-[2025-01-09 12:34:56] [INFO] MyApp: User logged in with password:***REDACTED***
-[2025-01-09 12:34:56] [ERROR] MyApp: Payment failed for credit_card=***REDACTED***
+[2025-01-09 12:34:56] [INFO] MyApp: User logged in with password:***REDACTED***
+[2025-01-09 12:34:56] [ERROR] MyApp: Payment failed for credit_card=***REDACTED***

(The redacted output stays the same; only the original literals change.) This should keep the example clear while satisfying secret‑scanning tools.

logging/README.md (1)

56-69: Redaction API docs align well with implementation

The new Redaction section accurately describes how SetRedactionRules treats keys as literal prefixes plus non-space tokens and how SetRedactionRegex behaves (including invalid-pattern errors). If you want to be extra explicit, you could add a short note that each call replaces any previously configured redaction rules, but as written the docs are already clear and consistent with the code.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6030a6a and 1157f3a.

📒 Files selected for processing (5)
  • logging/EXAMPLES.md (1 hunks)
  • logging/README.md (1 hunks)
  • logging/logging.go (4 hunks)
  • logging/logging_test.go (2 hunks)
  • time/time_test.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • logging/logging_test.go
🔇 Additional comments (1)
logging/logging.go (1)

34-38: Redaction integration into Logger is well-placed and consistent

The introduction of RedactionRule, the redactionRules field, and the redact helper is clean, and applying redaction in log before color formatting and output keeps the behavior predictable. Using regexp.QuoteMeta for SetRedactionRules to treat rule keys as literal prefixes while appending [^\s]+ gives a sensible default for “prefix + token” style secrets, and the replacement of only the message body (not prefix, level, or timestamp) is exactly what you want here.

Also applies to: 43-47, 74-92, 114-122, 136-139

kashifkhan0771 and others added 2 commits December 9, 2025 11:11
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Kashif Khan <70996046+kashifkhan0771@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
logging/logging.go (2)

74-91: Consider returning an error for API consistency.

While regexp.MustCompile on line 85 is safe here (since regexp.QuoteMeta escapes all special characters), this creates an inconsistent API with SetRedactionRegex which returns an error. For a library, panic-free error handling is preferable.

Apply this diff to return an error:

-func (l *Logger) SetRedactionRules(rules map[string]string) {
+func (l *Logger) SetRedactionRules(rules map[string]string) error {
 	l.redactionRules = make([]RedactionRule, 0, len(rules))
 	for pattern, replacement := range rules {
 		// Create a regex that matches the pattern followed by any non-space characters
-		regex := regexp.MustCompile(regexp.QuoteMeta(pattern) + `[^\s]+`)
+		regex, err := regexp.Compile(regexp.QuoteMeta(pattern) + `[^\s]+`)
+		if err != nil {
+			return fmt.Errorf("failed to compile pattern '%s': %w", pattern, err)
+		}
 		l.redactionRules = append(l.redactionRules, RedactionRule{
 			Pattern:     regex,
 			Replacement: pattern + replacement,
 		})
 	}
+	return nil
 }

115-123: Consider documenting the order-dependent rule application.

Rules are applied sequentially (line 119), meaning earlier rules can affect later ones. While this is likely intentional, documenting this behavior would help users understand that rule order matters and avoid unintended cascading replacements.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1157f3a and f46d7de.

📒 Files selected for processing (1)
  • logging/logging.go (4 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-11-16T00:59:45.522Z
Learnt from: andremartinsds
Repo: kashifkhan0771/utils PR: 40
File: url/url.go:76-79
Timestamp: 2024-11-16T00:59:45.522Z
Learning: In the `BuildURL` function within `url/url.go`, the current path validation regex is appropriate and should not be changed without a specific reason.

Applied to files:

  • logging/logging.go
📚 Learning: 2025-08-12T07:38:35.407Z
Learnt from: addvanced
Repo: kashifkhan0771/utils PR: 177
File: slugger/slugger.go:39-43
Timestamp: 2025-08-12T07:38:35.407Z
Learning: In the slugger package, the user addvanced mentioned they have a separate branch that refactors the substitution logic from using strings.ReplaceAll in a loop to using strings.NewReplacer, which is a more appropriate and efficient approach for handling multiple string replacements.

Applied to files:

  • logging/logging.go
🔇 Additional comments (4)
logging/logging.go (4)

10-10: LGTM!

The regexp import is necessary for the redaction functionality and uses the standard library.


34-38: LGTM!

The RedactionRule type is well-designed with appropriate field types and clear documentation.


137-140: LGTM!

The redaction is correctly applied before color coding and formatting. The length check efficiently skips the redact call when no rules are configured.


93-113: Verify concurrent access safety for redaction rule updates.

The previous partial-update concern has been resolved—the code now validates all patterns in a local slice before assigning to l.redactionRules (line 111).

However, both SetRedactionRegex and SetRedactionRules modify l.redactionRules without synchronization, while the log() method reads it. If redaction rules are updated concurrently with logging, a data race can occur.

Verify the intended usage pattern: if the logger is configured once at startup and used immutably thereafter, no fix is needed. If runtime reconfiguration concurrent with logging is expected, add mutex protection to synchronize access to l.redactionRules.

Copy link
Owner

@kashifkhan0771 kashifkhan0771 left a comment

Choose a reason for hiding this comment

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

❤️

Signed-off-by: Kashif Khan <70996046+kashifkhan0771@users.noreply.github.com>
@kashifkhan0771
Copy link
Owner

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 9, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@kashifkhan0771
Copy link
Owner

Me after pushing last commit and now with my own rules I cannot merge this
what

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
logging/logging.go (1)

74-91: Make SetRedactionRules robust to whitespace between key and value

The current pattern requires a non-whitespace character immediately after pattern. For example:

  • pattern = "password=", log "password=secret" → matches
  • pattern = "password:", log "password: secret" → does not match (space after : breaks the pattern)

Callers then need to pass "password: " (with trailing space) as the key, which is easy to miss.

Use \s* to allow optional whitespace between the pattern and the value:

- regex := regexp.MustCompile(regexp.QuoteMeta(pattern) + `[^\s]+`)
+ regex := regexp.MustCompile(regexp.QuoteMeta(pattern) + `\s*[^\s]+`)

This handles both password=secret and password: secret without requiring callers to remember a trailing space.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f46d7de and 4114f56.

📒 Files selected for processing (1)
  • logging/logging.go (4 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-11-16T00:59:45.522Z
Learnt from: andremartinsds
Repo: kashifkhan0771/utils PR: 40
File: url/url.go:76-79
Timestamp: 2024-11-16T00:59:45.522Z
Learning: In the `BuildURL` function within `url/url.go`, the current path validation regex is appropriate and should not be changed without a specific reason.

Applied to files:

  • logging/logging.go
📚 Learning: 2025-08-12T07:38:35.407Z
Learnt from: addvanced
Repo: kashifkhan0771/utils PR: 177
File: slugger/slugger.go:39-43
Timestamp: 2025-08-12T07:38:35.407Z
Learning: In the slugger package, the user addvanced mentioned they have a separate branch that refactors the substitution logic from using strings.ReplaceAll in a loop to using strings.NewReplacer, which is a more appropriate and efficient approach for handling multiple string replacements.

Applied to files:

  • logging/logging.go
🔇 Additional comments (4)
logging/logging.go (4)

34-48: RedactionRule struct and Logger wiring look appropriate

The struct shape (*regexp.Regexp + replacement) and storing a slice of rules on Logger is a clean, extensible way to plug redaction into the existing logger without exposing internal details.


93-114: Atomic SetRedactionRegex configuration looks good

Building rules in a local slice and assigning to l.redactionRules only after all patterns compile ensures that on a compile error the logger’s redaction config is left untouched. This addresses the previous partial‑update concern and gives callers clear all‑or‑nothing semantics.


116-124: redact implementation is straightforward and appropriate

Applying each compiled regex in sequence over a working copy of the message is clear and maintainable, and should be performant enough for the expected number of rules.


138-141: Redaction hook placement in log() is sensible

Conditionally applying l.redact only when rules exist avoids extra work, and doing it before formatting/coloring ensures that only redacted content is written to the output while leaving prefixes and level metadata untouched.

@kashifkhan0771 kashifkhan0771 merged commit 6b45f2c into kashifkhan0771:main Dec 9, 2025
3 checks passed
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.

3 participants