From 8d24ba105d927ff88319463d801b3faf25ef24d2 Mon Sep 17 00:00:00 2001 From: Ge Song Date: Wed, 17 Jan 2024 15:58:30 +0800 Subject: [PATCH] log/add processor logtext; --- log/README.md | 12 +-- log/logsdk/logjson/option.go | 5 +- log/logsdk/logjson/pool.go | 2 + log/logsdk/logjson/processor.go | 5 +- log/logsdk/logjson/writer.go | 5 ++ log/logsdk/logtext/option.go | 69 +++++++++++++++ log/logsdk/logtext/processor.go | 146 ++++++++++++++++++++++++++++++++ 7 files changed, 233 insertions(+), 11 deletions(-) create mode 100644 log/logsdk/logtext/option.go create mode 100644 log/logsdk/logtext/processor.go diff --git a/log/README.md b/log/README.md index 41395b8..3a4044e 100644 --- a/log/README.md +++ b/log/README.md @@ -21,11 +21,12 @@ func main() { } ``` -`log` 模块包含日志处理的代码, 由 3 个包组成: +`log` 模块包含日志处理的代码, 由 4 个包组成: 1. [logsdk](./logsdk): 日志实现; 2. [logjson](./logsdk/logjson): 控制台 JSON 日志处理器(`Processor`); -3. [log](.): 根目录, 提供全局 `Logger`, 把 `Logger` / `Entry` 相关的方法封装成函数. +3. [logtext](./logsdk/logtext): 控制台文本日志处理器(`Processor`); +4. [log](.): 根目录, 提供全局 `Logger`, 把 `Logger` / `Entry` 相关的方法封装成函数. ## 基本概念 @@ -88,7 +89,7 @@ log.Logger() 关闭强制生成调用栈, `log.Logger().SetReportStackLevel(logsdk.LevelDisabled)`; -记录 panic, 在 `defer v = recover()` 后执行 `log.Z(context.Background(), v)`, 其中 `Z` 需要是附加调用栈的等级; +记录 panic, 在 `defer v = recover()` 后执行 `log.Z(context.Background(), v)`, 其中 `Z` 需要是附加调用栈的等级(例如: `Error`); mock, 实现 mock 日志处理器对生成的日志进行处理; @@ -96,5 +97,6 @@ mock, 实现 mock 日志处理器对生成的日志进行处理; ## 日志处理器 -- [logsdk/logjson](logsdk/logjson) 控制台 JSON 日志 -- [go get git.blauwelle.com/go/crate/logotel](../logotel) OpenTelemetry 日志 +- [logsdk/logjson](logsdk/logjson) 控制台 JSON 日志. +- [logtext](./logsdk/logtext): 控制台文本日志. +- [go get git.blauwelle.com/go/crate/logotel](../logotel) OpenTelemetry 日志. diff --git a/log/logsdk/logjson/option.go b/log/logsdk/logjson/option.go index 7869ac8..0e3e389 100644 --- a/log/logsdk/logjson/option.go +++ b/log/logsdk/logjson/option.go @@ -2,7 +2,6 @@ package logjson import ( "io" - "os" "time" ) @@ -60,10 +59,10 @@ func newConfig(opts ...Option) *config { opt.apply(cfg) } if !cfg.hasPool { - cfg.bytesBufferPool = NewBytesBufferPool(bytesBufferInitialSize, bytesBufferMaximumSize) + cfg.bytesBufferPool = DefaultBytesBufferPool } if cfg.output == nil { - cfg.output = NewSyncWriter(os.Stderr) + cfg.output = DefaultStderrSyncWriter } if cfg.timestampFormat == "" { cfg.timestampFormat = time.RFC3339Nano diff --git a/log/logsdk/logjson/pool.go b/log/logsdk/logjson/pool.go index cd8ba3f..7526740 100644 --- a/log/logsdk/logjson/pool.go +++ b/log/logsdk/logjson/pool.go @@ -10,6 +10,8 @@ const ( bytesBufferMaximumSize = 4096 ) +var DefaultBytesBufferPool = NewBytesBufferPool(bytesBufferInitialSize, bytesBufferMaximumSize) + type BytesBufferPool interface { Get() *bytes.Buffer Put(buffer *bytes.Buffer) diff --git a/log/logsdk/logjson/processor.go b/log/logsdk/logjson/processor.go index 5447119..7c51454 100644 --- a/log/logsdk/logjson/processor.go +++ b/log/logsdk/logjson/processor.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "io" - "os" "git.blauwelle.com/go/crate/log/logsdk" ) @@ -69,11 +68,11 @@ func (processor *Processor) Process(_ context.Context, entry logsdk.ReadonlyEntr // Encode 2次分配 if err := encoder.Encode(m); err != nil { - _, _ = fmt.Fprintf(os.Stderr, "JSON processor cannot encode log %#v: %s\n", m, err.Error()) + _, _ = fmt.Fprintf(processor.output, "JSON processor cannot encode log %#v: %s\n", m, err.Error()) } if _, err := buf.WriteTo(processor.output); err != nil { - _, _ = fmt.Fprintf(os.Stderr, "JSON processor cannot write log: %s\n", err.Error()) + _, _ = fmt.Fprintf(processor.output, "JSON processor cannot write log: %s\n", err.Error()) } } diff --git a/log/logsdk/logjson/writer.go b/log/logsdk/logjson/writer.go index d5bed83..c6fabb1 100644 --- a/log/logsdk/logjson/writer.go +++ b/log/logsdk/logjson/writer.go @@ -2,9 +2,14 @@ package logjson import ( "io" + "os" "sync" ) +var ( + DefaultStderrSyncWriter = NewSyncWriter(os.Stderr) +) + // NewSyncWriter 返回写互斥的 io.Writer func NewSyncWriter(writer io.Writer) io.Writer { return &syncWriter{ diff --git a/log/logsdk/logtext/option.go b/log/logsdk/logtext/option.go new file mode 100644 index 0000000..1ae569d --- /dev/null +++ b/log/logsdk/logtext/option.go @@ -0,0 +1,69 @@ +package logtext + +import ( + "io" + "time" + + "git.blauwelle.com/go/crate/log/logsdk/logjson" +) + +type Option interface { + apply(cfg *config) +} + +func WithBufferPool(pool logjson.BytesBufferPool) Option { + return optionFunc(func(cfg *config) { + cfg.bytesBufferPool = pool + }) +} + +func WithOutput(w io.Writer) Option { + return optionFunc(func(cfg *config) { + cfg.output = w + }) +} + +func WithTimeFormat(format string) Option { + return optionFunc(func(cfg *config) { + cfg.timeFormat = format + cfg.timePadding = len(format) + }) +} + +func WithDisableTime(disable bool) Option { + return optionFunc(func(cfg *config) { + cfg.disableTime = disable + }) +} + +type config struct { + bytesBufferPool logjson.BytesBufferPool + output io.Writer + timeFormat string + timePadding int + disableTime bool +} + +func newConfig(opts ...Option) *config { + cfg := new(config) + for _, opt := range opts { + opt.apply(cfg) + } + if cfg.bytesBufferPool == nil { + cfg.bytesBufferPool = logjson.DefaultBytesBufferPool + } + if cfg.output == nil { + cfg.output = logjson.DefaultStderrSyncWriter + } + if cfg.timeFormat == "" { + cfg.timeFormat = time.RFC3339Nano + cfg.timePadding = len(cfg.timeFormat) + } + return cfg +} + +type optionFunc func(cfg *config) + +func (fn optionFunc) apply(cfg *config) { + fn(cfg) +} diff --git a/log/logsdk/logtext/processor.go b/log/logsdk/logtext/processor.go new file mode 100644 index 0000000..71c46a9 --- /dev/null +++ b/log/logsdk/logtext/processor.go @@ -0,0 +1,146 @@ +package logtext + +import ( + "bytes" + "context" + "fmt" + "io" + "strconv" + + "git.blauwelle.com/go/crate/log/logsdk" + "git.blauwelle.com/go/crate/log/logsdk/logjson" +) + +var _ logsdk.EntryProcessor = &Processor{} + +func New(opts ...Option) *Processor { + cfg := newConfig(opts...) + return &Processor{ + bytesBufferPool: cfg.bytesBufferPool, + output: cfg.output, + timeFormat: cfg.timeFormat, + timePadding: cfg.timePadding, + disableTime: cfg.disableTime, + } +} + +type Processor struct { + bytesBufferPool logjson.BytesBufferPool + output io.Writer + timeFormat string + timePadding int + disableTime bool +} + +func (processor *Processor) Process(_ context.Context, entry logsdk.ReadonlyEntry) { + buf := processor.bytesBufferPool.Get() + buf.Reset() + defer processor.bytesBufferPool.Put(buf) + + buf.Write(formatLevel(entry.Level)) + + if !processor.disableTime { + buf.WriteByte(' ') + timeValue := entry.Time.Format(processor.timeFormat) + buf.WriteString(timeValue) + writeSpace(buf, processor.timePadding-len(timeValue)) + } + + for _, field := range entry.Fields { + buf.WriteByte(' ') + buf.WriteString(field.Key) + buf.WriteByte('=') + writeValue(buf, field.Value) + } + + if entry.Message != "" { + buf.WriteByte(' ') + buf.WriteString(entry.Message) + } + + buf.WriteByte('\n') + + if entry.Caller.IsValid() && len(entry.Stack) == 0 { + writeFrame(buf, entry.Caller) + } + + if len(entry.Stack) > 0 { + for _, frame := range entry.Stack { + writeFrame(buf, frame) + } + } + + if _, err := buf.WriteTo(processor.output); err != nil { + _, _ = fmt.Fprintf(processor.output, "TEXT processor cannot write log: %s\n", err.Error()) + } +} + +func writeSpace(buf *bytes.Buffer, n int) { + for i := 0; i < n; i++ { + buf.WriteByte(' ') + } +} + +func writeFrame(buf *bytes.Buffer, frame logsdk.Frame) { + buf.WriteByte('|') + buf.WriteByte(' ') + buf.WriteString(frame.Function) + buf.WriteByte('\n') + buf.WriteByte('|') + buf.WriteByte(' ') + buf.WriteByte('\t') + buf.WriteString(frame.File) + buf.WriteByte(':') + buf.WriteString(strconv.Itoa(frame.Line)) + buf.WriteByte('\n') +} + +func writeValue(buf *bytes.Buffer, value any) { + switch value := value.(type) { + case nil: + buf.WriteString("") + case string: + buf.WriteString(value) + case error: + buf.WriteString(value.Error()) + case fmt.Stringer: + buf.WriteString(value.String()) + default: + _, _ = fmt.Fprintf(buf, "%v", value) + } +} + +func formatLevel(level logsdk.Level) []byte { + switch level { + case logsdk.LevelPanic: + return []byte(LevelPanicValue) + case logsdk.LevelFatal: + return []byte(LevelFatalValue) + case logsdk.LevelError: + return []byte(LevelErrorValue) + case logsdk.LevelWarn: + return []byte(LevelWarnValue) + case logsdk.LevelInfo: + return []byte(LevelInfoValue) + case logsdk.LevelDebug: + return []byte(LevelDebugValue) + case logsdk.LevelTrace: + return []byte(LevelTraceValue) + case logsdk.LevelDisabled: + fallthrough + default: + return []byte(levelUnknownValue) + } +} + +const ( + LevelPanicValue = "PNC" + LevelFatalValue = "FTL" + LevelErrorValue = "ERR" + LevelWarnValue = "WRN" + LevelInfoValue = "INF" + LevelDebugValue = "DBG" + LevelTraceValue = "TRC" + + levelUnknownValue = "UNK" +)