Repository: googleapis/go-spanner-cassandra
Pull request #30: feat: support configurable log level on the global zap logger
Author: login: ShuranZhang
Created: 2025-04-15T21:26:05Z | Merged: 2025-04-16T19:31:03Z



Diff:
diff --git a/adapter/options.go b/adapter/options.go
index e865814..7cb952f 100644
--- a/adapter/options.go
+++ b/adapter/options.go
@@ -20,15 +20,15 @@ package adapter
 type Options struct {
 	// Spanner database uri to connect to.
 	DatabaseUri string
-	// Optional Spanner service endpoint.
+	// Optional Spanner service endpoint. Defaults to spanner.googleapis.com:443
 	SpannerEndpoint string
 	// Protocol type (ie: cassandra).
 	Protocol Protocol
-	// Number of channels when dial grpc connection.
+	// Number of channels when dial grpc connection. Defaults to 4.
 	NumGrpcChannels int
-	// Optional Endpoint to start TCP server. If not specified, defaults to
-	// 127.0.0.1:9042
+	// Optional Endpoint to start TCP server. Defaults to localhost:9042
 	TCPEndpoint string
-	// Whether to disable automatic grpc retry for AdaptMessage API
+	// Optional boolean indicate whether to disable automatic grpc retry for
+	// AdaptMessage API. Defauls to false.
 	DisableAdaptMessageRetry bool
 }
diff --git a/cassandra/gocql/spanner.go b/cassandra/gocql/spanner.go
index 8bb6637..d48c0cd 100644
--- a/cassandra/gocql/spanner.go
+++ b/cassandra/gocql/spanner.go
@@ -24,6 +24,7 @@ import (
 
 	"github.com/gocql/gocql"
 	"github.com/googleapis/go-spanner-cassandra/adapter"
+	"github.com/googleapis/go-spanner-cassandra/logger"
 )
 
 // Map from cluster config to local proxies.
