Compare commits
	
		
			4 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| a6805f8990 | |||
| 02fed6e55f | |||
| 1712042f64 | |||
| f8a72d3579 | 
							
								
								
									
										12
									
								
								colors.go
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								colors.go
									
									
									
									
									
								
							| @@ -1,3 +1,7 @@ | ||||
| // Package cylogger provides ANSI color constants and utilities for terminal output. | ||||
| // This file contains all the color constants used by the logger for styling log messages. | ||||
| // It includes regular colors, bold colors, underlined colors, background colors, | ||||
| // high intensity colors, and utility functions for generating random colors. | ||||
| package cylogger | ||||
|  | ||||
| import ( | ||||
| @@ -122,6 +126,14 @@ var colors = []int{22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 3 | ||||
| var colorsIndex int = -1 | ||||
| var shuffled bool | ||||
|  | ||||
| // GenerateRandomAnsiColor generates a random ANSI color code from a curated set of colors. | ||||
| // The colors are shuffled once and then cycled through in order. | ||||
| // This is useful for generating unique colors for different log sources or components. | ||||
| // The function returns a formatted ANSI color code that can be used for text styling. | ||||
| // Example: | ||||
| // | ||||
| //	color := cylogger.GenerateRandomAnsiColor() | ||||
| //	fmt.Printf("%sColored text%s", color, cylogger.Reset) | ||||
| func GenerateRandomAnsiColor() string { | ||||
| 	if !shuffled { | ||||
| 		rand.Shuffle(len(colors), func(i int, j int) { | ||||
|   | ||||
							
								
								
									
										12
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								go.mod
									
									
									
									
									
								
							| @@ -1,3 +1,13 @@ | ||||
| module git.site.quack-lab.dev/dave/cylogger | ||||
|  | ||||
| go 1.24.2 | ||||
| go 1.23 | ||||
|  | ||||
| require github.com/hexops/valast v1.5.0 | ||||
|  | ||||
| require ( | ||||
| 	github.com/google/go-cmp v0.5.9 // indirect | ||||
| 	golang.org/x/mod v0.7.0 // indirect | ||||
| 	golang.org/x/sys v0.3.0 // indirect | ||||
| 	golang.org/x/tools v0.4.0 // indirect | ||||
| 	mvdan.cc/gofumpt v0.4.0 // indirect | ||||
| ) | ||||
|   | ||||
							
								
								
									
										26
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= | ||||
| github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= | ||||
| github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= | ||||
| github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | ||||
| github.com/hexops/autogold v0.8.1 h1:wvyd/bAJ+Dy+DcE09BoLk6r4Fa5R5W+O+GUzmR985WM= | ||||
| github.com/hexops/autogold v0.8.1/go.mod h1:97HLDXyG23akzAoRYJh/2OBs3kd80eHyKPvZw0S5ZBY= | ||||
| github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= | ||||
| github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= | ||||
| github.com/hexops/valast v1.5.0 h1:FBTuvVi0wjTngtXJRZXMbkN/Dn6DgsUsBwch2DUJU8Y= | ||||
| github.com/hexops/valast v1.5.0/go.mod h1:Jcy1pNH7LNraVaAZDLyv21hHg2WBv9Nf9FL6fGxU7o4= | ||||
| github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= | ||||
| github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= | ||||
| github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | ||||
| github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= | ||||
| github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= | ||||
| github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= | ||||
| golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= | ||||
| golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= | ||||
| golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= | ||||
| golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= | ||||
| golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4= | ||||
| golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= | ||||
| mvdan.cc/gofumpt v0.4.0 h1:JVf4NN1mIpHogBj7ABpgOyZc65/UUOkKQFkoURsz4MM= | ||||
| mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ= | ||||
							
								
								
									
										416
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										416
									
								
								main.go
									
									
									
									
									
								
							| @@ -1,3 +1,28 @@ | ||||
| // Package cylogger provides a feature-rich, colored logging library for Go applications. | ||||
| // It offers structured logging with multiple levels, colored output, goroutine tracking, | ||||
| // panic recovery, and object dumping capabilities. | ||||
| // | ||||
| // Key Features: | ||||
| //   - Multiple log levels (Error, Warning, Info, Debug, Trace, Dump, Lua) | ||||
| //   - Colored terminal output with customizable styling | ||||
| //   - Structured logging with fields and context | ||||
| //   - Goroutine ID tracking | ||||
| //   - Panic recovery and safe goroutine execution | ||||
| //   - Object dumping for debugging and testing | ||||
| //   - Multiple output destinations (stdout, files) | ||||
| //   - Thread-safe operations | ||||
| // | ||||
| // Basic Usage: | ||||
| // | ||||
| //	cylogger.Init(cylogger.LevelInfo) | ||||
| //	cylogger.Info("Application started") | ||||
| //	cylogger.WithField("user", "john").Info("User logged in") | ||||
| // | ||||
| // Advanced Usage: | ||||
| // | ||||
| //	logger := cylogger.New(os.Stdout, "", log.LstdFlags) | ||||
| //	logger.SetLevel(cylogger.LevelDebug) | ||||
| //	logger.WithFields(map[string]interface{}{"service": "api", "version": "1.0"}).Info("Service initialized") | ||||
| package cylogger | ||||
|  | ||||
| import ( | ||||
| @@ -13,38 +38,70 @@ import ( | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/hexops/valast" | ||||
| ) | ||||
|  | ||||
| // TODO: Enable turning colors on and off maybe even per stream? | ||||
|  | ||||
| var loglevel = flag.String("loglevel", "info", "log level") | ||||
|  | ||||
| // LogLevel defines the severity of log messages | ||||
| // LogLevel defines the severity and type of log messages. | ||||
| // Lower values indicate higher priority. Messages are only logged if their level | ||||
| // is less than or equal to the current logger level. | ||||
| type LogLevel int | ||||
|  | ||||
| const ( | ||||
| 	// LevelError is for critical errors that should always be displayed | ||||
| 	// LevelError is for critical errors that should always be displayed. | ||||
| 	// These represent serious problems that may cause the application to fail. | ||||
| 	LevelError LogLevel = iota | ||||
| 	// LevelWarning is for important warnings | ||||
|  | ||||
| 	// LevelWarning is for important warnings that indicate potential issues. | ||||
| 	// These represent situations that are not errors but may require attention. | ||||
| 	LevelWarning | ||||
| 	// LevelInfo is for informational messages | ||||
|  | ||||
| 	// LevelInfo is for general informational messages about application flow. | ||||
| 	// These provide context about what the application is doing. | ||||
| 	LevelInfo | ||||
| 	// LevelDebug is for detailed debugging information | ||||
|  | ||||
| 	// LevelDebug is for detailed debugging information. | ||||
| 	// These are useful for diagnosing problems during development. | ||||
| 	LevelDebug | ||||
| 	// LevelTrace is for very detailed tracing information | ||||
|  | ||||
| 	// LevelTrace is for very detailed tracing information. | ||||
| 	// These provide the most verbose output for deep debugging. | ||||
| 	LevelTrace | ||||
| 	// LevelLua is specifically for output from Lua scripts | ||||
|  | ||||
| 	// LevelDump is for dumping objects to console for regressive tests. | ||||
| 	// This level is specifically designed for object inspection and testing. | ||||
| 	LevelDump | ||||
|  | ||||
| 	// LevelLua is specifically for output from Lua scripts. | ||||
| 	// This level bypasses normal level filtering and is always shown. | ||||
| 	LevelLua | ||||
|  | ||||
| 	// LevelPrefix is used internally for styling user prefixes. | ||||
| 	// This is not a logging level but a style identifier. | ||||
| 	LevelPrefix | ||||
| ) | ||||
|  | ||||
| // LevelStyle defines the visual style for a log level | ||||
| // LevelStyle defines the visual styling configuration for a log level. | ||||
| // It controls how log messages appear in the terminal with colors and formatting. | ||||
| type LevelStyle struct { | ||||
| 	Tag                    string // e.g., "ERROR", "INFO" | ||||
| 	TagColor               string // ANSI code for tag text | ||||
| 	TagBackgroundColor     string // ANSI code for tag background | ||||
| 	MessageColor           string // ANSI code for message text | ||||
| 	MessageBackgroundColor string // ANSI code for message background | ||||
| 	// Tag is the text label displayed for this log level (e.g., "ERROR", "INFO") | ||||
| 	Tag string | ||||
|  | ||||
| 	// TagColor is the ANSI color code for the tag text | ||||
| 	TagColor string | ||||
|  | ||||
| 	// TagBackgroundColor is the ANSI color code for the tag background | ||||
| 	TagBackgroundColor string | ||||
|  | ||||
| 	// MessageColor is the ANSI color code for the message text | ||||
| 	MessageColor string | ||||
|  | ||||
| 	// MessageBackgroundColor is the ANSI color code for the message background | ||||
| 	MessageBackgroundColor string | ||||
| } | ||||
|  | ||||
| // levelStyles maps LogLevel to its display style | ||||
| @@ -75,6 +132,12 @@ var levelStyles = map[LogLevel]LevelStyle{ | ||||
| 		Tag:      "TRACE", | ||||
| 		TagColor: BPurple, // Bold Purple | ||||
| 	}, | ||||
| 	LevelDump: { | ||||
| 		Tag:                "DUMP", | ||||
| 		TagColor:           BIMagenta, // Bold Intense Magenta | ||||
| 		TagBackgroundColor: On_White,  // White background | ||||
| 		MessageColor:       IMagenta,  // White text | ||||
| 	}, | ||||
| 	LevelLua: { | ||||
| 		Tag:      "LUA", | ||||
| 		TagColor: BBlue, // Bold Blue | ||||
| @@ -85,30 +148,56 @@ var levelStyles = map[LogLevel]LevelStyle{ | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| // Logger is our custom logger with level support | ||||
| // Logger is a thread-safe, feature-rich logger that supports multiple output destinations, | ||||
| // structured logging, colored output, and various log levels. It can be used both as a | ||||
| // standalone logger or as part of a structured logging system with context fields. | ||||
| type Logger struct { | ||||
| 	mu            sync.Mutex | ||||
| 	out           io.Writer | ||||
| 	currentLevel  LogLevel | ||||
| 	prefix        string | ||||
| 	userPrefix    string | ||||
| 	flag          int | ||||
| 	useColors     bool | ||||
| 	callerOffset  int | ||||
| 	// mu protects all fields from concurrent access | ||||
| 	mu sync.Mutex | ||||
|  | ||||
| 	// out is the list of output destinations for log messages | ||||
| 	out []io.Writer | ||||
|  | ||||
| 	// currentLevel determines which log messages are actually written | ||||
| 	currentLevel LogLevel | ||||
|  | ||||
| 	// prefix is the standard Go log prefix (timestamp, file, etc.) | ||||
| 	prefix string | ||||
|  | ||||
| 	// userPrefix is a custom user-defined prefix for log messages | ||||
| 	userPrefix string | ||||
|  | ||||
| 	// flag controls which standard log information is included (timestamp, file, line, etc.) | ||||
| 	flag int | ||||
|  | ||||
| 	// useColors determines whether ANSI color codes are included in output | ||||
| 	useColors bool | ||||
|  | ||||
| 	// callerOffset adjusts the stack depth for caller information | ||||
| 	callerOffset int | ||||
|  | ||||
| 	// defaultFields are key-value pairs included in all log messages from this logger | ||||
| 	defaultFields map[string]interface{} | ||||
|  | ||||
| 	// showGoroutine determines whether goroutine ID is included in log messages | ||||
| 	showGoroutine bool | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	// Default is the global logger instance | ||||
| 	// Default is the global logger instance used by package-level functions. | ||||
| 	// It is automatically initialized when first used if not explicitly set. | ||||
| 	Default *Logger | ||||
| 	// defaultLogLevel is the default log level if not specified | ||||
|  | ||||
| 	// defaultLogLevel is the default log level used when initializing the logger | ||||
| 	defaultLogLevel = LevelInfo | ||||
| 	// Global mutex for DefaultLogger initialization | ||||
|  | ||||
| 	// initMutex protects the initialization of the Default logger from race conditions | ||||
| 	initMutex sync.Mutex | ||||
| ) | ||||
|  | ||||
| // ParseLevel converts a string log level to LogLevel | ||||
| // ParseLevel converts a string representation of a log level to the corresponding LogLevel. | ||||
| // It accepts case-insensitive strings like "error", "warning", "info", "debug", "trace", "dump", and "lua". | ||||
| // If the string is not recognized, it returns the default log level (LevelInfo). | ||||
| func ParseLevel(levelStr string) LogLevel { | ||||
| 	switch strings.ToUpper(levelStr) { | ||||
| 	case "ERROR": | ||||
| @@ -121,6 +210,8 @@ func ParseLevel(levelStr string) LogLevel { | ||||
| 		return LevelDebug | ||||
| 	case "TRACE": | ||||
| 		return LevelTrace | ||||
| 	case "DUMP": | ||||
| 		return LevelDump | ||||
| 	case "LUA": | ||||
| 		return LevelLua | ||||
| 	default: | ||||
| @@ -128,7 +219,8 @@ func ParseLevel(levelStr string) LogLevel { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // String returns the string representation of the log level | ||||
| // String returns the string representation of the log level. | ||||
| // It returns the tag name (e.g., "ERROR", "INFO") or a formatted representation for unknown levels. | ||||
| func (l LogLevel) String() string { | ||||
| 	if name, ok := levelStyles[l]; ok { | ||||
| 		return name.Tag | ||||
| @@ -136,10 +228,12 @@ func (l LogLevel) String() string { | ||||
| 	return fmt.Sprintf("Level(%d)", l) | ||||
| } | ||||
|  | ||||
| // New creates a new Logger instance | ||||
| // New creates a new Logger instance with the specified output destination, prefix, and flags. | ||||
| // The logger is initialized with default settings: LevelInfo log level, colors enabled, | ||||
| // goroutine tracking enabled, and no default fields. | ||||
| func New(out io.Writer, prefix string, flag int) *Logger { | ||||
| 	return &Logger{ | ||||
| 		out:           out, | ||||
| 		out:           []io.Writer{out}, | ||||
| 		currentLevel:  defaultLogLevel, | ||||
| 		prefix:        prefix, | ||||
| 		userPrefix:    "", | ||||
| @@ -151,12 +245,22 @@ func New(out io.Writer, prefix string, flag int) *Logger { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // InitFlag initializes the default logger using the loglevel flag value. | ||||
| // This library defines a "loglevel" command-line flag that accepts values like "error", "warning", "info", "debug", "trace", "dump", or "lua". | ||||
| // This function should be called after flag.Parse() to use command-line log level configuration. | ||||
| // Example usage: | ||||
| // | ||||
| //	flag.Parse() | ||||
| //	cylogger.InitFlag() | ||||
| //	// Now the logger level is set based on the -loglevel flag | ||||
| func InitFlag() { | ||||
| 	level := ParseLevel(*loglevel) | ||||
| 	Init(level) | ||||
| } | ||||
|  | ||||
| // Init initializes the DefaultLogger | ||||
| // Init initializes the Default logger with the specified log level. | ||||
| // If the Default logger is already initialized, it only updates the log level. | ||||
| // This function is thread-safe and can be called multiple times. | ||||
| func Init(level LogLevel) { | ||||
| 	initMutex.Lock() | ||||
| 	defer initMutex.Unlock() | ||||
| @@ -167,45 +271,63 @@ func Init(level LogLevel) { | ||||
| 	Default.SetLevel(level) | ||||
| } | ||||
|  | ||||
| // SetLevel sets the current log level | ||||
| // SetLevel sets the current log level for the logger. | ||||
| // Only messages with a level less than or equal to this level will be logged. | ||||
| // This method is thread-safe. | ||||
| func (l *Logger) SetLevel(level LogLevel) { | ||||
| 	l.mu.Lock() | ||||
| 	defer l.mu.Unlock() | ||||
| 	l.currentLevel = level | ||||
| } | ||||
|  | ||||
| // GetLevel returns the current log level | ||||
| // GetLevel returns the current log level of the logger. | ||||
| // This method is thread-safe. | ||||
| func (l *Logger) GetLevel() LogLevel { | ||||
| 	l.mu.Lock() | ||||
| 	defer l.mu.Unlock() | ||||
| 	return l.currentLevel | ||||
| } | ||||
|  | ||||
| // SetCallerOffset sets the caller offset for correct file and line reporting | ||||
| // SetCallerOffset sets the caller offset for correct file and line reporting. | ||||
| // This is useful when the logger is wrapped in helper functions to ensure | ||||
| // the correct caller information is displayed. | ||||
| func (l *Logger) SetCallerOffset(offset int) { | ||||
| 	l.mu.Lock() | ||||
| 	defer l.mu.Unlock() | ||||
| 	l.callerOffset = offset | ||||
| } | ||||
|  | ||||
| // SetShowGoroutine sets whether to include goroutine ID in log messages | ||||
| // SetShowGoroutine sets whether to include goroutine ID in log messages. | ||||
| // This is useful for debugging concurrent applications where you need to track | ||||
| // which goroutine generated each log message. | ||||
| func (l *Logger) SetShowGoroutine(show bool) { | ||||
| 	l.mu.Lock() | ||||
| 	defer l.mu.Unlock() | ||||
| 	l.showGoroutine = show | ||||
| } | ||||
|  | ||||
| // ShowGoroutine returns whether goroutine ID is included in log messages | ||||
| // ShowGoroutine returns whether goroutine ID is included in log messages. | ||||
| // This method is thread-safe. | ||||
| func (l *Logger) ShowGoroutine() bool { | ||||
| 	l.mu.Lock() | ||||
| 	defer l.mu.Unlock() | ||||
| 	return l.showGoroutine | ||||
| } | ||||
|  | ||||
| // WithField adds a field to the logger's context | ||||
| // WithField creates a new logger instance with an additional field in its context. | ||||
| // The field will be included in all log messages from the returned logger, appearing as key=value pairs appended to the message. | ||||
| // This is useful for structured logging where you want to add context to a group of log messages. | ||||
| // Example: | ||||
| // | ||||
| //	logger := cylogger.WithField("user", "john") | ||||
| //	logger.Info("User logged in")  // Output: "User logged in user=john" | ||||
| //	logger.Error("Login failed")    // Output: "Login failed user=john" | ||||
| // | ||||
| // This is particularly useful for scoped logging in functions or loops where you want to track context | ||||
| // without continuously logging an ID or identifier. | ||||
| func (l *Logger) WithField(key string, value interface{}) *Logger { | ||||
| 	newLogger := &Logger{ | ||||
| 		out:           l.out, | ||||
| 		out:           append([]io.Writer(nil), l.out...), | ||||
| 		currentLevel:  l.currentLevel, | ||||
| 		prefix:        l.prefix, | ||||
| 		userPrefix:    l.userPrefix, | ||||
| @@ -226,10 +348,20 @@ func (l *Logger) WithField(key string, value interface{}) *Logger { | ||||
| 	return newLogger | ||||
| } | ||||
|  | ||||
| // WithFields adds multiple fields to the logger's context | ||||
| // WithFields creates a new logger instance with multiple additional fields in its context. | ||||
| // All fields will be included in all log messages from the returned logger, appearing as key=value pairs appended to the message. | ||||
| // This is useful for structured logging where you want to add multiple context fields. | ||||
| // Example: | ||||
| // | ||||
| //	fields := map[string]interface{}{"user": "john", "role": "admin", "session": "abc123"} | ||||
| //	logger := cylogger.WithFields(fields) | ||||
| //	logger.Info("Processing request")  // Output: "Processing request user=john role=admin session=abc123" | ||||
| // | ||||
| // This is particularly useful for scoped logging where you need to track multiple pieces of context | ||||
| // without repeatedly logging them in each message. | ||||
| func (l *Logger) WithFields(fields map[string]interface{}) *Logger { | ||||
| 	newLogger := &Logger{ | ||||
| 		out:           l.out, | ||||
| 		out:           append([]io.Writer(nil), l.out...), | ||||
| 		currentLevel:  l.currentLevel, | ||||
| 		prefix:        l.prefix, | ||||
| 		userPrefix:    l.userPrefix, | ||||
| @@ -252,6 +384,18 @@ func (l *Logger) WithFields(fields map[string]interface{}) *Logger { | ||||
| 	return newLogger | ||||
| } | ||||
|  | ||||
| // WithPrefix creates a new logger instance with an additional user prefix. | ||||
| // The prefix will be displayed in all log messages from the returned logger, appearing as [prefix] before the message. | ||||
| // This is useful for adding context or module identification to log messages. | ||||
| // Example: | ||||
| // | ||||
| //	logger := cylogger.WithPrefix("AUTH") | ||||
| //	logger.Info("User authenticated")  // Output: "[AUTH] User authenticated" | ||||
| //	logger.Error("Login failed")     // Output: "[AUTH] Login failed" | ||||
| // | ||||
| // This is particularly useful for scoped logging in functions, loops, or modules where you want to know | ||||
| // what the log line refers to without continuously logging an ID or identifier. | ||||
| // Unlike WithField which appends fields to the end, WithPrefix prepends the prefix to the beginning. | ||||
| func (l *Logger) WithPrefix(prefix string) *Logger { | ||||
| 	if Default == nil { | ||||
| 		Init(defaultLogLevel) | ||||
| @@ -260,7 +404,7 @@ func (l *Logger) WithPrefix(prefix string) *Logger { | ||||
| 		l = Default | ||||
| 	} | ||||
| 	newLogger := &Logger{ | ||||
| 		out:           l.out, | ||||
| 		out:           append([]io.Writer(nil), l.out...), | ||||
| 		currentLevel:  l.currentLevel, | ||||
| 		prefix:        l.prefix, | ||||
| 		userPrefix:    strings.TrimSpace(l.userPrefix + " " + prefix), | ||||
| @@ -278,6 +422,17 @@ func (l *Logger) WithPrefix(prefix string) *Logger { | ||||
| 	return newLogger | ||||
| } | ||||
|  | ||||
| // ToFile creates a new logger instance that writes to both the original outputs and a file. | ||||
| // The file is opened in append mode and will be created if it doesn't exist. | ||||
| // This is useful for logging to both console and file simultaneously. | ||||
| // Example: | ||||
| // | ||||
| //	logger := cylogger.New(os.Stdout, "", log.LstdFlags) | ||||
| //	fileLogger := logger.ToFile("app.log") | ||||
| //	fileLogger.Info("This will appear in both console and app.log") | ||||
| // | ||||
| // The file is opened with os.O_CREATE|os.O_WRONLY|os.O_APPEND flags, so it will be created if it doesn't exist | ||||
| // and new log entries will be appended to the end of the file. | ||||
| func (l *Logger) ToFile(filename string) *Logger { | ||||
| 	file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) | ||||
| 	if err != nil { | ||||
| @@ -290,7 +445,7 @@ func (l *Logger) ToFile(filename string) *Logger { | ||||
| 		l = Default | ||||
| 	} | ||||
| 	newLogger := &Logger{ | ||||
| 		out:           io.MultiWriter(l.out, file), | ||||
| 		out:           append([]io.Writer(nil), l.out...), | ||||
| 		currentLevel:  l.currentLevel, | ||||
| 		prefix:        l.prefix, | ||||
| 		userPrefix:    l.userPrefix, | ||||
| @@ -300,10 +455,17 @@ func (l *Logger) ToFile(filename string) *Logger { | ||||
| 		defaultFields: make(map[string]interface{}), | ||||
| 		showGoroutine: l.showGoroutine, | ||||
| 	} | ||||
| 	// Copy existing fields | ||||
| 	for k, v := range l.defaultFields { | ||||
| 		newLogger.defaultFields[k] = v | ||||
| 	} | ||||
| 	// Append the new file writer | ||||
| 	newLogger.out = append(newLogger.out, file) | ||||
| 	return newLogger | ||||
| } | ||||
|  | ||||
| // GetGoroutineID extracts the goroutine ID from the runtime stack | ||||
| // GetGoroutineID extracts the current goroutine ID from the runtime stack. | ||||
| // This is used internally by the logger to include goroutine information in log messages. | ||||
| func GetGoroutineID() string { | ||||
| 	buf := make([]byte, 64) | ||||
| 	n := runtime.Stack(buf, false) | ||||
| @@ -477,42 +639,84 @@ func (l *Logger) log(level LogLevel, format string, args ...interface{}) { | ||||
|  | ||||
| 	// Get formatted message with potential background color | ||||
| 	msg := l.formatMessage(level, format, args...) | ||||
| 	fmt.Fprintln(l.out, msg) | ||||
| 	for _, w := range l.out { | ||||
| 		fmt.Fprintln(w, msg) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Error logs an error message | ||||
| // Error logs an error message at LevelError. | ||||
| // Error messages are always displayed regardless of the current log level. | ||||
| func (l *Logger) Error(format string, args ...interface{}) { | ||||
| 	l.log(LevelError, format, args...) | ||||
| } | ||||
|  | ||||
| // Warning logs a warning message | ||||
| // Warning logs a warning message at LevelWarning. | ||||
| // Warning messages indicate potential issues that should be noted. | ||||
| func (l *Logger) Warning(format string, args ...interface{}) { | ||||
| 	l.log(LevelWarning, format, args...) | ||||
| } | ||||
|  | ||||
| // Info logs an informational message | ||||
| // Info logs an informational message at LevelInfo. | ||||
| // Info messages provide general information about application flow. | ||||
| func (l *Logger) Info(format string, args ...interface{}) { | ||||
| 	l.log(LevelInfo, format, args...) | ||||
| } | ||||
|  | ||||
| // Debug logs a debug message | ||||
| // Debug logs a debug message at LevelDebug. | ||||
| // Debug messages provide detailed information useful for diagnosing problems. | ||||
| func (l *Logger) Debug(format string, args ...interface{}) { | ||||
| 	l.log(LevelDebug, format, args...) | ||||
| } | ||||
|  | ||||
| // Trace logs a trace message | ||||
| // Trace logs a trace message at LevelTrace. | ||||
| // Trace messages provide the most detailed information for deep debugging. | ||||
| func (l *Logger) Trace(format string, args ...interface{}) { | ||||
| 	l.log(LevelTrace, format, args...) | ||||
| } | ||||
|  | ||||
| // Lua logs a Lua message | ||||
| // Dump logs objects using valast for regressive tests at LevelDump. | ||||
| // This is useful for debugging and testing where you need to inspect object state. | ||||
| // The objects are formatted using valast for readable output. | ||||
| // Example: | ||||
| // | ||||
| //	type User struct { Name string; Age int } | ||||
| //	user := User{Name: "John", Age: 30} | ||||
| //	logger.Dump("User data", user) | ||||
| //	// Output: "User data:\nUser{Name: \"John\", Age: 30}" | ||||
| // | ||||
| // This is particularly useful for regressive testing where you need to capture | ||||
| // the exact state of objects for comparison or debugging purposes. | ||||
| func (l *Logger) Dump(message string, objects ...interface{}) { | ||||
| 	if len(objects) == 0 { | ||||
| 		l.log(LevelDump, message) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var dumpContent strings.Builder | ||||
| 	dumpContent.WriteString(message) | ||||
| 	dumpContent.WriteString(":\n") | ||||
|  | ||||
| 	for i, obj := range objects { | ||||
| 		if i > 0 { | ||||
| 			dumpContent.WriteString("\n") | ||||
| 		} | ||||
| 		dumpContent.WriteString(valast.String(obj)) | ||||
| 	} | ||||
|  | ||||
| 	l.log(LevelDump, dumpContent.String()) | ||||
| } | ||||
|  | ||||
| // Lua logs a Lua message at LevelLua. | ||||
| // Lua messages are always displayed regardless of the current log level. | ||||
| // This is specifically designed for output from Lua scripts. | ||||
| func (l *Logger) Lua(format string, args ...interface{}) { | ||||
| 	l.log(LevelLua, format, args...) | ||||
| } | ||||
|  | ||||
| // Global log functions that use DefaultLogger | ||||
|  | ||||
| // Error logs an error message using the default logger | ||||
| // Error logs an error message using the default logger. | ||||
| // This is a convenience function that uses the global Default logger instance. | ||||
| func Error(format string, args ...interface{}) { | ||||
| 	if Default == nil { | ||||
| 		Init(defaultLogLevel) | ||||
| @@ -520,7 +724,8 @@ func Error(format string, args ...interface{}) { | ||||
| 	Default.Error(format, args...) | ||||
| } | ||||
|  | ||||
| // Warning logs a warning message using the default logger | ||||
| // Warning logs a warning message using the default logger. | ||||
| // This is a convenience function that uses the global Default logger instance. | ||||
| func Warning(format string, args ...interface{}) { | ||||
| 	if Default == nil { | ||||
| 		Init(defaultLogLevel) | ||||
| @@ -528,7 +733,8 @@ func Warning(format string, args ...interface{}) { | ||||
| 	Default.Warning(format, args...) | ||||
| } | ||||
|  | ||||
| // Info logs an informational message using the default logger | ||||
| // Info logs an informational message using the default logger. | ||||
| // This is a convenience function that uses the global Default logger instance. | ||||
| func Info(format string, args ...interface{}) { | ||||
| 	if Default == nil { | ||||
| 		Init(defaultLogLevel) | ||||
| @@ -536,7 +742,8 @@ func Info(format string, args ...interface{}) { | ||||
| 	Default.Info(format, args...) | ||||
| } | ||||
|  | ||||
| // Debug logs a debug message using the default logger | ||||
| // Debug logs a debug message using the default logger. | ||||
| // This is a convenience function that uses the global Default logger instance. | ||||
| func Debug(format string, args ...interface{}) { | ||||
| 	if Default == nil { | ||||
| 		Init(defaultLogLevel) | ||||
| @@ -544,7 +751,8 @@ func Debug(format string, args ...interface{}) { | ||||
| 	Default.Debug(format, args...) | ||||
| } | ||||
|  | ||||
| // Trace logs a trace message using the default logger | ||||
| // Trace logs a trace message using the default logger. | ||||
| // This is a convenience function that uses the global Default logger instance. | ||||
| func Trace(format string, args ...interface{}) { | ||||
| 	if Default == nil { | ||||
| 		Init(defaultLogLevel) | ||||
| @@ -552,7 +760,17 @@ func Trace(format string, args ...interface{}) { | ||||
| 	Default.Trace(format, args...) | ||||
| } | ||||
|  | ||||
| // Lua logs a Lua message using the default logger | ||||
| // Dump logs objects using valast for regressive tests using the default logger. | ||||
| // This is a convenience function that uses the global Default logger instance. | ||||
| func Dump(message string, objects ...interface{}) { | ||||
| 	if Default == nil { | ||||
| 		Init(defaultLogLevel) | ||||
| 	} | ||||
| 	Default.Dump(message, objects...) | ||||
| } | ||||
|  | ||||
| // Lua logs a Lua message using the default logger. | ||||
| // This is a convenience function that uses the global Default logger instance. | ||||
| func Lua(format string, args ...interface{}) { | ||||
| 	if Default == nil { | ||||
| 		Init(defaultLogLevel) | ||||
| @@ -560,7 +778,9 @@ func Lua(format string, args ...interface{}) { | ||||
| 	Default.Lua(format, args...) | ||||
| } | ||||
|  | ||||
| // LogPanic logs a panic error and its stack trace | ||||
| // LogPanic logs a panic error and its stack trace using the default logger. | ||||
| // This is useful for logging panics that have been recovered elsewhere. | ||||
| // The function logs the panic value and full stack trace. | ||||
| func LogPanic(r interface{}) { | ||||
| 	if Default == nil { | ||||
| 		Init(defaultLogLevel) | ||||
| @@ -570,7 +790,8 @@ func LogPanic(r interface{}) { | ||||
| 	Default.Error("PANIC: %v\n%s", r, stack[:n]) | ||||
| } | ||||
|  | ||||
| // SetLevel sets the log level for the default logger | ||||
| // SetLevel sets the log level for the default logger. | ||||
| // This is a convenience function that uses the global Default logger instance. | ||||
| func SetLevel(level LogLevel) { | ||||
| 	if Default == nil { | ||||
| 		Init(level) | ||||
| @@ -579,7 +800,8 @@ func SetLevel(level LogLevel) { | ||||
| 	Default.SetLevel(level) | ||||
| } | ||||
|  | ||||
| // GetLevel gets the log level for the default logger | ||||
| // GetLevel gets the log level for the default logger. | ||||
| // This is a convenience function that uses the global Default logger instance. | ||||
| func GetLevel() LogLevel { | ||||
| 	if Default == nil { | ||||
| 		Init(defaultLogLevel) | ||||
| @@ -587,7 +809,8 @@ func GetLevel() LogLevel { | ||||
| 	return Default.GetLevel() | ||||
| } | ||||
|  | ||||
| // WithField returns a new logger with the field added to the default logger's context | ||||
| // WithField returns a new logger with the field added to the default logger's context. | ||||
| // This is a convenience function that uses the global Default logger instance. | ||||
| func WithField(key string, value interface{}) *Logger { | ||||
| 	if Default == nil { | ||||
| 		Init(defaultLogLevel) | ||||
| @@ -595,7 +818,8 @@ func WithField(key string, value interface{}) *Logger { | ||||
| 	return Default.WithField(key, value) | ||||
| } | ||||
|  | ||||
| // WithFields returns a new logger with the fields added to the default logger's context | ||||
| // WithFields returns a new logger with the fields added to the default logger's context. | ||||
| // This is a convenience function that uses the global Default logger instance. | ||||
| func WithFields(fields map[string]interface{}) *Logger { | ||||
| 	if Default == nil { | ||||
| 		Init(defaultLogLevel) | ||||
| @@ -603,7 +827,8 @@ func WithFields(fields map[string]interface{}) *Logger { | ||||
| 	return Default.WithFields(fields) | ||||
| } | ||||
|  | ||||
| // SetShowGoroutine enables or disables goroutine ID display in the default logger | ||||
| // SetShowGoroutine enables or disables goroutine ID display in the default logger. | ||||
| // This is a convenience function that uses the global Default logger instance. | ||||
| func SetShowGoroutine(show bool) { | ||||
| 	if Default == nil { | ||||
| 		Init(defaultLogLevel) | ||||
| @@ -611,7 +836,8 @@ func SetShowGoroutine(show bool) { | ||||
| 	Default.SetShowGoroutine(show) | ||||
| } | ||||
|  | ||||
| // ShowGoroutine returns whether goroutine ID is included in default logger's messages | ||||
| // ShowGoroutine returns whether goroutine ID is included in default logger's messages. | ||||
| // This is a convenience function that uses the global Default logger instance. | ||||
| func ShowGoroutine() bool { | ||||
| 	if Default == nil { | ||||
| 		Init(defaultLogLevel) | ||||
| @@ -619,6 +845,41 @@ func ShowGoroutine() bool { | ||||
| 	return Default.ShowGoroutine() | ||||
| } | ||||
|  | ||||
| // NoStdout creates a new logger instance that excludes stdout from its output destinations. | ||||
| // This is useful when you want to log only to files or other destinations, not to the console. | ||||
| // The original logger's stdout output is filtered out while keeping all other output destinations. | ||||
| func (l *Logger) NoStdout() *Logger { | ||||
| 	if Default == nil { | ||||
| 		Init(defaultLogLevel) | ||||
| 	} | ||||
| 	if l == nil { | ||||
| 		l = Default | ||||
| 	} | ||||
| 	newLogger := &Logger{ | ||||
| 		out:           nil, | ||||
| 		currentLevel:  l.currentLevel, | ||||
| 		prefix:        l.prefix, | ||||
| 		userPrefix:    l.userPrefix, | ||||
| 		flag:          l.flag, | ||||
| 		useColors:     l.useColors, | ||||
| 		callerOffset:  l.callerOffset, | ||||
| 		defaultFields: make(map[string]interface{}), | ||||
| 		showGoroutine: l.showGoroutine, | ||||
| 	} | ||||
| 	// Copy existing fields | ||||
| 	for k, v := range l.defaultFields { | ||||
| 		newLogger.defaultFields[k] = v | ||||
| 	} | ||||
| 	// Keep all writers except stdout | ||||
| 	for _, w := range l.out { | ||||
| 		if w == os.Stdout { | ||||
| 			continue | ||||
| 		} | ||||
| 		newLogger.out = append(newLogger.out, w) | ||||
| 	} | ||||
| 	return newLogger | ||||
| } | ||||
|  | ||||
| func main() { | ||||
| 	Init(LevelDebug) | ||||
|  | ||||
| @@ -657,4 +918,35 @@ func main() { | ||||
| 	// Test with custom prefix | ||||
| 	WithField("prefix", "custom").Info("Message with custom prefix") | ||||
| 	Lua("This is a Lua message") | ||||
|  | ||||
| 	// Test Dump functionality | ||||
| 	SetLevel(LevelDump) // Set level to show dump messages | ||||
|  | ||||
| 	type ProjectData struct { | ||||
| 		Title   string | ||||
| 		Name    string | ||||
| 		Data    string | ||||
| 		Commits string | ||||
| 	} | ||||
|  | ||||
| 	type Project struct { | ||||
| 		Id   int64 | ||||
| 		Data *ProjectData | ||||
| 	} | ||||
|  | ||||
| 	p := Project{ | ||||
| 		Id: 1, | ||||
| 		Data: &ProjectData{ | ||||
| 			Title:   "Test", | ||||
| 			Name:    "Mihai", | ||||
| 			Data:    "Some data", | ||||
| 			Commits: "Test Message", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	// Test single object dump | ||||
| 	Dump("FooBar called", p) | ||||
|  | ||||
| 	// Test multiple object dump | ||||
| 	Dump("Multiple objects", p, fields, "string value", 42) | ||||
| } | ||||
|   | ||||
							
								
								
									
										49
									
								
								safe.go
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								safe.go
									
									
									
									
									
								
							| @@ -1,3 +1,6 @@ | ||||
| // Package cylogger provides panic recovery and safe goroutine execution utilities. | ||||
| // These functions help prevent panics from crashing your application by catching | ||||
| // them and logging them appropriately. | ||||
| package cylogger | ||||
|  | ||||
| import ( | ||||
| @@ -5,7 +8,13 @@ import ( | ||||
| 	"runtime/debug" | ||||
| ) | ||||
|  | ||||
| // PanicHandler handles a panic and logs it | ||||
| // PanicHandler handles a panic and logs it with goroutine ID and stack trace. | ||||
| // This function should be used with defer to catch panics in goroutines or functions. | ||||
| // When a panic occurs, it logs the panic value, goroutine ID, and full stack trace. | ||||
| // Example: | ||||
| // | ||||
| //	defer cylogger.PanicHandler() | ||||
| //	// Your code that might panic | ||||
| func PanicHandler() { | ||||
| 	if r := recover(); r != nil { | ||||
| 		goroutineID := GetGoroutineID() | ||||
| @@ -14,8 +23,16 @@ func PanicHandler() { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // SafeGo launches a goroutine with panic recovery | ||||
| // Usage: logger.SafeGo(func() { ... your code ... }) | ||||
| // SafeGo launches a goroutine with panic recovery. | ||||
| // If the goroutine panics, the panic will be caught and logged instead of crashing the program. | ||||
| // This is useful for running potentially unstable code in goroutines. | ||||
| // Usage: cylogger.SafeGo(func() { ... your code ... }) | ||||
| // Example: | ||||
| // | ||||
| //	cylogger.SafeGo(func() { | ||||
| //	    // Code that might panic | ||||
| //	    riskyOperation() | ||||
| //	}) | ||||
| func SafeGo(f func()) { | ||||
| 	go func() { | ||||
| 		defer PanicHandler() | ||||
| @@ -23,8 +40,16 @@ func SafeGo(f func()) { | ||||
| 	}() | ||||
| } | ||||
|  | ||||
| // SafeGoWithArgs launches a goroutine with panic recovery and passes arguments | ||||
| // Usage: logger.SafeGoWithArgs(func(arg1, arg2 interface{}) { ... }, "value1", 42) | ||||
| // SafeGoWithArgs launches a goroutine with panic recovery and passes arguments. | ||||
| // If the goroutine panics, the panic will be caught and logged instead of crashing the program. | ||||
| // This is useful for running potentially unstable code in goroutines with specific arguments. | ||||
| // Usage: cylogger.SafeGoWithArgs(func(arg1, arg2 interface{}) { ... }, "value1", 42) | ||||
| // Example: | ||||
| // | ||||
| //	cylogger.SafeGoWithArgs(func(args ...interface{}) { | ||||
| //	    // Code that might panic with arguments | ||||
| //	    processData(args[0], args[1]) | ||||
| //	}, "data1", 42) | ||||
| func SafeGoWithArgs(f func(...interface{}), args ...interface{}) { | ||||
| 	go func() { | ||||
| 		defer PanicHandler() | ||||
| @@ -32,8 +57,18 @@ func SafeGoWithArgs(f func(...interface{}), args ...interface{}) { | ||||
| 	}() | ||||
| } | ||||
|  | ||||
| // SafeExec executes a function with panic recovery | ||||
| // Useful for code that should not panic | ||||
| // SafeExec executes a function with panic recovery and returns an error if a panic occurs. | ||||
| // This is useful for code that should not panic but might, allowing you to handle panics gracefully. | ||||
| // If the function panics, it returns an error describing the panic instead of crashing the program. | ||||
| // Example: | ||||
| // | ||||
| //	err := cylogger.SafeExec(func() { | ||||
| //	    // Code that might panic | ||||
| //	    riskyOperation() | ||||
| //	}) | ||||
| //	if err != nil { | ||||
| //	    // Handle the panic as an error | ||||
| //	} | ||||
| func SafeExec(f func()) (err error) { | ||||
| 	defer func() { | ||||
| 		if r := recover(); r != nil { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user