Add logger package with performance benchmarks
This commit is contained in:
121
README.md
121
README.md
@@ -96,6 +96,127 @@ fm.CloseManager()
|
|||||||
- `x-amz-meta-complete`: Upload completion status
|
- `x-amz-meta-complete`: Upload completion status
|
||||||
- `x-amz-meta-code`: Unique file code
|
- `x-amz-meta-code`: Unique file code
|
||||||
|
|
||||||
|
## logger
|
||||||
|
|
||||||
|
A lightweight, structured logging package for Go.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **Simple API**: Format strings and structured fields
|
||||||
|
- **Global & Instance**: Use default global logger or create custom instances
|
||||||
|
- **Context Support**: Extract trace_id from context automatically
|
||||||
|
- **Multiple Formats**: Text (default) and JSON
|
||||||
|
- **Level Filtering**: DEBUG, INFO, WARN, ERROR, FATAL, PANIC
|
||||||
|
- **No Dependencies**: Standard library only
|
||||||
|
- **High Performance**: ~727 ns/op for text format, zero-allocation when filtered
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "gitea.loveuer.com/loveuer/upkg/logger"
|
||||||
|
|
||||||
|
// Global logger (default)
|
||||||
|
logger.Info("hello %s", "world")
|
||||||
|
logger.Error("failed: %v", err)
|
||||||
|
|
||||||
|
// With fields
|
||||||
|
logger.InfoField("user logged in",
|
||||||
|
"user_id", 123,
|
||||||
|
"action", "login",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Context with trace_id
|
||||||
|
ctx := context.WithValue(ctx, "trace_id", "req-123")
|
||||||
|
logger.InfoCtx(ctx, "request processed")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Logger
|
||||||
|
|
||||||
|
```go
|
||||||
|
log := logger.New(
|
||||||
|
logger.WithLevel(logger.DEBUG),
|
||||||
|
logger.WithFormat(logger.JSON),
|
||||||
|
logger.WithOutput(logger.Stdout),
|
||||||
|
logger.WithCaller(true),
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Info("custom logger")
|
||||||
|
log.InfoField("event", "key", "value")
|
||||||
|
```
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
#### Level Methods
|
||||||
|
|
||||||
|
```go
|
||||||
|
log.Debug(format string, args ...)
|
||||||
|
log.Info(format string, args ...)
|
||||||
|
log.Warn(format string, args ...)
|
||||||
|
log.Error(format string, args ...)
|
||||||
|
log.Fatal(format string, args ...)
|
||||||
|
log.Panic(format string, args ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Context Methods
|
||||||
|
|
||||||
|
```go
|
||||||
|
log.InfoCtx(ctx, format string, args ...)
|
||||||
|
// Automatically adds trace_id from ctx as a field
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Field Methods
|
||||||
|
|
||||||
|
```go
|
||||||
|
log.InfoField(message string, keyValues ...interface{})
|
||||||
|
// Example: log.InfoField("user created", "id", 123, "name", "john")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
```go
|
||||||
|
logger.New(
|
||||||
|
WithLevel(INFO), // Minimum log level
|
||||||
|
WithFormat(TEXT), // TEXT or JSON
|
||||||
|
WithOutput(Stdout), // Stdout, Stderr, or use WithOutputFile
|
||||||
|
WithOutputFile("/path/to.log"), // Write to file
|
||||||
|
WithCaller(true), // Include file:line in output
|
||||||
|
WithFieldKey("trace_id"), // Context field key name
|
||||||
|
WithPrefix("myapp"), // Text format prefix
|
||||||
|
WithTimeFormat("2006-01-02"), // Custom time format
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
| Benchmark | ns/op | B/op | allocs |
|
||||||
|
|-----------|-------|------|--------|
|
||||||
|
| Info (Text) | 727 | 319 | 6 |
|
||||||
|
| Info (JSON) | 1932 | 1026 | 18 |
|
||||||
|
| Info (Filtered) | 2 | 0 | 0 |
|
||||||
|
| InfoField | 1889 | 1014 | 14 |
|
||||||
|
| InfoWithCaller | 1772 | 856 | 11 |
|
||||||
|
|
||||||
|
### Text Format Output
|
||||||
|
|
||||||
|
```
|
||||||
|
2026-01-17 15:30:45 INFO hello world
|
||||||
|
2026-01-17 15:30:45 INFO user logged in user_id=123 action=login
|
||||||
|
```
|
||||||
|
|
||||||
|
### JSON Format Output
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"time":"2026-01-17T15:30:45Z","level":"INFO","message":"hello world"}
|
||||||
|
{"time":"2026-01-17T15:30:45Z","level":"INFO","message":"user logged in","user_id":123,"action":"login"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Packages
|
||||||
|
|
||||||
|
| Package | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `controller/file_manager` | File storage abstraction (local/S3) |
|
||||||
|
| `logger` | Structured logging package |
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
99
logger/default.go
Normal file
99
logger/default.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultLogger *Logger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
defaultLogger = New()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Default() *Logger {
|
||||||
|
return defaultLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetDefault(l *Logger) {
|
||||||
|
defaultLogger = l
|
||||||
|
}
|
||||||
|
|
||||||
|
func Debug(format string, args ...interface{}) {
|
||||||
|
defaultLogger.log(DEBUG, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Info(format string, args ...interface{}) {
|
||||||
|
defaultLogger.log(INFO, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Warn(format string, args ...interface{}) {
|
||||||
|
defaultLogger.log(WARN, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(format string, args ...interface{}) {
|
||||||
|
defaultLogger.log(ERROR, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Fatal(format string, args ...interface{}) {
|
||||||
|
defaultLogger.log(FATAL, format, args...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Panic(format string, args ...interface{}) {
|
||||||
|
defaultLogger.log(PANIC, format, args...)
|
||||||
|
panic(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func DebugCtx(ctx context.Context, format string, args ...interface{}) {
|
||||||
|
defaultLogger.logCtx(DEBUG, ctx, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func InfoCtx(ctx context.Context, format string, args ...interface{}) {
|
||||||
|
defaultLogger.logCtx(INFO, ctx, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WarnCtx(ctx context.Context, format string, args ...interface{}) {
|
||||||
|
defaultLogger.logCtx(WARN, ctx, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrorCtx(ctx context.Context, format string, args ...interface{}) {
|
||||||
|
defaultLogger.logCtx(ERROR, ctx, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FatalCtx(ctx context.Context, format string, args ...interface{}) {
|
||||||
|
defaultLogger.logCtx(FATAL, ctx, format, args...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PanicCtx(ctx context.Context, format string, args ...interface{}) {
|
||||||
|
defaultLogger.logCtx(PANIC, ctx, format, args...)
|
||||||
|
panic(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func DebugField(message string, keyValues ...interface{}) {
|
||||||
|
defaultLogger.logField(DEBUG, message, keyValues...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func InfoField(message string, keyValues ...interface{}) {
|
||||||
|
defaultLogger.logField(INFO, message, keyValues...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WarnField(message string, keyValues ...interface{}) {
|
||||||
|
defaultLogger.logField(WARN, message, keyValues...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrorField(message string, keyValues ...interface{}) {
|
||||||
|
defaultLogger.logField(ERROR, message, keyValues...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FatalField(message string, keyValues ...interface{}) {
|
||||||
|
defaultLogger.logField(FATAL, message, keyValues...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PanicField(message string, keyValues ...interface{}) {
|
||||||
|
defaultLogger.logField(PANIC, message, keyValues...)
|
||||||
|
panic(message)
|
||||||
|
}
|
||||||
77
logger/formatter.go
Normal file
77
logger/formatter.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Formatter interface {
|
||||||
|
Format(writer io.Writer, entry *Entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
type textFormatter struct {
|
||||||
|
timeFormat string
|
||||||
|
prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonFormatter struct{}
|
||||||
|
|
||||||
|
func newFormatter(format Format) Formatter {
|
||||||
|
switch format {
|
||||||
|
case JSON:
|
||||||
|
return &jsonFormatter{}
|
||||||
|
default:
|
||||||
|
return &textFormatter{
|
||||||
|
timeFormat: "2006-01-02 15:04:05",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *textFormatter) Format(writer io.Writer, entry *Entry) {
|
||||||
|
parts := make([]string, 0, 8)
|
||||||
|
parts = append(parts, entry.Time.Format(f.timeFormat))
|
||||||
|
parts = append(parts, entry.Level.String())
|
||||||
|
|
||||||
|
if f.prefix != "" {
|
||||||
|
parts = append(parts, "["+f.prefix+"]")
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.Caller != "" {
|
||||||
|
parts = append(parts, entry.Caller)
|
||||||
|
}
|
||||||
|
|
||||||
|
parts = append(parts, entry.Message)
|
||||||
|
|
||||||
|
if len(entry.Fields) > 0 {
|
||||||
|
fieldStrs := make([]string, 0, len(entry.Fields))
|
||||||
|
for k, v := range entry.Fields {
|
||||||
|
fieldStrs = append(fieldStrs, fmt.Sprintf("%s=%v", k, v))
|
||||||
|
}
|
||||||
|
parts = append(parts, strings.Join(fieldStrs, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(writer, strings.Join(parts, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *jsonFormatter) Format(writer io.Writer, entry *Entry) {
|
||||||
|
m := map[string]interface{}{
|
||||||
|
"time": entry.Time.Format(time.RFC3339),
|
||||||
|
"level": entry.Level.String(),
|
||||||
|
"message": entry.Message,
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.Caller != "" {
|
||||||
|
m["caller"] = entry.Caller
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range entry.Fields {
|
||||||
|
m[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
data, _ := json.Marshal(m)
|
||||||
|
writer.Write(data)
|
||||||
|
writer.Write([]byte("\n"))
|
||||||
|
}
|
||||||
278
logger/logger.go
Normal file
278
logger/logger.go
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Level int8
|
||||||
|
|
||||||
|
const (
|
||||||
|
DEBUG Level = iota
|
||||||
|
INFO
|
||||||
|
WARN
|
||||||
|
ERROR
|
||||||
|
FATAL
|
||||||
|
PANIC
|
||||||
|
)
|
||||||
|
|
||||||
|
func (l Level) String() string {
|
||||||
|
switch l {
|
||||||
|
case DEBUG:
|
||||||
|
return "DEBUG"
|
||||||
|
case INFO:
|
||||||
|
return "INFO"
|
||||||
|
case WARN:
|
||||||
|
return "WARN"
|
||||||
|
case ERROR:
|
||||||
|
return "ERROR"
|
||||||
|
case FATAL:
|
||||||
|
return "FATAL"
|
||||||
|
case PANIC:
|
||||||
|
return "PANIC"
|
||||||
|
default:
|
||||||
|
return "UNKNOWN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Format int8
|
||||||
|
|
||||||
|
const (
|
||||||
|
TEXT Format = iota
|
||||||
|
JSON
|
||||||
|
)
|
||||||
|
|
||||||
|
type Output int8
|
||||||
|
|
||||||
|
const (
|
||||||
|
Stdout Output = iota
|
||||||
|
Stderr
|
||||||
|
)
|
||||||
|
|
||||||
|
type Entry struct {
|
||||||
|
Time time.Time
|
||||||
|
Level Level
|
||||||
|
Message string
|
||||||
|
Fields map[string]interface{}
|
||||||
|
Caller string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
opts options
|
||||||
|
mu sync.Mutex
|
||||||
|
writer io.Writer
|
||||||
|
formatter Formatter
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(opts ...Option) *Logger {
|
||||||
|
o := defaultOptions()
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&o)
|
||||||
|
}
|
||||||
|
|
||||||
|
var writer io.Writer
|
||||||
|
switch o.output {
|
||||||
|
case Stdout:
|
||||||
|
writer = os.Stdout
|
||||||
|
case Stderr:
|
||||||
|
writer = os.Stderr
|
||||||
|
default:
|
||||||
|
writer = os.Stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.outputFile != "" {
|
||||||
|
f, err := os.OpenFile(o.outputFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
|
if err == nil {
|
||||||
|
writer = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Logger{
|
||||||
|
opts: o,
|
||||||
|
writer: writer,
|
||||||
|
formatter: newFormatter(o.format),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) log(level Level, msg string, args ...interface{}) {
|
||||||
|
if level < l.opts.level {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := &Entry{
|
||||||
|
Time: time.Now(),
|
||||||
|
Level: level,
|
||||||
|
Message: fmt.Sprintf(msg, args...),
|
||||||
|
Fields: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.opts.caller {
|
||||||
|
entry.Caller = getCaller(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
l.formatter.Format(l.writer, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) logCtx(level Level, ctx context.Context, msg string, args ...interface{}) {
|
||||||
|
if level < l.opts.level {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := &Entry{
|
||||||
|
Time: time.Now(),
|
||||||
|
Level: level,
|
||||||
|
Message: fmt.Sprintf(msg, args...),
|
||||||
|
Fields: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx != nil {
|
||||||
|
if traceID := ctx.Value(l.opts.fieldKey); traceID != nil {
|
||||||
|
entry.Fields[l.opts.fieldKey] = traceID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.opts.caller {
|
||||||
|
entry.Caller = getCaller(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
l.formatter.Format(l.writer, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) logField(level Level, message string, keyValues ...interface{}) {
|
||||||
|
if level < l.opts.level {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := &Entry{
|
||||||
|
Time: time.Now(),
|
||||||
|
Level: level,
|
||||||
|
Message: message,
|
||||||
|
Fields: parseFields(keyValues),
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.opts.caller {
|
||||||
|
entry.Caller = getCaller(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
l.formatter.Format(l.writer, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Debug(format string, args ...interface{}) {
|
||||||
|
l.log(DEBUG, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Info(format string, args ...interface{}) {
|
||||||
|
l.log(INFO, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Warn(format string, args ...interface{}) {
|
||||||
|
l.log(WARN, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Error(format string, args ...interface{}) {
|
||||||
|
l.log(ERROR, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Fatal(format string, args ...interface{}) {
|
||||||
|
l.log(FATAL, format, args...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Panic(format string, args ...interface{}) {
|
||||||
|
l.log(PANIC, format, args...)
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
panic(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) DebugCtx(ctx context.Context, format string, args ...interface{}) {
|
||||||
|
l.logCtx(DEBUG, ctx, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) InfoCtx(ctx context.Context, format string, args ...interface{}) {
|
||||||
|
l.logCtx(INFO, ctx, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) WarnCtx(ctx context.Context, format string, args ...interface{}) {
|
||||||
|
l.logCtx(WARN, ctx, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) ErrorCtx(ctx context.Context, format string, args ...interface{}) {
|
||||||
|
l.logCtx(ERROR, ctx, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) FatalCtx(ctx context.Context, format string, args ...interface{}) {
|
||||||
|
l.logCtx(FATAL, ctx, format, args...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) PanicCtx(ctx context.Context, format string, args ...interface{}) {
|
||||||
|
l.logCtx(PANIC, ctx, format, args...)
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
panic(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) DebugField(message string, keyValues ...interface{}) {
|
||||||
|
l.logField(DEBUG, message, keyValues...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) InfoField(message string, keyValues ...interface{}) {
|
||||||
|
l.logField(INFO, message, keyValues...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) WarnField(message string, keyValues ...interface{}) {
|
||||||
|
l.logField(WARN, message, keyValues...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) ErrorField(message string, keyValues ...interface{}) {
|
||||||
|
l.logField(ERROR, message, keyValues...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) FatalField(message string, keyValues ...interface{}) {
|
||||||
|
l.logField(FATAL, message, keyValues...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) PanicField(message string, keyValues ...interface{}) {
|
||||||
|
l.logField(PANIC, message, keyValues...)
|
||||||
|
panic(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Close() error {
|
||||||
|
if f, ok := l.writer.(*os.File); ok {
|
||||||
|
return f.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCaller(skip int) string {
|
||||||
|
_, file, line, ok := runtime.Caller(skip)
|
||||||
|
if !ok {
|
||||||
|
return "?:?"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s:%d", file, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFields(keyValues []interface{}) map[string]interface{} {
|
||||||
|
fields := make(map[string]interface{})
|
||||||
|
for i := 0; i+1 < len(keyValues); i += 2 {
|
||||||
|
key, ok := keyValues[i].(string)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fields[key] = keyValues[i+1]
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
}
|
||||||
326
logger/logger_test.go
Normal file
326
logger/logger_test.go
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTextFormat(t *testing.T) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
l := New(
|
||||||
|
WithOutputFile(""),
|
||||||
|
WithFormat(TEXT),
|
||||||
|
WithTimeFormat("2006-01-02"),
|
||||||
|
)
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
l.writer = buf
|
||||||
|
l.mu.Unlock()
|
||||||
|
|
||||||
|
l.Info("hello %s", "world")
|
||||||
|
|
||||||
|
output := buf.String()
|
||||||
|
if !strings.Contains(output, "hello world") {
|
||||||
|
t.Errorf("expected 'hello world' in output, got: %s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONFormat(t *testing.T) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
l := New(
|
||||||
|
WithOutputFile(""),
|
||||||
|
WithFormat(JSON),
|
||||||
|
)
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
l.writer = buf
|
||||||
|
l.mu.Unlock()
|
||||||
|
|
||||||
|
l.Info("test message")
|
||||||
|
|
||||||
|
output := buf.String()
|
||||||
|
if !strings.Contains(output, `"message":"test message"`) {
|
||||||
|
t.Errorf("expected JSON message in output, got: %s", output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, `"level":"INFO"`) {
|
||||||
|
t.Errorf("expected level INFO in output, got: %s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithFields(t *testing.T) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
l := New(
|
||||||
|
WithOutputFile(""),
|
||||||
|
WithFormat(TEXT),
|
||||||
|
)
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
l.writer = buf
|
||||||
|
l.mu.Unlock()
|
||||||
|
|
||||||
|
l.InfoField("user logged in",
|
||||||
|
"user_id", 123,
|
||||||
|
"action", "login",
|
||||||
|
)
|
||||||
|
|
||||||
|
output := buf.String()
|
||||||
|
if !strings.Contains(output, "user_id=123") {
|
||||||
|
t.Errorf("expected user_id=123 in output, got: %s", output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "action=login") {
|
||||||
|
t.Errorf("expected action=login in output, got: %s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextTraceID(t *testing.T) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
l := New(
|
||||||
|
WithOutputFile(""),
|
||||||
|
WithFormat(JSON),
|
||||||
|
)
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
l.writer = buf
|
||||||
|
l.mu.Unlock()
|
||||||
|
|
||||||
|
ctx := context.WithValue(context.Background(), "trace_id", "req-123")
|
||||||
|
l.InfoCtx(ctx, "request processed")
|
||||||
|
|
||||||
|
output := buf.String()
|
||||||
|
if !strings.Contains(output, `"trace_id":"req-123"`) {
|
||||||
|
t.Errorf("expected trace_id in output, got: %s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCustomFieldKey(t *testing.T) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
l := New(
|
||||||
|
WithOutputFile(""),
|
||||||
|
WithFormat(JSON),
|
||||||
|
WithFieldKey("request_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
l.writer = buf
|
||||||
|
l.mu.Unlock()
|
||||||
|
|
||||||
|
ctx := context.WithValue(context.Background(), "request_id", "req-456")
|
||||||
|
l.InfoCtx(ctx, "request processed")
|
||||||
|
|
||||||
|
output := buf.String()
|
||||||
|
if !strings.Contains(output, `"request_id":"req-456"`) {
|
||||||
|
t.Errorf("expected request_id in output, got: %s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCaller(t *testing.T) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
l := New(
|
||||||
|
WithOutputFile(""),
|
||||||
|
WithFormat(TEXT),
|
||||||
|
WithCaller(true),
|
||||||
|
)
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
l.writer = buf
|
||||||
|
l.mu.Unlock()
|
||||||
|
|
||||||
|
l.Info("test caller")
|
||||||
|
|
||||||
|
output := buf.String()
|
||||||
|
if !strings.Contains(output, "logger_test.go:") {
|
||||||
|
t.Errorf("expected caller info in output, got: %s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLevelFiltering(t *testing.T) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
l := New(
|
||||||
|
WithOutputFile(""),
|
||||||
|
WithLevel(ERROR),
|
||||||
|
)
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
l.writer = buf
|
||||||
|
l.mu.Unlock()
|
||||||
|
|
||||||
|
l.Debug("debug message")
|
||||||
|
l.Info("info message")
|
||||||
|
l.Error("error message")
|
||||||
|
|
||||||
|
output := buf.String()
|
||||||
|
if strings.Contains(output, "debug message") {
|
||||||
|
t.Errorf("debug message should be filtered out, got: %s", output)
|
||||||
|
}
|
||||||
|
if strings.Contains(output, "info message") {
|
||||||
|
t.Errorf("info message should be filtered out, got: %s", output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "error message") {
|
||||||
|
t.Errorf("error message should be present, got: %s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGlobalLogger(t *testing.T) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
original := defaultLogger
|
||||||
|
defer func() { defaultLogger = original }()
|
||||||
|
|
||||||
|
defaultLogger = New(
|
||||||
|
WithOutputFile(""),
|
||||||
|
WithFormat(TEXT),
|
||||||
|
)
|
||||||
|
|
||||||
|
defaultLogger.mu.Lock()
|
||||||
|
defaultLogger.writer = buf
|
||||||
|
defaultLogger.mu.Unlock()
|
||||||
|
|
||||||
|
Info("global logger test")
|
||||||
|
|
||||||
|
output := buf.String()
|
||||||
|
if !strings.Contains(output, "global logger test") {
|
||||||
|
t.Errorf("expected global logger output, got: %s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmarks
|
||||||
|
|
||||||
|
func benchmarkLogger(format Format, caller bool) *Logger {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
l := New(
|
||||||
|
WithOutputFile(""),
|
||||||
|
WithFormat(format),
|
||||||
|
WithCaller(caller),
|
||||||
|
)
|
||||||
|
l.mu.Lock()
|
||||||
|
l.writer = buf
|
||||||
|
l.mu.Unlock()
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkInfoText(b *testing.B) {
|
||||||
|
l := benchmarkLogger(TEXT, false)
|
||||||
|
msg := "hello world"
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
l.Info("hello world")
|
||||||
|
_ = msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkInfoTextWithArgs(b *testing.B) {
|
||||||
|
l := benchmarkLogger(TEXT, false)
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
l.Info("user %s logged in from %s", "john", "192.168.1.1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkInfoJSON(b *testing.B) {
|
||||||
|
l := benchmarkLogger(JSON, false)
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
l.Info("hello world")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkInfoField(b *testing.B) {
|
||||||
|
l := benchmarkLogger(TEXT, false)
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
l.InfoField("user logged in",
|
||||||
|
"user_id", 123,
|
||||||
|
"action", "login",
|
||||||
|
"ip", "192.168.1.1",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkInfoFieldJSON(b *testing.B) {
|
||||||
|
l := benchmarkLogger(JSON, false)
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
l.InfoField("user logged in",
|
||||||
|
"user_id", 123,
|
||||||
|
"action", "login",
|
||||||
|
"ip", "192.168.1.1",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkInfoWithCaller(b *testing.B) {
|
||||||
|
l := benchmarkLogger(TEXT, true)
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
l.Info("hello world")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkInfoCtx(b *testing.B) {
|
||||||
|
l := benchmarkLogger(JSON, false)
|
||||||
|
ctx := context.WithValue(context.Background(), "trace_id", "req-123456789")
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
l.InfoCtx(ctx, "request processed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkInfoFiltered(b *testing.B) {
|
||||||
|
l := New(
|
||||||
|
WithOutputFile(""),
|
||||||
|
WithLevel(ERROR),
|
||||||
|
)
|
||||||
|
l.mu.Lock()
|
||||||
|
l.writer = &bytes.Buffer{}
|
||||||
|
l.mu.Unlock()
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
l.Info("this should be filtered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkManyFields(b *testing.B) {
|
||||||
|
l := benchmarkLogger(JSON, false)
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
l.InfoField("event",
|
||||||
|
"field1", "value1",
|
||||||
|
"field2", "value2",
|
||||||
|
"field3", "value3",
|
||||||
|
"field4", "value4",
|
||||||
|
"field5", "value5",
|
||||||
|
"field6", "value6",
|
||||||
|
"field7", "value7",
|
||||||
|
"field8", "value8",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkGlobalInfo(b *testing.B) {
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
Info("hello world")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkGlobalInfoField(b *testing.B) {
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
InfoField("event",
|
||||||
|
"key1", "value1",
|
||||||
|
"key2", "value2",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
75
logger/options.go
Normal file
75
logger/options.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
type options struct {
|
||||||
|
level Level
|
||||||
|
format Format
|
||||||
|
output Output
|
||||||
|
outputFile string
|
||||||
|
caller bool
|
||||||
|
fieldKey string
|
||||||
|
prefix string
|
||||||
|
timeFormat string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option func(*options)
|
||||||
|
|
||||||
|
func WithLevel(level Level) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.level = level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithFormat(format Format) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.format = format
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithOutput(output Output) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.output = output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithOutputFile(file string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.outputFile = file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCaller(caller bool) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.caller = caller
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithFieldKey(key string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.fieldKey = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPrefix(prefix string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.prefix = prefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTimeFormat(format string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.timeFormat = format
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultOptions() options {
|
||||||
|
return options{
|
||||||
|
level: INFO,
|
||||||
|
format: TEXT,
|
||||||
|
output: Stdout,
|
||||||
|
outputFile: "",
|
||||||
|
caller: false,
|
||||||
|
fieldKey: "trace_id",
|
||||||
|
prefix: "",
|
||||||
|
timeFormat: "2006-01-02 15:04:05",
|
||||||
|
}
|
||||||
|
}
|
||||||
129
logger/readme.md
Normal file
129
logger/readme.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# Logger
|
||||||
|
|
||||||
|
A lightweight, structured logging package for Go.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Simple API**: Format strings and structured fields
|
||||||
|
- **Global & Instance**: Use default global logger or create custom instances
|
||||||
|
- **Context Support**: Extract trace_id from context automatically
|
||||||
|
- **Multiple Formats**: Text (default) and JSON
|
||||||
|
- **Level Filtering**: DEBUG, INFO, WARN, ERROR, FATAL, PANIC
|
||||||
|
- **No Dependencies**: Standard library only
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "gitea.loveuer.com/loveuer/upkg/logger"
|
||||||
|
|
||||||
|
// Global logger (default)
|
||||||
|
logger.Info("hello %s", "world")
|
||||||
|
logger.Error("failed: %v", err)
|
||||||
|
|
||||||
|
// With fields
|
||||||
|
logger.InfoField("user logged in",
|
||||||
|
"user_id", 123,
|
||||||
|
"action", "login",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Context with trace_id
|
||||||
|
ctx := context.WithValue(ctx, "trace_id", "req-123")
|
||||||
|
logger.InfoCtx(ctx, "request processed")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Logger
|
||||||
|
|
||||||
|
```go
|
||||||
|
log := logger.New(
|
||||||
|
logger.WithLevel(logger.DEBUG),
|
||||||
|
logger.WithFormat(logger.JSON),
|
||||||
|
logger.WithOutput(logger.Stdout),
|
||||||
|
logger.WithCaller(true),
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Info("custom logger")
|
||||||
|
log.InfoField("event", "key", "value")
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### Level Methods
|
||||||
|
|
||||||
|
```go
|
||||||
|
log.Debug(format string, args ...)
|
||||||
|
log.Info(format string, args ...)
|
||||||
|
log.Warn(format string, args ...)
|
||||||
|
log.Error(format string, args ...)
|
||||||
|
log.Fatal(format string, args ...)
|
||||||
|
log.Panic(format string, args ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Context Methods
|
||||||
|
|
||||||
|
```go
|
||||||
|
log.InfoCtx(ctx, format string, args ...)
|
||||||
|
// Automatically adds trace_id from ctx as a field
|
||||||
|
```
|
||||||
|
|
||||||
|
### Field Methods
|
||||||
|
|
||||||
|
```go
|
||||||
|
log.InfoField(message string, keyValues ...interface{})
|
||||||
|
// Example: log.InfoField("user created", "id", 123, "name", "john")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
```go
|
||||||
|
logger.New(
|
||||||
|
WithLevel(INFO), // Minimum log level
|
||||||
|
WithFormat(TEXT), // TEXT or JSON
|
||||||
|
WithOutput(Stdout), // Stdout, Stderr, or use WithOutputFile
|
||||||
|
WithOutputFile("/path/to.log"), // Write to file
|
||||||
|
WithCaller(true), // Include file:line in output
|
||||||
|
WithFieldKey("trace_id"), // Context field key name
|
||||||
|
WithPrefix("myapp"), // Text format prefix
|
||||||
|
WithTimeFormat("2006-01-02"), // Custom time format
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Text Format Output
|
||||||
|
|
||||||
|
```
|
||||||
|
2026-01-17 15:30:45 INFO hello world
|
||||||
|
2026-01-17 15:30:45 INFO user logged in user_id=123 action=login
|
||||||
|
```
|
||||||
|
|
||||||
|
## JSON Format Output
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"time":"2026-01-17T15:30:45Z","level":"INFO","message":"hello world"}
|
||||||
|
{"time":"2026-01-17T15:30:45Z","level":"INFO","message":"user logged in","user_id":123,"action":"login"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Global Functions
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "gitea.loveuer.com/loveuer/upkg/logger"
|
||||||
|
|
||||||
|
func Debug(format string, args ...)
|
||||||
|
func Info(format string, args ...)
|
||||||
|
func Warn(format string, args ...)
|
||||||
|
func Error(format string, args ...)
|
||||||
|
func Fatal(format string, args ...)
|
||||||
|
func Panic(format string, args ...)
|
||||||
|
|
||||||
|
func DebugCtx(ctx context.Context, format string, args ...)
|
||||||
|
func InfoCtx(ctx context.Context, format string, args ...)
|
||||||
|
func WarnCtx(ctx context.Context, format string, args ...)
|
||||||
|
func ErrorCtx(ctx context.Context, format string, args ...)
|
||||||
|
func FatalCtx(ctx context.Context, format string, args ...)
|
||||||
|
func PanicCtx(ctx context.Context, format string, args ...)
|
||||||
|
|
||||||
|
func DebugField(message string, keyValues ...interface{})
|
||||||
|
func InfoField(message string, keyValues ...interface{})
|
||||||
|
func WarnField(message string, keyValues ...interface{})
|
||||||
|
func ErrorField(message string, keyValues ...interface{})
|
||||||
|
func FatalField(message string, keyValues ...interface{})
|
||||||
|
func PanicField(message string, keyValues ...interface{})
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user