增加日志系统
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` 接入阿里云短信。
|
- 短信验证码默认使用 mock 适配器,验证码会打印在后端日志中;生产环境可通过 `SMS_PROVIDER=aliyun` 接入阿里云短信。
|
||||||
- 阿里云短信需要配置 `ALIYUN_ACCESS_KEY_ID`、`ALIYUN_ACCESS_KEY_SECRET`、`ALIYUN_SMS_SIGN_NAME` 和 `ALIYUN_SMS_LOGIN_TEMPLATE_CODE`,模板变量名默认为 `code`。
|
- 阿里云短信需要配置 `ALIYUN_ACCESS_KEY_ID`、`ALIYUN_ACCESS_KEY_SECRET`、`ALIYUN_SMS_SIGN_NAME` 和 `ALIYUN_SMS_LOGIN_TEMPLATE_CODE`,模板变量名默认为 `code`。
|
||||||
- 实名认证使用 mock 适配器,登录后请求 `POST /api/realname/start`,提交合法姓名和 18 位身份证号会直接通过。
|
- 实名认证使用 mock 适配器,登录后请求 `POST /api/realname/start`,提交合法姓名和 18 位身份证号会直接通过。
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ REDIS_PASSWORD=
|
|||||||
REDIS_DB=0
|
REDIS_DB=0
|
||||||
|
|
||||||
JWT_SECRET=change-me
|
JWT_SECRET=change-me
|
||||||
|
|
||||||
|
LOG_LEVEL=info
|
||||||
|
LOG_DIR=logs
|
||||||
|
LOG_ENABLE_CONSOLE=true
|
||||||
|
LOG_ENABLE_FILE=true
|
||||||
|
|
||||||
STORAGE_ENDPOINT=http://localhost:9000
|
STORAGE_ENDPOINT=http://localhost:9000
|
||||||
STORAGE_BUCKET=hfb-sys
|
STORAGE_BUCKET=hfb-sys
|
||||||
STORAGE_ACCESS_KEY_ID=minioadmin
|
STORAGE_ACCESS_KEY_ID=minioadmin
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"hfb_sys/backend/internal/config"
|
"hfb_sys/backend/internal/config"
|
||||||
"hfb_sys/backend/internal/database"
|
"hfb_sys/backend/internal/database"
|
||||||
"hfb_sys/backend/internal/jobs/ordertimeout"
|
"hfb_sys/backend/internal/jobs/ordertimeout"
|
||||||
|
"hfb_sys/backend/internal/logging"
|
||||||
"hfb_sys/backend/internal/router"
|
"hfb_sys/backend/internal/router"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -18,10 +19,7 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cfg := config.Load()
|
cfg := config.Load()
|
||||||
logger, err := zap.NewProduction()
|
logger, err := logging.New(cfg.Log)
|
||||||
if cfg.AppEnv == "development" {
|
|
||||||
logger, err = zap.NewDevelopment()
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type Config struct {
|
|||||||
JWTSecret string
|
JWTSecret string
|
||||||
Storage StorageConfig
|
Storage StorageConfig
|
||||||
SMS SMSConfig
|
SMS SMSConfig
|
||||||
|
Log LogConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type StorageConfig struct {
|
type StorageConfig struct {
|
||||||
@@ -33,6 +34,13 @@ type SMSConfig struct {
|
|||||||
AliyunLoginTemplateCode string
|
AliyunLoginTemplateCode string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LogConfig struct {
|
||||||
|
Level string
|
||||||
|
Dir string
|
||||||
|
EnableConsole bool
|
||||||
|
EnableFile bool
|
||||||
|
}
|
||||||
|
|
||||||
func Load() Config {
|
func Load() Config {
|
||||||
return Config{
|
return Config{
|
||||||
AppEnv: getEnv("APP_ENV", "development"),
|
AppEnv: getEnv("APP_ENV", "development"),
|
||||||
@@ -56,6 +64,12 @@ func Load() Config {
|
|||||||
AliyunSignName: getEnv("ALIYUN_SMS_SIGN_NAME", ""),
|
AliyunSignName: getEnv("ALIYUN_SMS_SIGN_NAME", ""),
|
||||||
AliyunLoginTemplateCode: getEnv("ALIYUN_SMS_LOGIN_TEMPLATE_CODE", ""),
|
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
|
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