Skip to content

Commit 65a3b27

Browse files
committed
shared: introduce logger
There are some parts of the code that need logging. Before this change it was using `fmt.Print...` This PR introduces: 1. `logx` package as implementation independent logging layer 2. `logxzap` a package that implements `logx` using `go.uber.org/zap` 3. Introduces `WithLogger` configuration to override default logger 4. Make use of logger in different parts of the code
1 parent a9fec07 commit 65a3b27

10 files changed

Lines changed: 283 additions & 8 deletions

File tree

go.work.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,15 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 h1:KwuLovgQPcdjNMfFt9OhUd9a
88
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
99
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc=
1010
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
11+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
12+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
13+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
14+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
1115
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
16+
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
17+
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
18+
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
19+
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
1220
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
21+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
22+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

sdkv1/helper.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ var (
6565
// One way you can use it - to have this region in the logs, CloudWatch.
6666
WithAWSRegion = shared.WithAWSRegion
6767

68+
// WithLogger sets logger
69+
WithLogger = shared.WithLogger
70+
6871
// WithNodesListUpdatePeriod configures how often update list of nodes, while requests are running
6972
WithNodesListUpdatePeriod = shared.WithNodesListUpdatePeriod
7073

sdkv2/helper.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ var (
6767
// One way you can use it - to have this region in the logs, CloudWatch.
6868
WithAWSRegion = shared.WithAWSRegion
6969

70+
// WithLogger sets logger
71+
WithLogger = shared.WithLogger
72+
7073
// WithNodesListUpdatePeriod configures how often update list of nodes, while requests are running
7174
WithNodesListUpdatePeriod = shared.WithNodesListUpdatePeriod
7275

shared/cert_source.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import (
66
"os"
77
"sync"
88
"time"
9+
10+
"github.com/scylladb/alternator-client-golang/shared/logx"
911
)
1012

1113
// CertSource an interface that provides http clients with certificate
1214
type CertSource interface {
13-
GetClientCertificate(*tls.CertificateRequestInfo) (*tls.Certificate, error)
15+
GetClientCertificate(*tls.CertificateRequestInfo, logx.Logger) (*tls.Certificate, error)
1416
}
1517

1618
// CertFileSource serves certificate and key from a files
@@ -31,15 +33,18 @@ func NewFileCertificate(certPath, keyPath string) *CertFileSource {
3133
}
3234

3335
// GetClientCertificate implementation of tls.Config.GetClientCertificate that serves certificate from a file
34-
func (c *CertFileSource) GetClientCertificate(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) {
36+
func (c *CertFileSource) GetClientCertificate(
37+
_ *tls.CertificateRequestInfo,
38+
log logx.Logger,
39+
) (*tls.Certificate, error) {
3540
c.mutex.Lock()
3641
defer c.mutex.Unlock()
3742

3843
certStat, err := os.Stat(c.certPath)
3944
if err != nil {
4045
err = fmt.Errorf("failed to stat certificate file %s: %w", c.certPath, err)
4146
if c.cert != nil {
42-
fmt.Fprintf(os.Stderr, "ERROR: %s", err.Error())
47+
log.Error(err.Error())
4348
return c.cert, nil
4449
}
4550
return nil, err
@@ -53,7 +58,7 @@ func (c *CertFileSource) GetClientCertificate(_ *tls.CertificateRequestInfo) (*t
5358
if err != nil {
5459
err = fmt.Errorf("failed to load certificate file %s: %w", c.certPath, err)
5560
if c.cert != nil {
56-
fmt.Fprintf(os.Stderr, "ERROR: %s", err.Error())
61+
log.Error(err.Error())
5762
return c.cert, nil
5863
}
5964
return nil, err
@@ -77,6 +82,9 @@ func NewCertificate(cert tls.Certificate) *CertificateSource {
7782
}
7883

7984
// GetClientCertificate implementation of tls.Config.GetClientCertificate that serves provided certificate
80-
func (c *CertificateSource) GetClientCertificate(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) {
85+
func (c *CertificateSource) GetClientCertificate(
86+
_ *tls.CertificateRequestInfo,
87+
_ logx.Logger,
88+
) (*tls.Certificate, error) {
8189
return c.cert, nil
8290
}

shared/go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
module github.com/scylladb/alternator-client-golang/shared
22

33
go 1.24.0
4+
5+
require (
6+
go.uber.org/multierr v1.11.0 // indirect
7+
go.uber.org/zap v1.27.0 // indirect
8+
)

shared/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
2+
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
3+
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
4+
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=

shared/live_nodes.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ import (
99
"io"
1010
"net/http"
1111
"net/url"
12-
"os"
1312
"sync/atomic"
1413
"time"
1514

15+
"github.com/scylladb/alternator-client-golang/shared/logx"
16+
"github.com/scylladb/alternator-client-golang/shared/logxzap"
1617
"github.com/scylladb/alternator-client-golang/shared/rt"
1718
)
1819

@@ -47,6 +48,7 @@ type ALNConfig struct {
4748
IgnoreServerCertificateError bool
4849
ClientCertificateSource CertSource
4950
// A key writer for pre master key: https://wiki.wireshark.org/TLS#using-the-pre-master-secret
51+
Logger logx.Logger
5052
KeyLogWriter io.Writer
5153
// TLS session cache
5254
TLSSessionCache tls.ClientSessionCache
@@ -66,6 +68,7 @@ func NewDefaultALNConfig() ALNConfig {
6668
TLSSessionCache: defaultTLSSessionCache,
6769
MaxIdleHTTPConnections: 100,
6870
IdleHTTPConnectionTimeout: defaultIdleConnectionTimeout,
71+
Logger: logxzap.DefaultLogger(),
6972
}
7073
}
7174

@@ -122,6 +125,13 @@ func WithALNIgnoreServerCertificateError(value bool) ALNOption {
122125
}
123126
}
124127

128+
// WithLogger sets logger
129+
func WithLogger(logger logx.Logger) ALNOption {
130+
return func(config *ALNConfig) {
131+
config.Logger = logger
132+
}
133+
}
134+
125135
// WithALNClientCertificateFile provides client certificates http clients for both DynamoDB and Alternator requests
126136
// from files
127137
func WithALNClientCertificateFile(certFile, keyFile string) ALNOption {
@@ -335,6 +345,7 @@ func (aln *AlternatorLiveNodes) getNodes(endpoint *url.URL) ([]url.URL, error) {
335345
for _, node := range nodes {
336346
nodeURL, err := url.Parse(fmt.Sprintf("%s://%s:%d", aln.cfg.Scheme, node, aln.cfg.Port))
337347
if err != nil {
348+
aln.cfg.Logger.Error(fmt.Errorf("failed to parse node list entry: %w", err).Error())
338349
continue
339350
}
340351
uris = append(uris, *nodeURL)
@@ -349,7 +360,7 @@ func (aln *AlternatorLiveNodes) CheckIfRackAndDatacenterSetCorrectly() (err erro
349360
defer func() {
350361
if err == nil && len(errs) > 0 {
351362
for _, err := range errs {
352-
fmt.Fprintln(os.Stderr, err.Error())
363+
aln.cfg.Logger.Error(err.Error())
353364
}
354365
}
355366
}()

shared/logx/logx.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Package logx provides a lightweight, implementation-agnostic logging API.
2+
//
3+
// It defines a universal Logger interface with support for structured logging,
4+
// log levels (debug, info, warn, error), and contextual attributes.
5+
// Implementations can wrap any underlying logging library while exposing
6+
// a consistent API to application code.
7+
//
8+
// The package provides:
9+
// - Level: predefined logging severity levels.
10+
// - Attr: a key/value pair for structured log fields.
11+
// - A: helper function to quickly create an Attr.
12+
// - Noop: a Logger implementation that discards all log messages.
13+
package logx
14+
15+
// Level represents the severity of a log message.
16+
type Level int
17+
18+
const (
19+
// Debug is a level for messages for verbose output useful in development.
20+
Debug Level = iota
21+
// Info is a level for informational messages for normal application operation.
22+
Info
23+
// Warn is a level for unexpected situations that are recoverable.
24+
Warn
25+
// Error is a level for problems that require attention.
26+
Error
27+
)
28+
29+
// Attr represents a key/value pair used for structured logging.
30+
type Attr struct {
31+
Key string // The attribute name.
32+
Value any // The attribute value.
33+
}
34+
35+
// A creates an Attr from a key and value.
36+
func A(k string, v any) Attr { return Attr{Key: k, Value: v} }
37+
38+
// Logger describes a universal, implementation-agnostic logging API.
39+
type Logger interface {
40+
// Log logs a message at the given severity level with optional structured attributes.
41+
Log(lvl Level, msg string, attrs ...Attr)
42+
43+
// Debug logs a debug-level message with optional attributes.
44+
Debug(msg string, attrs ...Attr)
45+
// Info logs an informational message with optional attributes.
46+
Info(msg string, attrs ...Attr)
47+
// Warn logs a warning message with optional attributes.
48+
Warn(msg string, attrs ...Attr)
49+
// Error logs an error message with optional attributes.
50+
Error(msg string, attrs ...Attr)
51+
52+
// With returns a Logger that includes the given attributes with all future log messages.
53+
With(attrs ...Attr) Logger
54+
// Named returns a Logger with an additional name identifier for scoping logs.
55+
Named(name string) Logger
56+
57+
// Enabled reports whether logging is enabled for the given level.
58+
Enabled(lvl Level) bool
59+
}
60+
61+
// Noop is a Logger implementation that discards all log messages.
62+
type Noop struct{}
63+
64+
// Log discards the log message and attributes.
65+
func (Noop) Log(Level, string, ...Attr) {}
66+
67+
// Debug discards the debug message and attributes.
68+
func (Noop) Debug(string, ...Attr) {}
69+
70+
// Info discards the informational message and attributes.
71+
func (Noop) Info(string, ...Attr) {}
72+
73+
// Warn discards the warning message and attributes.
74+
func (Noop) Warn(string, ...Attr) {}
75+
76+
// Error discards the error message and attributes.
77+
func (Noop) Error(string, ...Attr) {}
78+
79+
// With returns the same Noop logger, ignoring the provided attributes.
80+
func (n Noop) With(...Attr) Logger { return n }
81+
82+
// Named returns the same Noop logger, ignoring the provided name.
83+
func (n Noop) Named(string) Logger { return n }
84+
85+
// Enabled always returns false, indicating logging is disabled.
86+
func (Noop) Enabled(Level) bool { return false }

shared/logxzap/logxzap.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Package logxzap provides an implementation of the logx.Logger interface
2+
// backed by the Uber Zap logging library.
3+
//
4+
// It allows applications to use the generic logx logging API while leveraging
5+
// Zap's high-performance structured logging capabilities. This helps maintain
6+
// a consistent logging abstraction across different logger backends.
7+
//
8+
// The package includes:
9+
// - Logger: an adapter that wraps zap.Logger and implements logx.Logger.
10+
// - New: creates a Logger from an existing zap.Logger.
11+
// - DefaultLogger: returns a preconfigured console logger at Debug level.
12+
//
13+
// This package is intended for use where you want to:
14+
// - Keep application logging code decoupled from the underlying logging library.
15+
// - Maintain structured, leveled logging with minimal overhead.
16+
// - Reuse the same logging interface (logx.Logger) across different backends.
17+
//
18+
// Example:
19+
//
20+
// import (
21+
// "github.com/scylladb/alternator-client-golang/shared/logx"
22+
// "github.com/scylladb/alternator-client-golang/shared/logxzap"
23+
// )
24+
//
25+
// func main() {
26+
// logger := logxzap.DefaultLogger()
27+
// logger.Info("Application started", logx.A("version", "1.0.0"))
28+
// }
29+
package logxzap
30+
31+
import (
32+
"os"
33+
34+
"go.uber.org/zap"
35+
"go.uber.org/zap/zapcore"
36+
37+
"github.com/scylladb/alternator-client-golang/shared/logx"
38+
)
39+
40+
// Logger is an adapter that implements the logx.Logger interface
41+
// using an underlying zap.Logger instance.
42+
type Logger struct {
43+
z *zap.Logger
44+
}
45+
46+
// New creates a new Logger that wraps the given zap.Logger.
47+
func New(z *zap.Logger) *Logger { return &Logger{z: z} }
48+
49+
// Log writes a log entry at the specified level with the given message
50+
// and optional structured attributes. If the level is disabled, the log
51+
// is skipped without formatting cost.
52+
func (l *Logger) Log(lvl logx.Level, msg string, attrs ...logx.Attr) {
53+
if ce := l.z.Check(toZapLevel(lvl), msg); ce != nil {
54+
ce.Write(toZapFields(attrs)...)
55+
}
56+
}
57+
58+
// Debug logs a message at the Debug level with optional structured attributes.
59+
func (l *Logger) Debug(msg string, attrs ...logx.Attr) {
60+
l.Log(logx.Debug, msg, attrs...)
61+
}
62+
63+
// Info logs a message at the Info level with optional structured attributes.
64+
func (l *Logger) Info(msg string, attrs ...logx.Attr) {
65+
l.Log(logx.Info, msg, attrs...)
66+
}
67+
68+
// Warn logs a message at the Warn level with optional structured attributes.
69+
func (l *Logger) Warn(msg string, attrs ...logx.Attr) {
70+
l.Log(logx.Warn, msg, attrs...)
71+
}
72+
73+
// Error logs a message at the Error level with optional structured attributes.
74+
func (l *Logger) Error(msg string, attrs ...logx.Attr) {
75+
l.Log(logx.Error, msg, attrs...)
76+
}
77+
78+
// With returns a new Logger instance that includes the given attributes
79+
// in all subsequent log entries.
80+
func (l *Logger) With(attrs ...logx.Attr) logx.Logger {
81+
return &Logger{z: l.z.With(toZapFields(attrs)...)}
82+
}
83+
84+
// Named returns a new Logger instance with an additional name scope.
85+
// The name appears in the log output and helps identify the log source.
86+
func (l *Logger) Named(name string) logx.Logger {
87+
return &Logger{z: l.z.Named(name)}
88+
}
89+
90+
// Enabled reports whether the specified log level is enabled.
91+
func (l *Logger) Enabled(lvl logx.Level) bool {
92+
return l.z.Core().Enabled(toZapLevel(lvl))
93+
}
94+
95+
// toZapLevel converts a logx.Level to the corresponding zapcore.Level.
96+
func toZapLevel(l logx.Level) zapcore.Level {
97+
switch l {
98+
case logx.Debug:
99+
return zapcore.DebugLevel
100+
case logx.Info:
101+
return zapcore.InfoLevel
102+
case logx.Warn:
103+
return zapcore.WarnLevel
104+
default:
105+
return zapcore.ErrorLevel
106+
}
107+
}
108+
109+
// toZapFields converts logx.Attr values to zap.Field values.
110+
func toZapFields(attrs []logx.Attr) []zap.Field {
111+
if len(attrs) == 0 {
112+
return nil
113+
}
114+
fs := make([]zap.Field, 0, len(attrs))
115+
for _, a := range attrs {
116+
fs = append(fs, zap.Any(a.Key, a.Value))
117+
}
118+
return fs
119+
}
120+
121+
// DefaultLogger creates a new Logger that writes to standard output
122+
// using zap's console encoder with ISO8601 timestamps and caller information.
123+
// Logging is enabled at the Debug level and above.
124+
func DefaultLogger() logx.Logger {
125+
cfg := zapcore.EncoderConfig{
126+
TimeKey: "T",
127+
LevelKey: "L",
128+
NameKey: "N",
129+
CallerKey: "C",
130+
MessageKey: "M",
131+
StacktraceKey: "S",
132+
LineEnding: zapcore.DefaultLineEnding,
133+
EncodeLevel: zapcore.CapitalLevelEncoder,
134+
EncodeTime: zapcore.ISO8601TimeEncoder,
135+
EncodeDuration: zapcore.StringDurationEncoder,
136+
EncodeCaller: zapcore.ShortCallerEncoder,
137+
}
138+
139+
consoleEncoder := zapcore.NewConsoleEncoder(cfg)
140+
core := zapcore.NewCore(consoleEncoder, zapcore.AddSync(os.Stdout), zap.DebugLevel)
141+
142+
return New(zap.New(core, zap.AddCaller()))
143+
}

0 commit comments

Comments
 (0)