// Copyright (c) 2026 Zeni Kim // Use of this source code is governed by MIT-style // license that can be found in the LICENSE file. package logger import ( "io" "log" "os" "strings" ) // logs file var logsFile *os.File // ANSI color codes for terminal output const ( colorReset = "\033[0m" colorRed = "\033[31m" colorYellow = "\033[33m" colorBlue = "\033[34m" colorCyan = "\033[36m" ) // Level represents a log severity level. type Level int const ( DEBUG Level = iota INFO WARNING ERROR ) // String returns the string representation of a log level. func (l Level) String() string { switch l { case DEBUG: return "debug" case INFO: return "info" case WARNING: return "warning" case ERROR: return "error" default: return "unknown" } } // coloredWriter wraps an io.Writer and replaces plain log prefixes with ANSI-colored ones // in the output stream. The original bytes pass through unmodified to any additional writers. type coloredWriter struct { writer io.Writer plain string colored string } func (w *coloredWriter) Write(p []byte) (n int, err error) { // Replace the plain prefix with the colored version in the output colored := strings.Replace(string(p), w.plain, w.colored, 1) return w.writer.Write([]byte(colored)) } type Logger struct { minLevel Level infoLogger *log.Logger warningLogger *log.Logger errorLogger *log.Logger debugLogger *log.Logger file *os.File } var l *Logger type LogsDriver interface { GetTarget() interface{} } type LogFileDriver struct { FilePath string } type LogNullDriver struct{} func (n LogNullDriver) GetTarget() interface{} { return nil } func (f LogFileDriver) GetTarget() interface{} { return f.FilePath } // levelPrefixes returns the plain and colored (with ANSI escapes) prefix strings for a given level. func levelPrefixes(level Level) (plain, colored string) { switch level { case INFO: return "info: ", colorBlue + "INFO" + colorReset + ": " case DEBUG: return "debug: ", colorCyan + "DEBUG" + colorReset + ": " case WARNING: return "warning: ", colorYellow + "WARNING" + colorReset + ": " case ERROR: return "error: ", colorRed + "ERROR" + colorReset + ": " default: return "info: ", "INFO: " } } // NewLogger creates a new Logger with the given driver and a minimum level of DEBUG (all levels logged). func NewLogger(driver LogsDriver) *Logger { stdoutEnabled := os.Getenv("LOG_STDOUT_ENABLE") == "true" return NewLoggerWithStdout(driver, DEBUG, stdoutEnabled) } // NewLoggerWithStdout creates a new Logger with the given driver, minimum log level, // and optionally writes to stdout in addition to the target file. // When stdoutEnabled is true, log messages are written to both the target and stdout. // ANSI color codes are applied to log prefixes in terminal output only. func NewLoggerWithStdout(driver LogsDriver, minLevel Level, stdoutEnabled bool) *Logger { if driver.GetTarget() == nil { l = &Logger{ minLevel: minLevel, infoLogger: log.New(io.Discard, "info: ", log.LstdFlags), warningLogger: log.New(io.Discard, "warning: ", log.LstdFlags), errorLogger: log.New(io.Discard, "error: ", log.LstdFlags), debugLogger: log.New(io.Discard, "debug: ", log.LstdFlags), } return l } path, ok := driver.GetTarget().(string) if !ok { panic("something wrong with the file path") } var err error logsFile, err = os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644) if err != nil { panic(err) } if stdoutEnabled { // Build each logger with a MultiWriter: the file gets the original plain output, // stdout gets the output with ANSI-colored prefixes. infoPlain, infoColored := levelPrefixes(INFO) debugPlain, debugColored := levelPrefixes(DEBUG) warnPlain, warnColored := levelPrefixes(WARNING) errPlain, errColored := levelPrefixes(ERROR) l = &Logger{ minLevel: minLevel, infoLogger: log.New( io.MultiWriter(logsFile, &coloredWriter{writer: os.Stdout, plain: infoPlain, colored: infoColored}), infoPlain, log.LstdFlags, ), warningLogger: log.New( io.MultiWriter(logsFile, &coloredWriter{writer: os.Stdout, plain: warnPlain, colored: warnColored}), warnPlain, log.LstdFlags, ), errorLogger: log.New( io.MultiWriter(logsFile, &coloredWriter{writer: os.Stdout, plain: errPlain, colored: errColored}), errPlain, log.LstdFlags, ), debugLogger: log.New( io.MultiWriter(logsFile, &coloredWriter{writer: os.Stdout, plain: debugPlain, colored: debugColored}), debugPlain, log.LstdFlags, ), file: logsFile, } } else { // No stdout: all output goes to file with plain prefixes l = &Logger{ minLevel: minLevel, infoLogger: log.New(logsFile, "info: ", log.LstdFlags), warningLogger: log.New(logsFile, "warning: ", log.LstdFlags), errorLogger: log.New(logsFile, "error: ", log.LstdFlags), debugLogger: log.New(logsFile, "debug: ", log.LstdFlags), file: logsFile, } } return l } // NewLoggerWithLevel creates a new Logger with the given driver and minimum log level. // Messages below the minimum level will be discarded. Stdout output is disabled by default. func NewLoggerWithLevel(driver LogsDriver, minLevel Level) *Logger { stdoutEnabled := os.Getenv("LOG_STDOUT_ENABLE") == "true" return NewLoggerWithStdout(driver, minLevel, stdoutEnabled) } // SetLevel sets the minimum log level for the logger. // Messages below this level will be discarded. func (l *Logger) SetLevel(level Level) { l.minLevel = level } // GetLevel returns the current minimum log level. func (l *Logger) GetLevel() Level { return l.minLevel } func ResolveLogger() *Logger { return l } func (l *Logger) Info(msg interface{}) { if l.minLevel <= INFO { l.infoLogger.Println(msg) } } func (l *Logger) Debug(msg interface{}) { if l.minLevel <= DEBUG { l.debugLogger.Println(msg) } } func (l *Logger) Warning(msg interface{}) { if l.minLevel <= WARNING { l.warningLogger.Println(msg) } } func (l *Logger) Error(msg interface{}) { if l.minLevel <= ERROR { l.errorLogger.Println(msg) } } func (l *Logger) Close() error { if l.file != nil { return l.file.Close() } return nil }