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() }