Files
headplane/internal/util/logger.go
T
2025-06-16 11:45:23 -04:00

141 lines
2.7 KiB
Go

package util
import (
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"time"
)
type LogLevel string
const (
LevelInfo LogLevel = "info"
LevelDebug LogLevel = "debug"
LevelError LogLevel = "error"
LevelFatal LogLevel = "fatal"
LevelMsg LogLevel = "msg"
)
type LogMessage struct {
Level LogLevel
Time string
Message any
}
type Logger struct {
debugEnabled bool
encoder *json.Encoder
pool *sync.Pool
}
var logger = NewLogger()
func GetLogger() *Logger {
return logger
}
func NewLogger() *Logger {
enc := json.NewEncoder(os.Stdout)
enc.SetEscapeHTML(false)
return &Logger{
encoder: enc,
pool: &sync.Pool{
New: func() any {
return &LogMessage{}
},
},
}
}
func (l *Logger) SetDebug(enabled bool) {
if enabled {
l.debugEnabled = true
l.Info("Enabling Debug logging for headplane-agent")
l.Info("Be careful, this will spam a lot of information")
}
}
func (l *Logger) log(level LogLevel, format string, v ...any) {
msg := fmt.Sprintf(format, v...)
timestamp := time.Now().Format(time.RFC3339)
// Manually construct compact JSON line for performance
line := `{"Level":"` + string(level) +
`","Time":"` + timestamp +
`","Message":"` + escapeString(msg) + `"}` + "\n"
if level == LevelError || level == LevelFatal {
os.Stderr.WriteString(line)
}
// Always write to stdout but also write to stderr for errors
os.Stdout.WriteString(line)
if level == LevelFatal {
os.Exit(1)
}
}
func (l *Logger) Debug(format string, v ...any) {
if l.debugEnabled {
l.log(LevelDebug, format, v...)
}
}
func (l *Logger) Info(format string, v ...any) { l.log(LevelInfo, format, v...) }
func (l *Logger) Error(format string, v ...any) { l.log(LevelError, format, v...) }
func (l *Logger) Fatal(format string, v ...any) { l.log(LevelFatal, format, v...) }
func (l *Logger) Msg(obj any) {
entry := l.pool.Get().(*LogMessage)
defer l.pool.Put(entry)
entry.Level = LevelMsg
entry.Time = time.Now().Format(time.RFC3339)
entry.Message = obj
// Because the encoder is tied to STDOUT we get a message
_ = l.encoder.Encode(entry)
// Reset the entry for reuse
entry.Level = ""
entry.Time = ""
entry.Message = nil
}
func escapeString(s string) string {
var b strings.Builder
b.Grow(len(s) + 16) // pre-grow to reduce reallocs
for i := 0; i < len(s); i++ {
c := s[i]
switch c {
case '"':
b.WriteString(`\"`)
case '\\':
b.WriteString(`\\`)
case '\b':
b.WriteString(`\b`)
case '\f':
b.WriteString(`\f`)
case '\n':
b.WriteString(`\n`)
case '\r':
b.WriteString(`\r`)
case '\t':
b.WriteString(`\t`)
default:
if c < 0x20 {
// Control characters like 0x01, 0x07 (bell), etc.
fmt.Fprintf(&b, `\u%04x`, c)
} else {
b.WriteByte(c)
}
}
}
return b.String()
}