@@ -33,13 +34,19 @@ var proxyMap = make(
 
 // Options represents the configuration for a virtual Spanner cluster.
 type Options struct {
+	// Optional Spanner service endpoint. Defaults to spanner.googleapis.com:443
 	SpannerEndpoint string
-	TCPEndpoint     string
-	DatabaseUri     string
-	// Number of channels when dial grpc connection.
+	// Optional Endpoint to start TCP server. Defaults to localhost:9042
+	TCPEndpoint string
+	// Required database uri to connect to.
+	DatabaseUri string
+	// Number of channels when dial grpc connection. Defaults to 4.
 	NumGrpcChannels int
-	// Whether to disable automatic grpc retry for AdaptMessage API
+	// Optional boolean indicate whether to disable automatic grpc retry for
+	// AdaptMessage API. Defauls to false.
 	DisableAdaptMessageRetry bool
+	// Optional log level. Defaults to info.
+	LogLevel string
 }
 
 type ProxyAddressTranslator struct {
@@ -56,6 +63,13 @@ func (t *ProxyAddressTranslator) Translate(ip net.IP, port int) (net.IP, int) {
 func NewCluster(
 	opts *Options,
 ) *gocql.ClusterConfig {
+	// Initialize a global logger with default INFO log level
+	err := logger.SetupGlobalLogger(opts.LogLevel)
+	if err != nil {
+		panic(
+			err,
+		)
+	}
 	// Create a new local Cassandra proxy.
 	proxy, err := adapter.NewTCPProxy(
 		adapter.Options{
diff --git a/cassandra/gocql/spanner_test.go b/cassandra/gocql/spanner_test.go
index a1ab668..f61c14e 100644
--- a/cassandra/gocql/spanner_test.go
+++ b/cassandra/gocql/spanner_test.go
@@ -28,6 +28,7 @@ import (
 
 	"github.com/gocql/gocql"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 // sample command to run all unit tests
@@ -199,3 +200,71 @@ func TestBatch(t *testing.T) {
 		})
 	}
 }
+
+func TestNewClusterPanicsOnInvalidLogLevel(t *testing.T) {
+	t.Cleanup(adapter.ResetGrpcFuncs())
+	testCases := []struct {
+		name        string
+		logLevel    string
+		expectPanic bool
+	}{
+		{
+			name:        "ShouldPanicOnInvalidLevel",
+			logLevel:    "invalid",
+			expectPanic: true,
+		},
+		{
+			name:        "ShouldNotPanicOnEmptyLevel",
+			logLevel:    "",
+			expectPanic: false,
+		},
+		{
+			name:        "ShouldNotPanicOnInfoLevel",
+			logLevel:    "info",
+			expectPanic: false,
+		},
+		{
+			name:        "ShouldNotPanicOnWarnLevel",
+			logLevel:    "warn",
+			expectPanic: false,
+		},
+		{
+			name:        "ShouldNotPanicOnErrorLevel",
+			logLevel:    "error",
+			expectPanic: false,
+		},
+		{
+			name:        "ShouldNotPanicOnFatalLevel",
+			logLevel:    "fatal",
+			expectPanic: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			adapter.MockCreateSessionGrpc()
+
+			opts := &Options{
+				DatabaseUri: "projects/test/instances/test/databases/test",
+				LogLevel:    tc.logLevel,
+			}
+
+			callNewCluster := func() {
+				cluster := NewCluster(opts)
+				if cluster != nil {
+					teardownCluster(t, cluster)
+				}
+			}
+
+			if tc.expectPanic {
+				require.Panics(
+					t,
+					callNewCluster,
+					"NewCluster should panic with invalid log level",
+				)
+			} else {
+				require.NotPanics(t, callNewCluster, "NewCluster should not panic with valid log level")
+			}
+		})
+	}
+}
diff --git a/cassandra_launcher.go b/cassandra_launcher.go
index e19916b..c3388d5 100644
--- a/cassandra_launcher.go
+++ b/cassandra_launcher.go
@@ -55,6 +55,12 @@ func main() {
 		"The number of channels when dial grpc connection. Default to 4.",
 	)
 
+	logLevel := flag.String(
+		"log",
+		"info",
+		"Log level. Default to info.",
+	)
+
 	flag.Parse()
 
 	if *databaseURI == "" {
@@ -67,6 +73,7 @@ func main() {
 		DatabaseUri:     *databaseURI,
 		TCPEndpoint:     *tcpEndpoint,
 		NumGrpcChannels: *numGrpcChannels,
+		LogLevel:        *logLevel,
 	}
 
 	cluster := spanner.NewCluster(opts)
diff --git a/logger/logger.go b/logger/logger.go
index bc2d7b1..bdc3189 100644
--- a/logger/logger.go
+++ b/logger/logger.go
@@ -19,28 +19,44 @@ package logger
 
 import (
 	"bytes"
+	"fmt"
 	"os"
 
 	"cloud.google.com/go/spanner/adapter/apiv1/adapterpb"
 
 	"github.com/datastax/go-cassandra-native-protocol/frame"
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 )
 
 var zapLog *zap.Logger
 
-func init() {
-	var err error
+func SetupGlobalLogger(level string) error {
 	var config zap.Config
+
 	if os.Getenv("ADAPTER_CLI_ENV") == "dev" {
 		config = zap.NewDevelopmentConfig()
 	} else {
 		config = zap.NewProductionConfig()
 	}
+
+	logLevel := zapcore.InfoLevel
+	if level != "" {
+		err := logLevel.Set(level)
+		if err != nil {
+			return fmt.Errorf("invalid log level '%s': %w", level, err)
+		}
+	}
+	config.Level.SetLevel(logLevel)
+
+	var err error
 	zapLog, err = config.Build(zap.AddCallerSkip(1))
 	if err != nil {
-		panic(err)
+		return fmt.Errorf("failed to build global logger: %w", err)
 	}
+	zapLog = zapLog.Named("go-spanner-cassandra")
+
+	return nil
 }
 
 func Info(message string, fields ...zap.Field) {


Reviews:
user: login: mayurkale22
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T17:56:01Z
user: login: ShuranZhang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T18:01:56Z
user: login: ShuranZhang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T18:03:20Z
user: login: mayurkale22
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T18:09:36Z
user: login: ShuranZhang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T18:15:17Z
user: login: mayurkale22
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T18:17:20Z
user: login: mayurkale22
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T19:28:42Z
user: login: ShuranZhang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T19:30:31Z

Review comments:
diff_hunk: @@ -19,28 +19,55 @@ package logger
 
 import (
 	"bytes"
+	"fmt"
 	"os"
 
 	"cloud.google.com/go/spanner/adapter/apiv1/adapterpb"
 
 	"github.com/datastax/go-cassandra-native-protocol/frame"
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 )
 
 var zapLog *zap.Logger
 
-func init() {
-	var err error
+// SetupGlobalLogger initializes the global logger.
+func SetupGlobalLogger(level string) error {
+	var err error // Declare error variable for level parsing
path: logger/logger.go
user: login: mayurkale22
body: comment not needed, looks redundant 
created_at: 2025-04-16T17:53:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,28 +19,55 @@ package logger
 
 import (
 	"bytes"
+	"fmt"
 	"os"
 
 	"cloud.google.com/go/spanner/adapter/apiv1/adapterpb"
 
 	"github.com/datastax/go-cassandra-native-protocol/frame"
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 )
 
 var zapLog *zap.Logger
 
-func init() {
-	var err error
+// SetupGlobalLogger initializes the global logger.
+func SetupGlobalLogger(level string) error {
+	var err error // Declare error variable for level parsing
path: logger/logger.go
user: login: ShuranZhang
body: Done.
created_at: 2025-04-16T18:01:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,28 +19,55 @@ package logger
 
 import (
 	"bytes"
+	"fmt"
 	"os"
 
 	"cloud.google.com/go/spanner/adapter/apiv1/adapterpb"
 
 	"github.com/datastax/go-cassandra-native-protocol/frame"
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 )
 
 var zapLog *zap.Logger
 
-func init() {
-	var err error
+// SetupGlobalLogger initializes the global logger.
+func SetupGlobalLogger(level string) error {
+	var err error // Declare error variable for level parsing
path: logger/logger.go
user: login: ShuranZhang
body: Done.
created_at: 2025-04-16T18:03:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,28 +19,54 @@ package logger
 
 import (
 	"bytes"
+	"fmt"
 	"os"
 
 	"cloud.google.com/go/spanner/adapter/apiv1/adapterpb"
 
 	"github.com/datastax/go-cassandra-native-protocol/frame"
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 )
 
 var zapLog *zap.Logger
 
-func init() {
-	var err error
+func SetupGlobalLogger(level string) error {
path: logger/logger.go
user: login: mayurkale22
body: What about this? little simplified version: 
```
	var config zap.Config

	if os.Getenv("ADAPTER_CLI_ENV") == "dev" {
		config = zap.NewDevelopmentConfig()
	} else {
		config = zap.NewProductionConfig()
	}

	logLevel := zapcore.InfoLevel
	if level != "" {
		err := logLevel.Set(level)
		if err != nil {
			return fmt.Errorf("invalid log level '%s': %w", level, err)
		}
	}
	config.Level.SetLevel(logLevel)

	var err error
	zapLog, err = config.Build(zap.AddCallerSkip(1))
	if err != nil {
		return fmt.Errorf("failed to build global logger: %w", err)
	}
	zapLog = zapLog.Named("go-spanner-cassandra")

	return nil
```
created_at: 2025-04-16T18:09:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,28 +19,54 @@ package logger
 
 import (
 	"bytes"
+	"fmt"
 	"os"
 
 	"cloud.google.com/go/spanner/adapter/apiv1/adapterpb"
 
 	"github.com/datastax/go-cassandra-native-protocol/frame"
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 )
 
 var zapLog *zap.Logger
 
-func init() {
-	var err error
+func SetupGlobalLogger(level string) error {
path: logger/logger.go
user: login: ShuranZhang
body: My intention was to prevent panic on start even if user sets an invalid log level. We will just log a warning and use default info level instead. We are treating other configs in the same way(ie for grpc channel if user sets a negative number, we use default 4 instead rather than panic the client).

 Lmk if you think just panic entire client is more desirable rather than fallback to info level.


created_at: 2025-04-16T18:15:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,28 +19,54 @@ package logger
 
 import (
 	"bytes"
+	"fmt"
 	"os"
 
 	"cloud.google.com/go/spanner/adapter/apiv1/adapterpb"
 
 	"github.com/datastax/go-cassandra-native-protocol/frame"
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 )
 
 var zapLog *zap.Logger
 
-func init() {
-	var err error
+func SetupGlobalLogger(level string) error {
path: logger/logger.go
user: login: mayurkale22
body: This will only happen during startup or initialization time, so should be fine
created_at: 2025-04-16T18:17:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,28 +19,54 @@ package logger
 
 import (
 	"bytes"
+	"fmt"
 	"os"
 
 	"cloud.google.com/go/spanner/adapter/apiv1/adapterpb"
 
 	"github.com/datastax/go-cassandra-native-protocol/frame"
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 )
 
 var zapLog *zap.Logger
 
-func init() {
-	var err error
+func SetupGlobalLogger(level string) error {
path: logger/logger.go
user: login: ShuranZhang
body: Done. Thanks!
created_at: 2025-04-16T19:30:30Z
author_association: CONTRIBUTOR

PR comments:
user: login: mayurkale22
created_at: 2025-04-16T17:51:47Z
body: Is this change to address https://github.com/googleapis/go-spanner-cassandra/issues/22?

author_association: CONTRIBUTOR
user: login: ShuranZhang
created_at: 2025-04-16T18:02:32Z
body: > Is this change to address #22?

Yes. I'll look into pull request template so we can link issue to fix in pr later.
author_association: CONTRIBUTOR