Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 49 additions & 64 deletions pkg/logger/logger.go
Original file line number Diff line number Diff line change
@@ -1,172 +1,157 @@
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

// Package logger provides a logging capability for toolhive for running locally as a CLI and in Kubernetes
// Package logger provides a logging capability for toolhive for running locally as a CLI and in Kubernetes.
// This package re-exports the logger from toolhive-core and provides viper integration.
package logger

import (
"strconv"
"time"

"github.com/go-logr/logr"
"github.com/go-logr/zapr"
"github.com/spf13/viper"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"

"github.com/stacklok/toolhive-core/env"
corelogger "github.com/stacklok/toolhive-core/logger"
)

// viperDebugProvider implements the DebugProvider interface using viper configuration.
type viperDebugProvider struct{}

// IsDebug returns true if the "debug" viper flag is set.
func (*viperDebugProvider) IsDebug() bool {
return viper.GetBool("debug")
}

// Debug logs a message at debug level using the singleton logger.
func Debug(msg string) {
zap.S().Debug(msg)
corelogger.Debug(msg)
}

// Debugf logs a message at debug level using the singleton logger.
func Debugf(msg string, args ...any) {
zap.S().Debugf(msg, args...)
corelogger.Debugf(msg, args...)
}

// Debugw logs a message at debug level using the singleton logger with additional key-value pairs.
func Debugw(msg string, keysAndValues ...any) {
zap.S().Debugw(msg, keysAndValues...)
corelogger.Debugw(msg, keysAndValues...)
}

// Info logs a message at info level using the singleton logger.
func Info(msg string) {
zap.S().Info(msg)
corelogger.Info(msg)
}

// Infof logs a message at info level using the singleton logger.
func Infof(msg string, args ...any) {
zap.S().Infof(msg, args...)
corelogger.Infof(msg, args...)
}

// Infow logs a message at info level using the singleton logger with additional key-value pairs.
func Infow(msg string, keysAndValues ...any) {
zap.S().Infow(msg, keysAndValues...)
corelogger.Infow(msg, keysAndValues...)
}

// Warn logs a message at warning level using the singleton logger.
func Warn(msg string) {
zap.S().Warn(msg)
corelogger.Warn(msg)
}

// Warnf logs a message at warning level using the singleton logger.
func Warnf(msg string, args ...any) {
zap.S().Warnf(msg, args...)
corelogger.Warnf(msg, args...)
}

// Warnw logs a message at warning level using the singleton logger with additional key-value pairs.
func Warnw(msg string, keysAndValues ...any) {
zap.S().Warnw(msg, keysAndValues...)
corelogger.Warnw(msg, keysAndValues...)
}

// Error logs a message at error level using the singleton logger.
func Error(msg string) {
zap.S().Error(msg)
corelogger.Error(msg)
}

// Errorf logs a message at error level using the singleton logger.
func Errorf(msg string, args ...any) {
zap.S().Errorf(msg, args...)
corelogger.Errorf(msg, args...)
}

// Errorw logs a message at error level using the singleton logger with additional key-value pairs.
func Errorw(msg string, keysAndValues ...any) {
zap.S().Errorw(msg, keysAndValues...)
corelogger.Errorw(msg, keysAndValues...)
}

// Panic logs a message at error level using the singleton logger and panics the program.
func Panic(msg string) {
zap.S().Panic(msg)
corelogger.Panic(msg)
}

// Panicf logs a message at error level using the singleton logger and panics the program.
func Panicf(msg string, args ...any) {
zap.S().Panicf(msg, args...)
corelogger.Panicf(msg, args...)
}

// Panicw logs a message at error level using the singleton logger with additional key-value pairs and panics the program.
func Panicw(msg string, keysAndValues ...any) {
zap.S().Panicw(msg, keysAndValues...)
corelogger.Panicw(msg, keysAndValues...)
}

// DPanic logs a message at error level using the singleton logger and panics the program.
func DPanic(msg string) {
zap.S().DPanic(msg)
corelogger.DPanic(msg)
}

// DPanicf logs a message at error level using the singleton logger and panics the program.
func DPanicf(msg string, args ...any) {
zap.S().DPanicf(msg, args...)
corelogger.DPanicf(msg, args...)
}

// DPanicw logs a message at error level using the singleton logger with additional key-value pairs and panics the program.
func DPanicw(msg string, keysAndValues ...any) {
zap.S().DPanicw(msg, keysAndValues...)
corelogger.DPanicw(msg, keysAndValues...)
}

// Fatal logs a message at error level using the singleton logger and exits the program.
func Fatal(msg string) {
zap.S().Fatal(msg)
corelogger.Fatal(msg)
}

