vendor/github.com/olekukonko/ll/lh/text.go
package lh
import (
"fmt"
"io"
"sort"
"strings"
"sync"
"time"
"github.com/olekukonko/ll/lx"
)
// 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 {
w 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) *TextHandler {
return &TextHandler{
w: w,
showTime: false,
timeFormat: time.RFC3339,
}
}
// 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]
}
}
// 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()
// Special handling for dump output
if e.Class == lx.ClassDump {
return h.handleDumpOutput(e)
}
// Raw entries are written directly without formatting
if e.Class == lx.ClassRaw {
_, err := h.w.Write([]byte(e.Message))
return err
}
// Handle standard log entries
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 {
var builder strings.Builder // Buffer for building formatted output
// Add timestamp if enabled
if h.showTime {
builder.WriteString(e.Timestamp.Format(h.timeFormat))
builder.WriteString(lx.Space)
}
// Format namespace based on style
switch e.Style {
case lx.NestedPath:
if e.Namespace != "" {
// Split namespace into parts and format as [parent]→[child]
parts := strings.Split(e.Namespace, lx.Slash)
for i, part := range parts {
builder.WriteString(lx.LeftBracket)
builder.WriteString(part)
builder.WriteString(lx.RightBracket)
if i < len(parts)-1 {
builder.WriteString(lx.Arrow)
}
}
builder.WriteString(lx.Colon)
builder.WriteString(lx.Space)
}
default: // FlatPath
if e.Namespace != "" {
// Format namespace as [parent/child]
builder.WriteString(lx.LeftBracket)
builder.WriteString(e.Namespace)
builder.WriteString(lx.RightBracket)
builder.WriteString(lx.Space)
}
}
// Add level and message
builder.WriteString(e.Level.String())
builder.WriteString(lx.Colon)
builder.WriteString(lx.Space)
builder.WriteString(e.Message)
// Add fields in sorted order
if len(e.Fields) > 0 {
var keys []string
for k := range e.Fields {
keys = append(keys, k)
}
// Sort keys for consistent output
sort.Strings(keys)
builder.WriteString(lx.Space)
builder.WriteString(lx.LeftBracket)
for i, k := range keys {
if i > 0 {
builder.WriteString(lx.Space)
}
// Format field as key=value
builder.WriteString(k)
builder.WriteString("=")
builder.WriteString(fmt.Sprint(e.Fields[k]))
}
builder.WriteString(lx.RightBracket)
}
// Add stack trace if present
if len(e.Stack) > 0 {
h.formatStack(&builder, e.Stack)
}
// Append newline for non-None levels
if e.Level != lx.LevelNone {
builder.WriteString(lx.Newline)
}
// Write formatted output to writer
_, err := h.w.Write([]byte(builder.String()))
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 {
// For text handler, we just add a newline before dump output
var builder strings.Builder // Buffer for building formatted output
// Add timestamp if enabled
if h.showTime {
builder.WriteString(e.Timestamp.Format(h.timeFormat))
builder.WriteString(lx.Newline)
}
// Add separator lines and dump content
builder.WriteString("---- BEGIN DUMP ----\n")
builder.WriteString(e.Message)
builder.WriteString("---- END DUMP ----\n")
// Write formatted output to writer
_, err := h.w.Write([]byte(builder.String()))
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 *strings.Builder, stack []byte) {
lines := strings.Split(string(stack), "\n")
if len(lines) == 0 {
return
}
// Start stack trace section
b.WriteString("\n[stack]\n")
// First line: goroutine
b.WriteString(" ┌─ ")
b.WriteString(lines[0])
b.WriteString("\n")
// Iterate through remaining lines
for i := 1; i < len(lines); i++ {
line := strings.TrimSpace(lines[i])
if line == "" {
continue
}
if strings.Contains(line, ".go") {
// File path lines get extra indent
b.WriteString(" ├ ")
} else {
// Function names
b.WriteString(" │ ")
}
b.WriteString(line)
b.WriteString("\n")
}
// End stack trace section
b.WriteString(" └\n")
}