starlog/typed.go

811 lines
18 KiB
Go
Raw Normal View History

2020-12-21 17:22:45 +08:00
package starlog
import (
2026-03-19 16:37:57 +08:00
"context"
"errors"
2020-12-21 17:22:45 +08:00
"fmt"
"io"
"math/rand"
2026-03-19 16:37:57 +08:00
"strconv"
"strings"
2020-12-21 17:22:45 +08:00
"sync"
2026-03-19 16:37:57 +08:00
"sync/atomic"
2020-12-21 17:22:45 +08:00
"time"
2020-12-29 14:05:14 +08:00
"b612.me/starlog/colorable"
2020-12-21 17:22:45 +08:00
)
const (
LvDebug = iota
LvInfo
LvNotice
LvWarning
LvError
LvCritical
LvPanic
LvFatal
)
2026-03-19 16:37:57 +08:00
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"
)
const defaultAsyncQueueCapacity uint64 = 1024
2020-12-21 17:22:45 +08:00
var (
2026-03-19 16:37:57 +08:00
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")
2020-12-21 17:22:45 +08:00
levels = map[int]string{
LvDebug: "DEBUG",
LvInfo: "INFO",
LvNotice: "NOTICE",
LvWarning: "WARNING",
LvError: "ERROR",
LvCritical: "CRITICAL",
LvPanic: "PANIC",
LvFatal: "FATAL",
}
2026-03-19 16:37:57 +08:00
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,
}
stdScreen io.Writer = colorable.NewColorableStdout()
errScreen io.Writer = colorable.NewColorableStderr()
defaultAsyncRuntimeOnce sync.Once
defaultAsyncRuntimeFallback *asyncRuntime
)
type asyncRuntime struct {
mu sync.Mutex
queue *starChanStack
started bool
stopChan chan struct{}
doneChan chan struct{}
dropCount uint64
asyncAlert func(error, LogData)
asyncAlertMu sync.RWMutex
fallbackSync uint32
timeout int64
2026-03-19 16:37:57 +08:00
writeErrCount uint64
writeErrHandler func(error, LogData)
writeErrMu sync.RWMutex
queueCapacity uint64
}
type asyncRuntimeContextKey struct{}
2020-12-21 17:22:45 +08:00
type starlog struct {
2026-03-19 16:37:57 +08:00
mu *sync.Mutex
runtime *asyncRuntime
2026-03-19 16:37:57 +08:00
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
2020-12-21 17:22:45 +08:00
}
type StarLogger struct {
thread string
handlerFunc func(LogData)
2020-12-21 17:22:45 +08:00
logcore *starlog
isStd bool
2026-03-19 16:37:57 +08:00
fields Fields
logErr error
logCtx context.Context
2020-12-21 17:22:45 +08:00
}
type logTransfer struct {
handlerFunc func(LogData)
LogData
}
type LogData struct {
Name string
Log string
Colors []Attr
2020-12-21 17:22:45 +08:00
}
2026-03-19 16:37:57 +08:00
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 newAsyncRuntime(queueCapacity uint64) *asyncRuntime {
if queueCapacity == 0 {
queueCapacity = defaultAsyncQueueCapacity
}
runtime := &asyncRuntime{
queueCapacity: queueCapacity,
}
atomic.StoreUint32(&runtime.fallbackSync, 1)
return runtime
}
func defaultAsyncRuntime() *asyncRuntime {
if Std != nil && Std.logcore != nil && Std.logcore.runtime != nil {
return Std.logcore.runtime
}
defaultAsyncRuntimeOnce.Do(func() {
defaultAsyncRuntimeFallback = newAsyncRuntime(defaultAsyncQueueCapacity)
})
return defaultAsyncRuntimeFallback
}
func withAsyncRuntime(ctx context.Context, runtime *asyncRuntime) context.Context {
if runtime == nil {
return ctx
}
if ctx == nil {
ctx = context.Background()
}
return context.WithValue(ctx, asyncRuntimeContextKey{}, runtime)
}
func runtimeFromContext(ctx context.Context) *asyncRuntime {
if ctx == nil {
return nil
}
runtime, _ := ctx.Value(asyncRuntimeContextKey{}).(*asyncRuntime)
return runtime
}
func reportWriteErrorWithContext(ctx context.Context, err error, data LogData) {
runtime := runtimeFromContext(ctx)
if runtime == nil {
runtime = defaultAsyncRuntime()
}
runtime.reportWriteError(err, data)
}
2020-12-21 17:22:45 +08:00
func newLogCore(out io.Writer) *starlog {
2026-03-19 16:37:57 +08:00
core := &starlog{
2020-12-21 17:22:45 +08:00
mu: &sync.Mutex{},
runtime: newAsyncRuntime(defaultAsyncQueueCapacity),
2020-12-21 17:22:45 +08:00
output: out,
2026-03-19 16:37:57 +08:00
minLevel: LvDebug,
2022-06-21 10:16:32 +08:00
errOutputLevel: LvError,
2020-12-21 17:22:45 +08:00
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},
2020-12-21 17:22:45 +08:00
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}...),
2020-12-21 17:22:45 +08:00
LvWarning: NewColor([]Attr{FgYellow}...),
LvError: NewColor([]Attr{FgMagenta}...),
LvCritical: NewColor([]Attr{FgRed, Bold}...),
LvPanic: NewColor([]Attr{FgRed, Bold}...),
LvFatal: NewColor([]Attr{FgRed}...),
},
2026-03-19 16:37:57 +08:00
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,
2020-12-21 17:22:45 +08:00
}
2026-03-19 16:37:57 +08:00
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)
2020-12-21 17:22:45 +08:00
}
func NewStarlog(out io.Writer) *StarLogger {
return &StarLogger{
handlerFunc: nil,
thread: "MAN",
logcore: newLogCore(out),
isStd: false,
2026-03-19 16:37:57 +08:00
fields: nil,
logErr: nil,
logCtx: nil,
2020-12-21 17:22:45 +08:00
}
}
2022-06-21 10:16:32 +08:00
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
}
2020-12-21 17:22:45 +08:00
func (logger *StarLogger) NewFlag() *StarLogger {
return &StarLogger{
thread: getRandomFlag(false),
handlerFunc: logger.handlerFunc,
logcore: logger.logcore,
isStd: false,
2026-03-19 16:37:57 +08:00
fields: cloneFields(logger.fields),
logErr: logger.logErr,
logCtx: logger.logCtx,
2020-12-21 17:22:45 +08:00
}
}
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 {
2026-03-19 16:37:57 +08:00
logger.logcore.mu.Lock()
defer logger.logcore.mu.Unlock()
return logger.logcore.name
}
2020-12-21 17:22:45 +08:00
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 (logger *starlog) asyncRuntime() *asyncRuntime {
if logger == nil || logger.runtime == nil {
return defaultAsyncRuntime()
}
return logger.runtime
}
func (logger *StarLogger) asyncRuntime() *asyncRuntime {
if logger == nil || logger.logcore == nil {
return defaultAsyncRuntime()
}
return logger.logcore.asyncRuntime()
}
func (logger *starlog) reportAsyncDrop(err error, data LogData) {
logger.asyncRuntime().reportAsyncDrop(err, data)
}
func (logger *starlog) reportWriteError(err error, data LogData) {
logger.asyncRuntime().reportWriteError(err, data)
}
func (runtime *asyncRuntime) snapshot() (*starChanStack, bool) {
if runtime == nil {
return nil, false
}
runtime.mu.Lock()
defer runtime.mu.Unlock()
return runtime.queue, runtime.started
}
func (runtime *asyncRuntime) Start() {
if runtime == nil {
return
}
runtime.mu.Lock()
if runtime.started {
runtime.mu.Unlock()
2020-12-21 17:22:45 +08:00
return
}
queue := newStarChanStack(runtime.queueCapacity)
stopChan := make(chan struct{})
doneChan := make(chan struct{})
runtime.queue = queue
runtime.stopChan = stopChan
runtime.doneChan = doneChan
runtime.started = true
runtime.mu.Unlock()
go func(queue *starChanStack, stop <-chan struct{}, done chan struct{}) {
2026-03-19 16:37:57 +08:00
defer close(done)
2020-12-21 17:22:45 +08:00
for {
select {
2026-03-19 16:37:57 +08:00
case <-stop:
2020-12-21 17:22:45 +08:00
return
default:
}
popped, err := queue.Pop()
2023-02-12 10:18:05 +08:00
if err != nil {
2026-03-19 16:37:57 +08:00
if errors.Is(err, io.EOF) {
return
}
2023-02-12 10:18:05 +08:00
return
2020-12-21 17:22:45 +08:00
}
val, ok := popped.(logTransfer)
2026-03-19 16:37:57 +08:00
if !ok {
continue
}
2020-12-21 17:22:45 +08:00
if val.handlerFunc != nil {
runtime.invokeAsyncHandlerSafely(val.handlerFunc, val.LogData)
2020-12-21 17:22:45 +08:00
}
}
}(queue, stopChan, doneChan)
2020-12-21 17:22:45 +08:00
}
func (runtime *asyncRuntime) Stop() {
if runtime == nil {
return
}
runtime.mu.Lock()
if !runtime.started {
runtime.mu.Unlock()
2020-12-21 17:22:45 +08:00
return
}
stopChan := runtime.stopChan
doneChan := runtime.doneChan
queue := runtime.queue
runtime.queue = nil
runtime.stopChan = nil
runtime.doneChan = nil
runtime.started = false
runtime.mu.Unlock()
2026-03-19 16:37:57 +08:00
if stopChan != nil {
func() {
defer func() {
recover()
}()
close(stopChan)
}()
}
if queue != nil {
_ = queue.Close()
2026-03-19 16:37:57 +08:00
}
if doneChan != nil {
<-doneChan
}
2020-12-21 17:22:45 +08:00
}
2021-09-23 10:44:56 +08:00
func (runtime *asyncRuntime) WaitDrain(ctx context.Context) error {
if ctx == nil {
ctx = context.Background()
}
for {
queue, started := runtime.snapshot()
if !started || queue == nil || queue.Len() == 0 {
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(5 * time.Millisecond):
}
}
2021-11-12 16:03:34 +08:00
}
2026-03-19 16:37:57 +08:00
func (runtime *asyncRuntime) Metrics() AsyncMetrics {
queue, started := runtime.snapshot()
snapshot := AsyncMetrics{
Started: started,
Dropped: runtime.GetAsyncDropCount(),
FallbackToSync: runtime.GetAsyncFallbackToSync(),
HandlerTimeout: runtime.GetAsyncHandlerTimeout(),
}
if queue != nil {
snapshot.QueueLength = queue.Len()
snapshot.QueueCapacity = queue.Cap()
snapshot.QueueFree = queue.Free()
}
return snapshot
2026-03-19 16:37:57 +08:00
}
func (runtime *asyncRuntime) SetAsyncErrorHandler(alert func(error, LogData)) {
if runtime == nil {
return
}
runtime.asyncAlertMu.Lock()
defer runtime.asyncAlertMu.Unlock()
runtime.asyncAlert = alert
}
func (runtime *asyncRuntime) SetAsyncFallbackToSync(enable bool) {
if runtime == nil {
return
}
2026-03-19 16:37:57 +08:00
if enable {
atomic.StoreUint32(&runtime.fallbackSync, 1)
2026-03-19 16:37:57 +08:00
return
}
atomic.StoreUint32(&runtime.fallbackSync, 0)
2026-03-19 16:37:57 +08:00
}
func (runtime *asyncRuntime) GetAsyncFallbackToSync() bool {
if runtime == nil {
return true
}
return atomic.LoadUint32(&runtime.fallbackSync) == 1
2026-03-19 16:37:57 +08:00
}
func (runtime *asyncRuntime) SetAsyncHandlerTimeout(timeout time.Duration) {
if runtime == nil {
return
}
2026-03-19 16:37:57 +08:00
if timeout < 0 {
timeout = 0
}
atomic.StoreInt64(&runtime.timeout, int64(timeout))
2026-03-19 16:37:57 +08:00
}
func (runtime *asyncRuntime) GetAsyncHandlerTimeout() time.Duration {
if runtime == nil {
return 0
}
return time.Duration(atomic.LoadInt64(&runtime.timeout))
2026-03-19 16:37:57 +08:00
}
func (runtime *asyncRuntime) GetAsyncDropCount() uint64 {
if runtime == nil {
return 0
}
return atomic.LoadUint64(&runtime.dropCount)
2026-03-19 16:37:57 +08:00
}
func (runtime *asyncRuntime) reportAsyncDrop(err error, data LogData) {
if runtime == nil {
return
}
atomic.AddUint64(&runtime.dropCount, 1)
runtime.asyncAlertMu.RLock()
alert := runtime.asyncAlert
runtime.asyncAlertMu.RUnlock()
2026-03-19 16:37:57 +08:00
if alert != nil {
func() {
defer func() {
recover()
}()
alert(err, data)
}()
}
}
func (runtime *asyncRuntime) invokeAsyncHandlerSafely(handler func(LogData), data LogData) bool {
if runtime == nil {
return invokeAsyncHandlerDirect(defaultAsyncRuntime(), handler, data)
}
2026-03-19 16:37:57 +08:00
if handler == nil {
return true
}
timeout := runtime.GetAsyncHandlerTimeout()
2026-03-19 16:37:57 +08:00
if timeout <= 0 {
return invokeAsyncHandlerDirect(runtime, handler, data)
2026-03-19 16:37:57 +08:00
}
done := make(chan bool, 1)
go func() {
done <- invokeAsyncHandlerDirect(runtime, handler, data)
2026-03-19 16:37:57 +08:00
}()
select {
case ok := <-done:
return ok
case <-time.After(timeout):
runtime.reportAsyncDrop(ErrAsyncHandlerTimeout, data)
2026-03-19 16:37:57 +08:00
return false
}
}
func invokeAsyncHandlerDirect(runtime *asyncRuntime, handler func(LogData), data LogData) (ok bool) {
2026-03-19 16:37:57 +08:00
defer func() {
if panicErr := recover(); panicErr != nil {
if runtime == nil {
runtime = defaultAsyncRuntime()
}
runtime.reportAsyncDrop(fmt.Errorf("%w: %v", ErrAsyncHandlerPanic, panicErr), data)
2026-03-19 16:37:57 +08:00
ok = false
}
}()
handler(data)
return true
}
func (runtime *asyncRuntime) SetWriteErrorHandler(alert func(error, LogData)) {
if runtime == nil {
return
}
runtime.writeErrMu.Lock()
defer runtime.writeErrMu.Unlock()
runtime.writeErrHandler = alert
2026-03-19 16:37:57 +08:00
}
func (runtime *asyncRuntime) GetWriteErrorCount() uint64 {
if runtime == nil {
return 0
}
return atomic.LoadUint64(&runtime.writeErrCount)
2026-03-19 16:37:57 +08:00
}
func (runtime *asyncRuntime) reportWriteError(err error, data LogData) {
if runtime == nil || err == nil {
2026-03-19 16:37:57 +08:00
return
}
atomic.AddUint64(&runtime.writeErrCount, 1)
runtime.writeErrMu.RLock()
alert := runtime.writeErrHandler
runtime.writeErrMu.RUnlock()
2026-03-19 16:37:57 +08:00
if alert != nil {
func() {
defer func() {
recover()
}()
alert(err, data)
}()
}
}
func (runtime *asyncRuntime) resetForTest() {
if runtime == nil {
return
}
runtime.Stop()
atomic.StoreUint64(&runtime.dropCount, 0)
runtime.SetAsyncErrorHandler(nil)
runtime.SetAsyncFallbackToSync(true)
runtime.SetAsyncHandlerTimeout(0)
atomic.StoreUint64(&runtime.writeErrCount, 0)
runtime.SetWriteErrorHandler(nil)
}
func StartStacks() {
defaultAsyncRuntime().Start()
}
func StopStacks() {
defaultAsyncRuntime().Stop()
}
func Stop() {
StopStacks()
}
func SetAsyncErrorHandler(alert func(error, LogData)) {
defaultAsyncRuntime().SetAsyncErrorHandler(alert)
}
func SetAsyncFallbackToSync(enable bool) {
defaultAsyncRuntime().SetAsyncFallbackToSync(enable)
}
func GetAsyncFallbackToSync() bool {
return defaultAsyncRuntime().GetAsyncFallbackToSync()
}
func SetAsyncHandlerTimeout(timeout time.Duration) {
defaultAsyncRuntime().SetAsyncHandlerTimeout(timeout)
}
func GetAsyncHandlerTimeout() time.Duration {
return defaultAsyncRuntime().GetAsyncHandlerTimeout()
}
func GetAsyncDropCount() uint64 {
return defaultAsyncRuntime().GetAsyncDropCount()
}
func reportAsyncDrop(err error, data LogData) {
defaultAsyncRuntime().reportAsyncDrop(err, data)
}
func invokeAsyncHandlerSafely(handler func(LogData), data LogData) bool {
return defaultAsyncRuntime().invokeAsyncHandlerSafely(handler, data)
}
func SetWriteErrorHandler(alert func(error, LogData)) {
defaultAsyncRuntime().SetWriteErrorHandler(alert)
}
func GetWriteErrorCount() uint64 {
return defaultAsyncRuntime().GetWriteErrorCount()
}
func reportWriteError(err error, data LogData) {
defaultAsyncRuntime().reportWriteError(err, data)
}
2026-03-19 16:37:57 +08:00
func resetAsyncMetricsForTest() {
defaultAsyncRuntime().resetForTest()
2026-03-19 16:37:57 +08:00
}