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 11 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
53 changes: 14 additions & 39 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ package cmd
import (
"errors"
"fmt"
"io"
"math"
"os"
"regexp"
"strings"

"github.com/charmbracelet/log"
log "github.com/charmbracelet/log"
"github.com/cloudposse/atmos/pkg/logger"
"github.com/elewis787/boa"
"github.com/spf13/cobra"

Expand Down Expand Up @@ -88,41 +87,14 @@ var RootCmd = &cobra.Command{
},
}

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:
log.SetLevel(log.InfoLevel)
}

var output io.Writer

switch atmosConfig.Logs.File {
case "/dev/stderr":
output = os.Stderr
case "/dev/stdout":
output = os.Stdout
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
func setupLogger(atmosConfig *schema.AtmosConfiguration) error {
// Initialize the logger using the configuration-driven approach
_, err := logger.InitializeLoggerFromCliConfig(atmosConfig)
if err != nil {
// Just return the error with added context, without logging
return fmt.Errorf("failed to initialize logger: %w", err)
}

log.SetOutput(output)
return nil
}

// TODO: This function works well, but we should generally avoid implementing manual flag parsing,
Expand Down Expand Up @@ -193,10 +165,13 @@ func Execute() error {
atmosConfig.Logs.File = v
}

var err error
// Set the log level for the charmbracelet/log package based on the atmosConfig
setupLogger(&atmosConfig)
err = setupLogger(&atmosConfig)
if err != nil {
logger.Fatal("Failed to initialize logger", "error", err)
}

var err error
// If CLI configuration was found, process its custom commands and command aliases
if initErr == nil {
err = processCustomCommands(atmosConfig, atmosConfig.Commands, RootCmd, true)
Expand Down
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()
}

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

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

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

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

// 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...)
}

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