span.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. package audit
  2. import (
  3. "context"
  4. "database/sql"
  5. "encoding/json"
  6. "time"
  7. "udeServer/internal/log"
  8. udeModel "udeServer/internal/model"
  9. )
  10. // SpanRecorder records type=2 interface-log entries for the outbound remote
  11. // HTTP calls to the agent program (外挂程序): /path/lists (ListDirChildren),
  12. // /api/fuseMount and /api/fuseUnMount. Ported from sdf-serve's
  13. // SpanRecorder.DoSimple: the entry links to the inbound request (type=1,
  14. // produced by LogMiddleware) through parent_span_id, so one trace shows the
  15. // whole local-request → remote-call chain.
  16. type SpanRecorder struct {
  17. LogModel udeModel.TSystemLogModel
  18. ConfigModel udeModel.TConfigModel
  19. Service string
  20. }
  21. // NewSpanRecorder builds the shared recorder; models and service name come
  22. // from ServiceContext at startup.
  23. func NewSpanRecorder(logModel udeModel.TSystemLogModel, configModel udeModel.TConfigModel, service string) *SpanRecorder {
  24. return &SpanRecorder{
  25. LogModel: logModel,
  26. ConfigModel: configModel,
  27. Service: service,
  28. }
  29. }
  30. // DoSimple wraps a remote-call closure and records one type=2 entry:
  31. // - ctx must carry the inbound trace/span (it does: the middleware injected
  32. // it before the handler ran);
  33. // - a fresh child span id is generated for this remote call;
  34. // - reqBody is the outbound payload digest/uploaded before the call runs
  35. // (for GET calls the caller passes a {method,url} description so the
  36. // digest still identifies the outbound request);
  37. // - remoteURL is stored in uri_path for display;
  38. // - fn performs the call and returns (result, code, err): code is the
  39. // remote HTTP status (0 when the call failed at transport level).
  40. func DoSimple[T any](sr *SpanRecorder, ctx context.Context, methodName, remoteURL string, reqBody []byte, fn func() (T, int64, error)) (T, error) {
  41. // 1.获取上下文的 traceId、父 SpanId(即入站请求的 SpanId)
  42. ctx, traceID, parentSpanID, _, _, appKey := EnsureTraceAndSpanID(ctx)
  43. // 2.生成新的 SpanId 并写回上下文,保持调用链可继续嵌套
  44. spanID := NewUUID()
  45. nextCtx := context.WithValue(ctx, SpanKey, spanID)
  46. // 3.请求体摘要与原文上传(MinIO 未就绪时降级为仅摘要)
  47. client, logConfig := GetGlobalConfig(sr.ConfigModel)
  48. reqDigestStr, reqUrl, reqErr := UploadAndDigest(reqBody, traceID, spanID, "[Device-Request]", client, "application/json")
  49. if reqErr != nil {
  50. log.RecordError(nextCtx, reqErr)
  51. }
  52. // 4.执行闭包并计时
  53. start := time.Now()
  54. result, code, callErr := fn()
  55. duration := time.Since(start).Milliseconds()
  56. // 5.响应体摘要与原文上传
  57. var respBody []byte
  58. if b, mErr := json.Marshal(result); mErr == nil {
  59. respBody = b
  60. }
  61. respDigestStr, respUrl, respErr := UploadAndDigest(respBody, traceID, spanID, "[Device-Response]", client, "application/json")
  62. if respErr != nil {
  63. log.RecordError(nextCtx, respErr)
  64. }
  65. // 6.构造 type=2 日志对象(UriPath 存远程完整 URL,便于定位四个外挂接口调用)
  66. if len(remoteURL) > 255 { // uri_path varchar(255),截断防插入失败
  67. remoteURL = remoteURL[:255]
  68. }
  69. entry := &udeModel.TSystemLog{
  70. Iden: NewUUID(),
  71. TraceId: traceID,
  72. SpanId: spanID,
  73. ParentSpanId: sql.NullString{String: parentSpanID, Valid: parentSpanID != ""}, //顶级链路父SpanId为""时填写null
  74. Type: InterfaceLog,
  75. ServiceName: sr.Service,
  76. UriPath: sql.NullString{String: remoteURL, Valid: remoteURL != ""},
  77. Method: methodName,
  78. Level: ChooseLevelByInterfaceCode(code, callErr),
  79. AppKey: sql.NullString{String: appKey, Valid: true},
  80. RequestBodyDigest: reqDigestStr,
  81. RequestBodyFileKey: reqUrl,
  82. ResponseCode: code,
  83. ResponseBodyDigest: respDigestStr,
  84. ResponseBodyFileKey: respUrl,
  85. DurationMs: duration,
  86. AuditStatus: UnAudited,
  87. }
  88. // 7.填充 ErrMsg
  89. if callErr != nil {
  90. entry.ErrorMessage = sql.NullString{String: callErr.Error(), Valid: true}
  91. } else {
  92. entry.ErrorMessage = sql.NullString{Valid: false}
  93. }
  94. // 8.填充 Hmac 值;密钥为空时用空串计算,保持列非空
  95. macBuffer := make([]byte, 0, 32)
  96. if mac, hErr := CalculateHMAC(entry, logConfig.HmacSecret, macBuffer); hErr == nil {
  97. entry.HMac = mac
  98. } else {
  99. log.RecordError(nextCtx, hErr)
  100. }
  101. entry.CreatedAt = time.Now()
  102. // 9.按配置库中 log_record_level 过滤后异步入库
  103. if SaveToDBByLogLevel(logConfig.LogRecordLevel, entry.Level) {
  104. go AsyncInsertLog(sr.LogModel, entry)
  105. }
  106. return result, callErr
  107. }
  108. // ChooseLevelByInterfaceCode maps a remote-call outcome to a log level:
  109. // call error or agent 5xx → ERROR; 2xx → INFO; other (4xx/3xx/0) → WARN.
  110. func ChooseLevelByInterfaceCode(code int64, callErr error) string {
  111. switch {
  112. case callErr != nil, code >= 500:
  113. return "ERROR"
  114. case code >= 200 && code < 300:
  115. return "INFO"
  116. default:
  117. return "WARN"
  118. }
  119. }
  120. // BuildRemoteGetRequest renders the outbound GET request as JSON so the
  121. // digest identifies the actual remote request (GET has no body).
  122. func BuildRemoteGetRequest(method, fullURL string) []byte {
  123. b, err := json.Marshal(map[string]string{"method": method, "url": fullURL})
  124. if err != nil {
  125. return nil
  126. }
  127. return b
  128. }