diff --git a/go.mod b/go.mod index 3db1f26..856afe8 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/jfrog/gofrog go 1.22 require ( + github.com/gookit/color v1.5.4 github.com/jfrog/archiver/v3 v3.6.0 github.com/minio/sha256-simd v1.0.1 github.com/pkg/errors v0.9.1 @@ -25,6 +26,7 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/ulikunitz/xz v0.5.11 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect + github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect golang.org/x/sync v0.6.0 golang.org/x/sys v0.17.0 // indirect golang.org/x/term v0.17.0 // indirect diff --git a/go.sum b/go.sum index e8a7206..65659ae 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5Jflh github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/jfrog/archiver/v3 v3.6.0 h1:OVZ50vudkIQmKMgA8mmFF9S0gA47lcag22N13iV3F1w= github.com/jfrog/archiver/v3 v3.6.0/go.mod h1:fCAof46C3rAXgZurS8kNRNdSVMKBbZs+bNNhPYxLldI= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= @@ -45,6 +47,8 @@ github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/log/logger.go b/log/logger.go new file mode 100644 index 0000000..629c420 --- /dev/null +++ b/log/logger.go @@ -0,0 +1,189 @@ +package log + +import ( + "fmt" + "io" + "log" + "os" + "strings" + "sync" + + "github.com/gookit/color" +) + +const ( + _logLevelEnv = "JFROG_LOG_LEVEL" +) + +type Log interface { + Debug(a ...interface{}) + Info(a ...interface{}) + Warn(a ...interface{}) + Error(a ...interface{}) + Output(a ...interface{}) + GetLogLevel() LevelType +} + +var ( + // The logger instance + _logger *Logger + // Used to ensure _logger is initialized only once + once sync.Once +) + +func GetLogger() *Logger { + once.Do(func() { + _logger = NewLogger(getLogLevel()) + }) + return _logger +} + +type LevelType int + +const ( + ERROR LevelType = iota + WARN + INFO + DEBUG +) + +func getLogLevel() LevelType { + switch strings.ToUpper(os.Getenv(_logLevelEnv)) { + case "ERROR": + return ERROR + case "WARN": + return WARN + case "DEBUG": + return DEBUG + default: + return INFO + } +} + +type Logger struct { + LogLevel LevelType + OutputLog *log.Logger + DebugLog *log.Logger + InfoLog *log.Logger + WarnLog *log.Logger + ErrorLog *log.Logger + // Mutex to protect access to the logger + mu sync.Mutex +} + +func NewLogger(logLevel LevelType) *Logger { + logger := new(Logger) + logger.SetLogLevel(logLevel) + logger.SetOutputWriter() + logger.SetLogsWriter() + return logger +} + +func (logger *Logger) SetLogLevel(levelEnum LevelType) { + logger.mu.Lock() + defer logger.mu.Unlock() + logger.LogLevel = levelEnum +} + +func (logger *Logger) SetOutputWriter() { + logger.OutputLog = log.New(io.Writer(os.Stdout), "", 0) +} + +func (logger *Logger) SetLogsWriter() { + stdErrWriter := io.Writer(os.Stderr) + logger.DebugLog = log.New(stdErrWriter, getLogPrefix(DEBUG), 0) + logger.InfoLog = log.New(stdErrWriter, getLogPrefix(INFO), 0) + logger.WarnLog = log.New(stdErrWriter, getLogPrefix(WARN), 0) + logger.ErrorLog = log.New(stdErrWriter, getLogPrefix(ERROR), 0) +} + +var prefixStyles = map[LevelType]struct { + logLevel string + color color.Color +}{ + DEBUG: {logLevel: "Debug", color: color.Cyan}, + INFO: {logLevel: "Info", color: color.Blue}, + WARN: {logLevel: "Warn", color: color.Yellow}, + ERROR: {logLevel: "Error", color: color.Red}, +} + +func getLogPrefix(logType LevelType) string { + if logPrefixStyle, ok := prefixStyles[logType]; ok { + return fmt.Sprintf("[%s] ", logPrefixStyle.logLevel) + } + return "" +} + +func Debug(a ...interface{}) { + GetLogger().Debug(a...) +} + +func Debugf(format string, a ...interface{}) { + GetLogger().Debug(fmt.Sprintf(format, a...)) +} + +func Info(a ...interface{}) { + GetLogger().Info(a...) +} + +func Infof(format string, a ...interface{}) { + GetLogger().Info(fmt.Sprintf(format, a...)) +} + +func Warn(a ...interface{}) { + GetLogger().Warn(a...) +} + +func Error(a ...interface{}) { + GetLogger().Error(a...) +} + +func Output(a ...interface{}) { + GetLogger().Output(a...) +} + +func (logger *Logger) GetLogLevel() LevelType { + return logger.LogLevel +} + +func (logger *Logger) Debug(a ...interface{}) { + logger.mu.Lock() + defer logger.mu.Unlock() + if logger.GetLogLevel() >= DEBUG { + logger.Println(logger.DebugLog, a...) + } +} + +func (logger *Logger) Info(a ...interface{}) { + logger.mu.Lock() + defer logger.mu.Unlock() + if logger.GetLogLevel() >= INFO { + logger.Println(logger.InfoLog, a...) + } +} + +func (logger *Logger) Warn(a ...interface{}) { + logger.mu.Lock() + defer logger.mu.Unlock() + if logger.GetLogLevel() >= WARN { + logger.Println(logger.WarnLog, a...) + } +} + +func (logger *Logger) Error(a ...interface{}) { + logger.mu.Lock() + defer logger.mu.Unlock() + if logger.GetLogLevel() >= ERROR { + logger.Println(logger.ErrorLog, a...) + } +} + +func (logger *Logger) Output(a ...interface{}) { + logger.mu.Lock() + defer logger.mu.Unlock() + logger.Println(logger.OutputLog, a...) +} + +func (logger *Logger) Println(log *log.Logger, values ...interface{}) { + log.Println(values...) +} diff --git a/log/logger_test.go b/log/logger_test.go new file mode 100644 index 0000000..fcef381 --- /dev/null +++ b/log/logger_test.go @@ -0,0 +1,67 @@ +package log + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func SetEnvironmentVariableForLogLevel(t *testing.T, level string) { + assert.NoError(t, os.Setenv(_logLevelEnv, level)) +} + +func ResetEnvironmentVariableForLogLevel(t *testing.T) { + assert.NoError(t, os.Unsetenv(_logLevelEnv)) +} + +func TestLogger_WithDefaultInfoLogLevel_LogsInfoAndAbove(t *testing.T) { + // Ensure default INFO level + SetEnvironmentVariableForLogLevel(t, "") + defer ResetEnvironmentVariableForLogLevel(t) + + logger := NewLogger(getLogLevel()) + + assert.Equal(t, INFO, logger.GetLogLevel()) +} + +func TestLogger_WithEnvironmentVariableSetToDebug_LogsAllLevels(t *testing.T) { + SetEnvironmentVariableForLogLevel(t, "DEBUG") + defer ResetEnvironmentVariableForLogLevel(t) + + logger := NewLogger(getLogLevel()) + + assert.Equal(t, DEBUG, logger.GetLogLevel()) +} + +func TestLogger_WithEnvironmentVariableSetToError_LogsOnlyErrors(t *testing.T) { + SetEnvironmentVariableForLogLevel(t, "ERROR") + defer ResetEnvironmentVariableForLogLevel(t) + + logger := NewLogger(getLogLevel()) + + assert.Equal(t, ERROR, logger.GetLogLevel()) +} + +func TestLogger_SetLogLevelChangesLogLevelAtRuntime(t *testing.T) { + logger := NewLogger(INFO) + logger.SetLogLevel(DEBUG) + + assert.Equal(t, DEBUG, logger.GetLogLevel()) +} + +func TestLogger_ConcurrentAccessToSetLogLevel_DoesNotPanic(t *testing.T) { + logger := NewLogger(INFO) + + done := make(chan bool) + for i := range 10 { + go func() { + logger.SetLogLevel(LevelType(i % 4)) + done <- true + }() + } + + for range 10 { + <-done + } +}