vendor/github.com/olekukonko/ll/ll.go
package ll
import (
"bufio"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"math"
"os"
"reflect"
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/olekukonko/cat"
"github.com/olekukonko/ll/lh"
"github.com/olekukonko/ll/lx"
)
// Logger manages logging configuration and behavior, encapsulating state such as enablement,
// log level, namespaces, context fields, output style, handler, middleware, and formatting.
// It is thread-safe, using a read-write mutex to protect concurrent access to its fields.
type Logger struct {
mu sync.RWMutex // Guards concurrent access to fields
enabled bool // Determines if logging is enabled
suspend atomic.Bool // uses suspend path for most actions eg. skipping namespace checks
level lx.LevelType // Minimum log level (e.g., Debug, Info, Warn, Error)
namespaces *lx.Namespace // Manages namespace enable/disable states
currentPath string // Current namespace path (e.g., "parent/child")
context map[string]interface{} // Contextual fields included in all logs
style lx.StyleType // Namespace formatting style (FlatPath or NestedPath)
handler lx.Handler // Output handler for logs (e.g., text, JSON)
middleware []Middleware // Middleware functions to process log entries
prefix string // Prefix prepended to log messages
indent int // Number of double spaces for message indentation
stackBufferSize int // Buffer size for capturing stack traces
separator string // Separator for namespace paths (e.g., "/")
entries atomic.Int64 // Tracks total log entries sent to handler
}
// New creates a new Logger with the given namespace and optional configurations.
// It initializes with defaults: disabled, Debug level, flat namespace style, text handler
// to os.Stdout, and an empty middleware chain. Options (e.g., WithHandler, WithLevel) can
// override defaults. The logger is thread-safe via mutex-protected methods.
// Example:
//
// logger := New("app", WithHandler(lh.NewTextHandler(os.Stdout))).Enable()
// logger.Info("Starting application") // Output: [app] INFO: Starting application
func New(namespace string, opts ...Option) *Logger {
logger := &Logger{
enabled: lx.DefaultEnabled, // Defaults to disabled (false)
level: lx.LevelDebug, // Default minimum log level
namespaces: defaultStore, // Shared namespace store
currentPath: namespace, // Initial namespace path
context: make(map[string]interface{}), // Empty context for fields
style: lx.FlatPath, // Default namespace style ([parent/child])
handler: lh.NewTextHandler(os.Stdout), // Default text output to stdout
middleware: make([]Middleware, 0), // Empty middleware chain
stackBufferSize: 4096, // Default stack trace buffer size
separator: lx.Slash, // Default namespace separator ("/")
}
// Apply provided configuration options
for _, opt := range opts {
opt(logger)
}
return logger
}
// AddContext adds a key-value pair to the logger's context, modifying it directly.
// Unlike Context, it mutates the existing context. It is thread-safe using a write lock.
// Example:
//
// logger := New("app").Enable()
// logger.AddContext("user", "alice")
// logger.Info("Action") // Output: [app] INFO: Action [user=alice]
func (l *Logger) AddContext(key string, value interface{}) *Logger {
l.mu.Lock()
defer l.mu.Unlock()
// Initialize context map if nil
if l.context == nil {
l.context = make(map[string]interface{})
}
l.context[key] = value
return l
}
// Benchmark logs the duration since a start time at Info level, including "start",
// "end", and "duration" fields. It is thread-safe via Fields and log methods.
// Example:
//
// logger := New("app").Enable()
// start := time.Now()
// logger.Benchmark(start) // Output: [app] INFO: benchmark [start=... end=... duration=...]
func (l *Logger) Benchmark(start time.Time) time.Duration {
duration := time.Since(start)
l.Fields(
"duration_ms", duration.Milliseconds(),
"duration", duration.String(),
).Infof("benchmark completed")
return duration
}
// CanLog checks if a log at the given level would be emitted, considering enablement,
// log level, namespaces, sampling, and rate limits. It is thread-safe via shouldLog.
// Example:
//
// logger := New("app").Enable().Level(lx.LevelWarn)
// canLog := logger.CanLog(lx.LevelInfo) // false
func (l *Logger) CanLog(level lx.LevelType) bool {
return l.shouldLog(level)
}
// Clear removes all middleware functions, resetting the middleware chain to empty.
// It is thread-safe using a write lock and returns the logger for chaining.
// Example:
//
// logger := New("app").Enable().Use(someMiddleware)
// logger.Clear()
// logger.Info("No middleware") // Output: [app] INFO: No middleware
func (l *Logger) Clear() *Logger {
l.mu.Lock()
defer l.mu.Unlock()
l.middleware = nil
return l
}
// Clone creates a new logger with the same configuration and namespace as the parent,
// but with a fresh context map to allow independent field additions. It is thread-safe
// using a read lock.
// Example:
//
// logger := New("app").Enable().Context(map[string]interface{}{"k": "v"})
// clone := logger.Clone()
// clone.Info("Cloned") // Output: [app] INFO: Cloned [k=v]
func (l *Logger) Clone() *Logger {
l.mu.RLock()
defer l.mu.RUnlock()
return &Logger{
enabled: l.enabled, // Copy enablement state
level: l.level, // Copy log level
namespaces: l.namespaces, // Share namespace store
currentPath: l.currentPath, // Copy namespace path
context: make(map[string]interface{}), // Fresh context map
style: l.style, // Copy namespace style
handler: l.handler, // Copy output handler
middleware: l.middleware, // Copy middleware chain
prefix: l.prefix, // Copy message prefix
indent: l.indent, // Copy indentation level
stackBufferSize: l.stackBufferSize, // Copy stack trace buffer size
separator: l.separator, // Default separator ("/")
suspend: l.suspend,
}
}
// Context creates a new logger with additional contextual fields, preserving existing
// fields and adding new ones. It returns a new logger to avoid mutating the parent and
// is thread-safe using a write lock.
// Example:
//
// logger := New("app").Enable()
// logger = logger.Context(map[string]interface{}{"user": "alice"})
// logger.Info("Action") // Output: [app] INFO: Action [user=alice]
func (l *Logger) Context(fields map[string]interface{}) *Logger {
l.mu.Lock()
defer l.mu.Unlock()
// Create a new logger with inherited configuration
newLogger := &Logger{
enabled: l.enabled,
level: l.level,
namespaces: l.namespaces,
currentPath: l.currentPath,
context: make(map[string]interface{}),
style: l.style,
handler: l.handler,
middleware: l.middleware,
prefix: l.prefix,
indent: l.indent,
stackBufferSize: l.stackBufferSize,
separator: l.separator,
suspend: l.suspend,
}
// Copy parent's context fields
for k, v := range l.context {
newLogger.context[k] = v
}
// Add new fields
for k, v := range fields {
newLogger.context[k] = v
}
return newLogger
}
// Dbg logs debug information, including the source file, line number, and expression
// value, capturing the calling line of code. It is useful for debugging without temporary
// print statements.
// Example:
//
// x := 42
// logger.Dbg(x) // Output: [file.go:123] x = 42
func (l *Logger) Dbg(values ...interface{}) {
// Skip logging if Info level is not enabled
if !l.shouldLog(lx.LevelInfo) {
return
}
l.dbg(2, values...)
}
// Debug logs a message at Debug level, formatting it and delegating to the internal
// log method. It is thread-safe.
// Example:
//
// logger := New("app").Enable().Level(lx.LevelDebug)
// logger.Debug("Debugging") // Output: [app] DEBUG: Debugging
func (l *Logger) Debug(args ...any) {
// check if suspended
if l.suspend.Load() {
return
}
// Skip logging if Debug level is not enabled
if !l.shouldLog(lx.LevelDebug) {
return
}
l.log(lx.LevelDebug, lx.ClassText, cat.Space(args...), nil, false)
}
// Debugf logs a formatted message at Debug level, delegating to Debug. It is thread-safe.
// Example:
//
// logger := New("app").Enable().Level(lx.LevelDebug)
// logger.Debugf("Debug %s", "message") // Output: [app] DEBUG: Debug message
func (l *Logger) Debugf(format string, args ...any) {
// check if suspended
if l.suspend.Load() {
return
}
l.Debug(fmt.Sprintf(format, args...))
}
// Disable deactivates logging, suppressing all logs regardless of level or namespace.
// It is thread-safe using a write lock and returns the logger for chaining.
// Example:
//
// logger := New("app").Enable().Disable()
// logger.Info("Ignored") // No output
func (l *Logger) Disable() *Logger {
l.mu.Lock()
defer l.mu.Unlock()
l.enabled = false
return l
}
// Dump displays a hex and ASCII representation of a value's binary form, using gob
// encoding or direct conversion. It is useful for inspecting binary data structures.
// Example:
//
// type Data struct { X int; Y string }
// logger.Dump(Data{42, "test"}) // Outputs hex/ASCII dump
func (l *Logger) Dump(values ...interface{}) {
// Iterate over each value to dump
for _, value := range values {
// Log value description and type
l.Infof("Dumping %v (%T)", value, value)
var by []byte
var err error
// Convert value to byte slice based on type
switch v := value.(type) {
case []byte:
by = v
case string:
by = []byte(v)
case float32:
// Convert float32 to 4-byte big-endian
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, math.Float32bits(v))
by = buf
case float64:
// Convert float64 to 8-byte big-endian
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, math.Float64bits(v))
by = buf
case int, int8, int16, int32, int64:
// Convert signed integer to 8-byte big-endian
by = make([]byte, 8)
binary.BigEndian.PutUint64(by, uint64(reflect.ValueOf(v).Int()))
case uint, uint8, uint16, uint32, uint64:
// Convert unsigned integer to 8-byte big-endian
by = make([]byte, 8)
binary.BigEndian.PutUint64(by, reflect.ValueOf(v).Uint())
case io.Reader:
// Read all bytes from io.Reader
by, err = io.ReadAll(v)
default:
// Fallback to JSON marshaling for complex types
by, err = json.Marshal(v)
}
// Log error if conversion fails
if err != nil {
l.Errorf("Dump error: %v", err)
continue
}
// Generate hex/ASCII dump
n := len(by)
rowcount := 0
stop := (n / 8) * 8
k := 0
s := strings.Builder{}
// Process 8-byte rows
for i := 0; i <= stop; i += 8 {
k++
if i+8 < n {
rowcount = 8
} else {
rowcount = min(k*8, n) % 8
}
// Write position and hex prefix
s.WriteString(fmt.Sprintf("pos %02d hex: ", i))
// Write hex values
for j := 0; j < rowcount; j++ {
s.WriteString(fmt.Sprintf("%02x ", by[i+j]))
}
// Pad with spaces for alignment
for j := rowcount; j < 8; j++ {
s.WriteString(fmt.Sprintf(" "))
}
// Write ASCII representation
s.WriteString(fmt.Sprintf(" '%s'\n", viewString(by[i:(i+rowcount)])))
}
// Log the hex/ASCII dump
l.log(lx.LevelNone, lx.ClassDump, s.String(), nil, false)
}
}
// Output logs each value as pretty-printed JSON for REST debugging.
// Each value is logged on its own line with [file:line] and a blank line after the header.
// Ideal for inspecting outgoing/incoming REST payloads.
func (l *Logger) Output(values ...interface{}) {
l.output(2, values...)
}
func (l *Logger) output(skip int, values ...interface{}) {
if !l.shouldLog(lx.LevelInfo) {
return
}
_, file, line, ok := runtime.Caller(skip)
if !ok {
return
}
shortFile := file
if idx := strings.LastIndex(file, "/"); idx >= 0 {
shortFile = file[idx+1:]
}
header := fmt.Sprintf("[%s:%d] JSON:\n", shortFile, line)
for _, v := range values {
// Always pretty-print with indent
b, err := json.MarshalIndent(v, " ", " ")
if err != nil {
b, _ = json.MarshalIndent(map[string]any{
"value": fmt.Sprintf("%+v", v),
"error": err.Error(),
}, " ", " ")
}
l.log(lx.LevelInfo, lx.ClassText, header+string(b), nil, false)
}
}
// Inspect logs one or more values in a **developer-friendly, deeply introspective format** at Info level.
// It includes the caller file and line number, and reveals **all fields** — including:
//
// - Private (unexported) fields → prefixed with `(field)`
// - Embedded structs (inlined)
// - Pointers and nil values → shown as `*(field)` or `nil`
// - Full struct nesting and type information
//
// This method uses `NewInspector` under the hood, which performs **full reflection-based traversal**.
// It is **not** meant for production logging or REST APIs — use `Output` for that.
//
// Ideal for:
// - Debugging complex internal state
// - Inspecting structs with private fields
// - Understanding struct embedding and pointer behavior
func (l *Logger) Inspect(values ...interface{}) {
o := NewInspector(l)
o.Log(2, values...)
}
// Enable activates logging, allowing logs to be emitted if other conditions (e.g., level,
// namespace) are met. It is thread-safe using a write lock and returns the logger for chaining.
// Example:
//
// logger := New("app").Enable()
// logger.Info("Started") // Output: [app] INFO: Started
func (l *Logger) Enable() *Logger {
l.mu.Lock()
defer l.mu.Unlock()
l.enabled = true
return l
}
// Enabled checks if the logger is enabled for logging. It is thread-safe using a read lock.
// Example:
//
// logger := New("app").Enable()
// if logger.Enabled() {
// logger.Info("Logging is enabled") // Output: [app] INFO: Logging is enabled
// }
func (l *Logger) Enabled() bool {
l.mu.RLock()
defer l.mu.RUnlock()
return l.enabled
}
// Err adds one or more errors to the logger’s context and logs them at Error level.
// Non-nil errors are stored in the "error" context field (single error or slice) and
// logged as a concatenated string (e.g., "failed 1; failed 2"). It is thread-safe and
// returns the logger for chaining.
// Example:
//
// logger := New("app").Enable()
// err1 := errors.New("failed 1")
// err2 := errors.New("failed 2")
// logger.Err(err1, err2).Info("Error occurred")
// // Output: [app] ERROR: failed 1; failed 2
// // [app] INFO: Error occurred [error=[failed 1 failed 2]]
func (l *Logger) Err(errs ...error) {
// Skip logging if Error level is not enabled
if !l.shouldLog(lx.LevelError) {
return
}
l.mu.Lock()
// Initialize context map if nil
if l.context == nil {
l.context = make(map[string]interface{})
}
// Collect non-nil errors and build log message
var nonNilErrors []error
var builder strings.Builder
count := 0
for i, err := range errs {
if err != nil {
if i > 0 && count > 0 {
builder.WriteString("; ")
}
builder.WriteString(err.Error())
nonNilErrors = append(nonNilErrors, err)
count++
}
}
if count > 0 {
if count == 1 {
// Store single error directly
l.context["error"] = nonNilErrors[0]
} else {
// Store slice of errors
l.context["error"] = nonNilErrors
}
// Log concatenated error messages
l.log(lx.LevelError, lx.ClassText, builder.String(), nil, false)
}
l.mu.Unlock()
}
// Error logs a message at Error level, formatting it and delegating to the internal
// log method. It is thread-safe.
// Example:
//
// logger := New("app").Enable()
// logger.Error("Error occurred") // Output: [app] ERROR: Error occurred
func (l *Logger) Error(args ...any) {
// check if suspended
if l.suspend.Load() {
return
}
// Skip logging if Error level is not enabled
if !l.shouldLog(lx.LevelError) {
return
}
l.log(lx.LevelError, lx.ClassText, cat.Space(args...), nil, false)
}
// Errorf logs a formatted message at Error level, delegating to Error. It is thread-safe.
// Example:
//
// logger := New("app").Enable()
// logger.Errorf("Error %s", "occurred") // Output: [app] ERROR: Error occurred
func (l *Logger) Errorf(format string, args ...any) {
// check if suspended
if l.suspend.Load() {
return
}
l.Error(fmt.Errorf(format, args...))
}
// Fatal logs a message at Error level with a stack trace and exits the program with
// exit code 1. It is thread-safe.
// Example:
//
// logger := New("app").Enable()
// logger.Fatal("Fatal error") // Output: [app] ERROR: Fatal error [stack=...], then exits
func (l *Logger) Fatal(args ...any) {
// check if suspended
if l.suspend.Load() {
return
}
// Exit immediately if Error level is not enabled
if !l.shouldLog(lx.LevelError) {
os.Exit(1)
}
l.log(lx.LevelError, lx.ClassText, cat.Space(args...), nil, false)
os.Exit(1)
}
// Fatalf logs a formatted message at Error level with a stack trace and exits the program.
// It delegates to Fatal and is thread-safe.
// Example:
//
// logger := New("app").Enable()
// logger.Fatalf("Fatal %s", "error") // Output: [app] ERROR: Fatal error [stack=...], then exits
func (l *Logger) Fatalf(format string, args ...any) {
// check if suspended
if l.suspend.Load() {
return
}
l.Fatal(fmt.Sprintf(format, args...))
}
// Field starts a fluent chain for adding fields from a map, creating a FieldBuilder
// for type-safe field addition. It is thread-safe via the FieldBuilder’s logger.
// Example:
//
// logger := New("app").Enable()
// logger.Field(map[string]interface{}{"user": "alice"}).Info("Action") // Output: [app] INFO: Action [user=alice]
func (l *Logger) Field(fields map[string]interface{}) *FieldBuilder {
fb := &FieldBuilder{logger: l, fields: make(map[string]interface{})}
// check if suspended
if l.suspend.Load() {
return fb
}
// Copy fields from input map to FieldBuilder
for k, v := range fields {
fb.fields[k] = v
}
return fb
}
// Fields starts a fluent chain for adding fields using variadic key-value pairs,
// creating a FieldBuilder. Non-string keys or uneven pairs add an error field. It is
// thread-safe via the FieldBuilder’s logger.
// Example:
//
// logger := New("app").Enable()
// logger.Fields("user", "alice").Info("Action") // Output: [app] INFO: Action [user=alice]
func (l *Logger) Fields(pairs ...any) *FieldBuilder {
fb := &FieldBuilder{logger: l, fields: make(map[string]interface{})}
if l.suspend.Load() {
return fb
}
// Process key-value pairs
for i := 0; i < len(pairs)-1; i += 2 {
if key, ok := pairs[i].(string); ok {
fb.fields[key] = pairs[i+1]
} else {
// Log error for non-string keys
fb.fields["error"] = fmt.Errorf("non-string key in Fields: %v", pairs[i])
}
}
// Log error for uneven pairs
if len(pairs)%2 != 0 {
fb.fields["error"] = fmt.Errorf("uneven key-value pairs in Fields: [%v]", pairs[len(pairs)-1])
}
return fb
}
// GetContext returns the logger's context map of persistent key-value fields. It is
// thread-safe using a read lock.
// Example:
//
// logger := New("app").AddContext("user", "alice")
// ctx := logger.GetContext() // Returns map[string]interface{}{"user": "alice"}
func (l *Logger) GetContext() map[string]interface{} {
l.mu.RLock()
defer l.mu.RUnlock()
return l.context
}
// GetHandler returns the logger's current handler for customization or inspection.
// The returned handler should not be modified concurrently with logger operations.
// Example:
//
// logger := New("app")
// handler := logger.GetHandler() // Returns the current handler (e.g., TextHandler)
func (l *Logger) GetHandler() lx.Handler {
return l.handler
}
// GetLevel returns the minimum log level for the logger. It is thread-safe using a read lock.
// Example:
//
// logger := New("app").Level(lx.LevelWarn)
// if logger.GetLevel() == lx.LevelWarn {
// logger.Warn("Warning level set") // Output: [app] WARN: Warning level set
// }
func (l *Logger) GetLevel() lx.LevelType {
l.mu.RLock()
defer l.mu.RUnlock()
return l.level
}
// GetPath returns the logger's current namespace path. It is thread-safe using a read lock.
// Example:
//
// logger := New("app").Namespace("sub")
// path := logger.GetPath() // Returns "app/sub"
func (l *Logger) GetPath() string {
l.mu.RLock()
defer l.mu.RUnlock()
return l.currentPath
}
// GetSeparator returns the logger's namespace separator (e.g., "/"). It is thread-safe
// using a read lock.
// Example:
//
// logger := New("app").Separator(".")
// sep := logger.GetSeparator() // Returns "."
func (l *Logger) GetSeparator() string {
l.mu.RLock()
defer l.mu.RUnlock()
return l.separator
}
// GetStyle returns the logger's namespace formatting style (FlatPath or NestedPath).
// It is thread-safe using a read lock.
// Example:
//
// logger := New("app").Style(lx.NestedPath)
// if logger.GetStyle() == lx.NestedPath {
// logger.Info("Nested style") // Output: [app]: INFO: Nested style
// }
func (l *Logger) GetStyle() lx.StyleType {
l.mu.RLock()
defer l.mu.RUnlock()
return l.style
}
// Handler sets the handler for processing log entries, configuring the output destination
// and format (e.g., text, JSON). It is thread-safe using a write lock and returns the
// logger for chaining.
// Example:
//
// logger := New("app").Enable().Handler(lh.NewTextHandler(os.Stdout))
// logger.Info("Log") // Output: [app] INFO: Log
func (l *Logger) Handler(handler lx.Handler) *Logger {
l.mu.Lock()
defer l.mu.Unlock()
l.handler = handler
return l
}
// Indent sets the indentation level for log messages, adding two spaces per level. It is
// thread-safe using a write lock and returns the logger for chaining.
// Example:
//
// logger := New("app").Enable().Indent(2)
// logger.Info("Indented") // Output: [app] INFO: Indented
func (l *Logger) Indent(depth int) *Logger {
l.mu.Lock()
defer l.mu.Unlock()
l.indent = depth
return l
}
// Info logs a message at Info level, formatting it and delegating to the internal log
// method. It is thread-safe.
// Example:
//
// logger := New("app").Enable().Style(lx.NestedPath)
// logger.Info("Started") // Output: [app]: INFO: Started
func (l *Logger) Info(args ...any) {
if l.suspend.Load() {
return
}
if !l.shouldLog(lx.LevelInfo) {
return
}
l.log(lx.LevelInfo, lx.ClassText, cat.Space(args...), nil, false)
}
// Infof logs a formatted message at Info level, delegating to Info. It is thread-safe.
// Example:
//
// logger := New("app").Enable().Style(lx.NestedPath)
// logger.Infof("Started %s", "now") // Output: [app]: INFO: Started now
func (l *Logger) Infof(format string, args ...any) {
if l.suspend.Load() {
return
}
l.Info(fmt.Sprintf(format, args...))
}
// Len returns the total number of log entries sent to the handler, using atomic operations
// for thread safety.
// Example:
//
// logger := New("app").Enable()
// logger.Info("Test")
// count := logger.Len() // Returns 1
func (l *Logger) Len() int64 {
return l.entries.Load()
}
// Level sets the minimum log level, ignoring messages below it. It is thread-safe using
// a write lock and returns the logger for chaining.
// Example:
//
// logger := New("app").Enable().Level(lx.LevelWarn)
// logger.Info("Ignored") // No output
// logger.Warn("Logged") // Output: [app] WARN: Logged
func (l *Logger) Level(level lx.LevelType) *Logger {
l.mu.Lock()
defer l.mu.Unlock()
l.level = level
return l
}
// Line adds vertical spacing (newlines) to the log output, defaulting to 1 if no arguments
// are provided. Multiple values are summed for total lines. It is thread-safe and returns
// the logger for chaining.
// Example:
//
// logger := New("app").Enable()
// logger.Line(2).Info("After 2 newlines") // Adds 2 blank lines before logging
// logger.Line().Error("After 1 newline") // Defaults to 1
func (l *Logger) Line(lines ...int) *Logger {
line := 1 // Default to 1 newline
if len(lines) > 0 {
line = 0
// Sum all provided line counts
for _, n := range lines {
line += n
}
// Ensure at least 1 line
if line < 1 {
line = 1
}
}
l.log(lx.LevelNone, lx.ClassRaw, strings.Repeat(lx.Newline, line), nil, false)
return l
}
// Mark logs the current file and line number where it's called, without any additional debug information.
// It's useful for tracing execution flow without the verbosity of Dbg.
// Example:
//
// logger.Mark() // *MARK*: [file.go:123]
func (l *Logger) Mark(name ...string) {
l.mark(2, name...)
}
func (l *Logger) mark(skip int, names ...string) {
// Skip logging if Info level is not enabled
if !l.shouldLog(lx.LevelInfo) {
return
}
// Get caller information (file, line)
_, file, line, ok := runtime.Caller(skip)
if !ok {
l.log(lx.LevelError, lx.ClassText, "Mark: Unable to parse runtime caller", nil, false)
return
}
// Extract just the filename (without full path)
shortFile := file
if idx := strings.LastIndex(file, "/"); idx >= 0 {
shortFile = file[idx+1:]
}
name := strings.Join(names, l.separator)
if name == "" {
name = "MARK"
}
// Format as [filename:line]
out := fmt.Sprintf("[*%s*]: [%s:%d]\n", name, shortFile, line)
l.log(lx.LevelInfo, lx.ClassRaw, out, nil, false)
}
// Measure benchmarks function execution, logging the duration at Info level with a
// "duration" field. It is thread-safe via Fields and log methods.
// Example:
//
// logger := New("app").Enable()
// duration := logger.Measure(func() { time.Sleep(time.Millisecond) })
// // Output: [app] INFO: function executed [duration=~1ms]
func (l *Logger) Measure(fns ...func()) time.Duration {
start := time.Now()
for _, fn := range fns {
if fn != nil {
fn()
}
}
duration := time.Since(start)
l.Fields(
"duration_ns", duration.Nanoseconds(),
"duration", duration.String(),
"duration_ms", fmt.Sprintf("%.3fms", float64(duration.Nanoseconds())/1e6),
).Infof("execution completed")
return duration
}
// Namespace creates a child logger with a sub-namespace appended to the current path,
// inheriting the parent’s configuration but with an independent context. It is thread-safe
// using a read lock.
// Example:
//
// parent := New("parent").Enable()
// child := parent.Namespace("child")
// child.Info("Child log") // Output: [parent/child] INFO: Child log
func (l *Logger) Namespace(name string) *Logger {
if l.suspend.Load() {
return l
}
l.mu.RLock()
defer l.mu.RUnlock()
// Construct full namespace path
fullPath := name
if l.currentPath != "" {
fullPath = l.currentPath + l.separator + name
}
// Create child logger with inherited configuration
return &Logger{
enabled: l.enabled,
level: l.level,
namespaces: l.namespaces,
currentPath: fullPath,
context: make(map[string]interface{}),
style: l.style,
handler: l.handler,
middleware: l.middleware,
prefix: l.prefix,
indent: l.indent,
stackBufferSize: l.stackBufferSize,
separator: l.separator,
suspend: l.suspend,
}
}
// NamespaceDisable disables logging for a namespace and its children, invalidating the
// namespace cache. It is thread-safe via lx.Namespace’s sync.Map and returns the logger
// for chaining.
// Example:
//
// logger := New("parent").Enable().NamespaceDisable("parent/child")
// logger.Namespace("child").Info("Ignored") // No output
func (l *Logger) NamespaceDisable(relativePath string) *Logger {
l.mu.RLock()
fullPath := l.joinPath(l.currentPath, relativePath)
l.mu.RUnlock()
// Disable namespace in shared store
l.namespaces.Set(fullPath, false)
return l
}
// NamespaceEnable enables logging for a namespace and its children, invalidating the
// namespace cache. It is thread-safe via lx.Namespace’s sync.Map and returns the logger
// for chaining.
// Example:
//
// logger := New("parent").Enable().NamespaceEnable("parent/child")
// logger.Namespace("child").Info("Log") // Output: [parent/child] INFO: Log
func (l *Logger) NamespaceEnable(relativePath string) *Logger {
l.mu.RLock()
fullPath := l.joinPath(l.currentPath, relativePath)
l.mu.RUnlock()
// Enable namespace in shared store
l.namespaces.Set(fullPath, true)
return l
}
// NamespaceEnabled checks if a namespace is enabled, considering parent namespaces and
// caching results for performance. It is thread-safe using a read lock.
// Example:
//
// logger := New("parent").Enable().NamespaceDisable("parent/child")
// enabled := logger.NamespaceEnabled("parent/child") // false
func (l *Logger) NamespaceEnabled(relativePath string) bool {
l.mu.RLock()
fullPath := l.joinPath(l.currentPath, relativePath)
separator := l.separator
if separator == "" {
separator = lx.Slash
}
instanceEnabled := l.enabled
l.mu.RUnlock()
// Handle root path case
if fullPath == "" && relativePath == "" {
return instanceEnabled
}
if fullPath != "" {
// Check namespace rules
isEnabledByNSRule, isDisabledByNSRule := l.namespaces.Enabled(fullPath, separator)
if isDisabledByNSRule {
return false
}
if isEnabledByNSRule {
return true
}
}
// Fall back to logger's enabled state
return instanceEnabled
}
// Panic logs a message at Error level with a stack trace and triggers a panic. It is
// thread-safe.
// Example:
//
// logger := New("app").Enable()
// logger.Panic("Panic error") // Output: [app] ERROR: Panic error [stack=...], then panics
func (l *Logger) Panic(args ...any) {
// Build message by concatenating arguments with spaces
msg := cat.Space(args...)
if l.suspend.Load() {
panic(msg)
}
// Panic immediately if Error level is not enabled
if !l.shouldLog(lx.LevelError) {
panic(msg)
}
l.log(lx.LevelError, lx.ClassText, msg, nil, true)
panic(msg)
}
// Panicf logs a formatted message at Error level with a stack trace and triggers a panic.
// It delegates to Panic and is thread-safe.
// Example:
//
// logger := New("app").Enable()
// logger.Panicf("Panic %s", "error") // Output: [app] ERROR: Panic error [stack=...], then panics
func (l *Logger) Panicf(format string, args ...any) {
l.Panic(fmt.Sprintf(format, args...))
}
// Prefix sets a prefix prepended to all log messages. It is thread-safe using a write
// lock and returns the logger for chaining.
// Example:
//
// logger := New("app").Enable().Prefix("APP: ")
// logger.Info("Started") // Output: [app] INFO: APP: Started
func (l *Logger) Prefix(prefix string) *Logger {
l.mu.Lock()
defer l.mu.Unlock()
l.prefix = prefix
return l
}
// Print logs a message at Info level without format specifiers, minimizing allocations
// by concatenating arguments with spaces. It is thread-safe via the log method.
// Example:
//
// logger := New("app").Enable()
// logger.Print("message", "value") // Output: [app] INFO: message value
func (l *Logger) Print(args ...any) {
if l.suspend.Load() {
return
}
// Skip logging if Info level is not enabled
if !l.shouldLog(lx.LevelInfo) {
return
}
l.log(lx.LevelNone, lx.ClassRaw, cat.Space(args...), nil, false)
}
// Println logs a message at Info level without format specifiers, minimizing allocations
// by concatenating arguments with spaces. It is thread-safe via the log method.
// Example:
//
// logger := New("app").Enable()
// logger.Println("message", "value") // Output: [app] INFO: message value
func (l *Logger) Println(args ...any) {
if l.suspend.Load() {
return
}
// Skip logging if Info level is not enabled
if !l.shouldLog(lx.LevelInfo) {
return
}
l.log(lx.LevelNone, lx.ClassRaw, cat.SuffixWith(lx.Space, lx.Newline, args...), nil, false)
}
// Printf logs a formatted message at Info level, delegating to Print. It is thread-safe.
// Example:
//
// logger := New("app").Enable()
// logger.Printf("Message %s", "value") // Output: [app] INFO: Message value
func (l *Logger) Printf(format string, args ...any) {
if l.suspend.Load() {
return
}
l.Print(fmt.Sprintf(format, args...))
}
// Remove removes middleware by the reference returned from Use, delegating to the
// Middleware’s Remove method for thread-safe removal.
// Example:
//
// logger := New("app").Enable()
// mw := logger.Use(someMiddleware)
// logger.Remove(mw) // Removes middleware
func (l *Logger) Remove(m *Middleware) {
m.Remove()
}
// Resume reactivates logging for the current logger after it has been suspended.
// It clears the suspend flag, allowing logs to be emitted if other conditions (e.g., level, namespace)
// are met. Thread-safe with a write lock. Returns the logger for method chaining.
// Example:
//
// logger := New("app").Enable().Suspend()
// logger.Resume()
// logger.Info("Resumed") // Output: [app] INFO: Resumed
func (l *Logger) Resume() *Logger {
l.suspend.Store(false)
return l
}
// Separator sets the namespace separator for grouping namespaces and log entries (e.g., "/" or ".").
// It is thread-safe using a write lock and returns the logger for chaining.
// Example:
//
// logger := New("app").Separator(".")
// logger.Namespace("sub").Info("Log") // Output: [app.sub] INFO: Log
func (l *Logger) Separator(separator string) *Logger {
l.mu.Lock()
defer l.mu.Unlock()
l.separator = separator
return l
}
// Suspend temporarily deactivates logging for the current logger.
// It sets the suspend flag, suppressing all logs regardless of level or namespace until resumed.
// Thread-safe with a write lock. Returns the logger for method chaining.
// Example:
//
// logger := New("app").Enable()
// logger.Suspend()
// logger.Info("Ignored") // No output
func (l *Logger) Suspend() *Logger {
l.suspend.Store(true)
return l
}
// Suspended returns whether the logger is currently suspended.
// It provides thread-safe read access to the suspend flag using a write lock.
// Example:
//
// logger := New("app").Enable().Suspend()
// if logger.Suspended() {
// fmt.Println("Logging is suspended") // Prints message
// }
func (l *Logger) Suspended() bool {
return l.suspend.Load()
}
// Stack logs messages at Error level with a stack trace for each provided argument.
// It is thread-safe and skips logging if Debug level is not enabled.
// Example:
//
// logger := New("app").Enable()
// logger.Stack("Critical error") // Output: [app] ERROR: Critical error [stack=...]
func (l *Logger) Stack(args ...any) {
if l.suspend.Load() {
return
}
// Skip logging if Debug level is not enabled
if !l.shouldLog(lx.LevelDebug) {
return
}
for _, arg := range args {
l.log(lx.LevelError, lx.ClassText, cat.Concat(arg), nil, true)
}
}
// Stackf logs a formatted message at Error level with a stack trace, delegating to Stack.
// It is thread-safe.
// Example:
//
// logger := New("app").Enable()
// logger.Stackf("Critical %s", "error") // Output: [app] ERROR: Critical error [stack=...]
func (l *Logger) Stackf(format string, args ...any) {
if l.suspend.Load() {
return
}
l.Stack(fmt.Sprintf(format, args...))
}
// StackSize sets the buffer size for stack trace capture in Stack, Fatal, and Panic methods.
// It is thread-safe using a write lock and returns the logger for chaining.
// Example:
//
// logger := New("app").Enable().StackSize(65536)
// logger.Stack("Error") // Captures up to 64KB stack trace
func (l *Logger) StackSize(size int) *Logger {
l.mu.Lock()
defer l.mu.Unlock()
if size > 0 {
l.stackBufferSize = size
}
return l
}
// Style sets the namespace formatting style (FlatPath or NestedPath). FlatPath uses
// [parent/child], while NestedPath uses [parent]→[child]. It is thread-safe using a write
// lock and returns the logger for chaining.
// Example:
//
// logger := New("parent/child").Enable().Style(lx.NestedPath)
// logger.Info("Log") // Output: [parent]→[child]: INFO: Log
func (l *Logger) Style(style lx.StyleType) *Logger {
l.mu.Lock()
defer l.mu.Unlock()
l.style = style
return l
}
// Timestamped enables or disables timestamp logging for the logger and optionally sets the timestamp format.
// It is thread-safe, using a write lock to ensure safe concurrent access.
// If the logger's handler supports the lx.Timestamper interface, the timestamp settings are applied.
// The method returns the logger instance to support method chaining.
// Parameters:
//
// enable: Boolean to enable or disable timestamp logging
// format: Optional string(s) to specify the timestamp format
func (l *Logger) Timestamped(enable bool, format ...string) *Logger {
l.mu.Lock()
defer l.mu.Unlock()
if h, ok := l.handler.(lx.Timestamper); ok {
h.Timestamped(enable, format...)
}
return l
}
// Use adds a middleware function to process log entries before they are handled, returning
// a Middleware handle for removal. Middleware returning a non-nil error stops the log.
// It is thread-safe using a write lock.
// Example:
//
// logger := New("app").Enable()
// mw := logger.Use(ll.FuncMiddleware(func(e *lx.Entry) error {
// if e.Level < lx.LevelWarn {
// return fmt.Errorf("level too low")
// }
// return nil
// }))
// logger.Info("Ignored") // No output
// mw.Remove()
// logger.Info("Now logged") // Output: [app] INFO: Now logged
func (l *Logger) Use(fn lx.Handler) *Middleware {
l.mu.Lock()
defer l.mu.Unlock()
// Assign a unique ID to the middleware
id := len(l.middleware) + 1
// Append middleware to the chain
l.middleware = append(l.middleware, Middleware{id: id, fn: fn})
return &Middleware{
logger: l,
id: id,
}
}
// Warn logs a message at Warn level, formatting it and delegating to the internal log
// method. It is thread-safe.
// Example:
//
// logger := New("app").Enable()
// logger.Warn("Warning") // Output: [app] WARN: Warning
func (l *Logger) Warn(args ...any) {
if l.suspend.Load() {
return
}
// Skip logging if Warn level is not enabled
if !l.shouldLog(lx.LevelWarn) {
return
}
l.log(lx.LevelWarn, lx.ClassText, cat.Space(args...), nil, false)
}
// Warnf logs a formatted message at Warn level, delegating to Warn. It is thread-safe.
// Example:
//
// logger := New("app").Enable()
// logger.Warnf("Warning %s", "issued") // Output: [app] WARN: Warning issued
func (l *Logger) Warnf(format string, args ...any) {
if l.suspend.Load() {
return
}
l.Warn(fmt.Sprintf(format, args...))
}
// dbg is an internal helper for Dbg, logging debug information with source file and line
// number, extracting the calling line of code. It is thread-safe via the log method.
// Example (internal usage):
//
// logger.Dbg(x) // Calls dbg(2, x)
func (l *Logger) dbg(skip int, values ...interface{}) {
for _, exp := range values {
// Get caller information (file, line)
_, file, line, ok := runtime.Caller(skip)
if !ok {
l.log(lx.LevelError, lx.ClassText, "Dbg: Unable to parse runtime caller", nil, false)
return
}
// Open source file
f, err := os.Open(file)
if err != nil {
l.log(lx.LevelError, lx.ClassText, "Dbg: Unable to open expected file", nil, false)
return
}
// Scan file to find the line
scanner := bufio.NewScanner(f)
scanner.Split(bufio.ScanLines)
var out string
i := 1
for scanner.Scan() {
if i == line {
// Extract expression between parentheses
v := scanner.Text()[strings.Index(scanner.Text(), "(")+1 : len(scanner.Text())-strings.Index(reverseString(scanner.Text()), ")")-1]
// Format output with file, line, expression, and value
out = fmt.Sprintf("[%s:%d] %s = %+v", file[len(file)-strings.Index(reverseString(file), "/"):], line, v, exp)
break
}
i++
}
if err := scanner.Err(); err != nil {
l.log(lx.LevelError, lx.ClassText, err.Error(), nil, false)
return
}
// Log based on value type
switch exp.(type) {
case error:
l.log(lx.LevelError, lx.ClassText, out, nil, false)
default:
l.log(lx.LevelInfo, lx.ClassText, out, nil, false)
}
f.Close()
}
}
// joinPath joins a base path and a relative path using the logger's separator, handling
// empty base or relative paths. It is used internally for namespace path construction.
// Example (internal usage):
//
// logger.joinPath("parent", "child") // Returns "parent/child"
func (l *Logger) joinPath(base, relative string) string {
if base == "" {
return relative
}
if relative == "" {
return base
}
separator := l.separator
if separator == "" {
separator = lx.Slash // Default separator
}
return base + separator + relative
}
// log is the internal method for processing a log entry, applying rate limiting, sampling,
// middleware, and context before passing to the handler. Middleware returning a non-nil
// error stops the log. It is thread-safe with read/write locks for configuration and stack
// trace buffer.
// Example (internal usage):
//
// logger := New("app").Enable()
// logger.Info("Test") // Calls log(lx.LevelInfo, "Test", nil, false)
func (l *Logger) log(level lx.LevelType, class lx.ClassType, msg string, fields map[string]interface{}, withStack bool) {
// Skip logging if level is not enabled
if !l.shouldLog(level) {
return
}
var stack []byte
// Capture stack trace if requested
if withStack {
l.mu.RLock()
buf := make([]byte, l.stackBufferSize)
l.mu.RUnlock()
n := runtime.Stack(buf, false)
if fields == nil {
fields = make(map[string]interface{})
}
stack = buf[:n]
}
l.mu.RLock()
defer l.mu.RUnlock()
// Apply prefix and indentation to the message
var builder strings.Builder
if l.indent > 0 {
builder.WriteString(strings.Repeat(lx.DoubleSpace, l.indent))
}
if l.prefix != "" {
builder.WriteString(l.prefix)
}
builder.WriteString(msg)
finalMsg := builder.String()
// Create log entry
entry := &lx.Entry{
Timestamp: time.Now(),
Level: level,
Message: finalMsg,
Namespace: l.currentPath,
Fields: fields,
Style: l.style,
Class: class,
Stack: stack,
}
// Merge context fields, avoiding overwrites
if len(l.context) > 0 {
if entry.Fields == nil {
entry.Fields = make(map[string]interface{})
}
for k, v := range l.context {
if _, exists := entry.Fields[k]; !exists {
entry.Fields[k] = v
}
}
}
// Apply middleware, stopping if any returns an error
for _, mw := range l.middleware {
if err := mw.fn.Handle(entry); err != nil {
return
}
}
// Pass to handler if set
if l.handler != nil {
_ = l.handler.Handle(entry)
l.entries.Add(1)
}
}
// shouldLog determines if a log should be emitted based on enabled state, level, namespaces,
// sampling, and rate limits, caching namespace results for performance. It is thread-safe
// with a read lock.
// Example (internal usage):
//
// logger := New("app").Enable().Level(lx.LevelWarn)
// if logger.shouldLog(lx.LevelInfo) { // false
// // Log would be skipped
// }
func (l *Logger) shouldLog(level lx.LevelType) bool {
// Skip if global logging system is inactive
if !Active() {
return false
}
// check for suspend mode
if l.suspend.Load() {
return false
}
// Skip if log level is below minimum
if level > l.level {
return false
}
separator := l.separator
if separator == "" {
separator = lx.Slash
}
// Check namespace rules if path is set
if l.currentPath != "" {
isEnabledByNSRule, isDisabledByNSRule := l.namespaces.Enabled(l.currentPath, separator)
if isDisabledByNSRule {
return false
}
if isEnabledByNSRule {
return true
}
}
// Fall back to logger's enabled state
if !l.enabled {
return false
}
return true
}
// WithHandler sets the handler for the logger as a functional option for configuring
// a new logger instance.
// Example:
//
// logger := New("app", WithHandler(lh.NewJSONHandler(os.Stdout)))
func WithHandler(handler lx.Handler) Option {
return func(l *Logger) {
l.handler = handler
}
}
// WithTimestamped returns an Option that configures timestamp settings for the logger's existing handler.
// It enables or disables timestamp logging and optionally sets the timestamp format if the handler
// supports the lx.Timestamper interface. If no handler is set, the function has no effect.
// Parameters:
//
// enable: Boolean to enable or disable timestamp logging
// format: Optional string(s) to specify the timestamp format
func WithTimestamped(enable bool, format ...string) Option {
return func(l *Logger) {
if l.handler != nil { // Check if a handler is set
// Verify if the handler supports the lx.Timestamper interface
if h, ok := l.handler.(lx.Timestamper); ok {
h.Timestamped(enable, format...) // Apply timestamp settings to the handler
}
}
}
}
// WithLevel sets the minimum log level for the logger as a functional option for
// configuring a new logger instance.
// Example:
//
// logger := New("app", WithLevel(lx.LevelWarn))
func WithLevel(level lx.LevelType) Option {
return func(l *Logger) {
l.level = level
}
}
// WithStyle sets the namespace formatting style for the logger as a functional option
// for configuring a new logger instance.
// Example:
//
// logger := New("app", WithStyle(lx.NestedPath))
func WithStyle(style lx.StyleType) Option {
return func(l *Logger) {
l.style = style
}
}