logging_test.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. package middleware
  2. import (
  3. "context"
  4. "strings"
  5. "testing"
  6. "github.com/mark3labs/mcp-go/mcp"
  7. "github.com/mark3labs/mcp-go/server"
  8. "go.uber.org/zap"
  9. "go.uber.org/zap/zapcore"
  10. "go.uber.org/zap/zaptest/observer"
  11. )
  12. // setupObservedLogger 替换全局 logger 为可观测 logger,返回观测器与还原函数。
  13. func setupObservedLogger(t *testing.T) (*observer.ObservedLogs, func()) {
  14. t.Helper()
  15. core, obs := observer.New(zapcore.InfoLevel)
  16. orig := zap.L()
  17. zap.ReplaceGlobals(zap.New(core))
  18. return obs, func() { zap.ReplaceGlobals(orig) }
  19. }
  20. func newReq(name string, args map[string]any) mcp.CallToolRequest {
  21. var req mcp.CallToolRequest
  22. req.Params.Name = name
  23. req.Params.Arguments = args
  24. return req
  25. }
  26. func wrap(next server.ToolHandlerFunc) server.ToolHandlerFunc {
  27. return LoggingMiddleware()(next)
  28. }
  29. func TestLogging_Success(t *testing.T) {
  30. obs, restore := setupObservedLogger(t)
  31. defer restore()
  32. h := wrap(func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  33. return mcp.NewToolResultText(`{"ok":true}`), nil
  34. })
  35. _, err := h(context.Background(), newReq("service_site_query_info", map[string]any{"sn": "SN123"}))
  36. if err != nil {
  37. t.Fatalf("unexpected err: %v", err)
  38. }
  39. logs := obs.All()
  40. if len(logs) != 2 {
  41. t.Fatalf("want 2 log entries (start+success), got %d: %+v", len(logs), logs)
  42. }
  43. if logs[0].Message != "[tool] call start" {
  44. t.Errorf("first log = %q, want start", logs[0].Message)
  45. }
  46. if logs[1].Message != "[tool] call success" || logs[1].Level != zapcore.InfoLevel {
  47. t.Errorf("second log = %q level=%v, want success/info", logs[1].Message, logs[1].Level)
  48. }
  49. // status 字段
  50. if got := logs[1].ContextMap()["status"]; got != "success" {
  51. t.Errorf("status = %v, want success", got)
  52. }
  53. }
  54. func TestLogging_BusinessError(t *testing.T) {
  55. obs, restore := setupObservedLogger(t)
  56. defer restore()
  57. h := wrap(func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  58. return mcp.NewToolResultError("查询站点信息失败: not found"), nil
  59. })
  60. if _, err := h(context.Background(), newReq("service_site_query_info", nil)); err != nil {
  61. t.Fatalf("unexpected err: %v", err)
  62. }
  63. last := obs.All()[len(obs.All())-1]
  64. if last.Message != "[tool] call business_error" || last.Level != zapcore.WarnLevel {
  65. t.Fatalf("last log = %q level=%v, want business_error/warn", last.Message, last.Level)
  66. }
  67. if got := last.ContextMap()["status"]; got != "business_error" {
  68. t.Errorf("status = %v, want business_error", got)
  69. }
  70. }
  71. func TestLogging_GoError(t *testing.T) {
  72. obs, restore := setupObservedLogger(t)
  73. defer restore()
  74. h := wrap(func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  75. return nil, context.DeadlineExceeded
  76. })
  77. if _, err := h(context.Background(), newReq("data_mongo_query", nil)); err == nil {
  78. t.Fatal("expected error to propagate")
  79. }
  80. last := obs.All()[len(obs.All())-1]
  81. if last.Message != "[tool] call error" || last.Level != zapcore.ErrorLevel {
  82. t.Fatalf("last log = %q level=%v, want error/error", last.Message, last.Level)
  83. }
  84. }
  85. func TestLogging_Panic(t *testing.T) {
  86. obs, restore := setupObservedLogger(t)
  87. defer restore()
  88. h := wrap(func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  89. panic("boom")
  90. })
  91. defer func() {
  92. if r := recover(); r == nil {
  93. t.Fatal("expected panic to be re-thrown")
  94. }
  95. last := obs.All()[len(obs.All())-1]
  96. if last.Message != "[tool] call panic" || last.Level != zapcore.ErrorLevel {
  97. t.Fatalf("last log = %q level=%v, want panic/error", last.Message, last.Level)
  98. }
  99. if _, ok := last.ContextMap()["stack"]; !ok {
  100. t.Error("panic log missing stack field")
  101. }
  102. }()
  103. _, _ = h(context.Background(), newReq("service_site_query_info", nil))
  104. }
  105. // TestLogging_ParamMasking 验证参数中的敏感字段被脱敏后再写日志。
  106. func TestLogging_ParamMasking(t *testing.T) {
  107. obs, restore := setupObservedLogger(t)
  108. defer restore()
  109. h := wrap(func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  110. return mcp.NewToolResultText("ok"), nil
  111. })
  112. // phone 是常见 PII,MaskJSONBytes 应对其脱敏
  113. _, _ = h(context.Background(), newReq("service_site_query_info", map[string]any{"phone": "13800138000"}))
  114. start := obs.All()[0]
  115. params, _ := start.ContextMap()["params"].(string)
  116. if strings.Contains(params, "13800138000") {
  117. t.Errorf("params log leaked raw phone: %q", params)
  118. }
  119. }