chore(logging): 更新日志重定向与捕获功能
- 在 .gitignore 中添加调试日志文件的忽略规则,避免不必要的调试信息被提交 - 修改 application.go 中的 stdout 重定向逻辑,使用 log.CaptureStream 以支持更灵活的日志捕获 - 更新 README 文档,增加对日志重定向功能的说明
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
logrus "github.com/sirupsen/logrus"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// captureBuf 挂到 multiWriter,收集 EmitCaptured 写入的 JSON 行。
|
||||
type captureBuf struct {
|
||||
mu sync.Mutex
|
||||
lines []string
|
||||
}
|
||||
|
||||
func (c *captureBuf) Write(p []byte) (int, error) {
|
||||
c.mu.Lock()
|
||||
c.lines = append(c.lines, string(bytes.TrimSpace(p)))
|
||||
c.mu.Unlock()
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (c *captureBuf) lastJSON() map[string]interface{} {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if len(c.lines) == 0 {
|
||||
return nil
|
||||
}
|
||||
var m map[string]interface{}
|
||||
_ = json.Unmarshal([]byte(c.lines[len(c.lines)-1]), &m)
|
||||
return m
|
||||
}
|
||||
|
||||
func newLoggerWithCapture(logLevel int) (*Logger, *captureBuf) {
|
||||
buf := &captureBuf{}
|
||||
l := NewLogger(logLevel, "", 0)
|
||||
l.mw.add(buf)
|
||||
return l, buf
|
||||
}
|
||||
|
||||
func startFakeSeqServer(t *testing.T, onBody func(string)) *httptest.Server {
|
||||
t.Helper()
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/events/raw" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
onBody(string(body))
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}))
|
||||
}
|
||||
|
||||
func TestEmitCaptured_LogLevelZero(t *testing.T) {
|
||||
l, buf := newLoggerWithCapture(0)
|
||||
l.EmitCaptured(zerolog.InfoLevel, "stdout", "请求参数GetReqMap /api/test")
|
||||
m := buf.lastJSON()
|
||||
if m == nil {
|
||||
t.Fatal("expected captured line")
|
||||
}
|
||||
if m["source"] != "stdout" {
|
||||
t.Fatalf("source=%v want stdout", m["source"])
|
||||
}
|
||||
if m["level"] != "info" {
|
||||
t.Fatalf("level=%v want info", m["level"])
|
||||
}
|
||||
if !strings.Contains(m["message"].(string), "GetReqMap") {
|
||||
t.Fatalf("message=%v", m["message"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmitCaptured_StderrSource(t *testing.T) {
|
||||
l, buf := newLoggerWithCapture(0)
|
||||
l.EmitCaptured(zerolog.ErrorLevel, "stderr", "driver error")
|
||||
m := buf.lastJSON()
|
||||
if m["source"] != "stderr" {
|
||||
t.Fatalf("source=%v want stderr", m["source"])
|
||||
}
|
||||
if m["level"] != "error" {
|
||||
t.Fatalf("level=%v want error", m["level"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestToClef_PlainText(t *testing.T) {
|
||||
sw := &SeqWriter{instance: "127.0.0.1:8081"}
|
||||
clef := sw.toClef([]byte("plain log line\n"))
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(clef, &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if m["@mt"] != "plain log line" {
|
||||
t.Fatalf("@mt=%v", m["@mt"])
|
||||
}
|
||||
if m["source"] != "stdout" {
|
||||
t.Fatalf("source=%v", m["source"])
|
||||
}
|
||||
if m["@l"] != "Information" {
|
||||
t.Fatalf("@l=%v", m["@l"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestStdoutCapture_LongLine(t *testing.T) {
|
||||
l, buf := newLoggerWithCapture(1)
|
||||
pr, pw := io.Pipe()
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
CaptureStream(l, zerolog.InfoLevel, "stdout", pr)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
longLine := strings.Repeat("x", 70*1024) + "\n"
|
||||
shortLine := "after-long-line\n"
|
||||
if _, err := pw.Write([]byte(longLine)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := pw.Write([]byte(shortLine)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
buf.mu.Lock()
|
||||
n := len(buf.lines)
|
||||
buf.mu.Unlock()
|
||||
if n < 2 {
|
||||
t.Fatalf("expected >=2 captured lines, got %d", n)
|
||||
}
|
||||
last := buf.lastJSON()
|
||||
if last == nil || !strings.Contains(last["message"].(string), "after-long-line") {
|
||||
t.Fatalf("short line after long line not captured: %v", last)
|
||||
}
|
||||
_ = pw.Close()
|
||||
<-done
|
||||
}
|
||||
|
||||
func TestLogrusBridge_AfterRedirect(t *testing.T) {
|
||||
l, buf := newLoggerWithCapture(1)
|
||||
pr, pw := io.Pipe()
|
||||
go CaptureStream(l, zerolog.InfoLevel, "stdout", pr)
|
||||
|
||||
logrus.SetOutput(pw)
|
||||
logrus.SetLevel(logrus.InfoLevel)
|
||||
logrus.Info("wechat-sdk-log")
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
m := buf.lastJSON()
|
||||
if m == nil || !strings.Contains(m["message"].(string), "wechat-sdk-log") {
|
||||
t.Fatalf("logrus not captured: %v", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeqWriter_FakeServer(t *testing.T) {
|
||||
var gotBody string
|
||||
var mu sync.Mutex
|
||||
srv := startFakeSeqServer(t, func(body string) {
|
||||
mu.Lock()
|
||||
gotBody = body
|
||||
mu.Unlock()
|
||||
})
|
||||
defer srv.Close()
|
||||
|
||||
l := NewLogger(1, "", 0)
|
||||
l.SetSeqWriter(srv.URL, "", "test:8081")
|
||||
l.Info().Msg("seq-test-message")
|
||||
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
mu.Lock()
|
||||
b := gotBody
|
||||
mu.Unlock()
|
||||
if strings.Contains(b, "seq-test-message") {
|
||||
return
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("Seq did not receive message, body=%q", gotBody)
|
||||
}
|
||||
|
||||
func TestEmitRecoveredPanic_LogFields(t *testing.T) {
|
||||
l, buf := newLoggerWithCapture(0)
|
||||
l.EmitRecoveredPanic("test panic reason")
|
||||
m := buf.lastJSON()
|
||||
if m == nil {
|
||||
t.Fatal("no output")
|
||||
}
|
||||
if m["source"] != "panic" {
|
||||
t.Fatalf("source=%v", m["source"])
|
||||
}
|
||||
if m["level"] != "error" {
|
||||
t.Fatalf("level=%v", m["level"])
|
||||
}
|
||||
if !strings.Contains(m["message"].(string), "test panic reason") {
|
||||
t.Fatalf("message=%v", m["message"])
|
||||
}
|
||||
if stack, ok := m["stack"].(string); !ok || stack == "" {
|
||||
t.Fatalf("stack missing: %v", m["stack"])
|
||||
}
|
||||
}
|
||||
+86
-4
@@ -18,6 +18,27 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const maxCaptureLine = 10 << 20 // 10MB,支持 GetReqMap 等大 body 单行日志
|
||||
|
||||
var (
|
||||
origStderr io.Writer
|
||||
origStderrOnce sync.Once
|
||||
)
|
||||
|
||||
func saveOrigStderr() {
|
||||
origStderrOnce.Do(func() {
|
||||
origStderr = os.Stderr
|
||||
})
|
||||
}
|
||||
|
||||
func seqWriteStderr(format string, args ...interface{}) {
|
||||
if origStderr != nil {
|
||||
fmt.Fprintf(origStderr, format, args...)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorRecord 错误历史记录条目
|
||||
type ErrorRecord struct {
|
||||
Err error
|
||||
@@ -43,6 +64,7 @@ type Logger struct {
|
||||
// logFile: 文件路径模板(如 "logs/20060102.txt"),空则不写文件
|
||||
// maxErrors: 错误历史最大条数,0 则不记录
|
||||
func NewLogger(logLevel int, logFile string, maxErrors int) *Logger {
|
||||
saveOrigStderr()
|
||||
zerolog.CallerMarshalFunc = callerMarshalFunc
|
||||
zerolog.TimeFieldFormat = "2006-01-02 15:04:05"
|
||||
|
||||
@@ -97,6 +119,7 @@ func NewLogger(logLevel int, logFile string, maxErrors int) *Logger {
|
||||
|
||||
// NewLoggerNoCaller 创建不带自动 Caller 字段的日志实例(用于访问日志等 caller 无意义的场景)
|
||||
func NewLoggerNoCaller(logLevel int, logFile string, maxErrors int) *Logger {
|
||||
saveOrigStderr()
|
||||
zerolog.CallerMarshalFunc = callerMarshalFunc
|
||||
zerolog.TimeFieldFormat = "2006-01-02 15:04:05"
|
||||
|
||||
@@ -181,6 +204,62 @@ func (l *Logger) GetLevel() int {
|
||||
return l.logLevel
|
||||
}
|
||||
|
||||
// EmitCaptured 将捕获的 stdout/stderr 等输出直写 multiWriter,绕过主 Logger 的 level 过滤。
|
||||
// 保证 logLevel=0 时 fmt.Println / log.Println 仍能进 Seq。
|
||||
func (l *Logger) EmitCaptured(level zerolog.Level, source, msg string) {
|
||||
if l == nil || l.mw == nil || msg == "" {
|
||||
return
|
||||
}
|
||||
zl := zerolog.New(l.mw)
|
||||
zl.WithLevel(level).Timestamp().Str("source", source).Msg(msg)
|
||||
}
|
||||
|
||||
// EmitRecoveredPanic 记录 recover 到的 panic:错误原因 + 调用栈,source=panic。
|
||||
func (l *Logger) EmitRecoveredPanic(err interface{}) {
|
||||
if l == nil || l.mw == nil || err == nil {
|
||||
return
|
||||
}
|
||||
zl := zerolog.New(l.mw)
|
||||
zl.WithLevel(zerolog.ErrorLevel).Timestamp().
|
||||
Str("source", "panic").
|
||||
Str("stack", string(debugStack())).
|
||||
Msg(fmt.Sprint(err))
|
||||
}
|
||||
|
||||
func debugStack() []byte {
|
||||
buf := make([]byte, 64*1024)
|
||||
n := runtime.Stack(buf, false)
|
||||
return buf[:n]
|
||||
}
|
||||
|
||||
// CaptureStream 从 reader 持续读取行并写入 Logger(用于 stdout/stderr 管道)。
|
||||
// 单行超过 maxCaptureLine 时截断并标注,读取出错后重启读取,避免永久失效。
|
||||
func CaptureStream(l *Logger, level zerolog.Level, source string, r io.Reader) {
|
||||
rd := bufio.NewReader(r)
|
||||
for {
|
||||
line, err := rd.ReadString('\n')
|
||||
if len(line) > 0 {
|
||||
msg := strings.TrimRight(line, "\r\n")
|
||||
if msg != "" {
|
||||
if len(msg) > maxCaptureLine {
|
||||
msg = msg[:maxCaptureLine] + "...(truncated)"
|
||||
}
|
||||
l.EmitCaptured(level, source, msg)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
if len(line) == 0 {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
l.EmitCaptured(zerolog.ErrorLevel, source+"-capture", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 链式调用 API ---
|
||||
// 以下方法兼容两种调用风格:
|
||||
// 无参数:返回 *zerolog.Event 用于链式调用,如 l.Error().Str("k","v").Msg("...")
|
||||
@@ -501,7 +580,10 @@ func (w *SeqWriter) Write(p []byte) (int, error) {
|
||||
select {
|
||||
case w.queue <- clef:
|
||||
default:
|
||||
atomic.AddInt64(&w.dropped, 1)
|
||||
n := atomic.AddInt64(&w.dropped, 1)
|
||||
if n == 1 || n%100 == 0 {
|
||||
seqWriteStderr("[seq] queue full, dropped=%d\n", n)
|
||||
}
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
@@ -600,7 +682,7 @@ func (w *SeqWriter) flush(batch [][]byte) {
|
||||
}
|
||||
req, err := http.NewRequest("POST", w.seqUrl+"/api/events/raw?clef", &buf)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[seq] build request error: %v\n", err)
|
||||
seqWriteStderr("[seq] build request error: %v\n", err)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/vnd.serilog.clef")
|
||||
@@ -609,13 +691,13 @@ func (w *SeqWriter) flush(batch [][]byte) {
|
||||
}
|
||||
resp, err := w.client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[seq] send error: %v (dropped=%d)\n", err, atomic.LoadInt64(&w.dropped))
|
||||
seqWriteStderr("[seq] send error: %v (dropped=%d)\n", err, atomic.LoadInt64(&w.dropped))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 400 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
fmt.Fprintf(os.Stderr, "[seq] server returned %d: %s\n", resp.StatusCode, string(body))
|
||||
seqWriteStderr("[seq] server returned %d: %s\n", resp.StatusCode, string(body))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user