250 lines
6.7 KiB
Go
250 lines
6.7 KiB
Go
package lh
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/olekukonko/ll/lx"
|
|
)
|
|
|
|
type TextOption func(*TextHandler)
|
|
|
|
var textBufPool = sync.Pool{
|
|
New: func() any {
|
|
return new(bytes.Buffer)
|
|
},
|
|
}
|
|
|
|
// WithTextTimeFormat enables timestamp display and optionally sets a custom time format.
|
|
// It configures the TextHandler to include temporal information in each log entry,
|
|
// allowing for precise tracking of when log events occur.
|
|
// If the format string is empty, it defaults to time.RFC3339.
|
|
func WithTextTimeFormat(format string) TextOption {
|
|
return func(t *TextHandler) {
|
|
t.Timestamped(true, format)
|
|
}
|
|
}
|
|
|
|
// WithTextShowTime enables or disables timestamp display in log entries.
|
|
// This option provides direct control over the visibility of the time prefix
|
|
// without altering the underlying time format configured in the handler.
|
|
// Setting show to true will prepend timestamps to all subsequent regular log outputs.
|
|
func WithTextShowTime(show bool) TextOption {
|
|
return func(t *TextHandler) {
|
|
t.showTime = show
|
|
}
|
|
}
|
|
|
|
// TextHandler is a handler that outputs log entries as plain text.
|
|
// It formats log entries with namespace, level, message, fields, and optional stack traces,
|
|
// writing the result to the provided writer.
|
|
// Thread-safe if the underlying writer is thread-safe.
|
|
type TextHandler struct {
|
|
writer io.Writer // Destination for formatted log output
|
|
showTime bool // Whether to display timestamps
|
|
timeFormat string // Format for timestamps (defaults to time.RFC3339)
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// NewTextHandler creates a new TextHandler writing to the specified writer.
|
|
// It initializes the handler with the given writer, suitable for outputs like stdout or files.
|
|
// Example:
|
|
//
|
|
// handler := NewTextHandler(os.Stdout)
|
|
// logger := ll.New("app").Enable().Handler(handler)
|
|
// logger.Info("Test") // Output: [app] INFO: Test
|
|
func NewTextHandler(w io.Writer, opts ...TextOption) *TextHandler {
|
|
t := &TextHandler{
|
|
writer: w,
|
|
showTime: false,
|
|
timeFormat: time.RFC3339,
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
opt(t)
|
|
}
|
|
|
|
return t
|
|
}
|
|
|
|
// Timestamped enables or disables timestamp display and optionally sets a custom time format.
|
|
// If format is empty, defaults to RFC3339.
|
|
// Example:
|
|
//
|
|
// handler := NewTextHandler(os.Stdout).TextWithTime(true, time.StampMilli)
|
|
// // Output: Jan 02 15:04:05.000 [app] INFO: Test
|
|
func (h *TextHandler) Timestamped(enable bool, format ...string) {
|
|
h.showTime = enable
|
|
if len(format) > 0 && format[0] != "" {
|
|
h.timeFormat = format[0]
|
|
}
|
|
}
|
|
|
|
// Output sets a new writer for the TextHandler.
|
|
// Thread-safe - safe for concurrent use.
|
|
func (h *TextHandler) Output(w io.Writer) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
h.writer = w
|
|
}
|
|
|
|
// Handle processes a log entry and writes it as plain text.
|
|
// It delegates to specialized methods based on the entry's class (Dump, Raw, or regular).
|
|
// Returns an error if writing to the underlying writer fails.
|
|
// Thread-safe if the writer is thread-safe.
|
|
// Example:
|
|
//
|
|
// handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes "INFO: test"
|
|
func (h *TextHandler) Handle(e *lx.Entry) error {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
if e.Class == lx.ClassDump {
|
|
return h.handleDumpOutput(e)
|
|
}
|
|
|
|
if e.Class == lx.ClassRaw {
|
|
_, err := h.writer.Write([]byte(e.Message))
|
|
return err
|
|
}
|
|
|
|
return h.handleRegularOutput(e)
|
|
}
|
|
|
|
// handleRegularOutput handles normal log entries.
|
|
// It formats the entry with namespace, level, message, fields, and stack trace (if present),
|
|
// writing the result to the handler's writer.
|
|
// Returns an error if writing fails.
|
|
// Example (internal usage):
|
|
//
|
|
// h.handleRegularOutput(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes "INFO: test"
|
|
func (h *TextHandler) handleRegularOutput(e *lx.Entry) error {
|
|
buf := textBufPool.Get().(*bytes.Buffer)
|
|
buf.Reset()
|
|
defer textBufPool.Put(buf)
|
|
|
|
if h.showTime {
|
|
buf.WriteString(e.Timestamp.Format(h.timeFormat))
|
|
buf.WriteString(lx.Space)
|
|
}
|
|
|
|
switch e.Style {
|
|
case lx.NestedPath:
|
|
if e.Namespace != "" {
|
|
parts := strings.Split(e.Namespace, lx.Slash)
|
|
for i, part := range parts {
|
|
buf.WriteString(lx.LeftBracket)
|
|
buf.WriteString(part)
|
|
buf.WriteString(lx.RightBracket)
|
|
if i < len(parts)-1 {
|
|
buf.WriteString(lx.Arrow)
|
|
}
|
|
}
|
|
buf.WriteString(lx.Colon)
|
|
buf.WriteString(lx.Space)
|
|
}
|
|
default: // FlatPath
|
|
if e.Namespace != "" {
|
|
buf.WriteString(lx.LeftBracket)
|
|
buf.WriteString(e.Namespace)
|
|
buf.WriteString(lx.RightBracket)
|
|
buf.WriteString(lx.Space)
|
|
}
|
|
}
|
|
|
|
buf.WriteString(e.Level.Name(e.Class))
|
|
// buf.WriteString(lx.Space)
|
|
buf.WriteString(lx.Colon)
|
|
buf.WriteString(lx.Space)
|
|
buf.WriteString(e.Message)
|
|
|
|
if len(e.Fields) > 0 {
|
|
buf.WriteString(lx.Space)
|
|
buf.WriteString(lx.LeftBracket)
|
|
for i, pair := range e.Fields {
|
|
if i > 0 {
|
|
buf.WriteString(lx.Space)
|
|
}
|
|
buf.WriteString(pair.Key)
|
|
buf.WriteString("=")
|
|
fmt.Fprint(buf, pair.Value)
|
|
}
|
|
buf.WriteString(lx.RightBracket)
|
|
}
|
|
|
|
if len(e.Stack) > 0 {
|
|
h.formatStack(buf, e.Stack)
|
|
}
|
|
|
|
if e.Level != lx.LevelNone {
|
|
buf.WriteString(lx.Newline)
|
|
}
|
|
|
|
_, err := h.writer.Write(buf.Bytes())
|
|
return err
|
|
}
|
|
|
|
// handleDumpOutput specially formats hex dump output (plain text version).
|
|
// It wraps the dump message with BEGIN/END separators for clarity.
|
|
// Returns an error if writing fails.
|
|
// Example (internal usage):
|
|
//
|
|
// h.handleDumpOutput(&lx.Entry{Class: lx.ClassDump, Message: "pos 00 hex: 61"}) // Writes "---- BEGIN DUMP ----\npos 00 hex: 61\n---- END DUMP ----\n"
|
|
func (h *TextHandler) handleDumpOutput(e *lx.Entry) error {
|
|
buf := textBufPool.Get().(*bytes.Buffer)
|
|
buf.Reset()
|
|
defer textBufPool.Put(buf)
|
|
|
|
if h.showTime {
|
|
buf.WriteString(e.Timestamp.Format(h.timeFormat))
|
|
buf.WriteString(lx.Newline)
|
|
}
|
|
|
|
buf.WriteString("---- BEGIN DUMP ----\n")
|
|
buf.WriteString(e.Message)
|
|
buf.WriteString("---- END DUMP ----\n\n")
|
|
|
|
_, err := h.writer.Write(buf.Bytes())
|
|
return err
|
|
}
|
|
|
|
// formatStack formats a stack trace for plain text output.
|
|
// It structures the stack trace with indentation and separators for readability,
|
|
// including goroutine and function/file details.
|
|
// Example (internal usage):
|
|
//
|
|
// h.formatStack(&builder, []byte("goroutine 1 [running]:\nmain.main()\n\tmain.go:10")) // Appends formatted stack trace
|
|
func (h *TextHandler) formatStack(b *bytes.Buffer, stack []byte) {
|
|
lines := strings.Split(string(stack), "\n")
|
|
if len(lines) == 0 {
|
|
return
|
|
}
|
|
|
|
b.WriteString("\n[stack]\n")
|
|
|
|
b.WriteString(" ┌─ ")
|
|
b.WriteString(lines[0])
|
|
b.WriteString("\n")
|
|
|
|
for i := 1; i < len(lines); i++ {
|
|
line := strings.TrimSpace(lines[i])
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
if strings.Contains(line, ".go") {
|
|
b.WriteString(" ├ ")
|
|
} else {
|
|
b.WriteString(" │ ")
|
|
}
|
|
b.WriteString(line)
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
b.WriteString(" └\n")
|
|
}
|