stonelog/log.go
Rene Nochebuena 94c6e882fb
Some checks failed
Go CI/CD / go-ci (push) Has been cancelled
Add a structured logging utility with custom prefixes and suffixes
Introduce a logging system supporting multiple severity levels, JSON formatting, and customizable prefixes and suffixes inspired by Dr. Stone. Includes random success and failure quotes to make logging more dynamic and engaging.
2025-04-09 22:54:16 -06:00

265 lines
7.3 KiB
Go

package stonelog
import (
"encoding/json"
"fmt"
"log"
"math/rand"
"os"
"path/filepath"
"runtime"
"strconv"
"time"
)
// logEntry represents the structure for a single log entry.
// Time is the timestamp of the log entry in RFC3339 format.
// Level specifies the severity level of the log message.
// Caller describes the source file and line generating the log.
// Message contains the actual log details or description.
type logEntry struct {
Time string `json:"time"`
Level string `json:"level"`
Caller string `json:"caller"`
Message string `json:"message"`
}
// StoneLevel represents the severity level of logging categories.
type StoneLevel int
// String returns the string representation of the StoneLevel.
func (l StoneLevel) String() string {
return stoneLevelNames[l]
}
// Color returns the color code associated with the StoneLevel.
func (l StoneLevel) Color() string {
return levelColors[l]
}
// TRACE represents the trace log level.
// DEBUG represents the debug log level.
// INFO represents the info log level.
// WARN represents the warning log level.
// ERROR represents the error log level.
// FATAL represents the fatal log level.
// PANIC represents the panic log level.
const (
TRACE StoneLevel = iota
DEBUG
INFO
WARN
ERROR
FATAL
PANIC
)
// stoneLevelNames maps StoneLevel constants to their string representations.
var stoneLevelNames = map[StoneLevel]string{
TRACE: "TRACE",
DEBUG: "DEBUG",
INFO: "INFO",
WARN: "WARN",
ERROR: "ERROR",
FATAL: "FATAL",
PANIC: "PANIC",
}
// ANSI color codes for terminal text formatting.
const (
colorReset = "\033[0m"
colorRed = "\033[31m"
colorGreen = "\033[32m"
colorYellow = "\033[33m"
colorMagenta = "\033[35m"
colorCyan = "\033[36m"
colorWhite = "\033[37m"
)
// levelColors maps StoneLevel constants to their corresponding color codes.
var levelColors = map[StoneLevel]string{
TRACE: colorWhite,
DEBUG: colorCyan,
INFO: colorGreen,
WARN: colorYellow,
ERROR: colorRed,
FATAL: colorMagenta,
PANIC: colorRed + "\033[1m",
}
// stoneLogger is a logger instance for logging stone-related operations.
// currentLogLevel indicates the active logging level for stoneLogger.
// disableCharacterSuffixes disables suffixes in log outputs when true.
var (
stoneLogger *log.Logger
currentLogLevel StoneLevel
useJSON bool
callerFrameDepth = 3
disableCharacterSuffixes bool
)
// InitStoneLog initializes the logging system with the specified parameters.
// level sets the logging severity level threshold.
// jsonMode determines whether logs should be output in JSON format.
// disableSuffixes disables character suffixes in log messages.
func InitStoneLog(level StoneLevel, jsonMode, disableSuffixes bool) {
currentLogLevel = level
stoneLogger = log.New(os.Stdout, "", 0)
useJSON = jsonMode
disableCharacterSuffixes = disableSuffixes
}
// SetLogLevel sets the current log level to the specified StoneLevel.
func SetLogLevel(level StoneLevel) {
currentLogLevel = level
Observation("Log level set to %v", level)
}
// Trace logs a message at the TRACE using the specified format and arguments.
func Trace(format string, args ...interface{}) {
logMessage(TRACE, format, args...)
}
// Debug logs a message at the DEBUG level using the provided format and arguments.
func Debug(format string, args ...interface{}) {
logMessage(DEBUG, format, args...)
}
// Observation logs a message at the INFO level with optional formatting arguments.
func Observation(format string, args ...interface{}) {
logMessage(INFO, format, args...)
}
// Hypothesis logs a message with WARN level using the provided format and arguments.
func Hypothesis(format string, args ...interface{}) {
logMessage(WARN, format, args...)
}
// Failure logs a message at the ERROR level using the provided format and arguments.
func Failure(format string, args ...interface{}) {
logMessage(ERROR, format, args...)
}
// Meltdown logs a message at the FATAL level and terminates the program.
func Meltdown(format string, args ...interface{}) {
logMessage(FATAL, format, args...)
}
// Panic logs a message with PANIC severity and terminates the application.
func Panic(format string, args ...interface{}) {
logMessage(PANIC, format, args...)
}
// getCallerInfo retrieves the file name and line number of the caller.
// It returns the information in the format "file:line" or "unknown:0" on failure.
func getCallerInfo() string {
_, file, line, ok := runtime.Caller(callerFrameDepth)
if !ok {
return "unknown:0"
}
return filepath.Base(file) + ":" + strconv.Itoa(line)
}
// logMessage handles the logging of messages with varying severity levels.
// If the log level is below the configured threshold, it is ignored.
// Formats the message, adds a random prefix, and appends optional suffixes.
// Converts the output to JSON format if enabled or applies a custom formatter.
// Executes fatal or panic behaviors when specified severity levels are met.
func logMessage(level StoneLevel, format string, args ...interface{}) {
if level < FATAL && currentLogLevel > level {
return
}
msg := fmt.Sprintf(format, args...)
msg = getRandomPrefix(level) + " " + msg
// Determine if success or failure quote is needed
isSuccess := level <= WARN
if !disableCharacterSuffixes {
msg += getRandomQuote(isSuccess)
}
var formatted string
if useJSON {
caller := getCallerInfo()
entry := logEntry{
Time: time.Now().UTC().Format(time.RFC3339),
Level: level.String(),
Caller: caller,
Message: msg,
}
jsonData, _ := json.Marshal(entry)
formatted = string(jsonData)
} else {
formatted = stoneFormat(level, msg)
}
// Handle special logging behaviors for FATAL and PANIC
switch level {
case FATAL:
stoneLogger.Fatalln(formatted)
case PANIC:
stoneLogger.Panicln(formatted)
default:
stoneLogger.Println(formatted)
}
}
// getRandomPrefix returns a random prefix based on the provided StoneLevel.
// It selects a prefix from predefined lists corresponding to the log level.
func getRandomPrefix(level StoneLevel) string {
switch level {
case TRACE:
idx := rand.Intn(len(stoneTracePrefixes))
return stoneTracePrefixes[idx]
case DEBUG:
idx := rand.Intn(len(stoneDebugPrefixes))
return stoneDebugPrefixes[idx]
case INFO:
idx := rand.Intn(len(stoneInfoPrefixes))
return stoneInfoPrefixes[idx]
case WARN:
idx := rand.Intn(len(stoneWarnPrefixes))
return stoneWarnPrefixes[idx]
case ERROR:
idx := rand.Intn(len(stoneErrorPrefixes))
return stoneErrorPrefixes[idx]
case FATAL:
idx := rand.Intn(len(stonePanicPrefixes))
return stonePanicPrefixes[idx]
default:
idx := rand.Intn(len(stoneInfoPrefixes))
return stoneInfoPrefixes[idx]
}
}
// getRandomQuote returns a random character quote based on success or failure.
// The isSuccess parameter indicates if the quote is for a success (true) or failure.
func getRandomQuote(isSuccess bool) string {
if isSuccess {
idx := rand.Intn(len(characterSuccessQuoteSuffixes))
return characterSuccessQuoteSuffixes[idx]
} else {
idx := rand.Intn(len(characterFailQuoteSuffixes))
return characterFailQuoteSuffixes[idx]
}
}
// stoneFormat formats a log message with level, timestamp, caller, and color.
func stoneFormat(level StoneLevel, msg string) string {
now := time.Now().UTC().Format(time.RFC3339)
caller := getCallerInfo()
return fmt.Sprintf(
"%s%s [%s] [%s] %s%s", level.Color(), now, level.String(), caller, msg,
colorReset,
)
}