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 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
317a096
Add charmbracelet logger tests; rename NewLogger function
Cerebrovinny Mar 8, 2025
da9600b
Refactor logger init, add charmbracelet support
Cerebrovinny Mar 8, 2025
39cdea6
Add logger pkg, refactor logging setup, add tests
Cerebrovinny Mar 8, 2025
675b318
Refactor logger init to use pointer config, update tests
Cerebrovinny Mar 8, 2025
fdfb3bb
Remove alias for config import; add file permissions const
Cerebrovinny Mar 8, 2025
f8ce952
Improve error logging for CLI config issues
Cerebrovinny Mar 8, 2025
3abe383
Improve error handling; add const for log file permissions
Cerebrovinny Mar 8, 2025
3bd7849
Improve error handling in CLI config; update log file comment.
Cerebrovinny Mar 9, 2025
00efab5
Handle config errors without exiting; return error in Execute
Cerebrovinny Mar 9, 2025
6113510
rollback root
Cerebrovinny Mar 9, 2025
5a7f7a9
Refactor logger setup; use config-driven initialization
Cerebrovinny Mar 9, 2025
8634014
Refactor logger setup; remove error handling, add io.Writer
Cerebrovinny Mar 9, 2025
3efcee9
revert alias
Cerebrovinny Mar 9, 2025
a3e053e
Add TRACE log level, refactor logger functions
Cerebrovinny Mar 10, 2025
15acbf6
Add yaml identation support for atmos describe
Cerebrovinny Mar 10, 2025
3201ab1
Merge branch 'main' into feature/DEV-3010-atmos-logger
Cerebrovinny Mar 10, 2025
0c1eb4b
Revert "Add yaml identation support for atmos describe"
Cerebrovinny Mar 10, 2025
2dfcbd5
Update pkg/logger/charmbracelet.go
Cerebrovinny Mar 10, 2025
a9e4709
Update pkg/logger/charmbracelet.go
Cerebrovinny Mar 10, 2025
8f21c58
pull back new name
Cerebrovinny Mar 10, 2025
e615ef9
Merge remote-tracking branch 'origin/feature/DEV-3010-atmos-logger' i…
Cerebrovinny Mar 10, 2025
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// GetCharmLogger returns a pre-configured Charmbracelet logger with Atmos styling.
// GetAtmosLogger returns a pre-configured Charmbracelet logger with Atmos styling.

func GetCharmLogger() *log.Logger {
Copy link
Member

@osterman osterman Mar 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func GetCharmLogger() *log.Logger {
func NewAtmosLogger() *log.Logger {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not getting an existing logger but creating a new one.

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 {
Comment on lines +27 to +28
Copy link
Member

@osterman osterman Mar 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// GetCharmLoggerWithOutput returns a pre-configured Charmbracelet logger with custom output.
func GetCharmLoggerWithOutput(output *os.File) *log.Logger {
// NewAtmosLoggerWithOutput returns a pre-configured Charmbracelet logger with custom output.
func NewAtmosLoggerWithOutput(output *os.File) *log.Logger {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also creating a new 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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
errorLevelLabel = "ERROR"
errorLevelLabel = "ERRO"

warnLevelLabel = "WARN"
infoLevelLabel = "INFO"
debugLevelLabel = "DEBU"
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() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this used?

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