增加日志系统
This commit is contained in:
@@ -42,6 +42,7 @@ npm run dev
|
||||
|
||||
## 开发态短信与实名
|
||||
|
||||
- 后端日志默认同时输出到控制台和 `backend/logs/app-YYYY-MM-DD.log`,按北京时间自动按天切分;可通过 `LOG_LEVEL=debug|info|warn|error` 调整级别。
|
||||
- 短信验证码默认使用 mock 适配器,验证码会打印在后端日志中;生产环境可通过 `SMS_PROVIDER=aliyun` 接入阿里云短信。
|
||||
- 阿里云短信需要配置 `ALIYUN_ACCESS_KEY_ID`、`ALIYUN_ACCESS_KEY_SECRET`、`ALIYUN_SMS_SIGN_NAME` 和 `ALIYUN_SMS_LOGIN_TEMPLATE_CODE`,模板变量名默认为 `code`。
|
||||
- 实名认证使用 mock 适配器,登录后请求 `POST /api/realname/start`,提交合法姓名和 18 位身份证号会直接通过。
|
||||
|
||||
@@ -7,6 +7,12 @@ REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
|
||||
JWT_SECRET=change-me
|
||||
|
||||
LOG_LEVEL=info
|
||||
LOG_DIR=logs
|
||||
LOG_ENABLE_CONSOLE=true
|
||||
LOG_ENABLE_FILE=true
|
||||
|
||||
STORAGE_ENDPOINT=http://localhost:9000
|
||||
STORAGE_BUCKET=hfb-sys
|
||||
STORAGE_ACCESS_KEY_ID=minioadmin
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"hfb_sys/backend/internal/config"
|
||||
"hfb_sys/backend/internal/database"
|
||||
"hfb_sys/backend/internal/jobs/ordertimeout"
|
||||
"hfb_sys/backend/internal/logging"
|
||||
"hfb_sys/backend/internal/router"
|
||||
|
||||
"go.uber.org/zap"
|
||||
@@ -18,10 +19,7 @@ import (
|
||||
|
||||
func main() {
|
||||
cfg := config.Load()
|
||||
logger, err := zap.NewProduction()
|
||||
if cfg.AppEnv == "development" {
|
||||
logger, err = zap.NewDevelopment()
|
||||
}
|
||||
logger, err := logging.New(cfg.Log)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ type Config struct {
|
||||
JWTSecret string
|
||||
Storage StorageConfig
|
||||
SMS SMSConfig
|
||||
Log LogConfig
|
||||
}
|
||||
|
||||
type StorageConfig struct {
|
||||
@@ -33,6 +34,13 @@ type SMSConfig struct {
|
||||
AliyunLoginTemplateCode string
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
Level string
|
||||
Dir string
|
||||
EnableConsole bool
|
||||
EnableFile bool
|
||||
}
|
||||
|
||||
func Load() Config {
|
||||
return Config{
|
||||
AppEnv: getEnv("APP_ENV", "development"),
|
||||
@@ -56,6 +64,12 @@ func Load() Config {
|
||||
AliyunSignName: getEnv("ALIYUN_SMS_SIGN_NAME", ""),
|
||||
AliyunLoginTemplateCode: getEnv("ALIYUN_SMS_LOGIN_TEMPLATE_CODE", ""),
|
||||
},
|
||||
Log: LogConfig{
|
||||
Level: getEnv("LOG_LEVEL", "info"),
|
||||
Dir: getEnv("LOG_DIR", "logs"),
|
||||
EnableConsole: getEnvBool("LOG_ENABLE_CONSOLE", true),
|
||||
EnableFile: getEnvBool("LOG_ENABLE_FILE", true),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,3 +92,15 @@ func getEnvInt(key string, fallback int) int {
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func getEnvBool(key string, fallback bool) bool {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
parsed, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type dailyWriter struct {
|
||||
mu sync.Mutex
|
||||
dir string
|
||||
prefix string
|
||||
location *time.Location
|
||||
day string
|
||||
file *os.File
|
||||
}
|
||||
|
||||
func newDailyWriter(dir string, prefix string, location *time.Location) *dailyWriter {
|
||||
return &dailyWriter{
|
||||
dir: dir,
|
||||
prefix: prefix,
|
||||
location: location,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *dailyWriter) Write(p []byte) (int, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
if err := w.rotateIfNeeded(time.Now().In(w.location)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return w.file.Write(p)
|
||||
}
|
||||
|
||||
func (w *dailyWriter) Sync() error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
if w.file == nil {
|
||||
return nil
|
||||
}
|
||||
return w.file.Sync()
|
||||
}
|
||||
|
||||
func (w *dailyWriter) rotateIfNeeded(now time.Time) error {
|
||||
day := now.Format("2006-01-02")
|
||||
if w.file != nil && w.day == day {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(w.dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if w.file != nil {
|
||||
_ = w.file.Close()
|
||||
w.file = nil
|
||||
}
|
||||
|
||||
path := filepath.Join(w.dir, fmt.Sprintf("%s-%s.log", w.prefix, day))
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.file = file
|
||||
w.day = day
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/config"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
func New(cfg config.LogConfig) (*zap.Logger, error) {
|
||||
location, err := time.LoadLocation("Asia/Shanghai")
|
||||
if err != nil {
|
||||
location = time.FixedZone("Asia/Shanghai", 8*60*60)
|
||||
}
|
||||
|
||||
level := zap.NewAtomicLevelAt(parseLevel(cfg.Level))
|
||||
encoderConfig := zap.NewProductionEncoderConfig()
|
||||
encoderConfig.TimeKey = "time"
|
||||
encoderConfig.LevelKey = "level"
|
||||
encoderConfig.MessageKey = "message"
|
||||
encoderConfig.CallerKey = "caller"
|
||||
encoderConfig.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
|
||||
enc.AppendString(t.In(location).Format("2006-01-02 15:04:05.000 -0700"))
|
||||
}
|
||||
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
|
||||
encoderConfig.EncodeDuration = zapcore.StringDurationEncoder
|
||||
encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
|
||||
|
||||
cores := make([]zapcore.Core, 0, 2)
|
||||
if cfg.EnableConsole {
|
||||
cores = append(cores, zapcore.NewCore(
|
||||
zapcore.NewConsoleEncoder(encoderConfig),
|
||||
zapcore.Lock(os.Stdout),
|
||||
level,
|
||||
))
|
||||
}
|
||||
if cfg.EnableFile {
|
||||
writer := newDailyWriter(cfg.Dir, "app", location)
|
||||
cores = append(cores, zapcore.NewCore(
|
||||
zapcore.NewConsoleEncoder(encoderConfig),
|
||||
writer,
|
||||
level,
|
||||
))
|
||||
}
|
||||
if len(cores) == 0 {
|
||||
cores = append(cores, zapcore.NewCore(
|
||||
zapcore.NewConsoleEncoder(encoderConfig),
|
||||
zapcore.Lock(os.Stdout),
|
||||
level,
|
||||
))
|
||||
}
|
||||
|
||||
return zap.New(
|
||||
zapcore.NewTee(cores...),
|
||||
zap.AddCaller(),
|
||||
zap.AddStacktrace(zapcore.ErrorLevel),
|
||||
), nil
|
||||
}
|
||||
|
||||
func parseLevel(value string) zapcore.Level {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "debug":
|
||||
return zapcore.DebugLevel
|
||||
case "warn", "warning":
|
||||
return zapcore.WarnLevel
|
||||
case "error":
|
||||
return zapcore.ErrorLevel
|
||||
default:
|
||||
return zapcore.InfoLevel
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user