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 } }