This is an automated email from the ASF dual-hosted git repository.

zike pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar-client-go.git


The following commit(s) were added to refs/heads/master by this push:
     new 1152cfc2 [feat] added a slog wrapper of the logger interface (#1234)
1152cfc2 is described below

commit 1152cfc2e3c2024117ecff93a46d90e7eab0fa15
Author: Ivan Penchev <[email protected]>
AuthorDate: Fri Jul 5 11:55:40 2024 +0200

    [feat] added a slog wrapper of the logger interface (#1234)
    
    ### Motivation
    
    This commit supports to use `log/slog` package from the standard library to 
control the level and output type of the logs.
    In order for us to not have to import logrus as a direct dependency for 
part of our testing suit, it would be nice if we can use `slog` package 
instead, and wrap that in the provided by `pulsar/log` interfaces.
    
    This ties in a bit with issue #1078 because it opens the door for users who 
are already working with log/slog in their projects. Plus, it's a gives more 
time for the Pulsar team to evaluate incorporating slog into the SDK.
    
    ### Modifications
    
    One additional file `/pulsar/log/wrapper_slog.go` is added.
    One additional function in the `pulsar/log` package, `NewLoggerWithSlog` , 
is exposed.
---
 README.md                            |   2 +-
 go.mod                               |   2 +-
 pulsar/client_impl_with_slog_test.go |  49 +++++++++
 pulsar/log/wrapper_slog.go           | 105 ++++++++++++++++++++
 pulsar/log/wrapper_slog_test.go      | 186 +++++++++++++++++++++++++++++++++++
 5 files changed, 342 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index c77cdea7..028e13a5 100644
--- a/README.md
+++ b/README.md
@@ -152,7 +152,7 @@ Run the tests:
 
 Run the tests with specific versions of GOLANG and PULSAR:
 
-    make test GOLANG_VERSION=1.20 PULSAR_VERSION=2.10.0
+    make test GO_VERSION=1.20 PULSAR_VERSION=2.10.0
 
 ## Contributing
 
diff --git a/go.mod b/go.mod
index bbbf9bd6..e3f95ba8 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/apache/pulsar-client-go
 
-go 1.18
+go 1.20
 
 require (
        github.com/99designs/keyring v1.2.1
diff --git a/pulsar/client_impl_with_slog_test.go 
b/pulsar/client_impl_with_slog_test.go
new file mode 100644
index 00000000..1882cd8d
--- /dev/null
+++ b/pulsar/client_impl_with_slog_test.go
@@ -0,0 +1,49 @@
+/// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//go:build go1.21
+
+package pulsar
+
+import (
+       "log/slog"
+       "os"
+       "testing"
+
+       "github.com/apache/pulsar-client-go/pulsar/log"
+       "github.com/stretchr/testify/assert"
+)
+
+func TestClientWithSlog(t *testing.T) {
+       sLogger := slog.New(slog.NewTextHandler(os.Stdout, 
&slog.HandlerOptions{Level: slog.LevelDebug}))
+
+       client, err := NewClient(ClientOptions{
+               URL:    serviceURL,
+               Logger: log.NewLoggerWithSlog(sLogger),
+       })
+       assert.NotNil(t, client)
+       assert.Nil(t, err)
+
+       producer, err := client.CreateProducer(ProducerOptions{
+               Topic: newTopicName(),
+       })
+       assert.NotNil(t, producer)
+       assert.Nil(t, err)
+
+       producer.Close()
+       client.Close()
+}
diff --git a/pulsar/log/wrapper_slog.go b/pulsar/log/wrapper_slog.go
new file mode 100644
index 00000000..b9728400
--- /dev/null
+++ b/pulsar/log/wrapper_slog.go
@@ -0,0 +1,105 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//go:build go1.21
+
+package log
+
+import (
+       "fmt"
+       "log/slog"
+)
+
+type slogWrapper struct {
+       logger *slog.Logger
+}
+
+func (s *slogWrapper) Debug(args ...any) {
+       message := s.tryDetermineMessage(args...)
+       s.logger.Debug(message)
+}
+
+func (s *slogWrapper) Info(args ...any) {
+       message := s.tryDetermineMessage(args...)
+       s.logger.Info(message)
+}
+
+func (s *slogWrapper) Error(args ...any) {
+       message := s.tryDetermineMessage(args...)
+       s.logger.Error(message)
+}
+
+func (s *slogWrapper) Warn(args ...any) {
+       message := s.tryDetermineMessage(args...)
+       s.logger.Warn(message)
+}
+
+func (s *slogWrapper) Debugf(format string, args ...any) {
+       s.logger.Debug(fmt.Sprintf(format, args...))
+}
+
+func (s *slogWrapper) Infof(format string, args ...any) {
+       s.logger.Info(fmt.Sprintf(format, args...))
+}
+
+func (s *slogWrapper) Warnf(format string, args ...any) {
+       s.logger.Warn(fmt.Sprintf(format, args...))
+}
+
+func (s *slogWrapper) Errorf(format string, args ...any) {
+       s.logger.Error(fmt.Sprintf(format, args...))
+}
+
+func (s *slogWrapper) SubLogger(fields Fields) Logger {
+       return &slogWrapper{
+               logger: s.logger.With(pulsarFieldsToKVSlice(fields)...),
+       }
+}
+
+func (s *slogWrapper) WithError(err error) Entry {
+       return s.WithField("error", err)
+}
+
+func (s *slogWrapper) WithField(name string, value any) Entry {
+       return &slogWrapper{
+               logger: s.logger.With(name, value),
+       }
+}
+
+func (s *slogWrapper) WithFields(fields Fields) Entry {
+       return &slogWrapper{
+               logger: s.logger.With(pulsarFieldsToKVSlice(fields)...),
+       }
+}
+
+func NewLoggerWithSlog(logger *slog.Logger) Logger {
+       return &slogWrapper{
+               logger: logger,
+       }
+}
+
+func pulsarFieldsToKVSlice(f Fields) []any {
+       ret := make([]any, 0, len(f)*2)
+       for k, v := range f {
+               ret = append(ret, k, v)
+       }
+       return ret
+}
+
+func (s *slogWrapper) tryDetermineMessage(value ...any) string {
+       return fmt.Sprint(value...)
+}
diff --git a/pulsar/log/wrapper_slog_test.go b/pulsar/log/wrapper_slog_test.go
new file mode 100644
index 00000000..2b4f9512
--- /dev/null
+++ b/pulsar/log/wrapper_slog_test.go
@@ -0,0 +1,186 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//go:build go1.21
+
+package log
+
+import (
+       "bytes"
+       "encoding/json"
+       "errors"
+       "fmt"
+       "log/slog"
+       "strings"
+       "testing"
+
+       "github.com/stretchr/testify/require"
+)
+
+func TestSlogLevels(t *testing.T) {
+       testCases := []struct {
+               level       slog.Level
+               logFunction func(logger Logger, msg string)
+       }{
+               {slog.LevelDebug, func(logger Logger, msg string) { 
logger.Debug(msg) }},
+               {slog.LevelInfo, func(logger Logger, msg string) { 
logger.Info(msg) }},
+               {slog.LevelWarn, func(logger Logger, msg string) { 
logger.Warn(msg) }},
+               {slog.LevelError, func(logger Logger, msg string) { 
logger.Error(msg) }},
+       }
+
+       for _, tc := range testCases {
+               t.Run(tc.level.String(), func(t *testing.T) {
+                       var logBuffer bytes.Buffer
+                       logMessage := "test message"
+                       loggerSlog := slog.New(slog.NewJSONHandler(&logBuffer, 
&slog.HandlerOptions{Level: tc.level}))
+                       pulsarLogger := NewLoggerWithSlog(loggerSlog)
+
+                       tc.logFunction(pulsarLogger, logMessage)
+
+                       logOutputSlog := logBuffer.String()
+                       verifyLogOutput(t, logOutputSlog, tc.level.String(), 
logMessage)
+               })
+       }
+}
+
+func TestSlogPrintMethods(t *testing.T) {
+       testCases := []struct {
+               level       slog.Level
+               logFunction func(logger Logger, format string, args ...any)
+       }{
+               {
+                       level: slog.LevelDebug,
+                       logFunction: func(logger Logger, format string, args 
...any) {
+                               logger.Debugf(format, args...)
+                       },
+               },
+               {
+                       level: slog.LevelInfo,
+                       logFunction: func(logger Logger, format string, args 
...any) {
+                               logger.Infof(format, args...)
+                       },
+               },
+               {
+                       level: slog.LevelWarn,
+                       logFunction: func(logger Logger, format string, args 
...any) {
+                               logger.Warnf(format, args...)
+                       },
+               },
+               {
+                       level: slog.LevelError,
+                       logFunction: func(logger Logger, format string, args 
...any) {
+                               logger.Errorf(format, args...)
+                       },
+               },
+       }
+
+       for _, tc := range testCases {
+               t.Run(tc.level.String()+"f", func(t *testing.T) {
+                       var logBuffer bytes.Buffer
+                       logMessage := "formatted message for %s"
+                       expectedMessage := "formatted message for " + 
tc.level.String()
+                       loggerSlog := slog.New(slog.NewJSONHandler(&logBuffer, 
&slog.HandlerOptions{Level: tc.level}))
+                       pulsarLogger := NewLoggerWithSlog(loggerSlog)
+
+                       tc.logFunction(pulsarLogger, logMessage, 
tc.level.String())
+
+                       logOutputSlog := logBuffer.String()
+                       verifyLogOutput(t, logOutputSlog, tc.level.String(), 
expectedMessage)
+               })
+       }
+}
+
+func TestSlogWrapperWithMethods(t *testing.T) {
+       testCases := []struct {
+               name           string
+               level          slog.Level
+               testMessage    string
+               setupLogger    func(logger Logger) Entry
+               expectedFields Fields
+       }{
+               {
+                       name:        "WithField",
+                       level:       slog.LevelInfo,
+                       testMessage: "Message with field",
+                       setupLogger: func(logger Logger) Entry {
+                               return logger.WithField("key", "value")
+                       },
+                       expectedFields: Fields{"key": "value"},
+               },
+               {
+                       name:        "WithFields",
+                       level:       slog.LevelInfo,
+                       testMessage: "Message with multiple fields",
+                       setupLogger: func(logger Logger) Entry {
+                               return logger.WithFields(Fields{"key1": 
"value1", "key2": "value2"})
+                       },
+                       expectedFields: Fields{"key1": "value1", "key2": 
"value2"},
+               },
+               {
+                       name:        "WithError",
+                       level:       slog.LevelInfo,
+                       testMessage: "Message with error field",
+                       setupLogger: func(logger Logger) Entry {
+                               return logger.WithError(errors.New("test 
error"))
+                       },
+                       expectedFields: Fields{"error": "test error"},
+               },
+       }
+
+       for _, tc := range testCases {
+               t.Run(tc.name, func(t *testing.T) {
+                       var logBuffer bytes.Buffer
+                       loggerSlog := slog.New(slog.NewJSONHandler(&logBuffer, 
&slog.HandlerOptions{Level: tc.level}))
+                       pulsarLogger := NewLoggerWithSlog(loggerSlog)
+
+                       entry := tc.setupLogger(pulsarLogger)
+                       switch tc.level {
+                       case slog.LevelDebug:
+                               entry.Debug(tc.testMessage)
+                       case slog.LevelInfo:
+                               entry.Info(tc.testMessage)
+                       case slog.LevelWarn:
+                               entry.Warn(tc.testMessage)
+                       case slog.LevelError:
+                               entry.Error(tc.testMessage)
+                       default:
+                               t.Errorf("Unsupported log level: %v", tc.level)
+                       }
+
+                       verifyLogOutput(t, logBuffer.String(), 
tc.level.String(), tc.testMessage, tc.expectedFields)
+               })
+       }
+}
+
+func verifyLogOutput(t *testing.T, logOutput, expectedLevel, expectedMessage 
string, expectedFields ...Fields) {
+       logLines := strings.Split(strings.TrimSpace(logOutput), "\n")
+       require.Len(t, logLines, 1, "There should be exactly one log line.")
+
+       var logEntry map[string]interface{}
+       err := json.Unmarshal([]byte(logLines[0]), &logEntry)
+       require.NoError(t, err, "Log entry should be valid JSON.")
+       require.Equal(t, expectedLevel, logEntry[slog.LevelKey], "Log level 
should match expected level.")
+       require.Equal(t, expectedMessage, logEntry[slog.MessageKey], "Log 
message should contain expected message.")
+
+       if len(expectedFields) > 0 {
+               for key, expectedValue := range expectedFields[0] {
+                       actualValue, ok := logEntry[key]
+                       require.True(t, ok, fmt.Sprintf("Expected key '%s' to 
be present in the log entry", key))
+                       require.Equal(t, expectedValue, actualValue, 
fmt.Sprintf("Value for key '%s' should match the expected value", key))
+               }
+       }
+}

Reply via email to