From 5b4dbca9da0770d7591a186c6634c8d78edd939f Mon Sep 17 00:00:00 2001 From: "haibo.chen" <495810242@qq.com> Date: Fri, 9 May 2025 17:00:32 +0800 Subject: [PATCH] update log --- .vscode/launch.json | 4 +- conf/config.yaml | 1 + main/main.go | 31 +++++-- pkg/api/api.go | 12 ++- pkg/media/media.go | 8 +- pkg/service/device.go | 8 +- pkg/service/inbound.go | 38 ++++---- pkg/service/outbound.go | 12 ++- pkg/service/uac.go | 12 +-- pkg/service/uas.go | 6 +- pkg/utils/logger.go | 201 ++++++++++++++++++++++++++++++++++++++++ 11 files changed, 278 insertions(+), 55 deletions(-) create mode 100644 pkg/utils/logger.go diff --git a/.vscode/launch.json b/.vscode/launch.json index a1f953c..3e5bc7c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,7 +12,7 @@ "program": "${workspaceFolder}/objs/srs-sip.exe", "cwd": "${workspaceFolder}/objs", "env": {}, - "args": [], + "args": ["-c", "${workspaceFolder}/conf/config.yaml"], "preLaunchTask": "build-windows", "windows": {} }, @@ -24,7 +24,7 @@ "program": "${workspaceFolder}/objs/srs-sip", "cwd": "${workspaceFolder}/objs", "env": {}, - "args": [], + "args": ["-c", "${workspaceFolder}/conf/config.yaml"], "preLaunchTask": "build-linux", "linux": {} } diff --git a/conf/config.yaml b/conf/config.yaml index ca75497..9fa0ac8 100644 --- a/conf/config.yaml +++ b/conf/config.yaml @@ -1,6 +1,7 @@ # 通用配置 common: log-level: "info" + log-file: "logs/srs-sip.log" # GB28181配置 gb28181: diff --git a/main/main.go b/main/main.go index 2648524..d193512 100644 --- a/main/main.go +++ b/main/main.go @@ -3,6 +3,7 @@ package main import ( "context" "flag" + "log/slog" "net/http" "os" "os/signal" @@ -14,10 +15,10 @@ import ( "github.com/gorilla/handlers" "github.com/gorilla/mux" - "github.com/ossrs/go-oryx-lib/logger" "github.com/ossrs/srs-sip/pkg/api" "github.com/ossrs/srs-sip/pkg/config" "github.com/ossrs/srs-sip/pkg/service" + "github.com/ossrs/srs-sip/pkg/utils" ) func WaitTerminationSignal(cancel context.CancelFunc) { @@ -29,12 +30,11 @@ func WaitTerminationSignal(cancel context.CancelFunc) { } func main() { - // 定义配置文件路径参数 configPath := flag.String("c", "", "配置文件路径") flag.Parse() if *configPath == "" { - logger.E(nil, "错误: 通过 -c 参数指定配置文件路径,比如:./srs-sip -c conf/config.yaml") + slog.Error("error: specify the config file path, like: ./srs-sip -c conf/config.yaml") return } @@ -42,18 +42,28 @@ func main() { conf, err := config.LoadConfig(*configPath) if err != nil { - logger.E(nil, "load config failed: %v", err) + slog.Error("load config failed", "error", err) return } + if err := utils.SetupLogger(conf.Common.LogLevel, conf.Common.LogFile); err != nil { + slog.Error("setup logger failed", "error", err) + return + } + + slog.Info("*****************************************************") + slog.Info(" ☆☆☆ 欢迎使用 SRS-SIP 服务 ☆☆☆") + slog.Info("*****************************************************") + slog.Info("srs-sip service starting", "config", *configPath, "log_file", conf.Common.LogFile) + sipSvr, err := service.NewService(ctx, conf) if err != nil { - logger.Ef("create service failed. err is %v", err.Error()) + slog.Error("create service failed", "error", err.Error()) return } if err := sipSvr.Start(); err != nil { - logger.Ef("start sip service failed. err is %v", err.Error()) + slog.Error("start sip service failed", "error", err.Error()) return } @@ -71,7 +81,7 @@ func main() { // 先注册API路由 apiSvr, err := api.NewHttpApiServer(conf, sipSvr) if err != nil { - logger.Ef("create http service failed. err is %v", err.Error()) + slog.Error("create http service failed", "error", err.Error()) return } apiSvr.Start(router) @@ -80,6 +90,7 @@ func main() { router.PathPrefix("/").Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // 如果是API路径,直接返回404 if strings.HasPrefix(r.URL.Path, "/srs-sip/v1/") { + slog.Info("api path not found", "path", r.URL.Path) http.NotFound(w, r) return } @@ -89,6 +100,7 @@ func main() { _, err := os.Stat(filePath) if os.IsNotExist(err) { // 如果文件不存在,返回 index.html + slog.Info("file not found, redirect to index", "path", r.URL.Path) r.URL.Path = "/" } fs.ServeHTTP(w, r) @@ -106,13 +118,14 @@ func main() { IdleTimeout: 30 * time.Second, ReadHeaderTimeout: 5 * time.Second, } - logger.Tf(ctx, "http server listen on %s, home is %v", httpPort, conf.Http.Dir) + slog.Info("http server listen", "port", httpPort, "home", conf.Http.Dir) if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - logger.Ef(ctx, "listen on %s failed", httpPort) + slog.Error("listen failed", "port", httpPort, "error", err) } }() WaitTerminationSignal(cancel) sipSvr.Stop() + slog.Info("srs-sip service stopped") } diff --git a/pkg/api/api.go b/pkg/api/api.go index d819510..2782ce1 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -1,11 +1,10 @@ package api import ( - "context" + "log/slog" "net/http" "github.com/gorilla/mux" - "github.com/ossrs/go-oryx-lib/logger" "github.com/ossrs/srs-sip/pkg/config" "github.com/ossrs/srs-sip/pkg/service" ) @@ -29,7 +28,7 @@ func (h *HttpApiServer) Start(router *mux.Router) { // 创建一个子路由,所有API都以/srs-sip/v1为前缀 apiRouter := router.PathPrefix("/srs-sip/v1").Subrouter() - logger.Tf(context.Background(), "Registering API routes under /srs-sip/v1") + slog.Info("Registering API routes under /srs-sip/v1") h.RegisterRoutes(apiRouter) // 打印所有注册的路由,包含更详细的信息 @@ -38,8 +37,11 @@ func (h *HttpApiServer) Start(router *mux.Router) { pathRegexp, _ := route.GetPathRegexp() methods, _ := route.GetMethods() queries, _ := route.GetQueriesTemplates() - logger.Tf(context.Background(), "Route Details: Path=%v, Regexp=%v, Methods=%v, Queries=%v", - pathTemplate, pathRegexp, methods, queries) + slog.Debug("Route Details", + "path", pathTemplate, + "regexp", pathRegexp, + "methods", methods, + "queries", queries) return nil }) } diff --git a/pkg/media/media.go b/pkg/media/media.go index bdfab49..1d0f2e5 100644 --- a/pkg/media/media.go +++ b/pkg/media/media.go @@ -5,11 +5,11 @@ import ( "context" "encoding/json" "io" + "log/slog" "net/http" "time" "github.com/ossrs/go-oryx-lib/errors" - "github.com/ossrs/go-oryx-lib/logger" ) type IMedia interface { @@ -30,7 +30,7 @@ func apiRequest(ctx context.Context, r string, req interface{}, res interface{}) return errors.Wrapf(err, "Marshal body %v", req) } } - logger.Tf(ctx, "Request url api=%v with %v bytes", r, buf.Len()) + slog.Debug("API request", "url", r, "size", buf.Len()) method := "POST" if req == nil { @@ -56,7 +56,7 @@ func apiRequest(ctx context.Context, r string, req interface{}, res interface{}) if err != nil { return errors.Wrapf(err, "Read response for %v", buf.String()) } - logger.Tf(ctx, "Response from %v is %v bytes", r, len(b2)) + slog.Debug("API response", "url", r, "size", len(b2)) errorCode := struct { Code int `json:"code"` @@ -71,7 +71,7 @@ func apiRequest(ctx context.Context, r string, req interface{}, res interface{}) if err := json.Unmarshal(b2, res); err != nil { return errors.Wrapf(err, "Unmarshal %v", string(b2)) } - logger.Tf(ctx, "Parse response to code=%v ok, %v", errorCode.Code, res) + slog.Debug("Parse API response", "code", errorCode.Code, "response", res) return nil } diff --git a/pkg/service/device.go b/pkg/service/device.go index feae61d..d29f210 100644 --- a/pkg/service/device.go +++ b/pkg/service/device.go @@ -1,12 +1,11 @@ package service import ( - "context" "fmt" + "log/slog" "sync" "time" - "github.com/ossrs/go-oryx-lib/logger" "github.com/ossrs/srs-sip/pkg/models" ) @@ -98,7 +97,10 @@ func (dm *deviceManager) checkHeartbeats() { device.SourceAddr = "" device.Online = false dm.devices.Store(key, device) - logger.Wf(context.Background(), "Device %s is offline due to heartbeat timeout, HeartBeatInterval: %v", device.DeviceID, device.HeartBeatInterval) + slog.Warn("Device is offline due to heartbeat timeout", + "device_id", device.DeviceID, + "heartbeat_interval", device.HeartBeatInterval, + "heartbeat_count", device.HeartBeatCount) } } return true diff --git a/pkg/service/inbound.go b/pkg/service/inbound.go index 06c6935..b50418c 100644 --- a/pkg/service/inbound.go +++ b/pkg/service/inbound.go @@ -3,12 +3,12 @@ package service import ( "bytes" "encoding/xml" + "log/slog" "net" "net/http" "strconv" "github.com/emiago/sipgo/sip" - "github.com/ossrs/go-oryx-lib/logger" "github.com/ossrs/srs-sip/pkg/models" "github.com/ossrs/srs-sip/pkg/service/stack" "golang.org/x/net/html/charset" @@ -31,7 +31,7 @@ func (s *UAS) isSameIP(addr1, addr2 string) bool { func (s *UAS) onRegister(req *sip.Request, tx sip.ServerTransaction) { id := req.From().Address.User if len(id) != GB28181_ID_LENGTH { - logger.E(s.ctx, "invalid device ID") + slog.Error("invalid device ID") return } @@ -50,7 +50,7 @@ func (s *UAS) onRegister(req *sip.Request, tx sip.ServerTransaction) { // Validate Authorization authInfo := ParseAuthorization(authHeader[0].Value()) if !ValidateAuth(authInfo, s.conf.GB28181.Auth.Password) { - logger.Ef(s.ctx, "%s auth failed, source: %s", id, req.Source()) + slog.Error("auth failed", "device_id", id, "source", req.Source()) s.respondRegister(req, http.StatusForbidden, "Auth Failed", tx) return } @@ -61,20 +61,20 @@ func (s *UAS) onRegister(req *sip.Request, tx sip.ServerTransaction) { exp := exps[0] expSec, err := strconv.ParseInt(exp.Value(), 10, 32) if err != nil { - logger.Ef(s.ctx, "parse expires header error: %s", err.Error()) + slog.Error("parse expires header error", "error", err.Error()) return } if expSec == 0 { isUnregister = true } } else { - logger.E(s.ctx, "empty expires header") + slog.Error("empty expires header") return } if isUnregister { DM.RemoveDevice(id) - logger.Wf(s.ctx, "Device %s unregistered", id) + slog.Warn("Device unregistered", "device_id", id) return } else { if d, ok := DM.GetDevice(id); !ok { @@ -84,13 +84,13 @@ func (s *UAS) onRegister(req *sip.Request, tx sip.ServerTransaction) { NetworkType: req.Transport(), }) s.respondRegister(req, http.StatusOK, "OK", tx) - logger.Tf(s.ctx, "%s Register success, source:%s, req: %s", id, req.Source(), req.String()) + slog.Info("Register success", "device_id", id, "source", req.Source(), "request", req.String()) go s.ConfigDownload(id) go s.Catalog(id) } else { if d.SourceAddr != "" && !s.isSameIP(d.SourceAddr, req.Source()) { - logger.Ef(s.ctx, "Device %s[%s] already registered, please change another ID.", id, d.SourceAddr, req.Source()) + slog.Error("Device already registered", "device_id", id, "old_source", d.SourceAddr, "new_source", req.Source()) // TODO: 如果ID重复,应采用虚拟ID s.respondRegister(req, http.StatusBadRequest, "Conflict Device ID", tx) } else { @@ -99,7 +99,7 @@ func (s *UAS) onRegister(req *sip.Request, tx sip.ServerTransaction) { DM.UpdateDevice(id, d) s.respondRegister(req, http.StatusOK, "OK", tx) - logger.Tf(s.ctx, "%s Re-register success, source:%s, req: %s", id, req.Source(), req.String()) + slog.Info("Re-register success", "device_id", id, "source", req.Source(), "request", req.String()) } } } @@ -114,21 +114,21 @@ func (s *UAS) respondRegister(req *sip.Request, code sip.StatusCode, reason stri func (s *UAS) onMessage(req *sip.Request, tx sip.ServerTransaction) { id := req.From().Address.User if len(id) != 20 { - logger.Ef(s.ctx, "invalid device ID %s", req.String()) + slog.Error("invalid device ID", "request", req.String()) } - //logger.Tf(s.ctx, "Received MESSAGE: %s", req.String()) + slog.Debug("Received MESSAGE", "request", req.String()) temp := &models.XmlMessageInfo{} decoder := xml.NewDecoder(bytes.NewReader([]byte(req.Body()))) decoder.CharsetReader = charset.NewReaderLabel if err := decoder.Decode(temp); err != nil { - logger.Ef(s.ctx, "decode message error: %s\n message:%s", err.Error(), req.Body()) + slog.Error("decode message error", "error", err.Error(), "message", req.Body()) } var body string switch temp.CmdType { case "Keepalive": - logger.T(s.ctx, "Keepalive") + slog.Debug("Keepalive") if d, ok := DM.GetDevice(temp.DeviceID); ok && d.Online { // 更新设备心跳时间 DM.UpdateDeviceHeartbeat(temp.DeviceID) @@ -138,16 +138,16 @@ func (s *UAS) onMessage(req *sip.Request, tx sip.ServerTransaction) { } case "SensorCatalog": // 兼容宇视,非国标 case "Catalog": - logger.T(s.ctx, "Catalog") + slog.Debug("Catalog") DM.UpdateChannels(temp.DeviceID, temp.DeviceList...) //go s.AutoInvite(temp.DeviceID, temp.DeviceList...) case "ConfigDownload": - logger.T(s.ctx, "ConfigDownload") + slog.Debug("ConfigDownload") DM.UpdateDeviceConfig(temp.DeviceID, &temp.BasicParam) case "Alarm": - logger.T(s.ctx, "Alarm") + slog.Debug("Alarm") case "RecordInfo": - logger.T(s.ctx, "RecordInfo") + slog.Debug("RecordInfo") // 从 recordQueryResults 中获取对应通道的结果通道 if ch, ok := s.recordQueryResults.Load(temp.DeviceID); ok { // 发送查询结果 @@ -155,7 +155,7 @@ func (s *UAS) onMessage(req *sip.Request, tx sip.ServerTransaction) { resultChan <- temp } default: - logger.Wf(s.ctx, "Not supported CmdType: %s", temp.CmdType) + slog.Warn("Not supported CmdType", "cmd_type", temp.CmdType) response := sip.NewResponseFromRequest(req, http.StatusBadRequest, "", nil) tx.Respond(response) return @@ -164,6 +164,6 @@ func (s *UAS) onMessage(req *sip.Request, tx sip.ServerTransaction) { } func (s *UAS) onNotify(req *sip.Request, tx sip.ServerTransaction) { - logger.T(s.ctx, "Received NOTIFY request") + slog.Debug("Received NOTIFY request") tx.Respond(sip.NewResponseFromRequest(req, http.StatusOK, "OK", nil)) } diff --git a/pkg/service/outbound.go b/pkg/service/outbound.go index 4e8ff8c..1333341 100644 --- a/pkg/service/outbound.go +++ b/pkg/service/outbound.go @@ -2,12 +2,12 @@ package service import ( "fmt" + "log/slog" "strings" "time" "github.com/emiago/sipgo/sip" "github.com/ossrs/go-oryx-lib/errors" - "github.com/ossrs/go-oryx-lib/logger" "github.com/ossrs/srs-sip/pkg/media" "github.com/ossrs/srs-sip/pkg/models" "github.com/ossrs/srs-sip/pkg/service/stack" @@ -110,7 +110,7 @@ Scale: %.1f } func (s *UAS) AddSession(key string, status Session) { - logger.Tf(s.ctx, "AddSession: %s, %+v", key, status) + slog.Info("AddSession", "key", key, "status", status) s.Streams.Store(key, status) } @@ -314,7 +314,7 @@ func (s *UAS) Bye(req models.ByeRequest) error { defer func() { if err := s.media.Unpublish(session.Ssrc); err != nil { - logger.Ef(s.ctx, "unpublish stream error: %s", err) + slog.Error("unpublish stream error", "error", err, "stream_id", session.Ssrc) } s.RemoveSession(key) }() @@ -523,7 +523,11 @@ func (s *UAS) QueryRecord(deviceID, channelID string, startTime, endTime int64) return nil, errors.Errorf("context done") case records := <-resultChan: allRecords = append(allRecords, records.RecordList...) - logger.Tf(s.ctx, "[channel %s] 应收总数 %d, 实收总数 %d, 本次收到 %d", channelID, records.SumNum, len(allRecords), len(records.RecordList)) + slog.Info("Record query result", + "channel", channelID, + "expected_count", records.SumNum, + "actual_count", len(allRecords), + "batch_count", len(records.RecordList)) if len(allRecords) == records.SumNum { return allRecords, nil diff --git a/pkg/service/uac.go b/pkg/service/uac.go index 5ca0d7a..066f835 100644 --- a/pkg/service/uac.go +++ b/pkg/service/uac.go @@ -3,11 +3,11 @@ package service import ( "context" "fmt" + "log/slog" "github.com/emiago/sipgo" "github.com/emiago/sipgo/sip" "github.com/ossrs/go-oryx-lib/errors" - "github.com/ossrs/go-oryx-lib/logger" "github.com/ossrs/srs-sip/pkg/config" "github.com/ossrs/srs-sip/pkg/service/stack" ) @@ -26,7 +26,7 @@ type UAC struct { func NewUac() *UAC { ip, err := config.GetLocalIP() if err != nil { - logger.E("get local ip failed") + slog.Error("get local ip failed", "error", err) return nil } @@ -89,7 +89,7 @@ func (c *UAC) doRegister() error { } rs, _ := c.getResponse(tx) - logger.Tf(c.ctx, "register response: %s", rs.String()) + slog.Info("register response", "response", rs.String()) return nil } @@ -101,15 +101,15 @@ func (c *UAC) OnRequest(req *sip.Request, tx sip.ServerTransaction) { } func (c *UAC) onInvite(req *sip.Request, tx sip.ServerTransaction) { - logger.T(c.ctx, "onInvite") + slog.Debug("onInvite") } func (c *UAC) onBye(req *sip.Request, tx sip.ServerTransaction) { - logger.T(c.ctx, "onBye") + slog.Debug("onBye") } func (c *UAC) onMessage(req *sip.Request, tx sip.ServerTransaction) { - logger.Tf(c.ctx, "onMessage %s", req.String()) + slog.Debug("onMessage", "request", req.String()) } func (c *UAC) getResponse(tx sip.ClientTransaction) (*sip.Response, error) { diff --git a/pkg/service/uas.go b/pkg/service/uas.go index 8ba241c..9c79485 100644 --- a/pkg/service/uas.go +++ b/pkg/service/uas.go @@ -4,12 +4,12 @@ import ( "context" "errors" "fmt" + "log/slog" "net" "os" "sync" "github.com/emiago/sipgo" - "github.com/ossrs/go-oryx-lib/logger" "github.com/ossrs/srs-sip/pkg/config" "github.com/ossrs/srs-sip/pkg/db" "github.com/ossrs/srs-sip/pkg/media" @@ -100,7 +100,7 @@ func (s *UAS) startUDP() error { return fmt.Errorf("cannot listen on the UDP signaling port %d: %w", s.conf.GB28181.Port, err) } s.sipConnUDP = lis - logger.Tf(s.ctx, "sip signaling listening on UDP %s:%d", lis.LocalAddr().String(), s.conf.GB28181.Port) + slog.Info("sip signaling listening on UDP", "address", lis.LocalAddr().String(), "port", s.conf.GB28181.Port) go func() { if err := s.sipSvr.ServeUDP(lis); err != nil { @@ -119,7 +119,7 @@ func (s *UAS) startTCP() error { return fmt.Errorf("cannot listen on the TCP signaling port %d: %w", s.conf.GB28181.Port, err) } s.sipConnTCP = lis - logger.Tf(s.ctx, "sip signaling listening on TCP %s:%d", lis.Addr().String(), s.conf.GB28181.Port) + slog.Info("sip signaling listening on TCP", "address", lis.Addr().String(), "port", s.conf.GB28181.Port) go func() { if err := s.sipSvr.ServeTCP(lis); err != nil && !errors.Is(err, net.ErrClosed) { diff --git a/pkg/utils/logger.go b/pkg/utils/logger.go new file mode 100644 index 0000000..3f9e5b1 --- /dev/null +++ b/pkg/utils/logger.go @@ -0,0 +1,201 @@ +package utils + +import ( + "context" + "fmt" + "io" + "log/slog" + "os" + "path/filepath" + "strings" + "sync" +) + +var logLevelMap = map[string]slog.Level{ + "debug": slog.LevelDebug, + "info": slog.LevelInfo, + "warn": slog.LevelWarn, + "error": slog.LevelError, +} + +// 自定义格式处理器,以 [时间] [级别] [消息] 格式输出日志 +type CustomFormatHandler struct { + mu sync.Mutex + w io.Writer + level slog.Level + attrs []slog.Attr + groups []string +} + +// NewCustomFormatHandler 创建一个新的自定义格式处理器 +func NewCustomFormatHandler(w io.Writer, opts *slog.HandlerOptions) *CustomFormatHandler { + if opts == nil { + opts = &slog.HandlerOptions{} + } + + // 获取日志级别,如果opts.Level是nil则默认为Info + var level slog.Level + if opts.Level != nil { + level = opts.Level.Level() + } else { + level = slog.LevelInfo + } + + return &CustomFormatHandler{ + w: w, + level: level, + } +} + +// Enabled 实现 slog.Handler 接口 +func (h *CustomFormatHandler) Enabled(ctx context.Context, level slog.Level) bool { + return level >= h.level +} + +// Handle 实现 slog.Handler 接口,以自定义格式输出日志 +func (h *CustomFormatHandler) Handle(ctx context.Context, record slog.Record) error { + h.mu.Lock() + defer h.mu.Unlock() + + // 时间格式 + timeStr := record.Time.Format("2006-01-02 15:04:05.000") + + // 日志级别 + var levelStr string + switch { + case record.Level >= slog.LevelError: + levelStr = "ERROR" + case record.Level >= slog.LevelWarn: + levelStr = "WARN " + case record.Level >= slog.LevelInfo: + levelStr = "INFO " + default: + levelStr = "DEBUG" + } + + // 构建日志行 + logLine := fmt.Sprintf("[%s] [%s] %s", timeStr, levelStr, record.Message) + + // 处理其他属性 + var attrs []string + record.Attrs(func(attr slog.Attr) bool { + attrs = append(attrs, fmt.Sprintf("%s=%v", attr.Key, attr.Value)) + return true + }) + + if len(attrs) > 0 { + logLine += " " + strings.Join(attrs, " ") + } + + // 写入日志 + _, err := fmt.Fprintln(h.w, logLine) + return err +} + +// WithAttrs 实现 slog.Handler 接口 +func (h *CustomFormatHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + h2 := *h + h2.attrs = append(h.attrs[:], attrs...) + return &h2 +} + +// WithGroup 实现 slog.Handler 接口 +func (h *CustomFormatHandler) WithGroup(name string) slog.Handler { + h2 := *h + h2.groups = append(h.groups[:], name) + return &h2 +} + +// MultiHandler 实现了 slog.Handler 接口,将日志同时发送到多个处理器 +type MultiHandler struct { + handlers []slog.Handler +} + +// Enabled 实现 slog.Handler 接口 +func (h *MultiHandler) Enabled(ctx context.Context, level slog.Level) bool { + // 如果任何一个处理器启用了该级别,则返回 true + for _, handler := range h.handlers { + if handler.Enabled(ctx, level) { + return true + } + } + return false +} + +// Handle 实现 slog.Handler 接口 +func (h *MultiHandler) Handle(ctx context.Context, record slog.Record) error { + // 将记录发送到所有处理器 + for _, handler := range h.handlers { + if handler.Enabled(ctx, record.Level) { + if err := handler.Handle(ctx, record); err != nil { + return err + } + } + } + return nil +} + +// WithAttrs 实现 slog.Handler 接口 +func (h *MultiHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + newHandlers := make([]slog.Handler, len(h.handlers)) + for i, handler := range h.handlers { + newHandlers[i] = handler.WithAttrs(attrs) + } + return &MultiHandler{handlers: newHandlers} +} + +// WithGroup 实现 slog.Handler 接口 +func (h *MultiHandler) WithGroup(name string) slog.Handler { + newHandlers := make([]slog.Handler, len(h.handlers)) + for i, handler := range h.handlers { + newHandlers[i] = handler.WithGroup(name) + } + return &MultiHandler{handlers: newHandlers} +} + +// SetupLogger 设置日志输出 +func SetupLogger(logLevel string, logFile string) error { + // 创建标准错误输出的处理器,使用自定义格式 + stdHandler := NewCustomFormatHandler(os.Stderr, &slog.HandlerOptions{ + Level: logLevelMap[logLevel], + }) + + // 如果没有指定日志文件,则仅使用标准错误处理器 + if logFile == "" { + slog.SetDefault(slog.New(stdHandler)) + return nil + } + + // 确保日志文件所在目录存在 + logDir := filepath.Dir(logFile) + if err := os.MkdirAll(logDir, 0755); err != nil { + return err + } + + // 打开日志文件,如果不存在则创建,追加写入模式 + file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return err + } + + // 创建文件输出的处理器,使用自定义格式 + fileHandler := NewCustomFormatHandler(file, &slog.HandlerOptions{ + Level: logLevelMap[logLevel], + }) + + // 创建多输出处理器 + multiHandler := &MultiHandler{ + handlers: []slog.Handler{stdHandler, fileHandler}, + } + + // 设置全局日志处理器 + slog.SetDefault(slog.New(multiHandler)) + return nil +} + +// InitDefaultLogger 初始化默认日志处理器 +func InitDefaultLogger(level slog.Level) { + slog.SetDefault(slog.New(NewCustomFormatHandler(os.Stderr, &slog.HandlerOptions{ + Level: level, + }))) +}