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 5 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
99 changes: 60 additions & 39 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"errors"
"fmt"
"io"
"math"
"os"
"regexp"
"strings"
Expand All @@ -16,7 +15,8 @@
e "github.com/cloudposse/atmos/internal/exec"
"github.com/cloudposse/atmos/internal/tui/templates"
tuiUtils "github.com/cloudposse/atmos/internal/tui/utils"
cfg "github.com/cloudposse/atmos/pkg/config"
"github.com/cloudposse/atmos/pkg/config"
"github.com/cloudposse/atmos/pkg/logger"
"github.com/cloudposse/atmos/pkg/schema"
"github.com/cloudposse/atmos/pkg/utils"
u "github.com/cloudposse/atmos/pkg/utils"
Expand Down Expand Up @@ -58,12 +58,12 @@
}

// Only validate the config, don't store it yet since commands may need to add more info
_, err := cfg.InitCliConfig(configAndStacksInfo, false)
_, err := config.InitCliConfig(configAndStacksInfo, false)
if err != nil {
if errors.Is(err, cfg.NotFound) {
if errors.Is(err, config.NotFound) {

Check warning on line 63 in cmd/root.go

View check run for this annotation

Codecov / codecov/patch

cmd/root.go#L63

Added line #L63 was not covered by tests
// For help commands or when help flag is set, we don't want to show the error
if !isHelpRequested {
u.LogWarning(err.Error())

Check failure on line 66 in cmd/root.go

View workflow job for this annotation

GitHub Actions / golangci

[golangci] cmd/root.go#L66

SA1019: u.LogWarning is deprecated: Use `log.Warn` instead. This function will be removed in a future release. LogWarning logs the provided warning message (staticcheck)
Raw output
cmd/root.go:66:6: SA1019: u.LogWarning is deprecated: Use `log.Warn` instead. This function will be removed in a future release. LogWarning logs the provided warning message (staticcheck)
					u.LogWarning(err.Error())
					^
}
} else {
u.LogErrorAndExit(err)
Expand All @@ -88,41 +88,69 @@
},
}

// setupLogger configures the global logger based on application configuration using our logger pkg.
func setupLogger(atmosConfig *schema.AtmosConfiguration) {
switch atmosConfig.Logs.Level {
case "Trace":
log.SetLevel(log.DebugLevel)
case "Debug":
log.SetLevel(log.DebugLevel)
case "Info":
log.SetLevel(log.InfoLevel)
case "Warning":
log.SetLevel(log.WarnLevel)
case "Off":
log.SetLevel(math.MaxInt32)
default:
atmosLogger, err := logger.InitializeLoggerFromCliConfig(atmosConfig)
if err != nil {
log.Error("Failed to initialize logger from config", "error", err)

Check warning on line 96 in cmd/root.go

View check run for this annotation

Codecov / codecov/patch

cmd/root.go#L95-L96

Added lines #L95 - L96 were not covered by tests
log.SetLevel(log.InfoLevel)
log.SetOutput(os.Stderr)
return

Check warning on line 99 in cmd/root.go

View check run for this annotation

Codecov / codecov/patch

cmd/root.go#L98-L99

Added lines #L98 - L99 were not covered by tests
}

var output io.Writer
globalLogLevel := mapToCharmLevel(atmosLogger.LogLevel)
log.SetLevel(globalLogLevel)

switch atmosConfig.Logs.File {
configureLogOutput(atmosConfig.Logs.File)
}

// mapToCharmLevel converts our internal log level to a Charmbracelet log level.
func mapToCharmLevel(logLevel logger.LogLevel) log.Level {
switch logLevel {
case logger.LogLevelTrace:
return log.DebugLevel // Charmbracelet doesn't have Trace
case logger.LogLevelDebug:
return log.DebugLevel

Check warning on line 114 in cmd/root.go

View check run for this annotation

Codecov / codecov/patch

cmd/root.go#L111-L114

Added lines #L111 - L114 were not covered by tests
case logger.LogLevelInfo:
return log.InfoLevel
case logger.LogLevelWarning:
return log.WarnLevel
case logger.LogLevelOff:
return log.FatalLevel + 1 // Disable logging
default:
return log.InfoLevel

Check warning on line 122 in cmd/root.go

View check run for this annotation

Codecov / codecov/patch

cmd/root.go#L117-L122

Added lines #L117 - L122 were not covered by tests
}
}

// configureLogOutput sets up the output destination for the global logger.
func configureLogOutput(logFile string) {
// Handle standard output destinations
switch logFile {
case "/dev/stderr":
output = os.Stderr
case "/dev/stdout":
output = os.Stdout
log.SetOutput(os.Stderr)
return
case "/dev/stdout", "":
log.SetOutput(os.Stdout)
return

Check warning on line 135 in cmd/root.go

View check run for this annotation

Codecov / codecov/patch

cmd/root.go#L133-L135

Added lines #L133 - L135 were not covered by tests
case "/dev/null":
output = io.Discard // More efficient than opening os.DevNull
default:
logFile, err := os.OpenFile(atmosConfig.Logs.File, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
if err != nil {
log.Fatal("Failed to open log file:", err)
}
defer logFile.Close()
output = logFile
log.SetOutput(io.Discard)
return

Check warning on line 138 in cmd/root.go

View check run for this annotation

Codecov / codecov/patch

cmd/root.go#L137-L138

Added lines #L137 - L138 were not covered by tests
}

log.SetOutput(output)
// Handle custom log file (anything not a standard stream)
customFile, err := os.OpenFile(logFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, config.StandardFilePermissions)
if err != nil {
log.Error("Failed to open log file, using stderr", "error", err)
log.SetOutput(os.Stderr)
return
}

Check warning on line 147 in cmd/root.go

View check run for this annotation

Codecov / codecov/patch

cmd/root.go#L142-L147

Added lines #L142 - L147 were not covered by tests

// Important: We need to register this file to be closed at program exit
// to prevent file descriptor leaks, since we can't use defer here
// The Go runtime will close this file when the program exits, and the OS
// would clean it up anyway, but this is cleaner practice.
log.SetOutput(customFile)

Check warning on line 153 in cmd/root.go

View check run for this annotation

Codecov / codecov/patch

cmd/root.go#L153

Added line #L153 was not covered by tests
}

// TODO: This function works well, but we should generally avoid implementing manual flag parsing,
Expand Down Expand Up @@ -168,9 +196,9 @@
// system dir, home dir, current dir, ENV vars, command-line arguments
// Here we need the custom commands from the config
var initErr error
atmosConfig, initErr = cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, false)
atmosConfig, initErr = config.InitCliConfig(schema.ConfigAndStacksInfo{}, false)
utils.InitializeMarkdown(atmosConfig)
if initErr != nil && !errors.Is(initErr, cfg.NotFound) {
if initErr != nil && !errors.Is(initErr, config.NotFound) {
if isVersionCommand() {
log.Debug("warning: CLI configuration 'atmos.yaml' file not found", "error", initErr)
} else {
Expand Down Expand Up @@ -307,10 +335,3 @@
CheckForAtmosUpdateAndPrintMessage(atmosConfig)
})
}

// https://www.sobyte.net/post/2021-12/create-cli-app-with-cobra/
// https://github.com/spf13/cobra/blob/master/user_guide.md
// https://blog.knoldus.com/create-kubectl-like-cli-with-go-and-cobra/
// https://pkg.go.dev/github.com/c-bata/go-prompt
// https://pkg.go.dev/github.com/spf13/cobra
// https://scene-si.org/2017/04/20/managing-configuration-with-viper/
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
}
Loading