starlog/typed.go
2026-03-19 16:37:57 +08:00

585 lines
13 KiB
Go

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