Skip to content

Commit 0551533

Browse files
committed
Add logger to httpclient_rate_handler_test.go and refactor zaplogger_structured_messaging_test.go
1 parent b0b50ec commit 0551533

File tree

5 files changed

+386
-186
lines changed

5 files changed

+386
-186
lines changed

httpclient/httpclient_rate_handler_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88
"time"
99

10+
"github.com/deploymenttheory/go-api-http-client/logger"
1011
"github.com/stretchr/testify/assert"
1112
)
1213

@@ -84,7 +85,7 @@ func TestParseRateLimitHeaders(t *testing.T) {
8485
resp.Header.Add(k, v)
8586
}
8687

87-
wait := parseRateLimitHeaders(resp, &mockLogger{})
88+
wait := parseRateLimitHeaders(resp, logger.NewMockLogger())
8889

8990
// Allow a small margin of error due to processing time
9091
assert.InDelta(t, tt.expectedWait, wait, float64(1*time.Second), "Wait duration should be within expected range")

logger/zaplogger_logger.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// zaplogger_logger.go
2+
// Ref: https://betterstack.com/community/guides/logging/go/zap/#logging-errors-with-zap
3+
package logger
4+
5+
import (
6+
"fmt"
7+
"os"
8+
9+
"go.uber.org/zap"
10+
"go.uber.org/zap/zapcore"
11+
)
12+
13+
// Logger interface with structured logging capabilities at various levels.
14+
type Logger interface {
15+
SetLevel(level LogLevel)
16+
Debug(msg string, fields ...zapcore.Field)
17+
Info(msg string, fields ...zapcore.Field)
18+
Warn(msg string, fields ...zapcore.Field)
19+
Error(msg string, fields ...zapcore.Field) error
20+
Panic(msg string, fields ...zapcore.Field)
21+
Fatal(msg string, fields ...zapcore.Field)
22+
With(fields ...zapcore.Field) Logger
23+
GetLogLevel() LogLevel
24+
}
25+
26+
// defaultLogger is an implementation of the Logger interface using Uber's zap logging library.
27+
// It provides structured, leveled logging capabilities. The logLevel field controls the verbosity
28+
// of the logs that this logger will produce, allowing filtering of logs based on their importance.
29+
type defaultLogger struct {
30+
logger *zap.Logger // logger holds the reference to the zap.Logger instance.
31+
logLevel LogLevel // logLevel determines the current logging level (e.g., DEBUG, INFO, WARN).
32+
}
33+
34+
// SetLevel updates the logging level of the logger. It controls the verbosity of the logs,
35+
// allowing the option to filter out less severe messages based on the specified level.
36+
func (d *defaultLogger) SetLevel(level LogLevel) {
37+
d.logLevel = level
38+
}
39+
40+
// With adds contextual key-value pairs to the logger, returning a new logger instance with the context.
41+
// This is useful for creating a logger with common fields that should be included in all subsequent log entries.
42+
func (d *defaultLogger) With(fields ...zapcore.Field) Logger {
43+
return &defaultLogger{
44+
logger: d.logger.With(fields...),
45+
logLevel: d.logLevel,
46+
}
47+
}
48+
49+
// GetLogLevel returns the current logging level of the logger. This allows for checking the logger's
50+
// verbosity level programmatically, which can be useful in conditional logging scenarios.
51+
func (d *defaultLogger) GetLogLevel() LogLevel {
52+
return d.logLevel
53+
}
54+
55+
// Debug logs a message at the Debug level. This level is typically used for detailed troubleshooting
56+
// information that is only relevant during active development or debugging.
57+
func (d *defaultLogger) Debug(msg string, fields ...zapcore.Field) {
58+
if d.logLevel <= LogLevelDebug {
59+
d.logger.Debug(msg, fields...)
60+
}
61+
}
62+
63+
// Info logs a message at the Info level. This level is used for informational messages that highlight
64+
// the normal operation of the application.
65+
func (d *defaultLogger) Info(msg string, fields ...zapcore.Field) {
66+
if d.logLevel <= LogLevelInfo {
67+
d.logger.Info(msg, fields...)
68+
}
69+
}
70+
71+
// Warn logs a message at the Warn level. This level is used for potentially harmful situations or to
72+
// indicate that some issues may require attention.
73+
func (d *defaultLogger) Warn(msg string, fields ...zapcore.Field) {
74+
if d.logLevel <= LogLevelWarn {
75+
d.logger.Warn(msg, fields...)
76+
}
77+
}
78+
79+
// Error logs a message at the Error level. This level is used to log error events that might still allow
80+
// the application to continue running.
81+
// Error logs a message at the Error level and returns a formatted error.
82+
func (d *defaultLogger) Error(msg string, fields ...zapcore.Field) error {
83+
if d.logLevel <= LogLevelError {
84+
d.logger.Error(msg, fields...)
85+
}
86+
return fmt.Errorf(msg)
87+
}
88+
89+
// Panic logs a message at the Panic level and then panics. This level is used to log severe error events
90+
// that will likely lead the application to abort.
91+
func (d *defaultLogger) Panic(msg string, fields ...zapcore.Field) {
92+
if d.logLevel <= LogLevelPanic {
93+
d.logger.Panic(msg, fields...)
94+
}
95+
}
96+
97+
// Fatal logs a message at the Fatal level and then calls os.Exit(1). This level is used to log severe
98+
// error events that will result in the termination of the application.
99+
func (d *defaultLogger) Fatal(msg string, fields ...zapcore.Field) {
100+
if d.logLevel <= LogLevelFatal {
101+
d.logger.Fatal(msg, fields...)
102+
}
103+
}
104+
105+
// GetLoggerBasedOnEnv returns a zap.Logger instance configured for either
106+
// production or development based on the APP_ENV environment variable.
107+
// If APP_ENV is set to "development", it returns a development logger.
108+
// Otherwise, it defaults to a production logger.
109+
func GetLoggerBasedOnEnv() *zap.Logger {
110+
if os.Getenv("APP_ENV") == "development" {
111+
logger, err := zap.NewDevelopment()
112+
if err != nil {
113+
panic(err) // Handle error according to your application's error policy
114+
}
115+
return logger
116+
}
117+
118+
logger, err := zap.NewProduction()
119+
if err != nil {
120+
panic(err) // Handle error according to your application's error policy
121+
}
122+
return logger
123+
}

