From 14687cfc2f6de145d7acbec403cdf1f34e2ef06c Mon Sep 17 00:00:00 2001 From: Rene Nochebuena Date: Thu, 10 Apr 2025 18:30:35 -0600 Subject: [PATCH] Release v1.0.0 (#2) Reviewed-on: https://gitstormr.dev/stone-utils/stonelog/pulls/2 Reviewed-by: Cloud Administrator Co-authored-by: Rene Nochebuena Co-committed-by: Rene Nochebuena --- README.md | 64 ++++++++++ log.go | 255 +++++++++++++++++++++++++++++++++++++++ log_test.go | 114 +++++++++++++++++ prefixes.go | 30 +++-- sonar-project.properties | 7 +- suffixes.go | 47 ++++++++ testbin/main.go | 14 +++ 7 files changed, 522 insertions(+), 9 deletions(-) create mode 100644 log.go create mode 100644 log_test.go create mode 100644 suffixes.go create mode 100644 testbin/main.go diff --git a/README.md b/README.md index 4d5c3ba..57b29ae 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,70 @@ The most scientifically badass logging library from the Kingdom of Science! go get gitstormr.dev/stone-utils/stonelog@latest ``` +## ⚡ Initialization + +```go +package main + +import ( + "gitstormr.dev/stone-utils/stonelog" +) + +func main() { + // Basic configuration (INFO level by default) + stonelog.InitStoneLog(stonelog.INFO, false, false) // false = text mode (true = JSON), false = keep suffixes (true = don't add character quotes) + + // Example logs + stonelog.Observation("System initialized successfully!") + stonelog.Failure("Reactor core temperature critical!") +} +``` + +## 📜 Log Levels (Scientific Style!) + +| Function | Level Equivalent | Example Output | +|---------------|--------------------|--------------------------------------------------| +| Trace() | TRACE | 🔍 Science traces: Entering function X | +| Debug() | DEBUG | 🤔 Hypothesis forming: Variable X = 42 | +| Observation() | INFO | ✅ Experiment successful: User logged in | +| Hypothesis() | WARN | ⚠️ Anomaly detected: High memory usage | +| Failure() | ERROR | 💥 Critical malfunction: DB disconnected | +| Meltdown() | FATAL | ☠️ FINAL EXPERIMENT FAILED: Server exploded | +| Panic() | PANIC | 🚨 CHAIN REACTION DETECTED: Unrecoverable error | + +## 🎛️ Changing Log Level + +```go +// Dynamic change! (e.g., enable DEBUG in production) +stonelog.SetLogLevel(stonelog.DEBUG) +``` + +## 🔬 JSON Mode (For Production) + +```go +stonelog.InitStoneLab(stonelog.STONE_DEBUG, true, false) // true = JSON, false = Keep suffixes +// Output: {"time":"2023-07-15T12:00:00Z","level":"OBSERVATION","message":"✅ Experiment successful: System ready","caller":"main.go:15"} +``` + +## **🚨 WINDOWS COLOR SUPPORT NOTICE 🚨** + +*"HEY WINDOWS USERS! YOUR TERMINALS NEED SOME SCIENCE!" - Chrome* + +🔬 **Current Limitations:** +- Colors don't work in old Windows Command Prompt (CMD) +- Looks boring in black & white (like stone tablets!) +- Works perfectly in modern terminals + +💡 **Senku-Approved Solutions:** +1. Install **Windows Terminal** (Microsoft Store) +2. Use **WSL** (Windows Subsystem for Linux) +3. Try **VS Code** or **Git Bash** + +*"This temporary setback represents just 0.0000001% of our scientific progress!"* +- **Dr. Senku Ishigami** + +*(Chrome whispers: "Pssst... Linux terminals are SO BADASS!")* 🐧🔥 + **Join the Scientific Revolution!** > "This isn't just logging - it's 10 billion percent scientific progress!" - Senku Ishigami diff --git a/log.go b/log.go new file mode 100644 index 0000000..dbbe934 --- /dev/null +++ b/log.go @@ -0,0 +1,255 @@ +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) { + if level < TRACE || level > PANIC { + return + } + + 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 string based on the given StoneLevel. +// It selects from predefined lists corresponding to the severity of the level. +func getRandomPrefix(level StoneLevel) string { + // Map StoneLevel to their respective prefix slices + prefixMap := map[StoneLevel][]string{ + TRACE: stoneTracePrefixes, + DEBUG: stoneDebugPrefixes, + INFO: stoneInfoPrefixes, + WARN: stoneWarnPrefixes, + ERROR: stoneErrorPrefixes, + FATAL: stonePanicPrefixes, + PANIC: stonePanicPrefixes, + } + + prefixes := prefixMap[level] + + // Select and return a random prefix + return prefixes[rand.Intn(len(prefixes))] +} + +// 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, + ) +} diff --git a/log_test.go b/log_test.go new file mode 100644 index 0000000..f7fcbb1 --- /dev/null +++ b/log_test.go @@ -0,0 +1,114 @@ +package stonelog + +import ( + "errors" + "os/exec" + "testing" +) + +func Test_LogLevelString(t *testing.T) { + levelTests := []struct { + level StoneLevel + expected string + }{ + {TRACE, "TRACE"}, + {DEBUG, "DEBUG"}, + {INFO, "INFO"}, + {WARN, "WARN"}, + {ERROR, "ERROR"}, + {FATAL, "FATAL"}, + {PANIC, "PANIC"}, + } + + for _, tt := range levelTests { + if tt.level.String() != tt.expected { + t.Errorf("Expected %s, got %s", tt.expected, tt.level.String()) + } + } +} + +func Test_LogLevelColor(t *testing.T) { + levelTests := []struct { + level StoneLevel + expected string + }{ + {TRACE, colorWhite}, + {DEBUG, colorCyan}, + {INFO, colorGreen}, + {WARN, colorYellow}, + {ERROR, colorRed}, + {FATAL, colorMagenta}, + {PANIC, colorRed + "\033[1m"}, + } + + for _, tt := range levelTests { + if tt.level.Color() != tt.expected { + t.Errorf("Expected %s, got %s", tt.expected, tt.level.Color()) + } + } +} + +func Test_JSONLogs(t *testing.T) { + InitStoneLog(TRACE, true, false) + Trace("This is a trace log") + Debug("This is a debug log") + Observation("This is an observation log") + Hypothesis("This is a hypothesis log") + Failure("This is a failure log") + + SetLogLevel(INFO) +} + +func Test_PlainTextLogs(t *testing.T) { + InitStoneLog(TRACE, false, false) + Trace("This is a trace log") + Debug("This is a debug log") + Observation("This is an observation log") + Hypothesis("This is a hypothesis log") + Failure("This is a failure log") + + SetLogLevel(INFO) +} + +func Test_MutedPlainTextLogs(t *testing.T) { + InitStoneLog(ERROR, false, true) + + // This won't change the log level + SetLogLevel(-1) + + // Following won't appear + Trace("This is a trace log") + Debug("This is a debug log") + Observation("This is an observation log") + Hypothesis("This is a hypothesis log") + + // Until this one + Failure("This is a failure log") +} + +func Test_PanicPlainTextLogs(t *testing.T) { + defer func() { + if r := recover(); r != nil { + Observation("Recovered from panic: %v", r) + } + }() + + InitStoneLog(TRACE, false, true) + Panic("This is a panic log") +} + +func Test_FatalPlainTextLogs(t *testing.T) { + cmd := exec.Command( + "go", "run", "./testbin/main.go", + ) + cmd.Env = append(cmd.Env, "TEST_FATAL=1") + + err := cmd.Run() + + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + if exitErr.ExitCode() != 1 { + t.Errorf("Expected exit code 1, got %d", exitErr.ExitCode()) + } + } +} diff --git a/prefixes.go b/prefixes.go index b81c19c..0bb548c 100644 --- a/prefixes.go +++ b/prefixes.go @@ -1,10 +1,17 @@ package stonelog +// stoneTracePrefixes contains prefixes for trace-level scientific messages. +// stoneDebugPrefixes contains prefixes for debug-level diagnostics and processes. +// stoneInfoPrefixes contains prefixes for informational scientific outcomes. +// stoneWarnPrefixes contains prefixes for warning-level messages of potential risks. +// stoneErrorPrefixes contains prefixes for error-level critical issues detected. +// stoneFatalPrefixes contains prefixes for fatal-level critical failures. +// stonePanicPrefixes contains prefixes for panic-level catastrophic failures. var ( stoneTracePrefixes = []string{ - "Science traces", - "Lab sensors detect", - "Microscope focus on", + "Science traces:", + "Lab sensors detect:", + "Microscope focus on:", "Quantum scanner active:", "Data particles observed:", } @@ -41,10 +48,19 @@ var ( "Hypothesis disproven:", } + stoneFatalPrefixes = []string{ + "SCIENTIFIC APOCALYPSE:", + "FINAL EXPERIMENT FAILED:", + "LABORATORY SHUTDOWN:", + "CORE CRITICAL:", + "RADIOACTIVE TERMINATION:", + } + stonePanicPrefixes = []string{ - "¡¡¡CATASTROPHIC FAILURE!!!", - "Emergency protocol FAILED:", - "LEAVE THE LAB!", - "¡¡¡IMMINENT NUCLEAR FUSION!!!", + "CHAIN REACTION DETECTED:", + "SYSTEMIC COLLAPSE:", + "QUANTUM CATASTROPHE:", + "GALACTIC-LEVEL BUG DETECTED:", + "T-REX IN THE DATACENTER:", } ) diff --git a/sonar-project.properties b/sonar-project.properties index 31737c3..e3c95ed 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -2,6 +2,9 @@ sonar.projectKey=b97a45b2-c25d-4fd1-898d-136414896ce6 sonar.projectName=mother-utils/motherlog sonar.language=go sonar.sources=. -sonar.exclusions=**/*_test.go +sonar.exclusions=**/*_test.go, testbin/** sonar.tests=. -sonar.test.inclusions=**/*_test.go \ No newline at end of file +sonar.test.inclusions=**/*_test.go +sonar.go.tests.reportPaths=test-report.out +sonar.go.coverage.reportPaths=coverage.out +sonar.qualitygate.wait=true \ No newline at end of file diff --git a/suffixes.go b/suffixes.go new file mode 100644 index 0000000..23a44e6 --- /dev/null +++ b/suffixes.go @@ -0,0 +1,47 @@ +package stonelog + +// characterSuccessQuoteSuffixes contains success quote suffixes by characters. +// characterFailQuoteSuffixes contains failure quote suffixes by characters. +var ( + characterSuccessQuoteSuffixes = []string{ + " // Senku: '10 BILLION POINTS FOR SCIENCE! REVIVAL SUCCESSFUL!'", + " // Senku: 'Mwa-ha-ha! Another victory for the Kingdom of Science!'", + " // Senku: 'E=mc², baby! That’s how you optimize!'", + " // Senku: 'This experiment... is a 10 billion percent success!'", + + " // Chrome: 'SO BADASS! SENKU, YOU’RE A GENIUS!'", + " // Chrome: 'HOLY CRAP, SCIENCE JUST PUNCHED THE SKY!'", + " // Chrome: 'WE’RE OFFICIALLY WIZARDS NOW!'", + + " // Kohaku: 'Heh. Even I could’ve done that... maybe.'", + " // Kohaku: 'Science + fists = unstoppable!'", + + " // Ryusui: 'NAVIGATION SUCCESSFUL! Time to monetize this!'", + " // Ryusui: 'Money can’t buy this... BUT I’LL TRY!'", + + " // Kaseki: 'HOT BLOODED ENGINEERING... PERFECTED! (ᗒᗣᗕ)՞'", + " // Kaseki: 'I’LL CARVE A STATUE TO COMMEMORATE THIS MOMENT!'", + } + + characterFailQuoteSuffixes = []string{ + " // Senku: '10 BILLION REASONS TO FIX THIS... NOW.'", + " // Senku: 'This failure is... statistically impressive.'", + " // Senku: 'Mwa-ha-ha... *nervous laugh*... reboot everything.'", + + " // Chrome: 'NOT BADASS! NOT BADASS AT ALL! (╥﹏╥)'", + " // Chrome: 'MY BRAIN HURTS! IS THIS HOW SCIENCE WORKS?!'", + + " // Kohaku: 'Ugh. Can I just smash the server with a rock?'", + " // Kohaku: 'Senku, your science is broken. FIX IT.'", + + " // Gen: 'Ah~... so this is how the world ends~?'", + " // Gen: 'Mentally calculating... yep, we’re doomed~.'", + + " // Tsukasa: '...This is why I opposed technology.'", + + " // Kaseki: 'BACK IN MY DAY, ERRORS WERE FIXED WITH A HAMMER! 🔨'", + " // Kaseki: 'MY SOUL... IT BURNS WITH DEBUGGING RAGE!!!'", + + " // Francois: 'I’ll prepare a funeral tea for the deceased process.'", + } +) diff --git a/testbin/main.go b/testbin/main.go new file mode 100644 index 0000000..d51eccb --- /dev/null +++ b/testbin/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "os" + + "gitstormr.dev/stone-utils/stonelog" +) + +func main() { + if os.Getenv("TEST_FATAL") == "1" { + stonelog.InitStoneLog(stonelog.TRACE, false, false) + stonelog.Meltdown("A fatal error occurred") + } +}