Initial QSfera import
This commit is contained in:
+140
@@ -0,0 +1,140 @@
|
||||
package lx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Field represents a key-value pair where the key is a string and the value is of any type.
|
||||
type Field struct {
|
||||
Key string
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
// Fields represents a slice of key-value pairs.
|
||||
type Fields []Field
|
||||
|
||||
// Map converts the Fields slice to a map[string]interface{}.
|
||||
// This is useful for backward compatibility or when map operations are needed.
|
||||
// Example:
|
||||
//
|
||||
// fields := lx.Fields{{"user", "alice"}, {"age", 30}}
|
||||
// m := fields.Map() // Returns map[string]interface{}{"user": "alice", "age": 30}
|
||||
func (f Fields) Map() map[string]interface{} {
|
||||
m := make(map[string]interface{}, len(f))
|
||||
for _, pair := range f {
|
||||
m[pair.Key] = pair.Value
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Get returns the value for a given key and a boolean indicating if the key was found.
|
||||
// This provides O(n) lookup, which is fine for small numbers of fields.
|
||||
// Example:
|
||||
//
|
||||
// fields := lx.Fields{{"user", "alice"}, {"age", 30}}
|
||||
// value, found := fields.Get("user") // Returns "alice", true
|
||||
func (f Fields) Get(key string) (interface{}, bool) {
|
||||
for _, pair := range f {
|
||||
if pair.Key == key {
|
||||
return pair.Value, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Filter returns a new Fields slice containing only pairs where the predicate returns true.
|
||||
// Example:
|
||||
//
|
||||
// fields := lx.Fields{{"user", "alice"}, {"password", "secret"}, {"age", 30}}
|
||||
// filtered := fields.Filter(func(key string, value interface{}) bool {
|
||||
// return key != "password" // Remove sensitive fields
|
||||
// })
|
||||
func (f Fields) Filter(predicate func(key string, value interface{}) bool) Fields {
|
||||
result := make(Fields, 0, len(f))
|
||||
for _, pair := range f {
|
||||
if predicate(pair.Key, pair.Value) {
|
||||
result = append(result, pair)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Translate returns a new Fields slice with keys translated according to the provided mapping.
|
||||
// Keys not in the mapping are passed through unchanged. This is useful for adapters like Victoria.
|
||||
// Example:
|
||||
//
|
||||
// fields := lx.Fields{{"user", "alice"}, {"timestamp", time.Now()}}
|
||||
// translated := fields.Translate(map[string]string{
|
||||
// "user": "username",
|
||||
// "timestamp": "ts",
|
||||
// })
|
||||
// // Returns: {{"username", "alice"}, {"ts", time.Now()}}
|
||||
func (f Fields) Translate(mapping map[string]string) Fields {
|
||||
result := make(Fields, len(f))
|
||||
for i, pair := range f {
|
||||
if newKey, ok := mapping[pair.Key]; ok {
|
||||
result[i] = Field{Key: newKey, Value: pair.Value}
|
||||
} else {
|
||||
result[i] = pair
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Merge merges another Fields slice into this one, with the other slice's fields taking precedence
|
||||
// for duplicate keys (overwrites existing keys).
|
||||
// Example:
|
||||
//
|
||||
// base := lx.Fields{{"user", "alice"}, {"age", 30}}
|
||||
// additional := lx.Fields{{"age", 31}, {"city", "NYC"}}
|
||||
// merged := base.Merge(additional)
|
||||
// // Returns: {{"user", "alice"}, {"age", 31}, {"city", "NYC"}}
|
||||
func (f Fields) Merge(other Fields) Fields {
|
||||
result := make(Fields, 0, len(f)+len(other))
|
||||
|
||||
// Create a map to track which keys from 'other' we've seen
|
||||
seen := make(map[string]bool, len(other))
|
||||
|
||||
// First add all fields from 'f'
|
||||
result = append(result, f...)
|
||||
|
||||
// Then add fields from 'other', overwriting duplicates
|
||||
for _, pair := range other {
|
||||
// Check if this key already exists in result
|
||||
found := false
|
||||
for i, existing := range result {
|
||||
if existing.Key == pair.Key {
|
||||
result[i] = pair // Overwrite
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
result = append(result, pair)
|
||||
}
|
||||
seen[pair.Key] = true
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// String returns a human-readable string representation of the fields.
|
||||
// Example:
|
||||
//
|
||||
// fields := lx.Fields{{"user", "alice"}, {"age", 30}}
|
||||
// str := fields.String() // Returns: "[user=alice age=30]"
|
||||
func (f Fields) String() string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString(LeftBracket)
|
||||
for i, pair := range f {
|
||||
if i > 0 {
|
||||
builder.WriteString(Space)
|
||||
}
|
||||
builder.WriteString(pair.Key)
|
||||
builder.WriteString("=")
|
||||
builder.WriteString(fmt.Sprint(pair.Value))
|
||||
}
|
||||
builder.WriteString(RightBracket)
|
||||
return builder.String()
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package lx
|
||||
|
||||
import "io"
|
||||
|
||||
// Handler defines the interface for processing log entries.
|
||||
// Implementations (e.g., TextHandler, JSONHandler) format and output log entries to various
|
||||
// destinations (e.g., stdout, files). The Handle method returns an error if processing fails,
|
||||
// allowing the logger to handle output failures gracefully.
|
||||
// Example (simplified handler implementation):
|
||||
//
|
||||
// type MyHandler struct{}
|
||||
// func (h *MyHandler) Handle(e *Entry) error {
|
||||
// fmt.Printf("[%s] %s: %s\n", e.Namespace, e.Level.String(), e.Message)
|
||||
// return nil
|
||||
// }
|
||||
type Handler interface {
|
||||
Handle(e *Entry) error // Processes a log entry, returning any error
|
||||
}
|
||||
|
||||
// Outputter defines the interface for handlers that support dynamic output
|
||||
// destination changes. Implementations can switch their output writer at runtime.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// h := &JSONHandler{}
|
||||
// h.Output(os.Stderr) // Switch to stderr
|
||||
// h.Output(file) // Switch to file
|
||||
type Outputter interface {
|
||||
Output(w io.Writer)
|
||||
}
|
||||
|
||||
// HandlerOutputter combines the Handler and Outputter interfaces.
|
||||
// Types implementing this interface can both process log entries and
|
||||
// dynamically change their output destination at runtime.
|
||||
//
|
||||
// This is useful for creating flexible logging handlers that support
|
||||
// features like log rotation, output redirection, or runtime configuration.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// var ho HandlerOutputter = &TextHandler{}
|
||||
// // Handle log entries
|
||||
// ho.Handle(&Entry{...})
|
||||
// // Switch output destination
|
||||
// ho.Output(os.Stderr)
|
||||
//
|
||||
// Common implementations include TextHandler and JSONHandler when they
|
||||
// support output destination changes.
|
||||
type HandlerOutputter interface {
|
||||
Handler // Can process log entries
|
||||
Outputter // Can change output destination (has Output(w io.Writer) method)
|
||||
}
|
||||
|
||||
// Timestamper defines an interface for handlers that support timestamp configuration.
|
||||
// It includes a method to enable or disable timestamp logging and optionally set the timestamp format.
|
||||
type Timestamper interface {
|
||||
// Timestamped enables or disables timestamp logging and allows specifying an optional format.
|
||||
// Parameters:
|
||||
// enable: Boolean to enable or disable timestamp logging
|
||||
// format: Optional string(s) to specify the timestamp format
|
||||
Timestamped(enable bool, format ...string)
|
||||
}
|
||||
|
||||
// Wrap is a handler decorator function that transforms a log handler.
|
||||
// It takes an existing handler as input and returns a new, wrapped handler
|
||||
// that adds functionality (like filtering, transformation, or routing).
|
||||
type Wrap func(next Handler) Handler
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package lx
|
||||
|
||||
// Formatting constants for log output.
|
||||
// These constants define the characters used to format log messages, ensuring consistency
|
||||
// across handlers (e.g., text, JSON, colorized). They are used to construct namespace paths,
|
||||
// level indicators, and field separators in log entries.
|
||||
const (
|
||||
Space = " " // Single space for separating elements (e.g., between level and message)
|
||||
DoubleSpace = " " // Double space for indentation (e.g., for hierarchical output)
|
||||
Slash = "/" // Separator for namespace paths (e.g., "parent/child")
|
||||
Arrow = "→" // Arrow for NestedPath style namespaces (e.g., [parent]→[child])
|
||||
LeftBracket = "[" // Opening bracket for namespaces and fields (e.g., [app])
|
||||
RightBracket = "]" // Closing bracket for namespaces and fields (e.g., [app])
|
||||
Colon = ":" // Separator after namespace or level (e.g., [app]: INFO:) can also be "|"
|
||||
Dot = "." // Separator for namespace paths (e.g., "parent.child")
|
||||
Newline = "\n" // Newline for separating log entries or stack trace lines
|
||||
)
|
||||
|
||||
// DefaultEnabled defines the default logging state (disabled).
|
||||
// It specifies whether logging is enabled by default for new Logger instances in the ll package.
|
||||
// Set to false to prevent logging until explicitly enabled.
|
||||
const (
|
||||
DefaultEnabled = true // Default state for new loggers (disabled)
|
||||
)
|
||||
|
||||
// Log level constants, ordered by increasing severity.
|
||||
// These constants define the severity levels for log messages, used to filter logs based
|
||||
// on the logger’s minimum level. They are ordered to allow comparison (e.g., LevelDebug < LevelWarn).
|
||||
const (
|
||||
LevelNone LevelType = iota // Debug level for detailed diagnostic information
|
||||
LevelInfo // Info level for general operational messages
|
||||
LevelWarn // Warn level for warning conditions
|
||||
LevelError // Error level for error conditions requiring attention
|
||||
LevelFatal // Fatal level for critical error conditions
|
||||
LevelDebug // None level for logs without a specific severity (e.g., raw output)
|
||||
LevelUnknown // None level for logs without a specific severity (e.g., raw output)
|
||||
)
|
||||
|
||||
// String constants for each level
|
||||
const (
|
||||
DebugString = "DEBUG"
|
||||
InfoString = "INFO"
|
||||
WarnString = "WARN"
|
||||
WarningString = "WARNING"
|
||||
ErrorString = "ERROR"
|
||||
FatalString = "FATAL"
|
||||
NoneString = "NONE"
|
||||
UnknownString = "UNKNOWN"
|
||||
|
||||
TextString = "TEXT"
|
||||
JSONString = "JSON"
|
||||
DumpString = "DUMP"
|
||||
SpecialString = "SPECIAL"
|
||||
RawString = "RAW"
|
||||
InspectString = "INSPECT"
|
||||
DbgString = "DBG"
|
||||
TimedString = "TIMED"
|
||||
)
|
||||
|
||||
// Log class constants, defining the type of log entry.
|
||||
// These constants categorize log entries by their content or purpose, influencing how
|
||||
// handlers process them (e.g., text, JSON, hex dump).
|
||||
const (
|
||||
ClassText ClassType = iota // Text entries for standard log messages
|
||||
ClassJSON // JSON entries for structured output
|
||||
ClassDump // Dump entries for hex/ASCII dumps
|
||||
ClassSpecial // Special entries for custom or non-standard logs
|
||||
ClassRaw // Raw entries for unformatted output
|
||||
ClassInspect // Inspect entries for debugging
|
||||
ClassDbg // Inspect entries for debugging
|
||||
ClassTimed // Inspect entries for debugging
|
||||
ClassUnknown // Unknown output
|
||||
)
|
||||
|
||||
// Namespace style constants.
|
||||
// These constants define how namespace paths are formatted in log output, affecting the
|
||||
// visual representation of hierarchical namespaces.
|
||||
const (
|
||||
FlatPath StyleType = iota // Formats namespaces as [parent/child]
|
||||
NestedPath // Formats namespaces as [parent]→[child]
|
||||
)
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package lx
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// namespaceRule stores the cached result of Enabled.
|
||||
type namespaceRule struct {
|
||||
isEnabledByRule bool
|
||||
isDisabledByRule bool
|
||||
}
|
||||
|
||||
// Namespace manages thread-safe namespace enable/disable states with caching.
|
||||
// The store holds explicit user-defined rules (path -> bool).
|
||||
// The cache holds computed effective states for paths (path -> namespaceRule)
|
||||
// based on hierarchical rules to optimize lookups.
|
||||
type Namespace struct {
|
||||
store sync.Map // path (string) -> rule (bool: true=enable, false=disable)
|
||||
cache sync.Map // path (string) -> namespaceRule
|
||||
}
|
||||
|
||||
// Set defines an explicit enable/disable rule for a namespace path.
|
||||
// It clears the cache to ensure subsequent lookups reflect the change.
|
||||
func (ns *Namespace) Set(path string, enabled bool) {
|
||||
ns.store.Store(path, enabled)
|
||||
ns.clearCache()
|
||||
}
|
||||
|
||||
// Load retrieves an explicit rule from the store for a path.
|
||||
// Returns the rule (true=enable, false=disable) and whether it exists.
|
||||
// Does not consider hierarchy or caching.
|
||||
func (ns *Namespace) Load(path string) (rule interface{}, found bool) {
|
||||
return ns.store.Load(path)
|
||||
}
|
||||
|
||||
// Store directly sets a rule in the store, bypassing cache invalidation.
|
||||
// Intended for internal use or sync.Map parity; prefer Set for standard use.
|
||||
func (ns *Namespace) Store(path string, rule bool) {
|
||||
ns.store.Store(path, rule)
|
||||
}
|
||||
|
||||
// clearCache clears the cache of Enabled results.
|
||||
// Called by Set to ensure consistency after rule changes.
|
||||
func (ns *Namespace) clearCache() {
|
||||
ns.cache.Range(func(key, _ interface{}) bool {
|
||||
ns.cache.Delete(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// Enabled checks if a path is enabled by namespace rules, considering the most
|
||||
// specific rule (path or closest prefix) in the store. Results are cached.
|
||||
// Args:
|
||||
// - path: Absolute namespace path to check.
|
||||
// - separator: Character delimiting path segments (e.g., "/", ".").
|
||||
//
|
||||
// Returns:
|
||||
// - isEnabledByRule: True if an explicit rule enables the path.
|
||||
// - isDisabledByRule: True if an explicit rule disables the path.
|
||||
//
|
||||
// If both are false, no explicit rule applies to the path or its prefixes.
|
||||
func (ns *Namespace) Enabled(path string, separator string) (isEnabledByRule bool, isDisabledByRule bool) {
|
||||
if path == "" { // Root path has no explicit rule
|
||||
return false, false
|
||||
}
|
||||
|
||||
// Check cache
|
||||
if cachedValue, found := ns.cache.Load(path); found {
|
||||
if state, ok := cachedValue.(namespaceRule); ok {
|
||||
return state.isEnabledByRule, state.isDisabledByRule
|
||||
}
|
||||
ns.cache.Delete(path) // Remove invalid cache entry
|
||||
}
|
||||
|
||||
// Compute: Most specific rule wins
|
||||
parts := strings.Split(path, separator)
|
||||
computedIsEnabled := false
|
||||
computedIsDisabled := false
|
||||
|
||||
for i := len(parts); i >= 1; i-- {
|
||||
currentPrefix := strings.Join(parts[:i], separator)
|
||||
if val, ok := ns.store.Load(currentPrefix); ok {
|
||||
if rule := val.(bool); rule {
|
||||
computedIsEnabled = true
|
||||
computedIsDisabled = false
|
||||
} else {
|
||||
computedIsEnabled = false
|
||||
computedIsDisabled = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Cache result, including (false, false) for no rule
|
||||
ns.cache.Store(path, namespaceRule{
|
||||
isEnabledByRule: computedIsEnabled,
|
||||
isDisabledByRule: computedIsDisabled,
|
||||
})
|
||||
|
||||
return computedIsEnabled, computedIsDisabled
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
package lx
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LevelType represents the severity of a log message.
|
||||
// It is an integer type used to define log levels (Debug, Info, Warn, Error, None), with associated
|
||||
// string representations for display in log output.
|
||||
type LevelType int
|
||||
|
||||
// String converts a LevelType to its string representation.
|
||||
// It maps each level constant to a human-readable string, returning "UNKNOWN" for invalid levels.
|
||||
// Used by handlers to display the log level in output.
|
||||
// Example:
|
||||
//
|
||||
// var level lx.LevelType = lx.LevelInfo
|
||||
// fmt.Println(level.String()) // Output: INFO
|
||||
func (l LevelType) String() string {
|
||||
switch l {
|
||||
case LevelDebug:
|
||||
return DebugString
|
||||
case LevelInfo:
|
||||
return InfoString
|
||||
case LevelWarn:
|
||||
return WarnString
|
||||
case LevelError:
|
||||
return ErrorString
|
||||
case LevelFatal:
|
||||
return FatalString
|
||||
case LevelNone:
|
||||
return NoneString
|
||||
default:
|
||||
return UnknownString
|
||||
}
|
||||
}
|
||||
|
||||
func (l LevelType) Name(class ClassType) string {
|
||||
if class == ClassRaw || class == ClassDump || class == ClassInspect || class == ClassDbg || class == ClassTimed {
|
||||
return class.String()
|
||||
}
|
||||
return l.String()
|
||||
}
|
||||
|
||||
// LevelParse converts a string to its corresponding LevelType.
|
||||
// It parses a string (case-insensitive) and returns the corresponding LevelType, defaulting to
|
||||
// LevelUnknown for unrecognized strings. Supports "WARNING" as an alias for "WARN".
|
||||
func LevelParse(s string) LevelType {
|
||||
switch strings.ToUpper(s) {
|
||||
case DebugString:
|
||||
return LevelDebug
|
||||
case InfoString:
|
||||
return LevelInfo
|
||||
case WarnString, WarningString: // Allow both "WARN" and "WARNING"
|
||||
return LevelWarn
|
||||
case ErrorString:
|
||||
return LevelError
|
||||
case NoneString:
|
||||
return LevelNone
|
||||
default:
|
||||
return LevelUnknown
|
||||
}
|
||||
}
|
||||
|
||||
// Entry represents a single log entry passed to handlers.
|
||||
// It encapsulates all information about a log message, including its timestamp, severity,
|
||||
// content, namespace, metadata, and formatting style. Handlers process Entry instances
|
||||
// to produce formatted output (e.g., text, JSON). The struct is immutable once created,
|
||||
// ensuring thread-safety in handler processing.
|
||||
type Entry struct {
|
||||
Timestamp time.Time // Time the log was created
|
||||
Level LevelType // Severity level of the log (Debug, Info, Warn, Error, None)
|
||||
Message string // Log message content
|
||||
Namespace string // Namespace path (e.g., "parent/child")
|
||||
Fields Fields // Additional key-value metadata (e.g., {"user": "alice"})
|
||||
Style StyleType // Namespace formatting style (FlatPath or NestedPath)
|
||||
Error error // Associated error, if any (e.g., for error logs)
|
||||
Class ClassType // Type of log entry (Text, JSON, Dump, Special, Raw)
|
||||
Stack []byte // Stack trace data (if present)
|
||||
Id int `json:"-"` // Unique ID for the entry, ignored in JSON output
|
||||
}
|
||||
|
||||
// StyleType defines how namespace paths are formatted in log output.
|
||||
// It is an integer type used to select between FlatPath ([parent/child]) and NestedPath
|
||||
// ([parent]→[child]) styles, affecting how handlers render namespace hierarchies.
|
||||
type StyleType int
|
||||
|
||||
// ClassType represents the type of a log entry.
|
||||
// It is an integer type used to categorize log entries (Text, JSON, Dump, Special, Raw),
|
||||
// influencing how handlers process and format them.
|
||||
type ClassType int
|
||||
|
||||
// String converts a ClassType to its string representation.
|
||||
// It maps each class constant to a human-readable string, returning "UNKNOWN" for invalid classes.
|
||||
// Used by handlers to indicate the entry type in output (e.g., JSON fields).
|
||||
// Example:
|
||||
//
|
||||
// var class lx.ClassType = lx.ClassText
|
||||
// fmt.Println(class.String()) // Output: TEST
|
||||
func (t ClassType) String() string {
|
||||
switch t {
|
||||
case ClassText:
|
||||
|
||||
return TextString
|
||||
case ClassJSON:
|
||||
|
||||
return JSONString
|
||||
case ClassDump:
|
||||
return DumpString
|
||||
case ClassSpecial:
|
||||
return SpecialString
|
||||
case ClassInspect:
|
||||
return InspectString
|
||||
case ClassDbg:
|
||||
return DbgString
|
||||
case ClassRaw:
|
||||
return RawString
|
||||
case ClassTimed:
|
||||
return TimedString
|
||||
default:
|
||||
return UnknownString
|
||||
}
|
||||
}
|
||||
|
||||
// ParseClass converts a string to its corresponding ClassType.
|
||||
// It parses a string (case-insensitive) and returns the corresponding ClassType, defaulting to
|
||||
// ClassUnknown for unrecognized strings.
|
||||
func ParseClass(s string) ClassType {
|
||||
switch strings.ToUpper(s) {
|
||||
case TextString:
|
||||
return ClassText
|
||||
case JSONString:
|
||||
return ClassJSON
|
||||
case DumpString:
|
||||
return ClassDump
|
||||
case SpecialString:
|
||||
return ClassSpecial
|
||||
case RawString:
|
||||
return ClassRaw
|
||||
default:
|
||||
return ClassUnknown
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user