// Fatalf logs a message at error level using the singleton logger and exits the program.
func Fatalf(msg string, args ...any) {
zap.S().Fatalf(msg, args...)
corelogger.Fatalf(msg, args...)
}

// Fatalw logs a message at error level using the singleton logger with additional key-value pairs and exits the program.
func Fatalw(msg string, keysAndValues ...any) {
zap.S().Fatalw(msg, keysAndValues...)
corelogger.Fatalw(msg, keysAndValues...)
}

// NewLogr returns a logr.Logger which uses zap logger
func NewLogr() logr.Logger {
return zapr.NewLogger(zap.L())
return corelogger.NewLogr()
}

// Initialize creates and configures the appropriate logger.
// If the UNSTRUCTURED_LOGS is set to true, it will output plain log message
// with only time and LogLevelType (INFO, DEBUG, ERROR, WARN)).
// Otherwise it will create a standard structured slog logger
// Otherwise it will create a standard structured slog logger.
// This version uses viper for the debug flag configuration.
func Initialize() {
InitializeWithEnv(&env.OSReader{})
corelogger.InitializeWithDebug(&viperDebugProvider{})
}

// InitializeWithEnv creates and configures the appropriate logger with a custom environment reader.
// This allows for dependency injection of environment variable access for testing.
func InitializeWithEnv(envReader env.Reader) {
var config zap.Config
if unstructuredLogsWithEnv(envReader) {
config = zap.NewDevelopmentConfig()
config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
config.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout(time.Kitchen)
config.OutputPaths = []string{"stderr"}
config.DisableStacktrace = true
config.DisableCaller = true
} else {
config = zap.NewProductionConfig()
config.OutputPaths = []string{"stdout"}
}

// Set log level based on current debug flag
if viper.GetBool("debug") {
config.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
} else {
config.Level = zap.NewAtomicLevelAt(zap.InfoLevel)
}

zap.ReplaceGlobals(zap.Must(config.Build()))
}

func unstructuredLogsWithEnv(envReader env.Reader) bool {
unstructuredLogs, err := strconv.ParseBool(envReader.Getenv("UNSTRUCTURED_LOGS"))
if err != nil {
// at this point if the error is not nil, the env var wasn't set, or is ""
// which means we just default to outputting unstructured logs.
return true
}
return unstructuredLogs
// Deprecated: Use InitializeWithOptions from toolhive-core/logger instead.
func InitializeWithEnv(envReader interface{ Getenv(key string) string }) {
// Create a wrapper that implements env.Reader
corelogger.InitializeWithOptions(&envReaderWrapper{reader: envReader}, &viperDebugProvider{})
}

// envReaderWrapper wraps any type with Getenv method to implement env.Reader
type envReaderWrapper struct {
reader interface{ Getenv(key string) string }
}

func (w *envReaderWrapper) Getenv(key string) string {
return w.reader.Getenv(key)
}
34 changes: 18 additions & 16 deletions pkg/logger/logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,40 +10,42 @@ import (
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"go.uber.org/zap/zaptest/observer"

"github.com/stacklok/toolhive-core/env/mocks"
)

// TestUnstructuredLogsCheck tests the unstructuredLogs function
func TestUnstructuredLogsCheck(t *testing.T) {
// TestViperDebugProvider tests the viper-based debug provider
func TestViperDebugProvider(t *testing.T) {
t.Parallel()

tests := []struct {
name string
envValue string
debug bool
expected bool
}{
{"Default Case", "", true},
{"Explicitly True", "true", true},
{"Explicitly False", "false", false},
{"Invalid Value", "not-a-bool", true},
{"Debug Enabled", true, true},
{"Debug Disabled", false, false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// Create a new viper instance for this test
v := viper.New()
v.Set("debug", tt.debug)

mockEnv := mocks.NewMockReader(ctrl)
mockEnv.EXPECT().Getenv("UNSTRUCTURED_LOGS").Return(tt.envValue)
// Use the test viper instance
oldViper := viper.GetViper()
defer func() {
// Reset to original viper
*viper.GetViper() = *oldViper
}()
*viper.GetViper() = *v

if got := unstructuredLogsWithEnv(mockEnv); got != tt.expected {
t.Errorf("unstructuredLogsWithEnv() = %v, want %v", got, tt.expected)
provider := &viperDebugProvider{}
if got := provider.IsDebug(); got != tt.expected {
t.Errorf("viperDebugProvider.IsDebug() = %v, want %v", got, tt.expected)
}
})
}
Expand Down