feat: complete API-driven projects with infrastructure and business acceptance

This commit is contained in:
2026-09-23 17:33:50 +08:00
parent 04a1f7999d
commit 2c74dc0859
137 changed files with 11926 additions and 105 deletions

View File

@@ -10,6 +10,7 @@ import (
"io"
"log/slog"
"os"
"runtime/debug"
"strings"
"sync"
"sync/atomic"
@@ -21,14 +22,16 @@ import (
// standard stream. File takes precedence over Output and appends to the named
// file.
type Config struct {
Level string `json:"level"`
Format string `json:"format"`
Output string `json:"output"`
File string `json:"file"`
Service string `json:"service"`
Environment string `json:"environment"`
AddSource bool `json:"addSource"`
Writer io.Writer `json:"-"`
Level string `json:"level"`
Format string `json:"format"`
Output string `json:"output"`
File string `json:"file"`
Service string `json:"service"`
Environment string `json:"environment"`
AddSource bool `json:"addSource"`
Writer io.Writer `json:"-"`
Rotation RotationConfig `json:"rotation"`
RedactKeys []string `json:"redactKeys"`
}
// Default returns the default production-friendly logger configuration.
@@ -62,13 +65,17 @@ var (
func init() {
// Keep package-level logging useful without requiring every service to
// repeat setup. Applications can replace this logger with Setup.
defaultLogger.Store(&Logger{logger: slog.New(slog.NewJSONHandler(os.Stderr, nil))})
logger, _ := New(Default())
defaultLogger.Store(logger)
}
// New creates an independent logger. The caller owns its lifecycle and should
// call Close when the process is shutting down.
func New(cfg Config) (*Logger, error) {
cfg = normalizeConfig(cfg)
if err := cfg.Rotation.validate(cfg); err != nil {
return nil, err
}
level, err := parseLevel(cfg.Level)
if err != nil {
return nil, err
@@ -81,7 +88,7 @@ func New(cfg Config) (*Logger, error) {
dynamicLevel := new(slog.LevelVar)
dynamicLevel.Set(level)
opts := &slog.HandlerOptions{Level: dynamicLevel, AddSource: cfg.AddSource}
opts := &slog.HandlerOptions{Level: dynamicLevel, AddSource: cfg.AddSource, ReplaceAttr: redactor(cfg.RedactKeys)}
var handler slog.Handler
if cfg.Format == "text" {
handler = slog.NewTextHandler(writer, opts)
@@ -208,6 +215,19 @@ func (l *Logger) ErrorContext(ctx context.Context, msg string, args ...any) {
l.logger.Log(ctx, slog.LevelError, msg, contextAttrs(ctx, args)...)
}
// ErrorStackContext records the current goroutine's stack at the logging call,
// not the historical location where an error was originally constructed.
func (l *Logger) ErrorStackContext(ctx context.Context, msg string, args ...any) {
attrs := make([]any, 0, len(args)+2)
attrs = append(attrs, args...)
attrs = append(attrs, "stack", string(debug.Stack()))
l.ErrorContext(ctx, msg, attrs...)
}
func ErrorStackContext(ctx context.Context, msg string, args ...any) {
defaultLogger.Load().ErrorStackContext(ctx, msg, args...)
}
func (l *Logger) Debug(msg string, args ...any) { l.DebugContext(context.Background(), msg, args...) }
func (l *Logger) Info(msg string, args ...any) { l.InfoContext(context.Background(), msg, args...) }
func (l *Logger) Warn(msg string, args ...any) { l.WarnContext(context.Background(), msg, args...) }
@@ -324,6 +344,9 @@ func openWriter(cfg Config) (io.Writer, func() error, error) {
return cfg.Writer, nil, nil
}
if cfg.File != "" {
if cfg.Rotation.Enabled {
return rotatingWriter(cfg)
}
file, err := os.OpenFile(cfg.File, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return nil, nil, err