Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(log): add slog-backed Logger type #22347

Merged
merged 5 commits into from
Oct 24, 2024
Merged

feat(log): add slog-backed Logger type #22347

merged 5 commits into from
Oct 24, 2024

Conversation

mark-rushakoff
Copy link
Member

@mark-rushakoff mark-rushakoff commented Oct 23, 2024

The standard library's log/slog package was released after the SDK added its standardized Logger interface. It is reasonable to expect new Go projects to depend on log/slog rather than zerolog or any other third party logging implementations. The existing Logger interface maps cleanly to the log/slog API.

This change adds a new cosmossdk.io/log/slog package that exports a single Logger type, backed by an *slog.Logger. Currently, we expect the caller to provide a fully configured *log/slog.Logger and pass it to cosmossdk.io/log/slog.FromSlog. There is no reason we can't have a New function to produce a wrapped slog logger, but that seems out of scope of the initial implementation.

Summary by CodeRabbit

  • New Features

    • Introduced a logger that utilizes the standard library log/slog for improved logging capabilities.
    • Added structured logging options with key-value pairs for enhanced log context.
  • Bug Fixes

    • Resolved context key collisions in version v1.4.1.
  • Documentation

    • Updated README to include details on using the new standard library-backed logger.
  • Tests

    • Added comprehensive tests to validate the functionality of the new logging system.

The standard library's log/slog package was released after the SDK added
its standardized Logger interface. It is reasonable to expect new Go
projects to depend on log/slog rather than zerolog or any other third
party logging implementations. The existing Logger interface maps
cleanly to the log/slog API.

This change adds a new cosmossdk.io/log/slog package that exports a
single Logger type, backed by an *slog.Logger. Currently, we expect the
caller to provide a fully configured *log/slog.Logger and pass it to
cosmossdk.io/log/slog.FromSlog. There is no reason we can't have a New
function to produce a wrapped slog logger, but that seems out of scope
of the initial implementation.

This comment has been minimized.

Copy link
Contributor

coderabbitai bot commented Oct 23, 2024

📝 Walkthrough
📝 Walkthrough

Walkthrough

The changes in this pull request involve updates to the logging system within the Cosmos SDK. A new logger implementation using the standard library's log/slog package has been introduced, along with corresponding documentation updates and a new test file. The changelog has been updated to reflect these changes and other version history improvements, including enhancements to JSON handling and context management.

Changes

File Change Summary
log/CHANGELOG.md Updated to include new entries for logger improvements and version history details.
log/README.md Added documentation for using the new cosmossdk.io/log/slog logger.
log/slog/logger.go Introduced Logger type implementing the log.Logger interface; added methods for various log levels.
log/slog/logger_test.go Added tests for the new logging functionality to validate JSON output for different log levels.

Possibly related PRs

  • perf(log): use sonic json lib  #22233: This PR introduces the Sonic JSON library for improved JSON handling in the logging system, which is directly referenced in the main PR's changelog updates.

Suggested reviewers

  • sontrinh16
  • tac0turtle

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

log/go.mod Outdated Show resolved Hide resolved
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: 6

🧹 Outside diff range and nitpick comments (3)
log/go.mod (2)

Line range hint 3-3: Go version must be updated to 1.21 or higher

The PR introduces slog-backed Logger, but slog was introduced in Go 1.21. The current Go version (1.20) doesn't support slog and will cause compilation errors.

Apply this diff:

-go 1.20
+go 1.21

9-9: Consider updating testify to v1.8.4

The current version (v1.8.1) is a bit outdated. Version 1.8.4 includes several bug fixes and improvements.

Apply this diff:

-	github.com/stretchr/testify v1.8.1
+	github.com/stretchr/testify v1.8.4
log/slog/logger.go (1)

13-17: Consider enhancing type documentation.

While the documentation is good, it could be more helpful to add an example usage.

Add a documentation example:

 // Logger satisfies [log.Logger] with logging backed by
 // an instance of [*slog.Logger].
