headplane/agent/internal/util/logger.go

118 lines
2.3 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 {
replacer := strings.NewReplacer(
`"`, `\"`,
`\`, `\\`,
"\n", `\n`,
"\t", `\t`,
)
return replacer.Replace(s)
}