archive.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. package audit
  2. import (
  3. "bufio"
  4. "context"
  5. "database/sql"
  6. "fmt"
  7. "io"
  8. "os"
  9. "path/filepath"
  10. "strings"
  11. "time"
  12. "udeServer/internal/model"
  13. "github.com/zeromicro/go-zero/core/stores/sqlx"
  14. )
  15. // LogArchive builds a temporary SQL dump file with one INSERT statement per
  16. // log entry and returns the temp file together with the MinIO object key.
  17. func LogArchive(logs []*model.TSystemLog, archiveName string) (*os.File, string, error) {
  18. tmpDir := os.TempDir()
  19. filename := fmt.Sprintf("%s_%d.sql", archiveName, time.Now().UnixNano())
  20. tmpPath := filepath.Join(tmpDir, filename)
  21. tmpFile, err := os.Create(tmpPath)
  22. objectKey := fmt.Sprintf("%s/%s", ObjectDirArchives, filename)
  23. if err != nil {
  24. return nil, "", fmt.Errorf("create temp file failed: %w", err)
  25. }
  26. defer func(tmpFile *os.File) {
  27. _ = tmpFile.Close()
  28. }(tmpFile)
  29. for _, entry := range logs {
  30. stmt := getLogStmt(entry)
  31. if _, err := tmpFile.WriteString(stmt); err != nil {
  32. return nil, "", fmt.Errorf("writing to file failed: %w", err)
  33. }
  34. }
  35. return tmpFile, objectKey, nil
  36. }
  37. // getLogStmt renders one INSERT statement for the entry. The table is left
  38. // unqualified so restoring works against whatever schema the DSN points to.
  39. func getLogStmt(entry *model.TSystemLog) string {
  40. return fmt.Sprintf(
  41. "INSERT INTO `t_system_log` ("+
  42. "`id`, `iden`, `trace_id`, `span_id`, `parent_span_id`, "+
  43. "`type`, `service_name`, "+
  44. "`uri_path`, `http_method`, `method`, `level`, "+
  45. "`app_key`, `user_id`, `user_name`, `client_ip`, "+
  46. "`request_header`, `request_body_digest`, `request_body_file_key`, "+
  47. "`response_code`, `response_body_digest`, `response_body_file_key`, "+
  48. "`error_message`, `duration_ms`, "+
  49. "`created_at`, `h_mac`, `audit_status`) VALUES ("+
  50. "%d, '%s', '%s', '%s', %s, "+
  51. "%d, '%s', "+
  52. "%s, %s, '%s', '%s', "+
  53. "%s, %s, %s, %s, "+
  54. "%s, '%s', '%s', "+
  55. "%d, '%s', '%s', "+
  56. "%s, %d, "+
  57. "'%s', '%s', %d);\n",
  58. entry.Id, entry.Iden, entry.TraceId, entry.SpanId, nullOrQuotedString(entry.ParentSpanId),
  59. entry.Type, entry.ServiceName,
  60. nullOrQuotedString(entry.UriPath), nullOrQuotedString(entry.HttpMethod), entry.Method, entry.Level,
  61. nullOrQuotedString(entry.AppKey), nullOrQuotedString(entry.UserId), nullOrQuotedString(entry.UserName), nullOrQuotedString(entry.ClientIp),
  62. nullOrQuotedString(entry.RequestHeader), entry.RequestBodyDigest, entry.RequestBodyFileKey,
  63. entry.ResponseCode, entry.ResponseBodyDigest, entry.ResponseBodyFileKey,
  64. nullOrQuotedString(entry.ErrorMessage), entry.DurationMs,
  65. entry.CreatedAt.Format("2006-01-02 15:04:05"), entry.HMac, entry.AuditStatus,
  66. )
  67. }
  68. func nullOrQuotedString(ns sql.NullString) string {
  69. if !ns.Valid {
  70. return "NULL"
  71. }
  72. s := ns.String
  73. if len(s) > 1 && s[0] == '\'' && s[len(s)-1] == '\'' {
  74. return s
  75. }
  76. // 转义单引号为两个单引号,防止SQL注入或语法错误
  77. safeStr := strings.ReplaceAll(s, "'", "''")
  78. return "'" + safeStr + "'"
  79. }
  80. // RestoreSQLStream streams the SQL dump file back into the database,
  81. // executing batches of statements through execFunc.
  82. func RestoreSQLStream(ctx context.Context, filePath string, batchSize int, execFunc func(context.Context, []string) error) error {
  83. // 打开文件
  84. file, err := os.Open(filePath)
  85. if err != nil {
  86. return err
  87. }
  88. defer func(file *os.File) {
  89. _ = file.Close()
  90. }(file)
  91. reader := bufio.NewReader(file)
  92. var buffer strings.Builder
  93. var batch []string
  94. for {
  95. line, err := reader.ReadString('\n')
  96. if err != nil && err != io.EOF {
  97. return err
  98. }
  99. buffer.WriteString(line)
  100. if strings.HasSuffix(strings.TrimSpace(line), ";") {
  101. stmt := strings.TrimSpace(buffer.String())
  102. if stmt != "" {
  103. batch = append(batch, stmt)
  104. }
  105. buffer.Reset()
  106. }
  107. // 到达批量大小时,执行一次批量插入
  108. if len(batch) >= batchSize {
  109. if err := execFunc(ctx, batch); err != nil {
  110. return err
  111. }
  112. // 清空数组
  113. batch = batch[:0]
  114. }
  115. if err == io.EOF {
  116. break
  117. }
  118. }
  119. // 补全插入剩下的数据
  120. if len(batch) > 0 {
  121. return execFunc(ctx, batch)
  122. }
  123. return nil
  124. }
  125. // ArchiveLogsToStorageAndRecord uploads the log dump to MinIO and, inside
  126. // one transaction, deletes the archived rows and records the archive run.
  127. // The MinIO client is guarded: when it is not loaded the caller should not
  128. // invoke this function (checked by the business logic).
  129. func ArchiveLogsToStorageAndRecord(
  130. ctx context.Context,
  131. logs []*model.TSystemLog,
  132. archiveName string,
  133. threshold int64,
  134. mode int64,
  135. minioClient *MinioClient,
  136. dbConn sqlx.SqlConn,
  137. ) (int, error) {
  138. // 构建归档文件
  139. tmpFile, objectKey, err := LogArchive(logs, archiveName)
  140. if err != nil {
  141. return 0, fmt.Errorf("日志归档文件生成失败: %w", err)
  142. }
  143. // 删除临时文件
  144. defer func(tmpFile *os.File) {
  145. _ = os.Remove(tmpFile.Name())
  146. }(tmpFile)
  147. // 上传
  148. fileKey, err := minioClient.UploadFile(ctx, tmpFile.Name(), objectKey)
  149. if err != nil {
  150. return 0, fmt.Errorf("归档文件上传失败: %w", err)
  151. }
  152. // 提取 ID
  153. var ids []int64
  154. for _, entry := range logs {
  155. ids = append(ids, entry.Id)
  156. }
  157. const deleteBatchSize = 1000
  158. // 入库事务
  159. err = dbConn.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error {
  160. // 删除日志
  161. for i := 0; i < len(ids); i += deleteBatchSize {
  162. end := i + deleteBatchSize
  163. if end > len(ids) {
  164. end = len(ids)
  165. }
  166. batchIds := ids[i:end]
  167. placeholders := strings.Repeat("?,", len(batchIds))
  168. placeholders = strings.TrimSuffix(placeholders, ",")
  169. query := fmt.Sprintf("DELETE FROM t_system_log WHERE id IN (%s)", placeholders)
  170. args := make([]interface{}, len(batchIds))
  171. for j, v := range batchIds {
  172. args[j] = v
  173. }
  174. if _, err := session.ExecCtx(ctx, query, args...); err != nil {
  175. return err
  176. }
  177. }
  178. // 插入记录
  179. archivedBy := operatorAdmin
  180. archivedAt := time.Now()
  181. _, err := session.ExecCtx(ctx,
  182. "INSERT INTO t_log_archive_record (archive_name, record_count, archive_file_key, archive_mode, batch_size, archived_by, archived_at) VALUES (?,?,?,?,?,?,?)",
  183. archiveName, int64(len(logs)), fileKey, mode, threshold, archivedBy, archivedAt)
  184. return err
  185. })
  186. return len(logs), err
  187. }