Initial QSfera import
This commit is contained in:
+307
@@ -0,0 +1,307 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/olekukonko/ll/lx"
|
||||
)
|
||||
|
||||
// Buffering holds configuration for the Buffered handler.
|
||||
type Buffering struct {
|
||||
BatchSize int // Flush when this many entries are buffered (default: 100)
|
||||
FlushInterval time.Duration // Maximum time between flushes (default: 10s)
|
||||
MaxBuffer int // Maximum buffer size before applying backpressure (default: 1000)
|
||||
OnOverflow func(int) // Called when buffer reaches MaxBuffer (default: logs warning)
|
||||
ErrorOutput io.Writer // Destination for internal errors like flush failures (default: os.Stderr)
|
||||
}
|
||||
|
||||
// BufferingOpt configures Buffered handler.
|
||||
type BufferingOpt func(*Buffering)
|
||||
|
||||
// WithBatchSize sets the batch size for flushing.
|
||||
// It specifies the number of log entries to buffer before flushing to the underlying handler.
|
||||
// Example:
|
||||
//
|
||||
// handler := NewBuffered(textHandler, WithBatchSize(50)) // Flush every 50 entries
|
||||
func WithBatchSize(size int) BufferingOpt {
|
||||
return func(c *Buffering) {
|
||||
c.BatchSize = size
|
||||
}
|
||||
}
|
||||
|
||||
// WithFlushInterval sets the maximum time between flushes.
|
||||
// It defines the interval at which buffered entries are flushed, even if the batch size is not reached.
|
||||
// Example:
|
||||
//
|
||||
// handler := NewBuffered(textHandler, WithFlushInterval(5*time.Second)) // Flush every 5 seconds
|
||||
func WithFlushInterval(d time.Duration) BufferingOpt {
|
||||
return func(c *Buffering) {
|
||||
c.FlushInterval = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithMaxBuffer sets the maximum buffer size before backpressure.
|
||||
// It limits the number of entries that can be queued in the channel, triggering overflow handling if exceeded.
|
||||
// Example:
|
||||
//
|
||||
// handler := NewBuffered(textHandler, WithMaxBuffer(500)) // Allow up to 500 buffered entries
|
||||
func WithMaxBuffer(size int) BufferingOpt {
|
||||
return func(c *Buffering) {
|
||||
c.MaxBuffer = size
|
||||
}
|
||||
}
|
||||
|
||||
// WithOverflowHandler sets the overflow callback.
|
||||
// It specifies a function to call when the buffer reaches MaxBuffer, typically for logging or metrics.
|
||||
// Example:
|
||||
//
|
||||
// handler := NewBuffered(textHandler, WithOverflowHandler(func(n int) { fmt.Printf("Overflow: %d entries\n", n) }))
|
||||
func WithOverflowHandler(fn func(int)) BufferingOpt {
|
||||
return func(c *Buffering) {
|
||||
c.OnOverflow = fn
|
||||
}
|
||||
}
|
||||
|
||||
// WithErrorOutput sets the destination for internal errors (e.g., downstream handler failures).
|
||||
// Defaults to os.Stderr if not set.
|
||||
// Example:
|
||||
//
|
||||
// // Redirect internal errors to a file or discard them
|
||||
// handler := NewBuffered(textHandler, WithErrorOutput(os.Stdout))
|
||||
func WithErrorOutput(w io.Writer) BufferingOpt {
|
||||
return func(c *Buffering) {
|
||||
c.ErrorOutput = w
|
||||
}
|
||||
}
|
||||
|
||||
// Buffered wraps any Handler to provide buffering capabilities.
|
||||
// It buffers log entries in a channel and flushes them based on batch size, time interval, or explicit flush.
|
||||
// The generic type H ensures compatibility with any lx.Handler implementation.
|
||||
// Thread-safe via channels and sync primitives.
|
||||
type Buffered[H lx.Handler] struct {
|
||||
handler H // Underlying handler to process log entries
|
||||
config *Buffering // Configuration for batching and flushing
|
||||
entries chan *lx.Entry // Channel for buffering log entries
|
||||
flushSignal chan struct{} // Channel to trigger explicit flushes
|
||||
shutdown chan struct{} // Channel to signal worker shutdown
|
||||
shutdownOnce sync.Once // Ensures Close is called only once
|
||||
wg sync.WaitGroup // Waits for worker goroutine to finish
|
||||
}
|
||||
|
||||
// NewBuffered creates a new buffered handler that wraps another handler.
|
||||
// It initializes the handler with default or provided configuration options and starts a worker goroutine.
|
||||
// Thread-safe via channel operations and finalizer for cleanup.
|
||||
// Example:
|
||||
//
|
||||
// textHandler := lh.NewTextHandler(os.Stdout)
|
||||
// buffered := NewBuffered(textHandler, WithBatchSize(50))
|
||||
func NewBuffered[H lx.Handler](handler H, opts ...BufferingOpt) *Buffered[H] {
|
||||
// Initialize default configuration
|
||||
config := &Buffering{
|
||||
BatchSize: 100, // Default: flush every 100 entries
|
||||
FlushInterval: 10 * time.Second, // Default: flush every 10 seconds
|
||||
MaxBuffer: 1000, // Default: max 1000 entries in buffer
|
||||
ErrorOutput: os.Stderr, // Default: report errors to stderr
|
||||
OnOverflow: func(count int) { // Default: log overflow to io.Discard (silent by default for overflow)
|
||||
fmt.Fprintf(io.Discard, "log buffer overflow: %d entries\n", count)
|
||||
},
|
||||
}
|
||||
|
||||
// Apply provided options
|
||||
for _, opt := range opts {
|
||||
opt(config)
|
||||
}
|
||||
|
||||
// Ensure sane configuration values
|
||||
if config.BatchSize < 1 {
|
||||
config.BatchSize = 1 // Minimum batch size is 1
|
||||
}
|
||||
if config.MaxBuffer < config.BatchSize {
|
||||
config.MaxBuffer = config.BatchSize * 10 // Ensure buffer is at least 10x batch size
|
||||
}
|
||||
if config.FlushInterval <= 0 {
|
||||
config.FlushInterval = 10 * time.Second // Minimum flush interval is 10s
|
||||
}
|
||||
if config.ErrorOutput == nil {
|
||||
config.ErrorOutput = os.Stderr
|
||||
}
|
||||
|
||||
// Initialize Buffered handler
|
||||
b := &Buffered[H]{
|
||||
handler: handler, // Set underlying handler
|
||||
config: config, // Set configuration
|
||||
entries: make(chan *lx.Entry, config.MaxBuffer), // Create buffered channel
|
||||
flushSignal: make(chan struct{}, 1), // Create single-slot flush signal channel
|
||||
shutdown: make(chan struct{}), // Create shutdown signal channel
|
||||
}
|
||||
|
||||
// Start worker goroutine
|
||||
b.wg.Add(1)
|
||||
go b.worker()
|
||||
|
||||
// Set finalizer for cleanup during garbage collection
|
||||
runtime.SetFinalizer(b, (*Buffered[H]).Final)
|
||||
return b
|
||||
}
|
||||
|
||||
// Handle implements the lx.Handler interface.
|
||||
// It buffers log entries in the entries channel or triggers a flush on overflow.
|
||||
// Returns an error if the buffer is full and flush cannot be triggered.
|
||||
// Thread-safe via non-blocking channel operations.
|
||||
// Example:
|
||||
//
|
||||
// buffered.Handle(&lx.Entry{Message: "test"}) // Buffers entry or triggers flush
|
||||
func (b *Buffered[H]) Handle(e *lx.Entry) error {
|
||||
select {
|
||||
case b.entries <- e: // Buffer entry if channel has space
|
||||
return nil
|
||||
default: // Handle buffer overflow
|
||||
if b.config.OnOverflow != nil {
|
||||
b.config.OnOverflow(len(b.entries)) // Call overflow handler
|
||||
}
|
||||
select {
|
||||
case b.flushSignal <- struct{}{}: // Trigger flush if possible
|
||||
return fmt.Errorf("log buffer overflow, triggering flush")
|
||||
default: // Flush already in progress
|
||||
return fmt.Errorf("log buffer overflow and flush already in progress")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flush triggers an immediate flush of buffered entries.
|
||||
// It sends a signal to the worker to process all buffered entries.
|
||||
// If a flush is already pending, it waits briefly and may exit without flushing.
|
||||
// Thread-safe via non-blocking channel operations.
|
||||
// Example:
|
||||
//
|
||||
// buffered.Flush() // Flushes all buffered entries
|
||||
func (b *Buffered[H]) Flush() {
|
||||
select {
|
||||
case b.flushSignal <- struct{}{}: // Signal worker to flush
|
||||
case <-time.After(100 * time.Millisecond): // Timeout if flush is pending
|
||||
// Flush already pending
|
||||
}
|
||||
}
|
||||
|
||||
// Close flushes any remaining entries and stops the worker.
|
||||
// It ensures shutdown is performed only once and waits for the worker to finish.
|
||||
// If the underlying handler implements a Close() error method, it will be called to release resources.
|
||||
// Thread-safe via sync.Once and WaitGroup.
|
||||
// Returns any error from the underlying handler's Close, or nil.
|
||||
// Example:
|
||||
//
|
||||
// buffered.Close() // Flushes entries and stops worker
|
||||
func (b *Buffered[H]) Close() error {
|
||||
var closeErr error
|
||||
b.shutdownOnce.Do(func() {
|
||||
close(b.shutdown) // Signal worker to shut down
|
||||
b.wg.Wait() // Wait for worker to finish
|
||||
runtime.SetFinalizer(b, nil) // Remove finalizer
|
||||
|
||||
// Check if underlying handler has a Close method and call it
|
||||
if closer, ok := any(b.handler).(interface{ Close() error }); ok {
|
||||
closeErr = closer.Close()
|
||||
}
|
||||
})
|
||||
return closeErr
|
||||
}
|
||||
|
||||
// Final ensures remaining entries are flushed during garbage collection.
|
||||
// It calls Close to flush entries and stop the worker.
|
||||
// Used as a runtime finalizer to prevent log loss.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// runtime.SetFinalizer(buffered, (*Buffered[H]).Final)
|
||||
func (b *Buffered[H]) Final() {
|
||||
b.Close()
|
||||
}
|
||||
|
||||
// Config returns the current configuration of the Buffered handler.
|
||||
// It provides access to BatchSize, FlushInterval, MaxBuffer, and OnOverflow settings.
|
||||
// Example:
|
||||
//
|
||||
// config := buffered.Config() // Access configuration
|
||||
func (b *Buffered[H]) Config() *Buffering {
|
||||
return b.config
|
||||
}
|
||||
|
||||
// worker processes entries and handles flushing.
|
||||
// It runs in a goroutine, buffering entries, flushing on batch size, timer, or explicit signal,
|
||||
// and shutting down cleanly when signaled.
|
||||
// Thread-safe via channel operations and WaitGroup.
|
||||
func (b *Buffered[H]) worker() {
|
||||
defer b.wg.Done() // Signal completion when worker exits
|
||||
batch := make([]*lx.Entry, 0, b.config.BatchSize) // Buffer for batching entries
|
||||
ticker := time.NewTicker(b.config.FlushInterval) // Timer for periodic flushes
|
||||
defer ticker.Stop() // Clean up ticker
|
||||
for {
|
||||
select {
|
||||
case entry := <-b.entries: // Receive new entry
|
||||
batch = append(batch, entry)
|
||||
// Flush if batch size is reached
|
||||
if len(batch) >= b.config.BatchSize {
|
||||
b.flushBatch(batch)
|
||||
batch = batch[:0]
|
||||
}
|
||||
case <-ticker.C: // Periodic flush
|
||||
if len(batch) > 0 {
|
||||
b.flushBatch(batch)
|
||||
batch = batch[:0]
|
||||
}
|
||||
case <-b.flushSignal: // Explicit flush
|
||||
if len(batch) > 0 {
|
||||
b.flushBatch(batch)
|
||||
batch = batch[:0]
|
||||
}
|
||||
b.drainRemaining() // Drain all entries from the channel
|
||||
case <-b.shutdown: // Shutdown signal
|
||||
if len(batch) > 0 {
|
||||
b.flushBatch(batch)
|
||||
}
|
||||
b.drainRemaining() // Flush remaining entries
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// flushBatch processes a batch of entries through the wrapped handler.
|
||||
// It writes each entry to the underlying handler, logging any errors to the configured ErrorOutput.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// b.flushBatch([]*lx.Entry{entry1, entry2})
|
||||
func (b *Buffered[H]) flushBatch(batch []*lx.Entry) {
|
||||
for _, entry := range batch {
|
||||
// Process each entry through the handler
|
||||
if err := b.handler.Handle(entry); err != nil {
|
||||
if b.config.ErrorOutput != nil {
|
||||
fmt.Fprintf(b.config.ErrorOutput, "log flush error: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// drainRemaining processes any remaining entries in the channel.
|
||||
// It flushes all entries from the entries channel to the underlying handler,
|
||||
// logging any errors to the configured ErrorOutput. Used during flush or shutdown.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// b.drainRemaining() // Flushes all pending entries
|
||||
func (b *Buffered[H]) drainRemaining() {
|
||||
for {
|
||||
select {
|
||||
case entry := <-b.entries: // Process next entry
|
||||
if err := b.handler.Handle(entry); err != nil {
|
||||
if b.config.ErrorOutput != nil {
|
||||
fmt.Fprintf(b.config.ErrorOutput, "log drain error: %v\n", err)
|
||||
}
|
||||
}
|
||||
default: // Exit when channel is empty
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
+1063
File diff suppressed because it is too large
Load Diff
+163
@@ -0,0 +1,163 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/olekukonko/ll/lx"
|
||||
)
|
||||
|
||||
// Dedup is a log handler that suppresses duplicate entries within a TTL window.
|
||||
// It wraps another handler (H) and filters out repeated log entries that match
|
||||
// within the deduplication period.
|
||||
type Dedup[H lx.Handler] struct {
|
||||
next H
|
||||
|
||||
ttl time.Duration
|
||||
cleanupEvery time.Duration
|
||||
keyFn func(*lx.Entry) uint64
|
||||
maxKeys int
|
||||
|
||||
// shards reduce lock contention by partitioning the key space
|
||||
shards [32]dedupShard
|
||||
|
||||
done chan struct{}
|
||||
wg sync.WaitGroup
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
type dedupShard struct {
|
||||
mu sync.Mutex
|
||||
seen map[uint64]int64
|
||||
}
|
||||
|
||||
// DedupOpt configures a Dedup handler.
|
||||
type DedupOpt[H lx.Handler] func(*Dedup[H])
|
||||
|
||||
// WithDedupKeyFunc customizes how deduplication keys are generated.
|
||||
func WithDedupKeyFunc[H lx.Handler](fn func(*lx.Entry) uint64) DedupOpt[H] {
|
||||
return func(d *Dedup[H]) { d.keyFn = fn }
|
||||
}
|
||||
|
||||
// WithDedupCleanupInterval sets how often expired deduplication keys are purged.
|
||||
func WithDedupCleanupInterval[H lx.Handler](every time.Duration) DedupOpt[H] {
|
||||
return func(d *Dedup[H]) {
|
||||
if every > 0 {
|
||||
d.cleanupEvery = every
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithDedupMaxKeys sets a soft limit on tracked deduplication keys.
|
||||
func WithDedupMaxKeys[H lx.Handler](max int) DedupOpt[H] {
|
||||
return func(d *Dedup[H]) {
|
||||
if max > 0 {
|
||||
d.maxKeys = max
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewDedup creates a deduplicating handler wrapper.
|
||||
func NewDedup[H lx.Handler](next H, ttl time.Duration, opts ...DedupOpt[H]) *Dedup[H] {
|
||||
if ttl <= 0 {
|
||||
ttl = 2 * time.Second
|
||||
}
|
||||
|
||||
d := &Dedup[H]{
|
||||
next: next,
|
||||
ttl: ttl,
|
||||
cleanupEvery: time.Minute,
|
||||
keyFn: defaultDedupKey,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Initialize shards
|
||||
for i := 0; i < len(d.shards); i++ {
|
||||
d.shards[i].seen = make(map[uint64]int64, 64)
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(d)
|
||||
}
|
||||
|
||||
d.wg.Add(1)
|
||||
go d.cleanupLoop()
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// Handle processes a log entry, suppressing duplicates within the TTL window.
|
||||
func (d *Dedup[H]) Handle(e *lx.Entry) error {
|
||||
now := time.Now().UnixNano()
|
||||
key := d.keyFn(e)
|
||||
|
||||
// Select shard based on key hash
|
||||
shardIdx := key % uint64(len(d.shards))
|
||||
shard := &d.shards[shardIdx]
|
||||
|
||||
shard.mu.Lock()
|
||||
exp, ok := shard.seen[key]
|
||||
if ok && now < exp {
|
||||
shard.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Basic guard against unbounded growth per shard
|
||||
// Using strict limits per shard avoids global atomic counters
|
||||
limitPerShard := d.maxKeys / len(d.shards)
|
||||
if d.maxKeys > 0 && len(shard.seen) >= limitPerShard {
|
||||
// Opportunistic cleanup of current shard
|
||||
d.cleanupShard(shard, now)
|
||||
}
|
||||
|
||||
shard.seen[key] = now + d.ttl.Nanoseconds()
|
||||
shard.mu.Unlock()
|
||||
|
||||
return d.next.Handle(e)
|
||||
}
|
||||
|
||||
// Close stops the cleanup goroutine and closes the underlying handler.
|
||||
func (d *Dedup[H]) Close() error {
|
||||
var err error
|
||||
d.once.Do(func() {
|
||||
close(d.done)
|
||||
d.wg.Wait()
|
||||
|
||||
if c, ok := any(d.next).(interface{ Close() error }); ok {
|
||||
err = c.Close()
|
||||
}
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// cleanupLoop runs periodically to purge expired deduplication keys.
|
||||
func (d *Dedup[H]) cleanupLoop() {
|
||||
defer d.wg.Done()
|
||||
|
||||
t := time.NewTicker(d.cleanupEvery)
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
now := time.Now().UnixNano()
|
||||
// Cleanup all shards sequentially to avoid massive CPU spike
|
||||
for i := 0; i < len(d.shards); i++ {
|
||||
d.shards[i].mu.Lock()
|
||||
d.cleanupShard(&d.shards[i], now)
|
||||
d.shards[i].mu.Unlock()
|
||||
}
|
||||
case <-d.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupShard removes expired keys from a specific shard.
|
||||
func (d *Dedup[H]) cleanupShard(shard *dedupShard, now int64) {
|
||||
for k, exp := range shard.seen {
|
||||
if now > exp {
|
||||
delete(shard.seen, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/olekukonko/ll/lx"
|
||||
)
|
||||
|
||||
var jsonBufPool = sync.Pool{
|
||||
New: func() any {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
// JSONHandler is a handler that outputs log entries as JSON objects.
|
||||
// It formats log entries with timestamp, level, message, namespace, fields, and optional
|
||||
// stack traces or dump segments, writing the result to the provided writer.
|
||||
// Thread-safe with a mutex to protect concurrent writes.
|
||||
type JSONHandler struct {
|
||||
writer io.Writer // Destination for JSON output
|
||||
timeFmt string // Format for timestamp (default: RFC3339Nano)
|
||||
pretty bool // Enable pretty printing with indentation if true
|
||||
//fieldMap map[string]string // Optional mapping for field names (not used in provided code)
|
||||
mu sync.Mutex // Protects concurrent access to writer
|
||||
}
|
||||
|
||||
// JsonOutput represents the JSON structure for a log entry.
|
||||
// It includes all relevant log data, such as timestamp, level, message, and optional
|
||||
// stack trace or dump segments, serialized as a JSON object.
|
||||
type JsonOutput struct {
|
||||
Time string `json:"ts"` // Timestamp in specified format
|
||||
Level string `json:"lvl"` // Log level (e.g., "INFO")
|
||||
Class string `json:"class"` // Entry class (e.g., "Text", "Dump")
|
||||
Msg string `json:"msg"` // Log message
|
||||
Namespace string `json:"ns"` // Namespace path
|
||||
Stack []byte `json:"stack"` // Stack trace (if present)
|
||||
Dump []dumpSegment `json:"dump"` // Hex/ASCII dump segments (for ClassDump)
|
||||
Fields map[string]interface{} `json:"fields"` // Custom fields
|
||||
}
|
||||
|
||||
// dumpSegment represents a single segment of a hex/ASCII dump.
|
||||
// Used for ClassDump entries to structure position, hex values, and ASCII representation.
|
||||
type dumpSegment struct {
|
||||
Offset int `json:"offset"` // Starting byte offset of the segment
|
||||
Hex []string `json:"hex"` // Hexadecimal values of bytes
|
||||
ASCII string `json:"ascii"` // ASCII representation of bytes
|
||||
}
|
||||
|
||||
// NewJSONHandler creates a new JSONHandler writing to the specified writer.
|
||||
// It initializes the handler with a default timestamp format (RFC3339Nano) and optional
|
||||
// configuration functions to customize settings like pretty printing.
|
||||
// Example:
|
||||
//
|
||||
// handler := NewJSONHandler(os.Stdout)
|
||||
// logger := ll.New("app").Enable().Handler(handler)
|
||||
// logger.Info("Test") // Output: {"ts":"...","lvl":"INFO","class":"Text","msg":"Test","ns":"app","stack":null,"dump":null,"fields":null}
|
||||
func NewJSONHandler(w io.Writer, opts ...func(*JSONHandler)) *JSONHandler {
|
||||
h := &JSONHandler{
|
||||
writer: w, // Set output writer
|
||||
timeFmt: time.RFC3339Nano, // Default timestamp format
|
||||
}
|
||||
// Apply configuration options
|
||||
for _, opt := range opts {
|
||||
opt(h)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// Handle processes a log entry and writes it as JSON.
|
||||
// It delegates to specialized methods based on the entry's class (Dump or regular),
|
||||
// ensuring thread-safety with a mutex.
|
||||
// Returns an error if JSON encoding or writing fails.
|
||||
// Example:
|
||||
//
|
||||
// handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes JSON object
|
||||
func (h *JSONHandler) Handle(e *lx.Entry) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
// Handle dump entries separately
|
||||
if e.Class == lx.ClassDump {
|
||||
return h.handleDump(e)
|
||||
}
|
||||
// Handle standard log entries
|
||||
return h.handleRegular(e)
|
||||
}
|
||||
|
||||
// Output sets the Writer destination for JSONHandler's output, ensuring thread safety with a mutex lock.
|
||||
func (h *JSONHandler) Output(w io.Writer) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.writer = w
|
||||
}
|
||||
|
||||
// handleRegular handles standard log entries (non-dump).
|
||||
// It converts the entry to a JsonOutput struct and encodes it as JSON,
|
||||
// applying pretty printing if enabled. Logs encoding errors to stderr for debugging.
|
||||
// Returns an error if encoding or writing fails.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// h.handleRegular(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes JSON object
|
||||
func (h *JSONHandler) handleRegular(e *lx.Entry) error {
|
||||
// Convert ordered fields to map for JSON output
|
||||
fieldsMap := make(map[string]interface{}, len(e.Fields))
|
||||
for _, pair := range e.Fields {
|
||||
fieldsMap[pair.Key] = pair.Value
|
||||
}
|
||||
|
||||
// Create JSON output structure
|
||||
entry := JsonOutput{
|
||||
Time: e.Timestamp.Format(h.timeFmt), // Format timestamp
|
||||
Level: e.Level.String(), // Convert level to string
|
||||
Class: e.Class.String(), // Convert class to string
|
||||
Msg: e.Message, // Set message
|
||||
Namespace: e.Namespace, // Set namespace
|
||||
Dump: nil, // No dump for regular entries
|
||||
Fields: fieldsMap, // Copy fields as map
|
||||
Stack: e.Stack, // Include stack trace if present
|
||||
}
|
||||
|
||||
// Acquire buffer from pool to avoid allocation and reduce syscalls
|
||||
buf := jsonBufPool.Get().(*bytes.Buffer)
|
||||
buf.Reset()
|
||||
defer jsonBufPool.Put(buf)
|
||||
|
||||
// Create JSON encoder writing to buffer
|
||||
enc := json.NewEncoder(buf)
|
||||
if h.pretty {
|
||||
// Enable indentation for pretty printing
|
||||
enc.SetIndent("", " ")
|
||||
}
|
||||
|
||||
// Encode JSON to buffer
|
||||
err := enc.Encode(entry)
|
||||
if err != nil {
|
||||
// Log encoding error for debugging
|
||||
fmt.Fprintf(os.Stderr, "JSON encode error: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Write buffer to underlying writer in one go
|
||||
_, err = h.writer.Write(buf.Bytes())
|
||||
return err
|
||||
}
|
||||
|
||||
// handleDump processes ClassDump entries, converting hex dump output to JSON segments.
|
||||
// It parses the dump message into structured segments with offset, hex, and ASCII data,
|
||||
// encoding them as a JsonOutput struct.
|
||||
// Returns an error if parsing or encoding fails.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// h.handleDump(&lx.Entry{Class: lx.ClassDump, Message: "pos 00 hex: 61 62 'ab'"}) // Writes JSON with dump segments
|
||||
func (h *JSONHandler) handleDump(e *lx.Entry) error {
|
||||
var segments []dumpSegment
|
||||
lines := strings.Split(e.Message, "\n")
|
||||
|
||||
// Parse each line of the dump message
|
||||
for _, line := range lines {
|
||||
if !strings.HasPrefix(line, "pos") {
|
||||
continue // Skip non-dump lines
|
||||
}
|
||||
parts := strings.SplitN(line, "hex:", 2)
|
||||
if len(parts) != 2 {
|
||||
continue // Skip invalid lines
|
||||
}
|
||||
// Parse position
|
||||
var offset int
|
||||
fmt.Sscanf(parts[0], "pos %d", &offset)
|
||||
|
||||
// Parse hex and ASCII
|
||||
hexAscii := strings.SplitN(parts[1], "'", 2)
|
||||
hexStr := strings.Fields(strings.TrimSpace(hexAscii[0]))
|
||||
|
||||
// Create dump segment
|
||||
segments = append(segments, dumpSegment{
|
||||
Offset: offset, // Set byte offset
|
||||
Hex: hexStr, // Set hex values
|
||||
ASCII: strings.Trim(hexAscii[1], "'"), // Set ASCII representation
|
||||
})
|
||||
}
|
||||
|
||||
// Convert ordered fields to map for JSON output
|
||||
fieldsMap := make(map[string]interface{}, len(e.Fields))
|
||||
for _, pair := range e.Fields {
|
||||
fieldsMap[pair.Key] = pair.Value
|
||||
}
|
||||
|
||||
// Acquire buffer from pool
|
||||
buf := jsonBufPool.Get().(*bytes.Buffer)
|
||||
buf.Reset()
|
||||
defer jsonBufPool.Put(buf)
|
||||
|
||||
// Encode JSON output with dump segments to buffer
|
||||
enc := json.NewEncoder(buf)
|
||||
if h.pretty {
|
||||
enc.SetIndent("", " ")
|
||||
}
|
||||
|
||||
err := enc.Encode(JsonOutput{
|
||||
Time: e.Timestamp.Format(h.timeFmt), // Format timestamp
|
||||
Level: e.Level.String(), // Convert level to string
|
||||
Class: e.Class.String(), // Convert class to string
|
||||
Msg: "dumping segments", // Fixed message for dumps
|
||||
Namespace: e.Namespace, // Set namespace
|
||||
Dump: segments, // Include parsed segments
|
||||
Fields: fieldsMap, // Copy fields as map
|
||||
Stack: e.Stack, // Include stack trace if present
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "JSON dump encode error: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Write buffer to underlying writer
|
||||
_, err = h.writer.Write(buf.Bytes())
|
||||
return err
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/cespare/xxhash/v2"
|
||||
"github.com/olekukonko/ll/lx"
|
||||
)
|
||||
|
||||
// rightPad pads a string with spaces on the right to reach the specified length.
|
||||
// Returns the original string if it's already at or exceeds the target length.
|
||||
// Uses strings.Builder for efficient memory allocation.
|
||||
func rightPad(str string, length int) string {
|
||||
if len(str) >= length {
|
||||
return str
|
||||
}
|
||||
var sb strings.Builder
|
||||
sb.Grow(length)
|
||||
sb.WriteString(str)
|
||||
sb.WriteString(strings.Repeat(" ", length-len(str)))
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
var dedupBufPool = sync.Pool{
|
||||
New: func() any { return new(bytes.Buffer) },
|
||||
}
|
||||
|
||||
// defaultDedupKey generates a deduplication key from log level and message.
|
||||
// Uses FNV-1a hash for speed and good distribution. Override with WithDedupKeyFunc
|
||||
// to include additional fields like namespace, caller, or structured fields.
|
||||
func defaultDedupKey(e *lx.Entry) uint64 {
|
||||
h := xxhash.New()
|
||||
|
||||
_, _ = h.Write([]byte(e.Level.String()))
|
||||
_, _ = h.Write([]byte{0})
|
||||
_, _ = h.Write([]byte(e.Message))
|
||||
_, _ = h.Write([]byte{0})
|
||||
_, _ = h.Write([]byte(e.Namespace))
|
||||
_, _ = h.Write([]byte{0})
|
||||
|
||||
if len(e.Fields) > 0 {
|
||||
m := e.Fields.Map()
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
buf := dedupBufPool.Get().(*bytes.Buffer)
|
||||
buf.Reset()
|
||||
defer dedupBufPool.Put(buf)
|
||||
|
||||
for _, k := range keys {
|
||||
fmt.Fprint(buf, k)
|
||||
buf.WriteByte('=')
|
||||
fmt.Fprint(buf, m[k])
|
||||
buf.WriteByte(0)
|
||||
}
|
||||
_, _ = h.Write(buf.Bytes())
|
||||
}
|
||||
|
||||
return h.Sum64()
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/olekukonko/ll/lx"
|
||||
)
|
||||
|
||||
// MemoryHandler is an lx.Handler that stores log entries in memory.
|
||||
// Useful for testing or buffering logs for later inspection.
|
||||
// It maintains a thread-safe slice of log entries, protected by a read-write mutex.
|
||||
type MemoryHandler struct {
|
||||
mu sync.RWMutex // Protects concurrent access to entries
|
||||
entries []*lx.Entry // Slice of stored log entries
|
||||
showTime bool // Whether to show timestamps when dumping
|
||||
timeFormat string // Time format for dumping
|
||||
}
|
||||
|
||||
// NewMemoryHandler creates a new MemoryHandler.
|
||||
// It initializes an empty slice for storing log entries, ready for use in logging or testing.
|
||||
// Example:
|
||||
//
|
||||
// handler := NewMemoryHandler()
|
||||
// logger := ll.New("app").Enable().Handler(handler)
|
||||
// logger.Info("Test") // Stores entry in memory
|
||||
func NewMemoryHandler() *MemoryHandler {
|
||||
return &MemoryHandler{
|
||||
entries: make([]*lx.Entry, 0), // Initialize empty slice for entries
|
||||
}
|
||||
}
|
||||
|
||||
// Timestamped enables/disables timestamp display when dumping and optionally sets a time format.
|
||||
// Consistent with TextHandler and ColorizedHandler signature.
|
||||
// Example:
|
||||
//
|
||||
// handler.Timestamped(true) // Enable with default format
|
||||
// handler.Timestamped(true, time.StampMilli) // Enable with custom format
|
||||
// handler.Timestamped(false) // Disable
|
||||
func (h *MemoryHandler) Timestamped(enable bool, format ...string) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
h.showTime = enable
|
||||
if len(format) > 0 && format[0] != "" {
|
||||
h.timeFormat = format[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Handle stores the log entry in memory.
|
||||
// It appends the provided entry to the entries slice, ensuring thread-safety with a write lock.
|
||||
// Always returns nil, as it does not perform I/O operations.
|
||||
// Example:
|
||||
//
|
||||
// handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Stores entry
|
||||
func (h *MemoryHandler) Handle(entry *lx.Entry) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.entries = append(h.entries, entry) // Append entry to slice
|
||||
return nil
|
||||
}
|
||||
|
||||
// Entries returns a copy of the stored log entries.
|
||||
// It creates a new slice with copies of all entries, ensuring thread-safety with a read lock.
|
||||
// The returned slice is safe for external use without affecting the handler's internal state.
|
||||
// Example:
|
||||
//
|
||||
// entries := handler.Entries() // Returns copy of stored entries
|
||||
func (h *MemoryHandler) Entries() []*lx.Entry {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
entries := make([]*lx.Entry, len(h.entries)) // Create new slice for copy
|
||||
copy(entries, h.entries) // Copy entries to new slice
|
||||
return entries
|
||||
}
|
||||
|
||||
// Reset clears all stored entries.
|
||||
// It truncates the entries slice to zero length, preserving capacity, using a write lock for thread-safety.
|
||||
// Example:
|
||||
//
|
||||
// handler.Reset() // Clears all stored entries
|
||||
func (h *MemoryHandler) Reset() {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.entries = h.entries[:0] // Truncate slice to zero length
|
||||
}
|
||||
|
||||
// Dump writes all stored log entries to the provided io.Writer in text format.
|
||||
// Entries are formatted as they would be by a TextHandler, including namespace, level,
|
||||
// message, and fields. Thread-safe with read lock.
|
||||
// Returns an error if writing fails.
|
||||
// Example:
|
||||
//
|
||||
// logger := ll.New("test", ll.WithHandler(NewMemoryHandler())).Enable()
|
||||
// logger.Info("Test message")
|
||||
// handler := logger.handler.(*MemoryHandler)
|
||||
// handler.Dump(os.Stdout) // Output: [test] INFO: Test message
|
||||
func (h *MemoryHandler) Dump(w io.Writer) error {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
// Create a temporary TextHandler to format entries
|
||||
tempHandler := NewTextHandler(w)
|
||||
tempHandler.Timestamped(h.showTime, h.timeFormat)
|
||||
|
||||
// Process each entry through the TextHandler
|
||||
for _, entry := range h.entries {
|
||||
if err := tempHandler.Handle(entry); err != nil {
|
||||
return fmt.Errorf("failed to dump entry: %writer", err) // Wrap and return write errors
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/olekukonko/ll/lx"
|
||||
)
|
||||
|
||||
// MultiHandler combines multiple handlers to process log entries concurrently.
|
||||
// It holds a list of lx.Handler instances and delegates each log entry to all handlers,
|
||||
// collecting any errors into a single combined error.
|
||||
// Thread-safe if the underlying handlers are thread-safe.
|
||||
type MultiHandler struct {
|
||||
Handlers []lx.Handler // List of handlers to process each log entry
|
||||
}
|
||||
|
||||
// NewMultiHandler creates a new MultiHandler with the specified handlers.
|
||||
// It accepts a variadic list of handlers to be executed in order.
|
||||
// The returned handler processes log entries by passing them to each handler in sequence.
|
||||
// Example:
|
||||
//
|
||||
// textHandler := NewTextHandler(os.Stdout)
|
||||
// jsonHandler := NewJSONHandler(os.Stdout)
|
||||
// multi := NewMultiHandler(textHandler, jsonHandler)
|
||||
// logger := ll.New("app").Enable().Handler(multi)
|
||||
// logger.Info("Test") // Processed by both text and JSON handlers
|
||||
func NewMultiHandler(h ...lx.Handler) *MultiHandler {
|
||||
return &MultiHandler{
|
||||
Handlers: h, // Initialize with provided handlers
|
||||
}
|
||||
}
|
||||
|
||||
// Len returns the number of handlers in the MultiHandler.
|
||||
// Useful for monitoring or debugging handler composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// multi := &MultiHandler{}
|
||||
// multi.Append(h1, h2, h3)
|
||||
// count := multi.Len() // Returns 3
|
||||
func (h *MultiHandler) Len() int {
|
||||
return len(h.Handlers)
|
||||
}
|
||||
|
||||
// Append adds one or more handlers to the MultiHandler.
|
||||
// Handlers will receive log entries in the order they were appended.
|
||||
// This method modifies the MultiHandler in place.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// multi := &MultiHandler{}
|
||||
// multi.Append(
|
||||
// lx.NewJSONHandler(os.Stdout),
|
||||
// lx.NewTextHandler(logFile),
|
||||
// )
|
||||
// // Now multi broadcasts to both stdout and file
|
||||
func (h *MultiHandler) Append(handlers ...lx.Handler) {
|
||||
h.Handlers = append(h.Handlers, handlers...)
|
||||
}
|
||||
|
||||
// Handle implements the Handler interface, calling Handle on each handler in sequence.
|
||||
// It collects any errors from handlers and combines them into a single error using errors.Join.
|
||||
// If no errors occur, it returns nil. Thread-safe if the underlying handlers are thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// multi.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Calls Handle on all handlers
|
||||
func (h *MultiHandler) Handle(e *lx.Entry) error {
|
||||
var errs []error // Collect errors from handlers
|
||||
for i, handler := range h.Handlers {
|
||||
// Process entry with each handler
|
||||
if err := handler.Handle(e); err != nil {
|
||||
// fmt.Fprintf(os.Stderr, "MultiHandler error for handler %d: %v\n", i, err)
|
||||
// Wrap error with handler index for context
|
||||
errs = append(errs, fmt.Errorf("handler %d: %writer", i, err))
|
||||
}
|
||||
}
|
||||
// Combine errors into a single error, or return nil if no errors
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/olekukonko/ll/lx"
|
||||
)
|
||||
|
||||
// Pipe chains multiple handler wrappers together, applying them from left to right.
|
||||
// The wrappers are composed such that the first wrapper in the list becomes
|
||||
// the innermost layer, and the last wrapper becomes the outermost layer.
|
||||
//
|
||||
// Usage pattern: Pipe(baseHandler, wrapper1, wrapper2, wrapper3)
|
||||
// Result: wrapper3(wrapper2(wrapper1(baseHandler)))
|
||||
//
|
||||
// This enables clean, declarative construction of handler middleware chains.
|
||||
//
|
||||
// Example - building a processing pipeline:
|
||||
//
|
||||
// base := lx.NewJSONHandler(os.Stdout)
|
||||
// handler := lh.Pipe(base,
|
||||
// lh.NewDedup(2*time.Second), // 1. Deduplicate first
|
||||
// lh.NewRateLimit(10, time.Second), // 2. Then rate limit
|
||||
// )
|
||||
// logger := lx.NewLogger(handler)
|
||||
//
|
||||
// In this example, logs flow: Dedup → RateLimit → AddTimestamp → JSONHandler
|
||||
func Pipe(h lx.Handler, wraps ...lx.Wrap) lx.Handler {
|
||||
for _, w := range wraps {
|
||||
if w != nil {
|
||||
h = w(h)
|
||||
}
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// PipeDedup returns a wrapper that applies deduplication to the handler.
|
||||
func PipeDedup(ttl time.Duration, opts ...DedupOpt[lx.Handler]) lx.Wrap {
|
||||
return func(next lx.Handler) lx.Handler {
|
||||
return NewDedup(next, ttl, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// PipeBuffer returns a wrapper that applies buffering to the handler.
|
||||
func PipeBuffer(opts ...BufferingOpt) lx.Wrap {
|
||||
return func(next lx.Handler) lx.Handler {
|
||||
return NewBuffered(next, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// PipeRotate returns a wrapper that applies log rotation.
|
||||
// Ideally, the 'next' handler should be one that writes to a file (like TextHandler or JSONHandler).
|
||||
//
|
||||
// If the underlying handler does not implement lx.HandlerOutputter (cannot change output destination),
|
||||
// or if rotation initialization fails, this will log a warning to stderr and return the
|
||||
// original handler unmodified to prevent application crashes.
|
||||
func PipeRotate(maxSizeBytes int64, src RotateSource) lx.Wrap {
|
||||
return func(next lx.Handler) lx.Handler {
|
||||
// Attempt to cast to HandlerOutputter (Handler + Outputter interface)
|
||||
h, ok := next.(lx.HandlerOutputter)
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, "ll/lh: PipeRotate skipped - handler does not implement SetOutput(io.Writer)\n")
|
||||
return next
|
||||
}
|
||||
|
||||
// Initialize the rotating handler
|
||||
r, err := NewRotating(h, maxSizeBytes, src)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ll/lh: PipeRotate initialization failed: %v\n", err)
|
||||
return next
|
||||
}
|
||||
return r
|
||||
}
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/olekukonko/ll/lx"
|
||||
)
|
||||
|
||||
// RotateSource defines the callbacks needed to implement log rotation.
|
||||
// It abstracts the destination lifecycle: opening, sizing, and rotating.
|
||||
//
|
||||
// Example for file rotation:
|
||||
//
|
||||
// src := lh.RotateSource{
|
||||
// Open: func() (io.WriteCloser, error) {
|
||||
// return os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
// },
|
||||
// Size: func() (int64, error) {
|
||||
// if fi, err := os.Stat("app.log"); err == nil {
|
||||
// return fi.Size(), nil
|
||||
// }
|
||||
// return 0, nil // File doesn't exist yet
|
||||
// },
|
||||
// Rotate: func() error {
|
||||
// // Rename current log before creating new one
|
||||
// return os.Rename("app.log", "app.log."+time.Now().Format("20060102-150405"))
|
||||
// },
|
||||
// }
|
||||
type RotateSource struct {
|
||||
// Open returns a fresh destination for log output.
|
||||
// Called on initialization and after rotation.
|
||||
Open func() (io.WriteCloser, error)
|
||||
|
||||
// Size returns the current size in bytes of the active destination.
|
||||
// Return an error if size cannot be determined (rotation will be skipped).
|
||||
Size func() (int64, error)
|
||||
|
||||
// Rotate performs cleanup/rotation actions before opening a new destination.
|
||||
// For files: rename or move the current log. Optional for other destinations.
|
||||
Rotate func() error
|
||||
}
|
||||
|
||||
// Rotating wraps a handler to rotate its output when maxSize is exceeded.
|
||||
// The wrapped handler must implement both Handler and Outputter interfaces.
|
||||
// Rotation is triggered on each Handle call if the current size >= maxSize.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// handler := lx.NewJSONHandler(os.Stdout)
|
||||
// src := lh.RotateSource{...} // see RotateSource example
|
||||
// rotator, err := lh.NewRotating(handler, 10*1024*1024, src) // 10 MB
|
||||
// logger := lx.NewLogger(rotator)
|
||||
// logger.Info("This log may trigger rotation when file reaches 10MB")
|
||||
type Rotating[H interface {
|
||||
lx.Handler
|
||||
lx.Outputter
|
||||
}] struct {
|
||||
mu sync.Mutex
|
||||
maxSize int64
|
||||
src RotateSource
|
||||
|
||||
out io.WriteCloser
|
||||
handler H
|
||||
}
|
||||
|
||||
// NewRotating creates a rotating wrapper around handler.
|
||||
// Handler's output will be replaced with destinations from src.Open.
|
||||
// If maxSizeBytes <= 0, rotation is disabled.
|
||||
// src.Rotate may be nil if no pre-open actions are needed.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a JSON handler that rotates at 5MB
|
||||
// handler := lx.NewJSONHandler(os.Stdout)
|
||||
// rotator, err := lh.NewRotating(handler, 5*1024*1024, src)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// // Use rotator as your logger's handler
|
||||
// logger := lx.NewLogger(rotator)
|
||||
func NewRotating[H interface {
|
||||
lx.Handler
|
||||
lx.Outputter
|
||||
}](handler H, maxSizeBytes int64, src RotateSource) (*Rotating[H], error) {
|
||||
r := &Rotating[H]{
|
||||
maxSize: maxSizeBytes,
|
||||
src: src,
|
||||
handler: handler,
|
||||
}
|
||||
if err := r.reopenLocked(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Handle processes a log entry, rotating output if necessary.
|
||||
// Thread-safe: can be called concurrently.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rotator.Handle(&lx.Entry{
|
||||
// Level: lx.InfoLevel,
|
||||
// Message: "Processing request",
|
||||
// Namespace: "api",
|
||||
// })
|
||||
func (r *Rotating[H]) Handle(e *lx.Entry) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if err := r.rotateIfNeededLocked(); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.handler.Handle(e)
|
||||
}
|
||||
|
||||
// Close releases resources (closes the current output).
|
||||
// Safe to call multiple times.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// defer rotator.Close()
|
||||
func (r *Rotating[H]) Close() error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.out != nil {
|
||||
return r.out.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// rotateIfNeededLocked checks current size and rotates if maxSize exceeded.
|
||||
// Called with mu already held.
|
||||
func (r *Rotating[H]) rotateIfNeededLocked() error {
|
||||
if r.maxSize <= 0 || r.src.Size == nil || r.src.Open == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
size, err := r.src.Size()
|
||||
if err != nil {
|
||||
// Size unknown - skip rotation
|
||||
return nil
|
||||
}
|
||||
if size < r.maxSize {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close current output
|
||||
if r.out != nil {
|
||||
_ = r.out.Close()
|
||||
r.out = nil
|
||||
}
|
||||
|
||||
// Run rotation hook (rename/move/commit)
|
||||
if r.src.Rotate != nil {
|
||||
if err := r.src.Rotate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Open fresh output
|
||||
return r.reopenLocked()
|
||||
}
|
||||
|
||||
// reopenLocked opens a new destination and sets it on the handler.
|
||||
// Called with mu already held.
|
||||
func (r *Rotating[H]) reopenLocked() error {
|
||||
out, err := r.src.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.out = out
|
||||
r.handler.Output(out)
|
||||
return nil
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/olekukonko/ll/lx"
|
||||
)
|
||||
|
||||
// SlogHandler adapts a slog.Handler to implement lx.Handler.
|
||||
// It converts lx.Entry objects to slog.Record objects and delegates to an underlying
|
||||
// slog.Handler for processing, enabling compatibility with Go's standard slog package.
|
||||
// Thread-safe if the underlying slog.Handler is thread-safe.
|
||||
type SlogHandler struct {
|
||||
slogHandler slog.Handler // Underlying slog.Handler for processing log records
|
||||
}
|
||||
|
||||
// NewSlogHandler creates a new SlogHandler wrapping the provided slog.Handler.
|
||||
// It initializes the handler with the given slog.Handler, allowing lx.Entry logs to be
|
||||
// processed by slog's logging infrastructure.
|
||||
// Example:
|
||||
//
|
||||
// slogText := slog.NewTextHandler(os.Stdout, nil)
|
||||
// handler := NewSlogHandler(slogText)
|
||||
// logger := ll.New("app").Enable().Handler(handler)
|
||||
// logger.Info("Test") // Output: level=INFO msg=Test namespace=app class=Text
|
||||
func NewSlogHandler(h slog.Handler) *SlogHandler {
|
||||
return &SlogHandler{slogHandler: h}
|
||||
}
|
||||
|
||||
// Handle converts an lx.Entry to slog.Record and delegates to the slog.Handler.
|
||||
// It maps the entry's fields, level, namespace, class, and stack trace to slog attributes,
|
||||
// passing the resulting record to the underlying slog.Handler.
|
||||
// Returns an error if the slog.Handler fails to process the record.
|
||||
// Thread-safe if the underlying slog.Handler is thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Processes as slog record
|
||||
//
|
||||
// Handle converts an lx.Entry to slog.Record and delegates to the slog.Handler.
|
||||
// It maps the entry's fields, level, namespace, class, and stack trace to slog attributes,
|
||||
// passing the resulting record to the underlying slog.Handler.
|
||||
// Returns an error if the slog.Handler fails to process the record.
|
||||
// Thread-safe if the underlying slog.Handler is thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Processes as slog record
|
||||
func (h *SlogHandler) Handle(e *lx.Entry) error {
|
||||
// Convert lx.LevelType to slog.Level
|
||||
level := toSlogLevel(e.Level)
|
||||
|
||||
// Create a slog.Record with the entry's data
|
||||
record := slog.NewRecord(
|
||||
e.Timestamp, // time.Time for log timestamp
|
||||
level, // slog.Level for log severity
|
||||
e.Message, // string for log message
|
||||
0, // pc (program counter, optional, not used)
|
||||
)
|
||||
|
||||
// Add standard fields as attributes
|
||||
record.AddAttrs(
|
||||
slog.String("namespace", e.Namespace), // Add namespace as string attribute
|
||||
slog.String("class", e.Class.String()), // Add class as string attribute
|
||||
)
|
||||
|
||||
// Add stack trace if present
|
||||
if len(e.Stack) > 0 {
|
||||
record.AddAttrs(slog.String("stack", string(e.Stack))) // Add stack trace as string
|
||||
}
|
||||
|
||||
// Add custom fields in order (preserving insertion order)
|
||||
for _, pair := range e.Fields {
|
||||
record.AddAttrs(slog.Any(pair.Key, pair.Value)) // Add each field as a key-value attribute
|
||||
}
|
||||
|
||||
// Handle the record with the underlying slog.Handler
|
||||
return h.slogHandler.Handle(context.Background(), record)
|
||||
}
|
||||
|
||||
// toSlogLevel converts lx.LevelType to slog.Level.
|
||||
// It maps the logging levels used by the lx package to those used by slog,
|
||||
// defaulting to slog.LevelInfo for unknown levels.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// level := toSlogLevel(lx.LevelDebug) // Returns slog.LevelDebug
|
||||
func toSlogLevel(level lx.LevelType) slog.Level {
|
||||
switch level {
|
||||
case lx.LevelDebug:
|
||||
return slog.LevelDebug
|
||||
case lx.LevelInfo:
|
||||
return slog.LevelInfo
|
||||
case lx.LevelWarn:
|
||||
return slog.LevelWarn
|
||||
case lx.LevelError, lx.LevelFatal:
|
||||
return slog.LevelError
|
||||
default:
|
||||
return slog.LevelInfo // Default for unknown levels
|
||||
}
|
||||
}
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user