+//
+// Example:
+//
+//	slogger := slog.New(slog.NewTextHandler(os.Stdout, nil))
+//	logger := slog.FromSlog(slogger)
+//	logger.Info("message", "key", "value")
 type Logger struct {
📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 762fad2 and cf20016.

⛔ Files ignored due to path filters (1)
  • log/go.sum is excluded by !**/*.sum
📒 Files selected for processing (5)
  • log/CHANGELOG.md (1 hunks)
  • log/README.md (1 hunks)
  • log/go.mod (1 hunks)
  • log/slog/logger.go (1 hunks)
  • log/slog/logger_test.go (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • log/README.md
🧰 Additional context used
📓 Path-based instructions (3)
log/CHANGELOG.md (1)

Pattern **/*.md: "Assess the documentation for misspellings, grammatical errors, missing documentation and correctness"

log/slog/logger.go (1)

Pattern **/*.go: Review the Golang code for conformity with the Uber Golang style guide, highlighting any deviations.

log/slog/logger_test.go (2)

Pattern **/*.go: Review the Golang code for conformity with the Uber Golang style guide, highlighting any deviations.


Pattern **/*_test.go: "Assess the unit test code assessing sufficient code coverage for the changes associated in the pull request"

🔇 Additional comments (5)
log/slog/logger.go (3)

1-9: LGTM! Well-documented package with clean imports.

The package documentation is clear and follows Go standards with proper documentation references.


11-11: LGTM! Proper interface compliance check.

Good use of compile-time interface compliance check.


44-47: LGTM! Well-documented implementation accessor.

The Impl method provides clear access to the underlying logger with proper documentation.

log/slog/logger_test.go (1)

1-11: LGTM! Well-structured package and imports.

The package name and import organization follow Go best practices. Good use of aliasing for the standard slog package to avoid naming conflicts.

log/CHANGELOG.md (1)

28-28: LGTM! The changelog entry is well-formatted and accurate.

The new entry follows the changelog guidelines, is placed in the appropriate section, and accurately describes the addition of the slog-backed logger feature.

Comment on lines +24 to +38
func (l Logger) Info(msg string, keyVals ...any) {
l.log.Info(msg, keyVals...)
}

func (l Logger) Warn(msg string, keyVals ...any) {
l.log.Warn(msg, keyVals...)
}

func (l Logger) Error(msg string, keyVals ...any) {
l.log.Error(msg, keyVals...)
}

func (l Logger) Debug(msg string, keyVals ...any) {
l.log.Debug(msg, keyVals...)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider validating key-value pairs in logging methods.

The logging methods accept variadic key-value pairs but don't validate that they come in pairs.

Add a helper function for validation:

func validateKeyVals(keyVals ...any) {
	if len(keyVals)%2 != 0 {
		panic("odd number of key-value pairs")
	}
}

Then use it in each method, for example:

 func (l Logger) Info(msg string, keyVals ...any) {
+	validateKeyVals(keyVals...)
 	l.log.Info(msg, keyVals...)
 }

Comment on lines 19 to 22
// FromSlog returns a Logger backed by an existing slog.Logger instance.
func FromSlog(log *slog.Logger) Logger {
return Logger{log: log}
}
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 23, 2024

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add parameter validation in constructor.

The constructor should validate that the input logger is not nil to prevent potential panics.

 func FromSlog(log *slog.Logger) Logger {
+	if log == nil {
+		panic("nil slog.Logger")
+	}
 	return Logger{log: log}
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// FromSlog returns a Logger backed by an existing slog.Logger instance.
func FromSlog(log *slog.Logger) Logger {
return Logger{log: log}
}
// FromSlog returns a Logger backed by an existing slog.Logger instance.
func FromSlog(log *slog.Logger) Logger {
if log == nil {
panic("nil slog.Logger")
}
return Logger{log: log}
}

Copy link
Member

@julienrbrt julienrbrt Oct 24, 2024

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

@julienrbrt, glad you agree!

(_/)
(•ᴗ•)
(")_(")

Comment on lines +40 to +42
func (l Logger) With(keyVals ...any) log.Logger {
return Logger{log: l.log.With(keyVals...)}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add validation in With method.

The With method should also validate key-value pairs.

 func (l Logger) With(keyVals ...any) log.Logger {
+	validateKeyVals(keyVals...)
 	return Logger{log: l.log.With(keyVals...)}
 }

Committable suggestion was skipped due to low confidence.

Comment on lines 26 to 62
var line logLine

logger.Debug("Message one", "num", 1)
require.NoError(t, json.Unmarshal(buf.Bytes(), &line))
require.Equal(t, logLine{
Level: stdslog.LevelDebug.String(),
Msg: "Message one",
Num: 1,
}, line)

buf.Reset()
logger.Info("Message two", "num", 2)
require.NoError(t, json.Unmarshal(buf.Bytes(), &line))
require.Equal(t, logLine{
Level: stdslog.LevelInfo.String(),
Msg: "Message two",
Num: 2,
}, line)

buf.Reset()
logger.Warn("Message three", "num", 3)
require.NoError(t, json.Unmarshal(buf.Bytes(), &line))
require.Equal(t, logLine{
Level: stdslog.LevelWarn.String(),
Msg: "Message three",
Num: 3,
}, line)

buf.Reset()
logger.Error("Message four", "num", 4)
require.NoError(t, json.Unmarshal(buf.Bytes(), &line))
require.Equal(t, logLine{
Level: stdslog.LevelError.String(),
Msg: "Message four",
Num: 4,
}, line)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance test robustness with additional test cases.

While the current tests cover basic logging functionality, consider the following improvements:

  1. Convert to table-driven tests to reduce code duplication
  2. Add error cases (e.g., invalid log levels)
  3. Test WithLevel functionality
  4. Validate behavior with nil/empty attributes

Here's a suggested table-driven test approach:

func TestSlog(t *testing.T) {
    tests := []struct {
        name     string
        level    slog.Level
        msg      string
        num      int
        expected logLine
    }{
        {
            name:  "debug message",
            level: slog.LevelDebug,
            msg:   "Message one",
            num:   1,
            expected: logLine{
                Level: slog.LevelDebug.String(),
                Msg:   "Message one",
                Num:   1,
            },
        },
        // Add more test cases
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            var buf bytes.Buffer
            logger := setupLogger(&buf)
            
            switch tt.level {
            case slog.LevelDebug:
                logger.Debug(tt.msg, "num", tt.num)
            // Add other cases
            }

            var line logLine
            require.NoError(t, json.Unmarshal(buf.Bytes(), &line))
            require.Equal(t, tt.expected, line)
        })
    }
}

log/slog/logger_test.go Outdated Show resolved Hide resolved
log/slog/logger_test.go Outdated Show resolved Hide resolved
Copy link
Member

@julienrbrt julienrbrt left a comment

Choose a reason for hiding this comment

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

ACK

@sontrinh16
Copy link
Member

hmmm maybe adding a new function to wrapped the slog is fine

@sontrinh16
Copy link
Member

or you going to do it in a follow up PR right ? @mark-rushakoff

@mark-rushakoff
Copy link
Member Author

or you going to do it in a follow up PR right ?

Yes, I think there will be one followup PR to be able to use this with the server/v2 code. Although that may overlap with the work in #22267.

@julienrbrt julienrbrt added this pull request to the merge queue Oct 24, 2024
@julienrbrt julienrbrt removed this pull request from the merge queue due to a manual request Oct 24, 2024
Copy link
Member

@julienrbrt julienrbrt left a comment

Choose a reason for hiding this comment

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

I approved too fast, using this means it is on the caller to configure and handle levels, log filtering, etc.. As this won't do it at all compared to NewLogger with its options

log/slog/logger.go Outdated Show resolved Hide resolved
log/README.md Outdated Show resolved Hide resolved
This is more in line with the existing zerolog-focused API.

Also update a couple more comments, and add a test for the With method.
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: 2

📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between a078c39 and 8f1f86e.

📒 Files selected for processing (3)
  • log/README.md (1 hunks)
  • log/slog/logger.go (1 hunks)
  • log/slog/logger_test.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • log/README.md
🧰 Additional context used
📓 Path-based instructions (2)
log/slog/logger.go (1)

Pattern **/*.go: Review the Golang code for conformity with the Uber Golang style guide, highlighting any deviations.

log/slog/logger_test.go (2)

Pattern **/*.go: Review the Golang code for conformity with the Uber Golang style guide, highlighting any deviations.


Pattern **/*_test.go: "Assess the unit test code assessing sufficient code coverage for the changes associated in the pull request"

🔇 Additional comments (7)
log/slog/logger.go (5)

1-9: LGTM! Clear package documentation and clean imports.

The package documentation clearly describes the purpose and the imports are minimal and necessary.


11-17: LGTM! Clean type definition with interface compliance check.

Good practice using var assertion to verify interface compliance at compile time.


19-25: LGTM! Constructor follows existing naming patterns.

The name NewCustomLogger aligns with existing patterns in the codebase, and the documentation clearly states the caller's responsibilities.


27-41: LGTM! Consistent implementation across logging methods.

The logging methods provide a clean pass-through to the underlying slog.Logger with consistent implementation patterns.


47-50: LGTM! Well-documented implementation accessor.

The Impl method follows Go conventions for providing access to the underlying implementation.

log/slog/logger_test.go (2)

1-10: LGTM! Well-organized imports and package declaration.

The package is correctly declared as slog_test for external testing, and imports are properly organized with appropriate aliasing.


19-23: Previous comment about expanding logLine struct is still applicable.

Comment on lines +12 to +17
func TestSlog(t *testing.T) {
var buf bytes.Buffer
h := stdslog.NewJSONHandler(&buf, &stdslog.HandlerOptions{
Level: stdslog.LevelDebug,
})
logger := slog.NewCustomLogger(stdslog.New(h))
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Verify interface compliance and enhance test setup.

Consider adding interface compliance verification and extracting the logger setup into a helper function:

func TestLoggerImplementsInterface(t *testing.T) {
    var _ log.Logger = (*slog.Logger)(nil)
}

func setupTestLogger() (*bytes.Buffer, *slog.Logger) {
    buf := &bytes.Buffer{}
    h := stdslog.NewJSONHandler(buf, &stdslog.HandlerOptions{
        Level: stdslog.LevelDebug,
    })
    return buf, slog.NewCustomLogger(stdslog.New(h))
}

Comment on lines +25 to +92
var line logLine

logger.Debug("Message one", "num", 1)
if err := json.Unmarshal(buf.Bytes(), &line); err != nil {
t.Fatal(err)
}
if want := (logLine{
Level: stdslog.LevelDebug.String(),
Msg: "Message one",
Num: 1,
}); want != line {
t.Fatalf("unexpected log record: want %v, got %v", want, line)
}

buf.Reset()
logger.Info("Message two", "num", 2)
if err := json.Unmarshal(buf.Bytes(), &line); err != nil {
t.Fatal(err)
}
if want := (logLine{
Level: stdslog.LevelInfo.String(),
Msg: "Message two",
Num: 2,
}); want != line {
t.Fatalf("unexpected log record: want %v, got %v", want, line)
}

buf.Reset()
logger.Warn("Message three", "num", 3)
if err := json.Unmarshal(buf.Bytes(), &line); err != nil {
t.Fatal(err)
}
if want := (logLine{
Level: stdslog.LevelWarn.String(),
Msg: "Message three",
Num: 3,
}); want != line {
t.Fatalf("unexpected log record: want %v, got %v", want, line)
}

buf.Reset()
logger.Error("Message four", "num", 4)
if err := json.Unmarshal(buf.Bytes(), &line); err != nil {
t.Fatal(err)
}
if want := (logLine{
Level: stdslog.LevelError.String(),
Msg: "Message four",
Num: 4,
}); want != line {
t.Fatalf("unexpected log record: want %v, got %v", want, line)
}

wLogger := logger.With("num", 5)
buf.Reset()
wLogger.Info("Using .With")

if err := json.Unmarshal(buf.Bytes(), &line); err != nil {
t.Fatal(err)
}
if want := (logLine{
Level: stdslog.LevelInfo.String(),
Msg: "Using .With",
Num: 5,
}); want != line {
t.Fatalf("unexpected log record: want %v, got %v", want, line)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add test cases for error conditions and edge cases.

While basic logging functionality is covered, consider adding tests for:

  1. Invalid key-value pairs (odd number of arguments)
  2. Empty or nil messages
  3. Special characters in messages
  4. Timestamp presence and format validation
  5. Concurrent logging safety

Example test case for invalid arguments:

func TestLoggerInvalidInput(t *testing.T) {
    buf := &bytes.Buffer{}
    logger := setupTestLogger(buf)
    
    // Should not panic with odd number of kvs
    logger.Info("test", "key") // missing value
    
    // Should handle nil/empty values
    logger.Info("", nil, "key", nil)
}

@julienrbrt julienrbrt added this pull request to the merge queue Oct 24, 2024
Merged via the queue into main with commit 7262cf3 Oct 24, 2024
72 of 73 checks passed
@julienrbrt julienrbrt deleted the mr/log-slog branch October 24, 2024 18:28
alpe added a commit that referenced this pull request Oct 25, 2024
* main: (157 commits)
  feat(core): add ConfigMap type (#22361)
  test: migrate e2e/genutil to systemtest (#22325)
  feat(accounts): re-introduce bundler (#21562)
  feat(log): add slog-backed Logger type (#22347)
  fix(x/tx): add feePayer as signer (#22311)
  test: migrate e2e/baseapp to integration tests (#22357)
  test: add x/circuit system tests (#22331)
  test: migrate e2e/tx tests to systemtest (#22152)
  refactor(simdv2): allow non-comet server components (#22351)
  build(deps): Bump rtCamp/action-slack-notify from 2.3.1 to 2.3.2 (#22352)
  fix(server/v2): respect context cancellation in start command (#22346)
  chore(simdv2): allow overriding the --home flag (#22348)
  feat(server/v2): add SimulateWithState to AppManager (#22335)
  docs(x/accounts): improve comments (#22339)
  chore: remove unused local variables (#22340)
  test: x/accounts systemtests (#22320)
  chore(client): use command's configured output (#22334)
  perf(log): use sonic json lib  (#22233)
  build(deps): bump x/tx (#22327)
  fix(x/accounts/lockup): fix proto path (#22319)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants