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 13 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
69 changes: 69 additions & 0 deletions examples/quick-start-simple/test_logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package main

import (
"errors"
"time"

"github.com/cloudposse/atmos/pkg/logger"
)

var ErrTest = errors.New("this is an error")

const (
KeyComponent = "component"
KeyStack = "stack"
KeyError = "err"
KeyDuration = "duration"
KeyPath = "path"
KeyDetails = "details"
)

func main() {
// Test individual log functions
testIndividualLogFunctions()

// Test complete Logger struct
testLoggerStruct()

// Test direct charmbracelet logger
testCharmLogger()

Check warning on line 29 in examples/quick-start-simple/test_logger.go

View check run for this annotation

Codecov / codecov/patch

examples/quick-start-simple/test_logger.go#L21-L29

Added lines #L21 - L29 were not covered by tests
}

func testIndividualLogFunctions() {
logger.Info("This is an info message")
logger.Debug("This is a debug message with context", KeyComponent, "station", KeyDuration, "500ms")
logger.Warn("This is a warning message", KeyStack, "prod-ue1")
logger.Error("Whoops! Something went wrong", KeyError, "kitchen on fire", KeyComponent, "weather")

time.Sleep(500 * time.Millisecond)

Check warning on line 38 in examples/quick-start-simple/test_logger.go

View check run for this annotation

Codecov / codecov/patch

examples/quick-start-simple/test_logger.go#L32-L38

Added lines #L32 - L38 were not covered by tests
}

func testLoggerStruct() {
atmosLogger, err := logger.InitializeLogger(logger.LogLevelTrace, "")
if err != nil {
panic(err)

Check warning on line 44 in examples/quick-start-simple/test_logger.go

View check run for this annotation

Codecov / codecov/patch

examples/quick-start-simple/test_logger.go#L41-L44

Added lines #L41 - L44 were not covered by tests
}

atmosLogger.Trace("This is a trace message")
atmosLogger.Debug("This is a debug message")
atmosLogger.Info("This is an info message")
atmosLogger.Warning("This is a warning message")
atmosLogger.Error(ErrTest)

time.Sleep(500 * time.Millisecond)

Check warning on line 53 in examples/quick-start-simple/test_logger.go

View check run for this annotation

Codecov / codecov/patch

examples/quick-start-simple/test_logger.go#L47-L53

Added lines #L47 - L53 were not covered by tests
}

func testCharmLogger() {
charmLogger := logger.GetCharmLogger()

charmLogger.SetTimeFormat(time.Kitchen)

charmLogger.Info("Processing component", KeyComponent, "station", KeyStack, "dev-ue1")
charmLogger.Debug("Found configuration", KeyPath, "/stacks/deploy/us-east-1/dev/station.yaml")
charmLogger.Warn("Component configuration outdated", KeyComponent, "weather", "lastUpdated", "90 days ago")
charmLogger.Error("Failed to apply changes",
KeyError, "validation failed",
KeyComponent, "weather",
KeyDetails, "required variables missing",
KeyStack, "dev-ue1")

Check warning on line 68 in examples/quick-start-simple/test_logger.go

View check run for this annotation

Codecov / codecov/patch

examples/quick-start-simple/test_logger.go#L56-L68

Added lines #L56 - L68 were not covered by tests
}
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 @@
if err != nil {
return DescribeAffectedCmdArgs{}, err
}
logger, err := l.NewLoggerFromCliConfig(atmosConfig)
logger, err := l.InitializeLoggerFromCliConfig(&atmosConfig)

Check warning on line 51 in internal/exec/describe_affected.go

View check run for this annotation

Codecov / codecov/patch

internal/exec/describe_affected.go#L51

Added line #L51 was not covered by tests
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 @@
return ProLockUnlockCmdArgs{}, err
}

logger, err := l.NewLoggerFromCliConfig(atmosConfig)
logger, err := l.InitializeLoggerFromCliConfig(&atmosConfig)

Check warning on line 42 in internal/exec/pro.go

View check run for this annotation

Codecov / codecov/patch

internal/exec/pro.go#L42

Added line #L42 was not covered by tests
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
)
132 changes: 132 additions & 0 deletions pkg/logger/charmbracelet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package logger

import (
"os"

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

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"
)

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

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

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

// Debug.
styles.Levels[log.DebugLevel] = lipgloss.NewStyle().
SetString(debugLevelLabel).
Padding(paddingVertical, paddingHorizontal, paddingVertical, paddingHorizontal).
Background(lipgloss.Color(theme.ColorBlue)).
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...)

Check warning on line 111 in pkg/logger/charmbracelet.go

View check run for this annotation

Codecov / codecov/patch

pkg/logger/charmbracelet.go#L110-L111

Added lines #L110 - L111 were not covered by tests
}

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

Check warning on line 116 in pkg/logger/charmbracelet.go

View check run for this annotation

Codecov / codecov/patch

pkg/logger/charmbracelet.go#L115-L116

Added lines #L115 - L116 were not covered by tests
}

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

Check warning on line 121 in pkg/logger/charmbracelet.go

View check run for this annotation

Codecov / codecov/patch

pkg/logger/charmbracelet.go#L120-L121

Added lines #L120 - L121 were not covered by tests
}

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

Check warning on line 126 in pkg/logger/charmbracelet.go

View check run for this annotation

Codecov / codecov/patch

pkg/logger/charmbracelet.go#L125-L126

Added lines #L125 - L126 were not covered by tests
}

// Fatal logs an error message and exits with status code 1.
func Fatal(message string, keyvals ...interface{}) {
helperLogger.Fatal(message, keyvals...)

Check warning on line 131 in pkg/logger/charmbracelet.go

View check run for this annotation

Codecov / codecov/patch

pkg/logger/charmbracelet.go#L130-L131

Added lines #L130 - L131 were not covered by tests
}
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