fwh_eventAlarmLog.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. package fwh_pro
  2. import (
  3. "errors"
  4. "fmt"
  5. "fwh-toolbox-server/domain"
  6. "net/url"
  7. "strconv"
  8. "time"
  9. "go.uber.org/zap"
  10. )
  11. type EventAlarmType int
  12. const (
  13. All EventAlarmType = iota // 0
  14. Event // 1
  15. Alarm // 2
  16. )
  17. // 查询事件事件日志
  18. func queryEventAndAlarmLog(sn string, params url.Values) (*domain.FwhResponse[[]domain.EventAlarmItem], error) {
  19. userId := getUserId()
  20. // 构造 query 参数
  21. params.Add("userId", strconv.FormatUint(uint64(userId), 10))
  22. params.Add("searchName", sn)
  23. params.Add("timeType", strconv.Itoa(1)) // 0-通讯时间;1-设备时间
  24. params.Add("pageSize", strconv.FormatUint(REQ_PAGE_SIZE, 10))
  25. params.Add("pageNum", strconv.Itoa(1))
  26. params.Add("land", "ZH_CN")
  27. response, err := FwhHttpGetUnmarshal[[]domain.EventAlarmItem](getFullEnergyUrl("/hes-gateway/manage/iotDeviceRunLogs"), params)
  28. if err != nil {
  29. return nil, err
  30. }
  31. return response, nil
  32. }
  33. // 查询所有事件告警
  34. // startTime&endTime 格式 YYYY-MM-DD HH:mm:SS
  35. func queryAllEventAlarm(sn, startTime, endTime string) (*domain.FwhResponse[[]domain.EventAlarmItem], error) {
  36. // 构造 query 参数
  37. params := url.Values{}
  38. params.Add("startTime", startTime)
  39. params.Add("endTime", endTime)
  40. return queryEventAndAlarmLog(sn, params)
  41. }
  42. // 查询事件日志
  43. // startTime&endTime 格式 YYYY-MM-DD HH:mm:SS
  44. func queryEventLog(sn, startTime, endTime string) (*domain.FwhResponse[[]domain.EventAlarmItem], error) {
  45. // 构造 query 参数
  46. params := url.Values{}
  47. params.Add("logType", strconv.Itoa(0))
  48. params.Add("startTime", startTime)
  49. params.Add("endTime", endTime)
  50. return queryEventAndAlarmLog(sn, params)
  51. }
  52. // 查询告警日志
  53. // startTime&endTime 格式 YYYY-MM-DD HH:mm:SS
  54. // alarmCode - 支持多个code,用‘,’隔开,“”代表查询所有
  55. func queryAlarmLog(sn, startTime, endTime, alarmCode string) (*domain.FwhResponse[[]domain.EventAlarmItem], error) {
  56. // 构造 query 参数
  57. params := url.Values{}
  58. params.Add("logType", strconv.Itoa(1))
  59. params.Add("alarmCode", alarmCode)
  60. params.Add("startTime", startTime)
  61. params.Add("endTime", endTime)
  62. return queryEventAndAlarmLog(sn, params)
  63. }
  64. // 查询实时告警
  65. func queryRealtimeAlarm(sn string) (*domain.FwhResponse[[]domain.EventAlarmItem], error) {
  66. // 构造 query 参数
  67. params := url.Values{}
  68. params.Add("logType", strconv.Itoa(2))
  69. return queryEventAndAlarmLog(sn, params)
  70. }
  71. func queryEventOrAlarmListOnePage(sn string, startTime, endTime time.Time, t EventAlarmType) ([]domain.EventAlarmItem, error) {
  72. // 三次请求尝试
  73. for i := 0; i < 3; i++ {
  74. eventLogRes, err := queryEventOrAlarmListOnePageExec(sn, startTime, endTime, t)
  75. if err != nil {
  76. zap.L().Warn("queryEventOrAlarmListOnePageExec failed and retry", zap.String("err", err.Error()))
  77. continue
  78. }
  79. if eventLogRes.Code != 200 {
  80. // return nil, fmt.Errorf("查询失败 code: %d, message: %s", eventLogRes.Code, eventLogRes.Message)
  81. zap.L().Warn("queryEventOrAlarmListOnePage 查询失败 retry", zap.Int("code", eventLogRes.Code), zap.String("msg", eventLogRes.Message))
  82. time.Sleep(time.Second * 5)
  83. continue
  84. }
  85. return *eventLogRes.Result, nil
  86. }
  87. return nil, errors.New("retry failed")
  88. }
  89. func queryEventOrAlarmListOnePageExec(sn string, startTime, endTime time.Time, t EventAlarmType) (*domain.FwhResponse[[]domain.EventAlarmItem], error) {
  90. startTimeStr := startTime.Format(time.DateTime)
  91. endTimeStr := endTime.Format(time.DateTime)
  92. // TEST
  93. fmt.Printf("startTime = %s\n", startTimeStr)
  94. fmt.Printf("endTime = %s\n", endTimeStr)
  95. var eventLogRes *domain.FwhResponse[[]domain.EventAlarmItem]
  96. var err error
  97. switch t {
  98. case Event:
  99. eventLogRes, err = queryEventLog(sn, startTimeStr, endTimeStr)
  100. case Alarm:
  101. eventLogRes, err = queryAlarmLog(sn, startTimeStr, endTimeStr, "")
  102. default:
  103. eventLogRes, err = queryAllEventAlarm(sn, startTimeStr, endTimeStr)
  104. }
  105. if err != nil {
  106. return nil, err
  107. }
  108. return eventLogRes, nil
  109. }
  110. // 从FWH获取事件或告警log数据
  111. //
  112. // 限制条件(重要):startTime/endTime 必须为「不含时区」的字面时间(用 UTC 表示)。
  113. // 本函数按天拆分时间段时用 time.Truncate(24h) 推进游标,Truncate 按 UTC 零点对齐;
  114. // 若传入带非 UTC 时区(如 America/Chicago)的时间,游标会回退到前一天,
  115. // currTime.Before(endTime) 恒成立,periods 切片无限 append,导致内存 OOM。
  116. // 调用方(DataDownload)需先用 stripTimeLocation 剥离时区再传入。
  117. func QueryEventOrAlarmListOnTimeRange(sn string, startTime, endTime time.Time, t EventAlarmType) ([]domain.EventAlarmItem, error) {
  118. // 保证 startTime <= endTime
  119. if startTime.After(endTime) {
  120. return nil, errors.New("startTime > endTime")
  121. }
  122. // 按天拆分时间段
  123. var periods []struct{ Start, End time.Time }
  124. currTime := startTime
  125. for currTime.Before(endTime) {
  126. // 当天结束时间
  127. dayEnd := time.Date(currTime.Year(), currTime.Month(), currTime.Day(), 23, 59, 59, 0, currTime.Location())
  128. // 如果当天结束大于 endTime,使用 endTime
  129. if dayEnd.After(endTime) {
  130. dayEnd = endTime
  131. }
  132. periods = append(periods, struct{ Start, End time.Time }{Start: currTime, End: dayEnd})
  133. // 下一天开始
  134. currTime = dayEnd.Add(time.Second).Truncate(24 * time.Hour)
  135. currTime = time.Date(currTime.Year(), currTime.Month(), currTime.Day(), 0, 0, 0, 0, currTime.Location())
  136. }
  137. zap.L().Debug("[fwh_pro] 事件/告警 periods 构建完成",
  138. zap.String("sn", sn), zap.Int("logType", int(t)),
  139. zap.Time("start", startTime), zap.Time("end", endTime),
  140. zap.Int("periods", len(periods)),
  141. )
  142. // 按天查询
  143. var eventLogList []domain.EventAlarmItem
  144. for _, period := range periods {
  145. var eventLogRes []domain.EventAlarmItem = nil
  146. periodEndTime := period.End
  147. pages := 0
  148. // 检查数据是否超过1000(单次查询上限)
  149. for {
  150. var err error
  151. pages++
  152. zap.L().Debug("[fwh_pro] 事件/告警分页请求发起",
  153. zap.String("sn", sn), zap.Int("logType", int(t)),
  154. zap.Time("periodStart", period.Start), zap.Time("periodEnd", periodEndTime),
  155. zap.Int("page", pages), zap.Int("accumulated", len(eventLogList)),
  156. )
  157. eventLogRes, err = queryEventOrAlarmListOnePage(sn, period.Start, periodEndTime, t)
  158. if err != nil {
  159. return nil, err
  160. }
  161. eventLogList = append(eventLogList, eventLogRes...)
  162. // 观测:累计规模,用于定位内存放大。正常一天一页即结束。
  163. zap.L().Debug("[fwh_pro] 事件/告警分页请求返回",
  164. zap.String("sn", sn),
  165. zap.Int("logType", int(t)),
  166. zap.Int("page", pages),
  167. zap.Int("pageRows", len(eventLogRes)),
  168. zap.Int("accumulated", len(eventLogList)),
  169. )
  170. // 判断是否未查完
  171. if len(eventLogRes) < int(REQ_PAGE_SIZE) {
  172. // 请求完成,退出
  173. break
  174. }
  175. // 跳转时间段,再次请求
  176. // 提取最后一条数据的设备时间
  177. next, err := time.Parse(time.DateTime, eventLogRes[len(eventLogRes)-1].LogTime)
  178. if err != nil {
  179. return nil, err
  180. }
  181. // 游标未能前进:说明同一秒内 >= REQ_PAGE_SIZE(1000) 条数据,
  182. // 单页游标无法翻过这一秒,继续循环会死循环并 OOM。现实中基本不会发生,
  183. // 此处仅作防护:记录错误并返回,不再无限拉取。
  184. if !next.Before(periodEndTime) {
  185. zap.L().Error("[fwh_pro] 事件/告警日志分页游标停滞,疑似同一秒内数据超过单页上限",
  186. zap.String("sn", sn),
  187. zap.Int("logType", int(t)),
  188. zap.Time("stuckAt", next),
  189. zap.Int("pageSize", int(REQ_PAGE_SIZE)),
  190. zap.Int("accumulated", len(eventLogList)),
  191. )
  192. return nil, fmt.Errorf("event/alarm log pagination stalled at %s: more than %d rows within one second", next.Format(time.DateTime), REQ_PAGE_SIZE)
  193. }
  194. periodEndTime = next
  195. }
  196. }
  197. return eventLogList, nil
  198. }
  199. // 查询快照信息
  200. // logTime 格式 YYYY-MM-DD
  201. func queryAlarmSnapshotInfo(sn, logTime string, page int) (*domain.FwhPageResponse[[]domain.AlarmSnapshotInfo], error) {
  202. // 构造 query 参数
  203. params := url.Values{}
  204. params.Add("logTime", logTime)
  205. params.Add("linkId", sn)
  206. params.Add("pageSize", strconv.FormatUint(REQ_PAGE_SIZE, 10))
  207. params.Add("pageNum", strconv.Itoa(page))
  208. params.Add("land", "ZH_CN")
  209. response, err := FwhHttpGetUnmarshalPage[[]domain.AlarmSnapshotInfo](getFullEnergyUrl("/hes-gateway/manage/selectAlarmSnapshotList"), params)
  210. if err != nil {
  211. return nil, err
  212. }
  213. return response, nil
  214. }
  215. func QueryAlarmSnapshotInfo(sn, logTime string) ([]domain.AlarmSnapshotInfo, error) {
  216. var page int = 1
  217. var infoList []domain.AlarmSnapshotInfo
  218. for {
  219. // 查询告警快照数据
  220. alarmSnapshotRes, err := queryAlarmSnapshotInfo(sn, logTime, page)
  221. if err != nil {
  222. return nil, fmt.Errorf("[QueryAlarmSnapshotInfo] %s", err.Error())
  223. }
  224. if alarmSnapshotRes.Code != 200 {
  225. return nil, fmt.Errorf("[QueryAlarmSnapshotInfo] 查询失败 code: %d, message: %s", alarmSnapshotRes.Code, alarmSnapshotRes.Message)
  226. }
  227. if alarmSnapshotRes.Total > 10000 {
  228. return nil, fmt.Errorf("[QueryAlarmSnapshotInfo] 数据超过10000条")
  229. }
  230. infoList = append(infoList, *alarmSnapshotRes.Result...)
  231. if alarmSnapshotRes.Total <= uint64(page*int(REQ_PAGE_SIZE)) {
  232. break
  233. }
  234. page++
  235. }
  236. return infoList, nil
  237. }