feat: add logging and collector implementations

- Introduced `hertzx` package with `NewHertz` function for server initialization.
- Implemented `logx` package with various log collectors: Console, Loki, Elasticsearch, and Prometheus.
- Added `Logger` struct to manage logging levels and collectors.
- Created environment variable loading functionality in `osx` package to support configuration.
- Enhanced logging capabilities with structured log entries and asynchronous collection.
This commit is contained in:
2025-11-01 13:20:02 +08:00
commit b0d224dc64
14 changed files with 1275 additions and 0 deletions

442
logx/log.go Normal file
View File

@@ -0,0 +1,442 @@
package logx
import (
"context"
"strings"
"sync"
"time"
"git.liteyuki.org/LiteyukiStudio/folium/osx"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// 支持的日志等级
type Level string
const (
LevelTrace Level = "trace"
LevelDebug Level = "debug"
LevelInfo Level = "info"
LevelWarn Level = "warn"
LevelError Level = "error"
LevelFatal Level = "fatal"
LevelPanic Level = "panic"
)
// Entry 表示一条日志的可传输结构
type Entry struct {
Level Level `json:"level"`
Time time.Time `json:"time"`
Msg string `json:"msg"`
Fields map[string]interface{} `json:"fields,omitempty"`
}
// Collector 是外部收集器接口(用户可实现)
type Collector interface {
Collect(ctx context.Context, e Entry) error
Close() error
}
// Logger 支持本地打印并把日志发送到已注册的 collectors
type Logger struct {
zap *zap.Logger
collectors []Collector
mu sync.RWMutex
wg sync.WaitGroup
minLevel Level // 低于此级别的日志将被忽略
}
// 全局单例
var (
Default *Logger
defaultOnce sync.Once
)
// buildLoggerFromEnv 基于环境构造 Logger不设置全局变量
func buildLoggerFromEnv() *Logger {
// 输出格式
output := strings.ToLower(osx.GetEnv("LOG_OUTPUT", "console"))
cfg := zap.NewProductionConfig()
if output == "json" {
cfg.Encoding = "json"
} else {
cfg.Encoding = "console"
}
cfg.EncoderConfig.TimeKey = "ts"
cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
// 让 zap 本身也服从 LOG_LEVEL
levelStr := strings.ToLower(osx.GetEnv("LOG_LEVEL", osx.GetEnv("LOG_MIN_LEVEL", "info")))
var zapLevel zapcore.Level
switch levelStr {
case "trace", "debug":
zapLevel = zapcore.DebugLevel
case "info":
zapLevel = zapcore.InfoLevel
case "warn", "warning":
zapLevel = zapcore.WarnLevel
case "error":
zapLevel = zapcore.ErrorLevel
case "fatal":
zapLevel = zapcore.FatalLevel
case "panic":
zapLevel = zapcore.PanicLevel
default:
zapLevel = zapcore.InfoLevel
}
cfg.Level = zap.NewAtomicLevelAt(zapLevel)
// 可通过环境关闭 stacktrace 输出
if strings.ToLower(osx.GetEnv("LOG_DISABLE_STACKTRACE", "false")) == "true" {
cfg.DisableStacktrace = true
}
zl, _ := cfg.Build()
l := &Logger{
zap: zl,
collectors: nil,
minLevel: LevelInfo,
}
// 最低日志级别(同步 env
switch levelStr {
case "trace":
l.minLevel = LevelTrace
case "debug":
l.minLevel = LevelDebug
case "info":
l.minLevel = LevelInfo
case "warn", "warning":
l.minLevel = LevelWarn
case "error":
l.minLevel = LevelError
case "fatal":
l.minLevel = LevelFatal
case "panic":
l.minLevel = LevelPanic
default:
l.minLevel = LevelInfo
}
// 从环境加载并注册 collectors实现位于 collector.go
cols := LoadCollectorsFromEnv()
for _, c := range cols {
l.RegisterCollector(c)
}
return l
}
// InitLogger 初始化全局单例(只第一次生效),返回单例
func InitLogger() *Logger {
defaultOnce.Do(func() {
Default = buildLoggerFromEnv()
})
return Default
}
// GetLogger 返回全局单例,如果尚未初始化则会自动 InitLogger
func GetLogger() *Logger {
return InitLogger()
}
// 删除旧的 New / NewDefault保留 New(...) 风格的局部 logger 创建函数改名为 NewLocal可选
func NewLocal(zl *zap.Logger) *Logger {
if zl == nil {
// 使用一个轻量 zap 实例,不触碰全局单例
cfg := zap.NewProductionConfig()
cfg.Encoding = "console"
cfg.EncoderConfig.TimeKey = "ts"
cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
zl2, _ := cfg.Build()
return &Logger{
zap: zl2,
minLevel: LevelDebug,
collectors: nil,
}
}
return &Logger{
zap: zl,
minLevel: LevelDebug,
collectors: nil,
}
}
// RegisterCollector 在运行时注册一个 Collector线程安全
func (l *Logger) RegisterCollector(c Collector) {
if c == nil {
return
}
l.mu.Lock()
defer l.mu.Unlock()
l.collectors = append(l.collectors, c)
}
// UnregisterCollector 注销已注册的 Collector按指针相等匹配
func (l *Logger) UnregisterCollector(c Collector) {
if c == nil {
return
}
l.mu.Lock()
defer l.mu.Unlock()
for i, col := range l.collectors {
if col == c {
l.collectors = append(l.collectors[:i], l.collectors[i+1:]...)
return
}
}
}
// ListCollectors 返回当前已注册的 collectors副本线程安全
func (l *Logger) ListCollectors() []Collector {
l.mu.RLock()
defer l.mu.RUnlock()
cp := make([]Collector, len(l.collectors))
copy(cp, l.collectors)
return cp
}
// Close 关闭所有 collectors阻塞直到异步发送结束
func (l *Logger) Close() error {
l.mu.RLock()
collectors := append([]Collector(nil), l.collectors...)
l.mu.RUnlock()
// 等待 async sending 完成
l.wg.Wait()
var firstErr error
for _, c := range collectors {
if err := c.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
_ = l.zap.Sync()
return firstErr
}
// SetLevel 设置最低日志级别,低于该级别的日志将被忽略
func (l *Logger) SetLevel(level Level) {
l.mu.Lock()
defer l.mu.Unlock()
l.minLevel = level
}
// level 排序辅助
var levelOrder = map[Level]int{
LevelTrace: 0,
LevelDebug: 1,
LevelInfo: 2,
LevelWarn: 3,
LevelError: 4,
LevelFatal: 5,
LevelPanic: 6,
}
func (l *Logger) log(ctx context.Context, level Level, msg string, fields map[string]interface{}) {
// 级别过滤
l.mu.RLock()
min := l.minLevel
l.mu.RUnlock()
if levelOrder[level] < levelOrder[min] {
return
}
// 本地打印(保持原有行为),并尽量保持 trace/level 字段
switch level {
case LevelTrace:
l.zap.Debug(msg, zap.String("level", string(LevelTrace)), zap.Any("fields", fields))
case LevelDebug:
l.zap.Debug(msg, zap.Any("fields", fields))
case LevelInfo:
l.zap.Info(msg, zap.Any("fields", fields))
case LevelWarn:
l.zap.Warn(msg, zap.Any("fields", fields))
case LevelError:
l.zap.Error(msg, zap.Any("fields", fields))
case LevelFatal:
// Fatal 会调用 os.Exit(1)
l.zap.Fatal(msg, zap.Any("fields", fields))
case LevelPanic:
// Panic 会 panic
l.zap.Panic(msg, zap.Any("fields", fields))
default:
l.zap.Info(msg, zap.Any("level", level), zap.Any("fields", fields))
}
// 向 collectors 异步发送
l.mu.RLock()
collectors := append([]Collector(nil), l.collectors...)
l.mu.RUnlock()
if len(collectors) == 0 {
return
}
e := Entry{
Level: level,
Time: time.Now(),
Msg: msg,
Fields: fields,
}
for _, c := range collectors {
c := c
l.wg.Add(1)
go func() {
defer l.wg.Done()
ctx2, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
if err := c.Collect(ctx2, e); err != nil {
// collectors 的错误不影响主流程,记录到本地日志
l.zap.Warn("collector collect error", zap.Error(err))
}
}()
}
}
// 便捷方法(增加 Trace, Fatal, Panic
// 保留 Logger 的方法签名:需要 ctx
func (l *Logger) Trace(ctx context.Context, msg string, fields map[string]interface{}) {
l.log(ctx, LevelTrace, msg, fields)
}
func (l *Logger) Debug(ctx context.Context, msg string, fields map[string]interface{}) {
l.log(ctx, LevelDebug, msg, fields)
}
func (l *Logger) Info(ctx context.Context, msg string, fields map[string]interface{}) {
l.log(ctx, LevelInfo, msg, fields)
}
func (l *Logger) Warn(ctx context.Context, msg string, fields map[string]interface{}) {
l.log(ctx, LevelWarn, msg, fields)
}
func (l *Logger) Error(ctx context.Context, msg string, fields map[string]interface{}) {
l.log(ctx, LevelError, msg, fields)
}
func (l *Logger) Fatal(ctx context.Context, msg string, fields map[string]interface{}) {
l.log(ctx, LevelFatal, msg, fields)
}
func (l *Logger) Panic(ctx context.Context, msg string, fields map[string]interface{}) {
l.log(ctx, LevelPanic, msg, fields)
}
// SimpleLogger 提供不带 ctx 的简洁 API可选 fields
type SimpleLogger struct {
parent *Logger
}
func (s *SimpleLogger) Trace(msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
s.parent.Trace(context.Background(), msg, f)
}
func (s *SimpleLogger) Debug(msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
s.parent.Debug(context.Background(), msg, f)
}
func (s *SimpleLogger) Info(msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
s.parent.Info(context.Background(), msg, f)
}
func (s *SimpleLogger) Warn(msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
s.parent.Warn(context.Background(), msg, f)
}
func (s *SimpleLogger) Error(msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
s.parent.Error(context.Background(), msg, f)
}
func (s *SimpleLogger) Fatal(msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
s.parent.Fatal(context.Background(), msg, f)
}
func (s *SimpleLogger) Panic(msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
s.parent.Panic(context.Background(), msg, f)
}
// Simple 返回一个绑定到当前 Logger 的 SimpleLogger不创建新实例
func (l *Logger) Simple() *SimpleLogger {
return &SimpleLogger{parent: l}
}
// 包级简洁 API无需 ctx自动使用 background ctx调用者更推荐显式使用 Ctx* 以保留 trace
func Trace(msg string, fields ...map[string]interface{}) { GetLogger().Simple().Trace(msg, fields...) }
func Debug(msg string, fields ...map[string]interface{}) { GetLogger().Simple().Debug(msg, fields...) }
func Info(msg string, fields ...map[string]interface{}) { GetLogger().Simple().Info(msg, fields...) }
func Warn(msg string, fields ...map[string]interface{}) { GetLogger().Simple().Warn(msg, fields...) }
func Error(msg string, fields ...map[string]interface{}) { GetLogger().Simple().Error(msg, fields...) }
func Fatal(msg string, fields ...map[string]interface{}) { GetLogger().Simple().Fatal(msg, fields...) }
func Panic(msg string, fields ...map[string]interface{}) { GetLogger().Simple().Panic(msg, fields...) }
// 包级带 ctx 的 API当你有请求上下文包含 trace时使用
func CtxTrace(ctx context.Context, msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
GetLogger().Trace(ctx, msg, f)
}
func CtxDebug(ctx context.Context, msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
GetLogger().Debug(ctx, msg, f)
}
func CtxInfo(ctx context.Context, msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
GetLogger().Info(ctx, msg, f)
}
func CtxWarn(ctx context.Context, msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
GetLogger().Warn(ctx, msg, f)
}
func CtxError(ctx context.Context, msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
GetLogger().Error(ctx, msg, f)
}
func CtxFatal(ctx context.Context, msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
GetLogger().Fatal(ctx, msg, f)
}
func CtxPanic(ctx context.Context, msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
GetLogger().Panic(ctx, msg, f)
}