Files
dygou_go/trace.go
T
2026-06-19 13:02:28 +08:00

276 lines
7.1 KiB
Go

package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"sync/atomic"
"time"
)
func initTrace(logDir string, cfg TraceConfig) (*traceRecorder, func()) {
if !cfg.Enabled {
return &traceRecorder{enabled: false}, func() {}
}
if logDir == "" {
logDir = defaultLogDir
}
if err := os.MkdirAll(logDir, 0755); err != nil {
logEvent("Trace", "error", "trace 目录创建失败,已关闭 trace: %v", err)
return &traceRecorder{enabled: false}, func() {}
}
path := filepath.Join(logDir, "douyu_trace_"+time.Now().In(cst).Format("20060102_150405")+".log")
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
if err != nil {
logEvent("Trace", "error", "trace 文件创建失败,已关闭 trace: %v", err)
return &traceRecorder{enabled: false}, func() {}
}
recorder := &traceRecorder{
enabled: true,
maxBodyBytes: cfg.MaxBodyBytes,
writer: newAsyncLogWriter(file, 2048),
filePath: path,
}
if cfg.MaxBodyBytes > 0 {
logEvent("Trace", "warn", "trace 已启用: %s,单段正文最多保留 %d 字节,不做脱敏", path, cfg.MaxBodyBytes)
} else {
logEvent("Trace", "warn", "trace 已启用: %s,全量保留请求和响应,不做脱敏", path)
}
return recorder, func() {
_ = recorder.Close()
_ = file.Close()
}
}
func (t *traceRecorder) Close() error {
if t == nil || t.writer == nil {
return nil
}
return t.writer.Close()
}
func (t *traceRecorder) record(label string, req *http.Request, status int, cost time.Duration, reqBody, respBody []byte, errText string, apiError int, apiMsg string) {
if t == nil || !t.enabled || req == nil || req.URL == nil {
return
}
if label == "服务时间" && status == http.StatusOK && errText == "" && apiError == 0 {
return
}
entry := traceEntry{
Seq: atomic.AddUint64(&t.seq, 1),
Time: time.Now().In(cst).Format("2006-01-02 15:04:05.000"),
Label: label,
Method: req.Method,
URL: req.URL.Redacted(),
Path: req.URL.Path,
Status: status,
Cost: cost,
ReqHeader: sanitizeHeader(req.Header),
ReqBody: sanitizeBodyForTrace(reqBody, req.Header.Get("Content-Type"), t.maxBodyBytes),
RespBody: sanitizeResponseBody(respBody, t.maxBodyBytes),
APIError: apiError,
APIMessage: apiMsg,
ErrorText: errText,
MaxBodyBytes: t.maxBodyBytes,
}
_, _ = t.writer.Write([]byte(formatTraceEntry(entry)))
}
func traceRequestBody(t *traceRecorder, req *http.Request) []byte {
if t == nil || !t.enabled || req == nil || req.GetBody == nil {
return nil
}
body, err := req.GetBody()
if err != nil {
return nil
}
defer body.Close()
if t.maxBodyBytes > 0 {
data, _ := io.ReadAll(io.LimitReader(body, int64(t.maxBodyBytes)+1))
return data
}
data, _ := io.ReadAll(body)
return data
}
func sanitizeHeader(header http.Header) map[string][]string {
out := make(map[string][]string, len(header))
for key, values := range header {
canonical := http.CanonicalHeaderKey(key)
copied := make([]string, len(values))
copy(copied, values)
out[canonical] = copied
}
return out
}
func sanitizeBodyForTrace(body []byte, contentType string, max int) string {
if len(body) == 0 {
return ""
}
mediaType, params, _ := mime.ParseMediaType(contentType)
if strings.HasPrefix(mediaType, "multipart/") {
boundary := params["boundary"]
if boundary != "" {
return sanitizeMultipartBody(body, boundary, max)
}
}
return sanitizeTextBody(body, max)
}
func sanitizeResponseBody(body []byte, max int) string {
return sanitizeTextBody(body, max)
}
func sanitizeTextBody(body []byte, max int) string {
raw := string(body)
if pretty, ok := prettyJSON(raw); ok {
return truncateTraceText(pretty, max)
}
return truncateTraceText(raw, max)
}
func sanitizeMultipartBody(body []byte, boundary string, max int) string {
reader := multipartReader(bytes.NewReader(body), boundary)
fields := make(map[string][]string)
for {
part, err := reader.NextPart()
if err == io.EOF {
break
}
if err != nil {
return truncateTraceText(string(body), max)
}
name := part.FormName()
if name == "" {
continue
}
var valueBytes []byte
if max > 0 {
valueBytes, _ = io.ReadAll(io.LimitReader(part, int64(max)+1))
} else {
valueBytes, _ = io.ReadAll(part)
}
value := truncateTraceText(string(valueBytes), max)
fields[name] = append(fields[name], value)
}
encoded, err := json.Marshal(fields)
if err != nil {
return ""
}
return string(encoded)
}
func multipartReader(r io.Reader, boundary string) *multipart.Reader {
return multipart.NewReader(r, boundary)
}
func truncateTraceText(s string, max int) string {
if max <= 0 || len(s) <= max {
return s
}
return s[:max] + fmt.Sprintf("...<truncated %d bytes>", len(s)-max)
}
type traceEntry struct {
Seq uint64
Time string
Label string
Method string
URL string
Path string
Status int
Cost time.Duration
ReqHeader map[string][]string
ReqBody string
RespBody string
APIError int
APIMessage string
ErrorText string
MaxBodyBytes int
}
func formatTraceEntry(e traceEntry) string {
var b strings.Builder
statusText := "成功"
if e.Status < 200 || e.Status >= 300 || e.APIError != 0 || e.ErrorText != "" {
statusText = "异常"
}
b.WriteString(strings.Repeat("=", 96))
b.WriteByte('\n')
fmt.Fprintf(&b, "#%06d %s [%s] %s %s\n", e.Seq, e.Time, statusText, e.Method, e.URL)
fmt.Fprintf(&b, "接口: %s\n", emptyDefault(e.Label, "-"))
fmt.Fprintf(&b, "路径: %s\n", emptyDefault(e.Path, "-"))
fmt.Fprintf(&b, "结果: HTTP %d | 耗时 %s | apiError=%d", e.Status, roundDuration(e.Cost), e.APIError)
if e.APIMessage != "" {
fmt.Fprintf(&b, " | msg=%s", e.APIMessage)
}
b.WriteByte('\n')
if e.ErrorText != "" {
fmt.Fprintf(&b, "错误: %s\n", e.ErrorText)
}
b.WriteString("\n[请求头]\n")
b.WriteString(formatTraceHeaders(e.ReqHeader))
b.WriteString("\n[请求体]\n")
b.WriteString(formatTraceBody(e.ReqBody))
b.WriteString("\n[响应体]\n")
b.WriteString(formatTraceBody(e.RespBody))
if e.MaxBodyBytes > 0 {
fmt.Fprintf(&b, "\n提示: 单段正文最多保留 %d 字节;trace 不做脱敏。\n", e.MaxBodyBytes)
} else {
b.WriteString("\n提示: trace 全量保留请求和响应,不做脱敏。\n")
}
b.WriteByte('\n')
return b.String()
}
func formatTraceHeaders(headers map[string][]string) string {
if len(headers) == 0 {
return "<empty>\n"
}
keys := make([]string, 0, len(headers))
for key := range headers {
keys = append(keys, key)
}
sort.Strings(keys)
var b strings.Builder
for _, key := range keys {
fmt.Fprintf(&b, "%s: %s\n", key, strings.Join(headers[key], ", "))
}
return b.String()
}
func formatTraceBody(body string) string {
body = strings.TrimSpace(body)
if body == "" {
return "<empty>\n"
}
if pretty, ok := prettyJSON(body); ok {
return pretty + "\n"
}
return body + "\n"
}
func prettyJSON(s string) (string, bool) {
var value any
if err := json.Unmarshal([]byte(s), &value); err != nil {
return "", false
}
out, err := json.MarshalIndent(value, "", " ")
if err != nil {
return "", false
}
return string(out), true
}