package starlog import ( "context" "errors" "fmt" "io" "math/rand" "strconv" "strings" "sync" "sync/atomic" "time" "b612.me/starlog/colorable" ) const ( LvDebug = iota LvInfo LvNotice LvWarning LvError LvCritical LvPanic LvFatal ) type ColorMode int const ( ColorModeOff ColorMode = iota ColorModeFullLine ColorModeLevelOnly ) type PendingDropPolicy int const ( PendingDropOldest PendingDropPolicy = iota PendingDropNewest PendingBlock ) type RedactFailMode int const ( RedactFailMaskAll RedactFailMode = iota RedactFailOpen RedactFailDrop ) const ( FieldTypeString = "string" FieldTypeNumber = "number" FieldTypeBool = "bool" FieldTypeError = "error" FieldTypeNil = "nil" FieldTypeOther = "other" ) var ( ErrAsyncHandlerPanic = errors.New("async handler panic") ErrAsyncHandlerTimeout = errors.New("async handler timeout") ErrAsyncQueueFull = errors.New("async queue full") ErrPendingWriteDropped = errors.New("pending write dropped") ErrInvalidLevel = errors.New("invalid log level") ErrRedactionFailed = errors.New("redaction failed") levels = map[int]string{ LvDebug: "DEBUG", LvInfo: "INFO", LvNotice: "NOTICE", LvWarning: "WARNING", LvError: "ERROR", LvCritical: "CRITICAL", LvPanic: "PANIC", LvFatal: "FATAL", } levelAliases = map[string]int{ "debug": LvDebug, "info": LvInfo, "notice": LvNotice, "warn": LvWarning, "warning": LvWarning, "err": LvError, "error": LvError, "critical": LvCritical, "crit": LvCritical, "panic": LvPanic, "fatal": LvFatal, } stacks *starChanStack stackStarted bool = false stackStopChan chan struct{} stackDoneChan chan struct{} stackMu sync.Mutex stackDrop uint64 stackAlert func(error, LogData) stackAlertMu sync.RWMutex stackFallback uint32 = 1 stackTimeout int64 writeErrCount uint64 writeErrHandler func(error, LogData) writeErrMu sync.RWMutex stdScreen io.Writer = colorable.NewColorableStdout() errScreen io.Writer = colorable.NewColorableStderr() ) type starlog struct { mu *sync.Mutex output io.Writer minLevel int errOutputLevel int showFuncName bool showThread bool showLevel bool showDeatilFile bool showColor bool switching bool showStd bool onlyColorLevel bool autoAppendNewline bool stopWriter bool id string name string colorList map[int][]Attr colorMe map[int]*Color keywordColors map[string][]Attr keywordOrder []string keywordColorizers map[string]*Color keywordMatcher *keywordMatcher keywordMatchOptions KeywordMatchOptions showFieldColor bool fieldKeyColor []Attr fieldTypeColors map[string][]Attr fieldValueColors map[string][]Attr entryHandler Handler redactor Redactor redactRules []RedactRule redactFailMode RedactFailMode redactMaskToken string redactErrorCount uint64 formatter Formatter sink Sink pendingCond *sync.Cond pendingWrites []string pendingWriteLimit int pendingDropPolicy PendingDropPolicy pendingDropCount uint64 pendingBlockCount uint64 pendingPeakLen uint64 rateLimiter *rateLimiter sampler *sampler deduper *deduper contextFields func(context.Context) Fields entryHandlerTimeout time.Duration } type StarLogger struct { thread string handlerFunc func(LogData) logcore *starlog isStd bool fields Fields logErr error logCtx context.Context } type logTransfer struct { handlerFunc func(LogData) LogData } type LogData struct { Name string Log string Colors []Attr } type PendingStats struct { Limit int Length int PeakLength int DropCount uint64 BlockCount uint64 Policy PendingDropPolicy Switching bool } type KeywordMatchOptions struct { IgnoreCase bool WholeWord bool } // Config is a logger core snapshot that can be read and applied atomically. // Prefer GetConfig + UpdateConfig/ApplyConfig for multi-field configuration. type Config struct { Name string Level int StdErrLevel int ShowFuncName bool ShowFlag bool ShowLevel bool ShowOriginFile bool ShowColor bool OnlyColorLevel bool ShowStd bool StopWriter bool AutoAppendNewline bool Switching bool LevelColors map[int][]Attr KeywordColors map[string][]Attr KeywordMatch KeywordMatchOptions ShowFieldColor bool FieldKeyColor []Attr FieldTypeColors map[string][]Attr FieldValueColors map[string][]Attr EntryHandler Handler EntryHandlerTimeout time.Duration Formatter Formatter Sink Sink Writer io.Writer PendingWriteLimit int PendingDropPolicy PendingDropPolicy Redactor Redactor RedactRules []RedactRule RedactFailMode RedactFailMode RedactMaskToken string RateLimit RateLimitConfig Sampling SamplingConfig Dedup DedupConfig ContextFieldExtractor func(context.Context) Fields } func newLogCore(out io.Writer) *starlog { core := &starlog{ mu: &sync.Mutex{}, output: out, minLevel: LvDebug, errOutputLevel: LvError, showFuncName: true, showThread: true, showLevel: true, showStd: true, showDeatilFile: true, switching: false, stopWriter: false, showColor: true, id: generateId(), colorList: map[int][]Attr{ LvDebug: []Attr{FgWhite}, LvInfo: []Attr{FgGreen}, LvNotice: []Attr{FgCyan}, LvWarning: []Attr{FgYellow}, LvError: []Attr{FgMagenta}, LvCritical: []Attr{FgRed, Bold}, LvPanic: []Attr{FgRed, Bold}, LvFatal: []Attr{FgRed}, }, colorMe: map[int]*Color{ LvDebug: NewColor([]Attr{FgWhite}...), LvInfo: NewColor([]Attr{FgGreen}...), LvNotice: NewColor([]Attr{FgCyan}...), LvWarning: NewColor([]Attr{FgYellow}...), LvError: NewColor([]Attr{FgMagenta}...), LvCritical: NewColor([]Attr{FgRed, Bold}...), LvPanic: NewColor([]Attr{FgRed, Bold}...), LvFatal: NewColor([]Attr{FgRed}...), }, keywordColors: make(map[string][]Attr), keywordOrder: nil, keywordColorizers: nil, keywordMatcher: nil, showFieldColor: true, fieldKeyColor: []Attr{FgHiBlue}, fieldTypeColors: map[string][]Attr{ FieldTypeString: []Attr{FgGreen}, FieldTypeNumber: []Attr{FgYellow}, FieldTypeBool: []Attr{FgMagenta}, FieldTypeError: []Attr{FgRed}, FieldTypeNil: []Attr{FgHiBlack}, FieldTypeOther: []Attr{FgCyan}, }, fieldValueColors: make(map[string][]Attr), redactRules: make([]RedactRule, 0, 4), redactFailMode: RedactFailMaskAll, redactMaskToken: "[REDACTED]", pendingWrites: make([]string, 0, 16), pendingWriteLimit: 1024, pendingDropPolicy: PendingDropOldest, rateLimiter: newRateLimiter(), sampler: newSampler(), deduper: newDeduper(), entryHandlerTimeout: 0, } core.rebuildKeywordCachesLocked() core.pendingCond = sync.NewCond(core.mu) return core } func ParseLevel(level string) (int, error) { val := strings.TrimSpace(strings.ToLower(level)) if val == "" { return 0, fmt.Errorf("%w: empty", ErrInvalidLevel) } if parsed, ok := levelAliases[val]; ok { return parsed, nil } num, err := strconv.Atoi(val) if err == nil { return num, nil } return 0, fmt.Errorf("%w: %s", ErrInvalidLevel, level) } func NewStarlog(out io.Writer) *StarLogger { return &StarLogger{ handlerFunc: nil, thread: "MAN", logcore: newLogCore(out), isStd: false, fields: nil, logErr: nil, logCtx: nil, } } func (logger *StarLogger) StdErrLevel() int { logger.logcore.mu.Lock() defer logger.logcore.mu.Unlock() return logger.logcore.errOutputLevel } func (logger *StarLogger) SetStdErrLevel(level int) { logger.logcore.mu.Lock() defer logger.logcore.mu.Unlock() logger.logcore.errOutputLevel = level } func (logger *StarLogger) NewFlag() *StarLogger { return &StarLogger{ thread: getRandomFlag(false), handlerFunc: logger.handlerFunc, logcore: logger.logcore, isStd: false, fields: cloneFields(logger.fields), logErr: logger.logErr, logCtx: logger.logCtx, } } func (logger *StarLogger) SetNewRandomFlag() { logger.thread = getRandomFlag(false) } func (logger *StarLogger) SetName(name string) { logger.logcore.mu.Lock() defer logger.logcore.mu.Unlock() logger.logcore.name = name } func (logger *StarLogger) GetName() string { logger.logcore.mu.Lock() defer logger.logcore.mu.Unlock() return logger.logcore.name } func getRandomFlag(isMain bool) string { rand.Seed(time.Now().UnixNano()) if isMain { return "MAN" } flag := "MAN" for flag == "MAN" { flag = string([]byte{uint8(rand.Intn(26) + 65), uint8(rand.Intn(26) + 65), uint8(rand.Intn(26) + 65)}) } return flag } func generateId() string { rand.Seed(time.Now().UnixNano()) return fmt.Sprintf("%dstar%db612%d", time.Now().UnixNano(), rand.Intn(1000000), rand.Intn(1000000)) } func StartStacks() { stackMu.Lock() if stackStarted { stackMu.Unlock() return } stackStarted = true stackStopChan = make(chan struct{}) stackDoneChan = make(chan struct{}) stacks = newStarChanStack(1024) stopChan := stackStopChan doneChan := stackDoneChan stackMu.Unlock() go func(stop <-chan struct{}, done chan struct{}) { defer close(done) defer func() { stackMu.Lock() stackStarted = false stackMu.Unlock() }() for { select { case <-stop: return default: } poped, err := stacks.Pop() if err != nil { if errors.Is(err, io.EOF) { return } return } val, ok := poped.(logTransfer) if !ok { continue } if val.handlerFunc != nil { invokeAsyncHandlerSafely(val.handlerFunc, val.LogData) } } }(stopChan, doneChan) } func StopStacks() { stackMu.Lock() if !stackStarted { stackMu.Unlock() return } stopChan := stackStopChan doneChan := stackDoneChan current := stacks stackStopChan = nil stackDoneChan = nil stackMu.Unlock() if stopChan != nil { func() { defer func() { recover() }() close(stopChan) }() } if current != nil { _ = current.Close() } if doneChan != nil { <-doneChan } } func Stop() { StopStacks() } func SetAsyncErrorHandler(alert func(error, LogData)) { stackAlertMu.Lock() defer stackAlertMu.Unlock() stackAlert = alert } func SetAsyncFallbackToSync(enable bool) { if enable { atomic.StoreUint32(&stackFallback, 1) return } atomic.StoreUint32(&stackFallback, 0) } func GetAsyncFallbackToSync() bool { return atomic.LoadUint32(&stackFallback) == 1 } func SetAsyncHandlerTimeout(timeout time.Duration) { if timeout < 0 { timeout = 0 } atomic.StoreInt64(&stackTimeout, int64(timeout)) } func GetAsyncHandlerTimeout() time.Duration { return time.Duration(atomic.LoadInt64(&stackTimeout)) } func GetAsyncDropCount() uint64 { return atomic.LoadUint64(&stackDrop) } func reportAsyncDrop(err error, data LogData) { atomic.AddUint64(&stackDrop, 1) stackAlertMu.RLock() alert := stackAlert stackAlertMu.RUnlock() if alert != nil { func() { defer func() { recover() }() alert(err, data) }() } } func invokeAsyncHandlerSafely(handler func(LogData), data LogData) bool { if handler == nil { return true } timeout := GetAsyncHandlerTimeout() if timeout <= 0 { return invokeAsyncHandlerDirect(handler, data) } done := make(chan bool, 1) go func() { done <- invokeAsyncHandlerDirect(handler, data) }() select { case ok := <-done: return ok case <-time.After(timeout): reportAsyncDrop(ErrAsyncHandlerTimeout, data) return false } } func invokeAsyncHandlerDirect(handler func(LogData), data LogData) (ok bool) { defer func() { if panicErr := recover(); panicErr != nil { reportAsyncDrop(fmt.Errorf("%w: %v", ErrAsyncHandlerPanic, panicErr), data) ok = false } }() handler(data) return true } func SetWriteErrorHandler(alert func(error, LogData)) { writeErrMu.Lock() defer writeErrMu.Unlock() writeErrHandler = alert } func GetWriteErrorCount() uint64 { return atomic.LoadUint64(&writeErrCount) } func reportWriteError(err error, data LogData) { if err == nil { return } atomic.AddUint64(&writeErrCount, 1) writeErrMu.RLock() alert := writeErrHandler writeErrMu.RUnlock() if alert != nil { func() { defer func() { recover() }() alert(err, data) }() } } func resetAsyncMetricsForTest() { atomic.StoreUint64(&stackDrop, 0) SetAsyncErrorHandler(nil) SetAsyncFallbackToSync(true) SetAsyncHandlerTimeout(0) atomic.StoreUint64(&writeErrCount, 0) SetWriteErrorHandler(nil) }