package log import ( "context" "fmt" "io" "log/slog" "os" "path" "runtime" "slices" "sync" "time" ) type gameLogHandler struct { h slog.Handler files []string } func NewGameLogHandler(files []string, opts *slog.HandlerOptions) *gameLogHandler { return &gameLogHandler{h: newTextHandler(os.Stdout, opts), files: files} } func (h *gameLogHandler) Enabled(ctx context.Context, level slog.Level) bool { return h.h.Enabled(ctx, level) } func (h *gameLogHandler) Handle(ctx context.Context, r slog.Record) error { if pc, file, _, ok := runtime.Caller(4); ok { basename := path.Base(file) if len(h.files) > 0 && !slices.Contains(h.files, basename) { return nil } r.PC = pc } return h.h.Handle(ctx, r) } func (h *gameLogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return &gameLogHandler{h: h.h.WithAttrs(attrs), files: h.files} } func (h *gameLogHandler) WithGroup(name string) slog.Handler { return &gameLogHandler{h: h.h.WithGroup(name), files: h.files} } type TextHandler struct { opts *slog.HandlerOptions // TODO: state for WithGroup and WithAttrs mu *sync.Mutex out io.Writer } func newTextHandler(out io.Writer, opts *slog.HandlerOptions) *TextHandler { h := &TextHandler{out: out, mu: &sync.Mutex{}} if opts != nil { h.opts = opts } if h.opts.Level == nil { h.opts.Level = slog.LevelInfo } return h } func (h *TextHandler) Enabled(ctx context.Context, level slog.Level) bool { return level >= h.opts.Level.Level() } func (h *TextHandler) WithGroup(name string) slog.Handler { // TODO: implement. return h } func (h *TextHandler) WithAttrs(attrs []slog.Attr) slog.Handler { // TODO: implement. return h } func (h *TextHandler) Handle(ctx context.Context, r slog.Record) error { buf := make([]byte, 0, 1024) if !r.Time.IsZero() { buf = h.appendAttr(buf, slog.Time(slog.TimeKey, r.Time)) } if r.Level == 99 { buf = fmt.Append(buf, "GAME ") } else { buf = fmt.Appendf(buf, "%s ", r.Level) } fs := runtime.CallersFrames([]uintptr{r.PC}) f, _ := fs.Next() basename := path.Base(f.File) buf = fmt.Appendf(buf, "%s:%d ", basename, f.Line) buf = h.appendAttr(buf, slog.String(slog.MessageKey, r.Message)) r.Attrs(func(a slog.Attr) bool { buf = h.appendAttr(buf, a) return true }) // Trim last whitespace and add newline buf = fmt.Append(buf[:len(buf)-1], "\n") h.mu.Lock() defer h.mu.Unlock() _, err := h.out.Write(buf) return err } func (h *TextHandler) appendAttr(buf []byte, a slog.Attr) []byte { // Resolve the Attr's value before doing anything else. a.Value = a.Value.Resolve() // Ignore empty Attrs. if a.Equal(slog.Attr{}) { return buf } switch a.Value.Kind() { case slog.KindTime: buf = fmt.Appendf(buf, "%s", a.Value.Time().Format(time.TimeOnly)) case slog.KindGroup: attrs := a.Value.Group() // Ignore empty groups. if len(attrs) == 0 { return buf } // If the key is non-empty, write it out and indent the rest of the attrs. // Otherwise, inline the attrs. if a.Key != "" { buf = fmt.Appendf(buf, "%s: ", a.Key) } buf = fmt.Append(buf, "[", a.Key) for _, ga := range attrs { buf = h.appendAttr(buf, ga) buf = fmt.Append(buf, ", ", a.Key) } buf = fmt.Append(buf[:len(buf)-2], "]", a.Key) default: buf = fmt.Appendf(buf, "%s", a.Value) } buf = fmt.Appendf(buf, "%*s", 1, "") return buf }