Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Style Atmos Logger with Theme #1121

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion internal/exec/describe_affected.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func parseDescribeAffectedCliArgs(cmd *cobra.Command, args []string) (DescribeAf
if err != nil {
return DescribeAffectedCmdArgs{}, err
}
logger, err := l.NewLoggerFromCliConfig(atmosConfig)
logger, err := l.InitializeLoggerFromCliConfig(&atmosConfig)
if err != nil {
return DescribeAffectedCmdArgs{}, err
}
Expand Down
2 changes: 1 addition & 1 deletion internal/exec/pro.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func parseLockUnlockCliArgs(cmd *cobra.Command, args []string) (ProLockUnlockCmd
return ProLockUnlockCmdArgs{}, err
}

logger, err := l.NewLoggerFromCliConfig(atmosConfig)
logger, err := l.InitializeLoggerFromCliConfig(&atmosConfig)
if err != nil {
return ProLockUnlockCmdArgs{}, err
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/config/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,6 @@ const (
AtmosYamlFuncEnv = "!env"

TerraformDefaultWorkspace = "default"

StandardFilePermissions = 0o644
)
147 changes: 147 additions & 0 deletions pkg/logger/charmbracelet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package logger

import (
"os"

"github.com/charmbracelet/lipgloss"
log "github.com/charmbracelet/log"
"github.com/cloudposse/atmos/pkg/ui/theme"
)

const TraceLevel log.Level = log.DebugLevel - 1

var helperLogger *log.Logger

func init() {
helperLogger = GetCharmLogger()
}

// GetCharmLogger returns a pre-configured Charmbracelet logger with Atmos styling.
func GetCharmLogger() *log.Logger {
styles := getAtmosLogStyles()
logger := log.New(os.Stderr)
logger.SetStyles(styles)
return logger
}

// GetCharmLoggerWithOutput returns a pre-configured Charmbracelet logger with custom output.
func GetCharmLoggerWithOutput(output *os.File) *log.Logger {
styles := getAtmosLogStyles()
logger := log.New(output)
logger.SetStyles(styles)
return logger
}

// getAtmosLogStyles returns custom styles for the Charmbracelet logger using Atmos theme colors.
func getAtmosLogStyles() *log.Styles {
styles := log.DefaultStyles()

const (
paddingVertical = 0
paddingHorizontal = 1
)

configureLogLevelStyles(styles, paddingVertical, paddingHorizontal)
configureKeyStyles(styles)

return styles
}

// configureLogLevelStyles configures the styles for different log levels.
func configureLogLevelStyles(styles *log.Styles, paddingVertical, paddingHorizontal int) {
const (
errorLevelLabel = "ERROR"
warnLevelLabel = "WARN"
infoLevelLabel = "INFO"
debugLevelLabel = "DEBUG"
traceLevelLabel = "TRACE"
)

// Style `Error` log messages.
styles.Levels[log.ErrorLevel] = lipgloss.NewStyle().
SetString(errorLevelLabel).
Padding(paddingVertical, paddingHorizontal, paddingVertical, paddingHorizontal).
Background(lipgloss.Color(theme.ColorPink)).
Foreground(lipgloss.Color(theme.ColorWhite))

// Style `Warning` log messages.
styles.Levels[log.WarnLevel] = lipgloss.NewStyle().
SetString(warnLevelLabel).
Padding(paddingVertical, paddingHorizontal, paddingVertical, paddingHorizontal).
Background(lipgloss.Color(theme.ColorPink)).
Foreground(lipgloss.Color(theme.ColorDarkGray))

// Style `Info` log messages.
styles.Levels[log.InfoLevel] = lipgloss.NewStyle().
SetString(infoLevelLabel).
Padding(paddingVertical, paddingHorizontal, paddingVertical, paddingHorizontal).
Background(lipgloss.Color(theme.ColorCyan)).
Foreground(lipgloss.Color(theme.ColorDarkGray))

// Style `Debug` log messages.
styles.Levels[log.DebugLevel] = lipgloss.NewStyle().
SetString(debugLevelLabel).
Padding(paddingVertical, paddingHorizontal, paddingVertical, paddingHorizontal).
Background(lipgloss.Color(theme.ColorBlue)).
Foreground(lipgloss.Color(theme.ColorWhite))

// Style `Trace` log messages.
styles.Levels[TraceLevel] = lipgloss.NewStyle().
SetString(traceLevelLabel).
Padding(paddingVertical, paddingHorizontal, paddingVertical, paddingHorizontal).
Background(lipgloss.Color(theme.ColorDarkGray)).
Foreground(lipgloss.Color(theme.ColorWhite))
}

// configureKeyStyles configures the styles for different log keys.
func configureKeyStyles(styles *log.Styles) {
const (
keyError = "error"
keyComponent = "component"
keyStack = "stack"
keyDuration = "duration"
)

// Custom style for 'err' key
styles.Keys[keyError] = lipgloss.NewStyle().Foreground(lipgloss.Color(theme.ColorPink))
styles.Values[keyError] = lipgloss.NewStyle().Bold(true)

// Custom style for 'component' key
styles.Keys[keyComponent] = lipgloss.NewStyle().Foreground(lipgloss.Color(theme.ColorPink))

// Custom style for 'stack' key
styles.Keys[keyStack] = lipgloss.NewStyle().Foreground(lipgloss.Color(theme.ColorBlue))

// Custom style for 'duration' key
styles.Keys[keyDuration] = lipgloss.NewStyle().Foreground(lipgloss.Color(theme.ColorGreen))
}

// Error logs an error message with context.
func Error(message string, keyvals ...interface{}) {
helperLogger.Error(message, keyvals...)
}

// Warn logs a warning message with context.
func Warn(message string, keyvals ...interface{}) {
helperLogger.Warn(message, keyvals...)
}

// Info logs an informational message with context.
func Info(message string, keyvals ...interface{}) {
helperLogger.Info(message, keyvals...)
}

// Debug logs a debug message with context.
func Debug(message string, keyvals ...interface{}) {
helperLogger.Debug(message, keyvals...)
}

// Trace logs a trace message with context.
func Trace(message string, keyvals ...interface{}) {
helperLogger.Log(TraceLevel, message, keyvals...)
}

// Fatal logs an error message and exits with status code 1.
func Fatal(message string, keyvals ...interface{}) {
helperLogger.Fatal(message, keyvals...)
}
92 changes: 92 additions & 0 deletions pkg/logger/charmbracelet_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package logger

import (
"os"
"path/filepath"
"testing"

"github.com/charmbracelet/lipgloss"
log "github.com/charmbracelet/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGetCharmLogger(t *testing.T) {
logger := GetCharmLogger()
require.NotNil(t, logger, "Should return a non-nil logger")

// These should not panic.
assert.NotPanics(t, func() {
logger.SetLevel(log.InfoLevel)
logger.SetTimeFormat("")
})
}

func TestGetCharmLoggerWithOutput(t *testing.T) {
tempDir := os.TempDir()
logFile := filepath.Join(tempDir, "charm_test.log")
defer os.Remove(logFile)

f, err := os.Create(logFile)
require.NoError(t, err, "Should create log file without error")

logger := GetCharmLoggerWithOutput(f)
require.NotNil(t, logger, "Should return a non-nil logger")

logger.SetTimeFormat("")
logger.Info("File test message")

f.Close()

data, err := os.ReadFile(logFile)
require.NoError(t, err, "Should read log file without error")

content := string(data)
assert.Contains(t, content, "INFO", "Should have INFO level in file")
assert.Contains(t, content, "File test message", "Should contain the message")
}

// Test the actual styling implementation.
func TestCharmLoggerStylingDetails(t *testing.T) {
styles := getAtmosLogStyles()

assert.NotEqual(t, lipgloss.Style{}, styles.Levels[log.ErrorLevel], "ERROR level should have styling")
assert.NotEqual(t, lipgloss.Style{}, styles.Levels[log.WarnLevel], "WARN level should have styling")
assert.NotEqual(t, lipgloss.Style{}, styles.Levels[log.InfoLevel], "INFO level should have styling")
assert.NotEqual(t, lipgloss.Style{}, styles.Levels[log.DebugLevel], "DEBUG level should have styling")

assert.Contains(t, styles.Levels[log.ErrorLevel].Render("ERROR"), "ERROR", "ERROR label should be styled")
assert.Contains(t, styles.Levels[log.WarnLevel].Render("WARN"), "WARN", "WARN label should be styled")
assert.Contains(t, styles.Levels[log.InfoLevel].Render("INFO"), "INFO", "INFO label should be styled")
assert.Contains(t, styles.Levels[log.DebugLevel].Render("DEBUG"), "DEBUG", "DEBUG label should be styled")

assert.NotNil(t, styles.Keys["err"], "err key should have styling")
assert.NotNil(t, styles.Values["err"], "err value should have styling")
assert.NotNil(t, styles.Keys["component"], "component key should have styling")
assert.NotNil(t, styles.Keys["stack"], "stack key should have styling")
}

func ExampleGetCharmLogger() {
logger := GetCharmLogger()

logger.SetTimeFormat("2006-01-02 15:04:05")
logger.SetLevel(log.InfoLevel)

logger.Info("User logged in", "user_id", "12345", "component", "auth")

logger.Error("Failed to process request",
"err", "connection timeout",
"component", "api",
"duration", "1.5s")

logger.Warn("Resource utilization high",
"component", "database",
"stack", "prod-ue1",
"usage", "95%")

logger.Debug("Processing request",
"request_id", "abc123",
"component", "api",
"endpoint", "/users",
"method", "GET")
}
Loading
Loading