logger/zaplogger_logger_test.go

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
// zaplogger_logger_test.go
2+
package logger
3+
4+
import (
5+
"fmt"
6+
"os"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/mock"
11+
"go.uber.org/zap"
12+
"go.uber.org/zap/zapcore"
13+
)
14+
15+
// osExit is a variable that holds a reference to os.Exit function.
16+
// It allows overriding os.Exit in tests to prevent exiting the test runner.
17+
var osExit = os.Exit
18+
19+
// MockLogger is a mock type for the Logger interface, embedding a *zap.Logger to satisfy the type requirement.
20+
type MockLogger struct {
21+
mock.Mock
22+
*zap.Logger
23+
}
24+
25+
// NewMockLogger creates a new instance of MockLogger with an embedded no-op *zap.Logger.
26+
func NewMockLogger() *MockLogger {
27+
return &MockLogger{
28+
Logger: zap.NewNop(),
29+
}
30+
}
31+
32+
// TestDefaultLogger_SetLevel tests the SetLevel method of defaultLogger
33+
func TestDefaultLogger_SetLevel(t *testing.T) {
34+
logger := zap.NewNop()
35+
dLogger := &defaultLogger{logger: logger}
36+
37+
dLogger.SetLevel(LogLevelWarn)
38+
assert.Equal(t, LogLevelWarn, dLogger.GetLogLevel())
39+
}
40+
41+
// TestDefaultLogger_With tests the With method functionality
42+
func TestDefaultLogger_With(t *testing.T) {
43+
logger := zap.NewNop()
44+
dLogger := &defaultLogger{logger: logger, logLevel: LogLevelInfo}
45+
46+
newLogger := dLogger.With(zap.String("key", "value"))
47+
assert.NotNil(t, newLogger, "New logger should not be nil")
48+
49+
// Assert that newLogger is a *defaultLogger and has a modified zap.Logger
50+
assert.IsType(t, &defaultLogger{}, newLogger, "New logger should be of type *defaultLogger")
51+
}
52+
53+
// TestDefaultLogger_GetLogLevel verifies that the GetLogLevel method of the defaultLogger struct
54+
// accurately returns the logger's current log level setting. This test ensures that the log level
55+
// set within the defaultLogger is properly retrievable.
56+
func TestDefaultLogger_GetLogLevel(t *testing.T) {
57+
// Define test cases for each log level
58+
logLevels := []struct {
59+
level LogLevel
60+
expected LogLevel
61+
}{
62+
{LogLevelDebug, LogLevelDebug},
63+
{LogLevelInfo, LogLevelInfo},
64+
{LogLevelWarn, LogLevelWarn},
65+
{LogLevelError, LogLevelError},
66+
{LogLevelDPanic, LogLevelDPanic},
67+
{LogLevelPanic, LogLevelPanic},
68+
{LogLevelFatal, LogLevelFatal},
69+
}
70+
71+
for _, tc := range logLevels {
72+
t.Run(fmt.Sprintf("LogLevel %d", tc.level), func(t *testing.T) {
73+
dLogger := &defaultLogger{logLevel: tc.level}
74+
75+
// Assert that GetLogLevel returns the correct log level for each case
76+
assert.Equal(t, tc.expected, dLogger.GetLogLevel(), fmt.Sprintf("GetLogLevel should return %d for set log level %d", tc.expected, tc.level))
77+
})
78+
}
79+
}
80+
81+
// TestDefaultLogger_Debug verifies that the Debug method of the defaultLogger struct correctly
82+
// invokes the underlying zap.Logger's Debug method when the log level is set to allow Debug messages.
83+
// The test uses a mockLogger to simulate the zap.Logger behavior, allowing verification of method calls
84+
// without actual logging output. This ensures that the Debug method adheres to the expected behavior
85+
// based on the current log level setting, providing confidence in the logging logic's correctness.
86+
func TestDefaultLogger_Debug(t *testing.T) {
87+
mockLogger := NewMockLogger()
88+
dLogger := &defaultLogger{logger: mockLogger.Logger, logLevel: LogLevelDebug}
89+
90+
mockLogger.On("Debug", "test message", mock.Anything).Once()
91+
92+
dLogger.Debug("test message")
93+
94+
mockLogger.AssertExpectations(t)
95+
}
96+
97+
// TestDefaultLogger_Info verifies the Info method of the defaultLogger struct.
98+
// It ensures that Info logs messages at the Info level when the logger's level allows for it.
99+
// The test uses a mockLogger to intercept and verify the call to the underlying zap.Logger's Info method.
100+
func TestDefaultLogger_Info(t *testing.T) {
101+
mockLogger := NewMockLogger()
102+
dLogger := &defaultLogger{logger: mockLogger.Logger, logLevel: LogLevelInfo}
103+
104+
mockLogger.On("Info", "info message", mock.Anything).Once()
105+
106+
dLogger.Info("info message")
107+
108+
mockLogger.AssertExpectations(t)
109+
}
110+
111+
// TestDefaultLogger_Warn verifies the Warn method of the defaultLogger struct.
112+
// This test checks that Warn correctly logs messages at the Warn level based on the logger's current level.
113+
// The behavior is validated using a mockLogger to capture and assert the call to the zap.Logger's Warn method.
114+
func TestDefaultLogger_Warn(t *testing.T) {
115+
mockLogger := NewMockLogger()
116+
dLogger := &defaultLogger{logger: mockLogger.Logger, logLevel: LogLevelWarn}
117+
118+
mockLogger.On("Warn", "warn message", mock.Anything).Once()
119+
120+
dLogger.Warn("warn message")
121+
122+
mockLogger.AssertExpectations(t)
123+
}
124+
125+
// TestDefaultLogger_Error checks the functionality of the Error method in the defaultLogger struct.
126+
// It ensures that Error logs messages at the Error level and returns an error as expected.
127+
// The test utilizes a mockLogger to track and affirm the invocation of zap.Logger's Error method.
128+
func TestDefaultLogger_Error(t *testing.T) {
129+
mockLogger := NewMockLogger()
130+
dLogger := &defaultLogger{logger: mockLogger.Logger, logLevel: LogLevelError}
131+
132+
expectedErrorMsg := "error message"
133+
mockLogger.On("Error", expectedErrorMsg, mock.Anything).Once()
134+
135+
err := dLogger.Error(expectedErrorMsg)
136+
137+
assert.Error(t, err)
138+
assert.Contains(t, err.Error(), expectedErrorMsg)
139+
mockLogger.AssertExpectations(t)
140+
}
141+
142+
// TestDefaultLogger_Panic ensures the Panic method of the defaultLogger behaves correctly.
143+
// This test verifies that Panic logs messages at the Panic level and triggers a panic as expected.
144+
// Due to the nature of panic, this test needs to recover from the panic to verify the behavior.
145+
func TestDefaultLogger_Panic(t *testing.T) {
146+
mockLogger := NewMockLogger()
147+
dLogger := &defaultLogger{logger: mockLogger.Logger, logLevel: LogLevelPanic}
148+
149+
mockLogger.On("Panic", "panic message", mock.Anything).Once()
150+
151+
assert.Panics(t, func() { dLogger.Panic("panic message") }, "The Panic method should trigger a panic")
152+
153+
mockLogger.AssertExpectations(t)
154+
}
155+
156+
// TestDefaultLogger_Fatal tests the Fatal method of the defaultLogger struct.
157+
// It confirms that Fatal logs messages at the Fatal level and then terminates the program.
158+
// Given the os.Exit call in Fatal, this test might need to intercept the os.Exit call to prevent test suite termination.
159+
func TestDefaultLogger_Fatal(t *testing.T) {
160+
mockLogger := NewMockLogger()
161+
dLogger := &defaultLogger{logger: mockLogger.Logger, logLevel: LogLevelFatal}
162+
163+
mockLogger.On("Fatal", "fatal message", mock.Anything).Once()
164+
165+
// Intercept os.Exit calls
166+
originalExit := osExit
167+
defer func() { osExit = originalExit }()
168+
var exitCode int
169+
osExit = func(code int) {
170+
exitCode = code
171+
}
172+
173+
dLogger.Fatal("fatal message")
174+
175+
assert.Equal(t, 1, exitCode, "Fatal should terminate the program with exit code 1")
176+
mockLogger.AssertExpectations(t)
177+
}
178+
179+
// Debug mocks the Debug method of the Logger interface
180+
func (m *MockLogger) Debug(msg string, fields ...zapcore.Field) {
181+
m.Called(msg, fields)
182+
}
183+
184+
// Info mocks the Info method of the Logger interface
185+
func (m *MockLogger) Info(msg string, fields ...zapcore.Field) {
186+
m.Called(msg, fields)
187+
}
188+
189+
// Warn mocks the Warn method of the Logger interface
190+
func (m *MockLogger) Warn(msg string, fields ...zapcore.Field) {
191+
m.Called(msg, fields)
192+
}
193+
194+
// Error mocks the Error method of the Logger interface
195+
func (m *MockLogger) Error(msg string, fields ...zapcore.Field) error {
196+
args := m.Called(msg, fields)
197+
return args.Error(0)
198+
}
199+
200+
// With mocks the With method of the Logger interface
201+
func (m *MockLogger) With(fields ...zapcore.Field) Logger {
202+
args := m.Called(fields)
203+
return args.Get(0).(Logger)
204+
}
205+
206+
// GetLogLevel mocks the GetLogLevel method of the Logger interface
207+
func (m *MockLogger) GetLogLevel() LogLevel {
208+
args := m.Called()
209+
return args.Get(0).(LogLevel)
210+
}
211+
212+
// TestGetLoggerBasedOnEnv tests the GetLoggerBasedOnEnv function for different environment settings
213+
func TestGetLoggerBasedOnEnv(t *testing.T) {
214+
tests := []struct {
215+
name string
216+
envValue string
217+
expectedLevel zap.AtomicLevel
218+
}{
219+
{"DevelopmentLogger", "development", zap.NewAtomicLevelAt(zap.DebugLevel)},
220+
{"ProductionLogger", "production", zap.NewAtomicLevelAt(zap.InfoLevel)},
221+
{"DefaultToProduction", "", zap.NewAtomicLevelAt(zap.InfoLevel)}, // default case
222+
}
223+
224+
for _, tt := range tests {
225+
t.Run(tt.name, func(t *testing.T) {
226+
// Set APP_ENV to the desired test value
227+
os.Setenv("APP_ENV", tt.envValue)
228+
defer os.Unsetenv("APP_ENV") // Clean up
229+
230+
logger := GetLoggerBasedOnEnv()
231+
232+
// Since we cannot directly access the logger's level, we check the logger's development/production status
233+
// which indirectly tells us about the log level configuration
234+
cfg := zap.NewProductionConfig()
235+
if tt.envValue == "development" {
236+
cfg = zap.NewDevelopmentConfig()
237+
}
238+
239+
assert.Equal(t, cfg.Level.Level(), logger.Core().Enabled(zapcore.Level(tt.expectedLevel.Level())), "Logger level should match expected")
240+
})
241+
}
242+
}

0 commit comments

Comments
 (0)