esHttpClient.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. package fwh_es_mqtt
  2. import (
  3. "bytes"
  4. "crypto/tls"
  5. "encoding/json"
  6. "fmt"
  7. "fwh-toolbox-server/config"
  8. "io"
  9. "net/http"
  10. "strings"
  11. "time"
  12. "go.uber.org/zap"
  13. )
  14. // 服务端是一个 Elasticsearch,接口遵循 Elasticsearch API 技术规范。
  15. // 该模块使用独立的 HTTP 客户端(basic auth + 可选 TLS 跳过校验),
  16. // 与项目通用 httpClient 解耦,原因:
  17. // 1. ES 使用 basic auth,通用客户端不支持;
  18. // 2. ES 走 HTTPS 自签证书(curl -k),需要跳过 TLS 校验;
  19. // 3. ES 的非 200(如 400/404) 返回体仍包含有用的错误信息,需要读取。
  20. const (
  21. esTimeout time.Duration = 3 * time.Minute // ES 聚合查询可能较慢,超时 3min
  22. )
  23. var (
  24. esHost string // 形如 "alb-prod.franklinwh.click:5603"
  25. esUser string
  26. esPassword string
  27. esIndexPrefix string // 形如 "iotcore-stat"
  28. esClient *http.Client
  29. )
  30. // EsHttpInit 由启动流程调用,注入 ES 连接配置。
  31. func EsHttpInit(cfg *config.FwhEsConfig) {
  32. esHost = cfg.EsHost
  33. esUser = cfg.EsUser
  34. esPassword = cfg.EsPassword
  35. esIndexPrefix = cfg.EsIndexPrefix
  36. transport := &http.Transport{
  37. TLSClientConfig: &tls.Config{
  38. InsecureSkipVerify: cfg.EsInsecureSkipVerify, // 对应 curl -k
  39. },
  40. }
  41. esClient = &http.Client{
  42. Timeout: esTimeout,
  43. Transport: transport,
  44. }
  45. }
  46. // getFullEsUrl 拼接完整的 ES 请求 URL。
  47. // relativePath 形如 "/iotcore-stat-2026.09.07/_search"
  48. func getFullEsUrl(relativePath string) string {
  49. if !strings.HasPrefix(relativePath, "/") {
  50. relativePath = "/" + relativePath
  51. }
  52. return "https://" + esHost + relativePath
  53. }
  54. // esDo 发送请求并读取响应体。
  55. // 非 200 时同样返回响应体(供上层解析 ES 错误信息),并附带 error。
  56. func esDo(method, relativePath string, body any) ([]byte, int, error) {
  57. if esClient == nil {
  58. return nil, 0, fmt.Errorf("[esDo] ES client 未初始化,请先调用 EsHttpInit")
  59. }
  60. var reader io.Reader
  61. if body != nil {
  62. jsonData, err := json.Marshal(body)
  63. if err != nil {
  64. zap.L().Error("[esDo] body marshal failed.", zap.Any("body", body), zap.String("err", err.Error()))
  65. return nil, 0, err
  66. }
  67. reader = bytes.NewReader(jsonData)
  68. }
  69. url := getFullEsUrl(relativePath)
  70. zap.L().Debug("[esDo]", zap.String("method", method), zap.String("url", url))
  71. req, err := http.NewRequest(method, url, reader)
  72. if err != nil {
  73. zap.L().Error("[esDo] NewRequest failed.", zap.String("err", err.Error()))
  74. return nil, 0, err
  75. }
  76. req.Header.Set("Content-Type", "application/json")
  77. req.Header.Set("Accept", "application/json")
  78. req.SetBasicAuth(esUser, esPassword)
  79. resp, err := esClient.Do(req)
  80. if err != nil {
  81. zap.L().Error("[esDo] Do Send failed.", zap.String("err", err.Error()))
  82. return nil, 0, err
  83. }
  84. defer resp.Body.Close()
  85. respBody, err := io.ReadAll(resp.Body)
  86. if err != nil {
  87. return nil, resp.StatusCode, err
  88. }
  89. if resp.StatusCode != http.StatusOK {
  90. zap.L().Warn("[esDo] Response failed",
  91. zap.String("status", resp.Status),
  92. zap.String("body", string(respBody)))
  93. return respBody, resp.StatusCode, fmt.Errorf("[esDo] ES response failed, status: %s", resp.Status)
  94. }
  95. return respBody, resp.StatusCode, nil
  96. }
  97. // esGet 发送 GET 请求(如 _count)。
  98. func esGet(relativePath string) ([]byte, int, error) {
  99. return esDo(http.MethodGet, relativePath, nil)
  100. }
  101. // esSearch 向指定索引发送 _search POST 请求,body 为聚合查询 DSL。
  102. func esSearch(index string, query any) ([]byte, error) {
  103. respBody, _, err := esDo(http.MethodPost, "/"+index+"/_search", query)
  104. if err != nil {
  105. return respBody, err
  106. }
  107. return respBody, nil
  108. }
  109. // esIndexName 根据日期(UTC 日期,格式 yyyy.MM.dd)拼接索引名。
  110. // 例如 date="2026.09.07" -> "iotcore-stat-2026.09.07"
  111. func esIndexName(date string) string {
  112. return esIndexPrefix + "-" + date
  113. }
  114. // esMonthIndexPattern 根据年月拼接“该月所有日索引”的通配模式。
  115. // 例如 year=2026, month=9 -> "iotcore-stat-2026.09.*"
  116. // ES 支持索引名通配,可一次跨该月全部日索引做聚合。
  117. func esMonthIndexPattern(year, month int) string {
  118. return fmt.Sprintf("%s-%04d.%02d.*", esIndexPrefix, year, month)
  119. }