package audit import ( "context" "database/sql" "encoding/json" "time" "udeServer/internal/log" udeModel "udeServer/internal/model" ) // SpanRecorder records type=2 interface-log entries for the outbound remote // HTTP calls to the agent program (外挂程序): /path/lists (ListDirChildren), // /api/fuseMount and /api/fuseUnMount. Ported from sdf-serve's // SpanRecorder.DoSimple: the entry links to the inbound request (type=1, // produced by LogMiddleware) through parent_span_id, so one trace shows the // whole local-request → remote-call chain. type SpanRecorder struct { LogModel udeModel.TSystemLogModel ConfigModel udeModel.TConfigModel Service string } // NewSpanRecorder builds the shared recorder; models and service name come // from ServiceContext at startup. func NewSpanRecorder(logModel udeModel.TSystemLogModel, configModel udeModel.TConfigModel, service string) *SpanRecorder { return &SpanRecorder{ LogModel: logModel, ConfigModel: configModel, Service: service, } } // DoSimple wraps a remote-call closure and records one type=2 entry: // - ctx must carry the inbound trace/span (it does: the middleware injected // it before the handler ran); // - a fresh child span id is generated for this remote call; // - reqBody is the outbound payload digest/uploaded before the call runs // (for GET calls the caller passes a {method,url} description so the // digest still identifies the outbound request); // - remoteURL is stored in uri_path for display; // - fn performs the call and returns (result, code, err): code is the // remote HTTP status (0 when the call failed at transport level). func DoSimple[T any](sr *SpanRecorder, ctx context.Context, methodName, remoteURL string, reqBody []byte, fn func() (T, int64, error)) (T, error) { // 1.获取上下文的 traceId、父 SpanId(即入站请求的 SpanId) ctx, traceID, parentSpanID, _, _, appKey := EnsureTraceAndSpanID(ctx) // 2.生成新的 SpanId 并写回上下文,保持调用链可继续嵌套 spanID := NewUUID() nextCtx := context.WithValue(ctx, SpanKey, spanID) // 3.请求体摘要与原文上传(MinIO 未就绪时降级为仅摘要) client, logConfig := GetGlobalConfig(sr.ConfigModel) reqDigestStr, reqUrl, reqErr := UploadAndDigest(reqBody, traceID, spanID, "[Device-Request]", client, "application/json") if reqErr != nil { log.RecordError(nextCtx, reqErr) } // 4.执行闭包并计时 start := time.Now() result, code, callErr := fn() duration := time.Since(start).Milliseconds() // 5.响应体摘要与原文上传 var respBody []byte if b, mErr := json.Marshal(result); mErr == nil { respBody = b } respDigestStr, respUrl, respErr := UploadAndDigest(respBody, traceID, spanID, "[Device-Response]", client, "application/json") if respErr != nil { log.RecordError(nextCtx, respErr) } // 6.构造 type=2 日志对象(UriPath 存远程完整 URL,便于定位四个外挂接口调用) if len(remoteURL) > 255 { // uri_path varchar(255),截断防插入失败 remoteURL = remoteURL[:255] } entry := &udeModel.TSystemLog{ Iden: NewUUID(), TraceId: traceID, SpanId: spanID, ParentSpanId: sql.NullString{String: parentSpanID, Valid: parentSpanID != ""}, //顶级链路父SpanId为""时填写null Type: InterfaceLog, ServiceName: sr.Service, UriPath: sql.NullString{String: remoteURL, Valid: remoteURL != ""}, Method: methodName, Level: ChooseLevelByInterfaceCode(code, callErr), AppKey: sql.NullString{String: appKey, Valid: true}, RequestBodyDigest: reqDigestStr, RequestBodyFileKey: reqUrl, ResponseCode: code, ResponseBodyDigest: respDigestStr, ResponseBodyFileKey: respUrl, DurationMs: duration, AuditStatus: UnAudited, } // 7.填充 ErrMsg if callErr != nil { entry.ErrorMessage = sql.NullString{String: callErr.Error(), Valid: true} } else { entry.ErrorMessage = sql.NullString{Valid: false} } // 8.填充 Hmac 值;密钥为空时用空串计算,保持列非空 macBuffer := make([]byte, 0, 32) if mac, hErr := CalculateHMAC(entry, logConfig.HmacSecret, macBuffer); hErr == nil { entry.HMac = mac } else { log.RecordError(nextCtx, hErr) } entry.CreatedAt = time.Now() // 9.按配置库中 log_record_level 过滤后异步入库 if SaveToDBByLogLevel(logConfig.LogRecordLevel, entry.Level) { go AsyncInsertLog(sr.LogModel, entry) } return result, callErr } // ChooseLevelByInterfaceCode maps a remote-call outcome to a log level: // call error or agent 5xx → ERROR; 2xx → INFO; other (4xx/3xx/0) → WARN. func ChooseLevelByInterfaceCode(code int64, callErr error) string { switch { case callErr != nil, code >= 500: return "ERROR" case code >= 200 && code < 300: return "INFO" default: return "WARN" } } // BuildRemoteGetRequest renders the outbound GET request as JSON so the // digest identifies the actual remote request (GET has no body). func BuildRemoteGetRequest(method, fullURL string) []byte { b, err := json.Marshal(map[string]string{"method": method, "url": fullURL}) if err != nil { return nil } return b }