支持ToElastic的时候当index符合特定类型时,动态设置ilm, 结果有待验证
This commit is contained in:
		
							parent
							
								
									e73c36725c
								
							
						
					
					
						commit
						b0eb71283c
					
				| @ -5,31 +5,55 @@ import ( | |||||||
| 	"os" | 	"os" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // Config 配置文件结构体 | ||||||
| type Config struct { | type Config struct { | ||||||
| 	Fluentd struct { | 	Fluentd       FluentdConfig       `json:"fluentd"` | ||||||
| 		URL string `json:"url"` | 	Elasticsearch ElasticsearchConfig `json:"elasticsearch"` | ||||||
| 	} `json:"fluentd"` |  | ||||||
| 	Elasticsearch struct { |  | ||||||
| 		URL  string `json:"url"` |  | ||||||
| 		Auth struct { |  | ||||||
| 			Username string `json:"username"` |  | ||||||
| 			Password string `json:"password"` |  | ||||||
| 		} `json:"auth"` |  | ||||||
| 	} `json:"elasticsearch"` |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // LoadConfig 从指定路径加载配置文件 | // FluentdConfig Fluentd 配置 | ||||||
|  | type FluentdConfig struct { | ||||||
|  | 	URL string `json:"url"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ElasticsearchConfig Elasticsearch 配置 | ||||||
|  | type ElasticsearchConfig struct { | ||||||
|  | 	URL  string     `json:"url"` | ||||||
|  | 	Auth AuthConfig `json:"auth"` | ||||||
|  | 	ILM  ILMConfig  `json:"ilm"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // AuthConfig 认证信息 | ||||||
|  | type AuthConfig struct { | ||||||
|  | 	Username string `json:"username"` | ||||||
|  | 	Password string `json:"password"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ILMConfig ILM 配置 | ||||||
|  | type ILMConfig struct { | ||||||
|  | 	DataTypes map[string]DataTypeConfig `json:"data_types"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // DataTypeConfig 每种数据类型的 ILM 配置 | ||||||
|  | type DataTypeConfig struct { | ||||||
|  | 	IndexPattern   string            `json:"index_pattern"` | ||||||
|  | 	MaxRetention   map[string]int    `json:"max_retention"` | ||||||
|  | 	MinRetention   int               `json:"min_retention"` | ||||||
|  | 	Shards         int               `json:"shards"` | ||||||
|  | 	Replicas       int               `json:"replicas"` | ||||||
|  | 	NormalRollover map[string]string `json:"normal_rollover"` | ||||||
|  | 	NormalPhases   map[string]int    `json:"normal_phases"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // LoadConfig 加载配置文件 | ||||||
| func LoadConfig(path string) (*Config, error) { | func LoadConfig(path string) (*Config, error) { | ||||||
| 	file, err := os.ReadFile(path) | 	data, err := os.ReadFile(path) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	var config Config | 	var config Config | ||||||
| 	err = json.Unmarshal(file, &config) | 	if err := json.Unmarshal(data, &config); err != nil { | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	return &config, nil | 	return &config, nil | ||||||
| } | } | ||||||
|  | |||||||
| @ -7,8 +7,60 @@ | |||||||
|         "auth": { |         "auth": { | ||||||
|             "username": "fluentd_user", |             "username": "fluentd_user", | ||||||
|             "password": "fluentd_password" |             "password": "fluentd_password" | ||||||
|  |         }, | ||||||
|  |         "ilm": { | ||||||
|  |             "data_types": { | ||||||
|  |                 "candle": { | ||||||
|  |                     "index_pattern": "logstash-%s.candle.*.%d-%02d", | ||||||
|  |                     "max_retention": { | ||||||
|  |                         "1m": 365, | ||||||
|  |                         "3m": 365, | ||||||
|  |                         "5m": 730, | ||||||
|  |                         "15m": 730, | ||||||
|  |                         "30m": 730, | ||||||
|  |                         "1h": 1095, | ||||||
|  |                         "2h": 1095, | ||||||
|  |                         "4h": 1825, | ||||||
|  |                         "6h": 1825, | ||||||
|  |                         "12h": 1825, | ||||||
|  |                         "1d": 2555, | ||||||
|  |                         "2d": 2555, | ||||||
|  |                         "5d": 2555, | ||||||
|  |                         "1W": 3650, | ||||||
|  |                         "1M": 3650, | ||||||
|  |                         "default": 365 | ||||||
|  |                     }, | ||||||
|  |                     "min_retention": 30, | ||||||
|  |                     "shards": 2, | ||||||
|  |                     "replicas": 1, | ||||||
|  |                     "normal_rollover": { | ||||||
|  |                         "max_size": "30GB", | ||||||
|  |                         "max_age": "14d" | ||||||
|  |                     }, | ||||||
|  |                     "normal_phases": { | ||||||
|  |                         "warm": 60, | ||||||
|  |                         "cold": 180 | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 "ma": { | ||||||
|  |                     "index_pattern": "logstash-%s.ma.*.%d-%02d", | ||||||
|  |                     "max_retention": { | ||||||
|  |                         "1d": 2555, | ||||||
|  |                         "default": 365 | ||||||
|  |                     }, | ||||||
|  |                     "min_retention": 30, | ||||||
|  |                     "shards": 2, | ||||||
|  |                     "replicas": 1, | ||||||
|  |                     "normal_rollover": { | ||||||
|  |                         "max_size": "50GB", | ||||||
|  |                         "max_age": "30d" | ||||||
|  |                     }, | ||||||
|  |                     "normal_phases": { | ||||||
|  |                         "warm": 90, | ||||||
|  |                         "cold": 270 | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|  | |||||||
							
								
								
									
										281
									
								
								elasticilm/ilm.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										281
									
								
								elasticilm/ilm.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,281 @@ | |||||||
|  | package elasticilm | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"math" | ||||||
|  | 	"net/http" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	cfg "gitea.zjmud.xyz/phyer/tanya/config" // 导入你的 config 包 | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // RetentionCalculator 计算保留时间的接口 | ||||||
|  | type RetentionCalculator interface { | ||||||
|  | 	Calculate(daysDiff int, period string) int | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // DefaultRetentionCalculator 默认保留时间计算器 | ||||||
|  | type DefaultRetentionCalculator struct { | ||||||
|  | 	MaxRetention map[string]int | ||||||
|  | 	MinRetention int | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Calculate 计算保留时间(天) | ||||||
|  | func (c DefaultRetentionCalculator) Calculate(daysDiff int, period string) int { | ||||||
|  | 	minutes := periodToMinutes(period) | ||||||
|  | 	maxRetention := c.MaxRetention[period] | ||||||
|  | 	if maxRetention == 0 { | ||||||
|  | 		maxRetention = c.MaxRetention["default"] | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 非线性衰减公式 | ||||||
|  | 	timeFactor := 1 - math.Sqrt(float64(daysDiff))/50 // 距今时间因子 | ||||||
|  | 	if timeFactor < 0 { | ||||||
|  | 		timeFactor = 0 | ||||||
|  | 	} | ||||||
|  | 	periodFactor := math.Sqrt(float64(minutes)) / math.Sqrt(43200) // 时间框架因子 | ||||||
|  | 	retention := float64(maxRetention) * timeFactor * periodFactor | ||||||
|  | 	if retention < float64(c.MinRetention) { | ||||||
|  | 		return c.MinRetention | ||||||
|  | 	} | ||||||
|  | 	return int(retention) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // periodToMinutes 将时间框架转换为分钟数 | ||||||
|  | func periodToMinutes(period string) int { | ||||||
|  | 	switch period { | ||||||
|  | 	case "1m": | ||||||
|  | 		return 1 | ||||||
|  | 	case "3m": | ||||||
|  | 		return 3 | ||||||
|  | 	case "5m": | ||||||
|  | 		return 5 | ||||||
|  | 	case "15m": | ||||||
|  | 		return 15 | ||||||
|  | 	case "30m": | ||||||
|  | 		return 30 | ||||||
|  | 	case "1h": | ||||||
|  | 		return 60 | ||||||
|  | 	case "2h": | ||||||
|  | 		return 120 | ||||||
|  | 	case "4h": | ||||||
|  | 		return 240 | ||||||
|  | 	case "6h": | ||||||
|  | 		return 360 | ||||||
|  | 	case "12h": | ||||||
|  | 		return 720 | ||||||
|  | 	case "1d", "1D": | ||||||
|  | 		return 1440 | ||||||
|  | 	case "2d": | ||||||
|  | 		return 2880 | ||||||
|  | 	case "5d": | ||||||
|  | 		return 7200 | ||||||
|  | 	case "1W": | ||||||
|  | 		return 10080 | ||||||
|  | 	case "1M": | ||||||
|  | 		return 43200 // 假设 30 天 | ||||||
|  | 	default: | ||||||
|  | 		return 1 | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ConfigureILM 动态配置 ILM 策略和索引模板 | ||||||
|  | func ConfigureILM(client *http.Client, config *cfg.Config, dataType, period string, indexTime time.Time) error { | ||||||
|  | 	// 从配置文件中获取 Elasticsearch 和 ILM 配置 | ||||||
|  | 	esConfig := config.Elasticsearch | ||||||
|  | 	dataConfig, ok := esConfig.ILM.DataTypes[dataType] | ||||||
|  | 	if !ok { | ||||||
|  | 		return fmt.Errorf("ILM configuration for data type '%s' not found in config", dataType) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 创建保留时间计算器 | ||||||
|  | 	calc := DefaultRetentionCalculator{ | ||||||
|  | 		MaxRetention: dataConfig.MaxRetention, | ||||||
|  | 		MinRetention: dataConfig.MinRetention, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 计算距今时间差和保留时间 | ||||||
|  | 	now := time.Now() | ||||||
|  | 	daysDiff := int(now.Sub(indexTime).Hours() / 24) | ||||||
|  | 	retentionDays := calc.Calculate(daysDiff, period) | ||||||
|  | 
 | ||||||
|  | 	// 构造 ILM 策略名称 | ||||||
|  | 	year, month := indexTime.Year(), int(indexTime.Month()) | ||||||
|  | 	policyName := fmt.Sprintf("logstash_%s_%s_%d_%02d_%s", dataType, period, year, month, getPhase(daysDiff, retentionDays)) | ||||||
|  | 
 | ||||||
|  | 	// 创建或更新 ILM 策略 | ||||||
|  | 	if err := ensureILMPolicy(client, esConfig, policyName, daysDiff, retentionDays, dataConfig); err != nil { | ||||||
|  | 		return fmt.Errorf("failed to ensure ILM policy: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 配置索引模板 | ||||||
|  | 	templateName := fmt.Sprintf("logstash-%s-%s-%d-%02d", dataType, period, year, month) | ||||||
|  | 	indexPattern := fmt.Sprintf(dataConfig.IndexPattern, period, year, month) | ||||||
|  | 	// 根据数据类型设置映射 | ||||||
|  | 	var mappings map[string]interface{} | ||||||
|  | 	switch dataType { | ||||||
|  | 	case "candle": | ||||||
|  | 		mappings = map[string]interface{}{ | ||||||
|  | 			"properties": map[string]interface{}{ | ||||||
|  | 				"dataTime":  map[string]string{"type": "date", "format": "yyyy-MM-dd HH:mm:ss"}, | ||||||
|  | 				"open":      map[string]string{"type": "float"}, | ||||||
|  | 				"high":      map[string]string{"type": "float"}, | ||||||
|  | 				"low":       map[string]string{"type": "float"}, | ||||||
|  | 				"close":     map[string]string{"type": "float"}, | ||||||
|  | 				"volume":    map[string]string{"type": "float"}, | ||||||
|  | 				"volumeCcy": map[string]string{"type": "float"}, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 	case "ma": | ||||||
|  | 		mappings = map[string]interface{}{ | ||||||
|  | 			"properties": map[string]interface{}{ | ||||||
|  | 				"dataTime": map[string]string{"type": "date", "format": "yyyy-MM-dd HH:mm:ss"}, | ||||||
|  | 				"ma_value": map[string]string{"type": "float"}, // MA 值 | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 	default: | ||||||
|  | 		return fmt.Errorf("unsupported data type: %s", dataType) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	template := map[string]interface{}{ | ||||||
|  | 		"index_patterns": []string{indexPattern}, | ||||||
|  | 		"template": map[string]interface{}{ | ||||||
|  | 			"settings": map[string]interface{}{ | ||||||
|  | 				"number_of_shards":     dataConfig.Shards, | ||||||
|  | 				"number_of_replicas":   dataConfig.Replicas, | ||||||
|  | 				"index.lifecycle.name": policyName, | ||||||
|  | 			}, | ||||||
|  | 			"mappings": mappings, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	templateURL := fmt.Sprintf("%s/_index_template/%s", esConfig.URL, templateName) | ||||||
|  | 	templateData, _ := json.Marshal(template) | ||||||
|  | 	req, err := http.NewRequest("PUT", templateURL, bytes.NewBuffer(templateData)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("failed to create template request: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	req.Header.Set("Content-Type", "application/json") | ||||||
|  | 	req.SetBasicAuth(esConfig.Auth.Username, esConfig.Auth.Password) | ||||||
|  | 
 | ||||||
|  | 	resp, err := client.Do(req) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("failed to send template request: %v", err) | ||||||
|  | 	} | ||||||
|  | 	defer resp.Body.Close() | ||||||
|  | 
 | ||||||
|  | 	if resp.StatusCode != http.StatusOK { | ||||||
|  | 		body, _ := io.ReadAll(resp.Body) | ||||||
|  | 		return fmt.Errorf("failed to create template, status: %d, response: %s", resp.StatusCode, string(body)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	fmt.Printf("Successfully configured ILM template: %s\n", templateName) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // getPhase 根据时间差和保留时间决定初始阶段 | ||||||
|  | func getPhase(daysDiff, retentionDays int) string { | ||||||
|  | 	if daysDiff > 730 || retentionDays < 180 { // 超过 2 年或保留时间少于 180 天 | ||||||
|  | 		return "cold" | ||||||
|  | 	} | ||||||
|  | 	return "normal" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ensureILMPolicy 创建或更新 ILM 策略 | ||||||
|  | func ensureILMPolicy(client *http.Client, esConfig cfg.ElasticsearchConfig, policyName string, daysDiff, retentionDays int, dataConfig cfg.DataTypeConfig) error { | ||||||
|  | 	policyURL := fmt.Sprintf("%s/_ilm/policy/%s", esConfig.URL, policyName) | ||||||
|  | 	resp, err := client.Get(policyURL) | ||||||
|  | 	if err == nil && resp.StatusCode == http.StatusOK { | ||||||
|  | 		resp.Body.Close() | ||||||
|  | 		return nil // 策略已存在 | ||||||
|  | 	} | ||||||
|  | 	resp.Body.Close() | ||||||
|  | 
 | ||||||
|  | 	var policy map[string]interface{} | ||||||
|  | 	if daysDiff > 730 || retentionDays < 180 { | ||||||
|  | 		policy = map[string]interface{}{ | ||||||
|  | 			"policy": map[string]interface{}{ | ||||||
|  | 				"phases": map[string]interface{}{ | ||||||
|  | 					"cold": map[string]interface{}{ | ||||||
|  | 						"min_age": "0d", | ||||||
|  | 						"actions": map[string]interface{}{ | ||||||
|  | 							"allocate": map[string]interface{}{ | ||||||
|  | 								"include": map[string]string{"_tier_preference": "data_cold"}, | ||||||
|  | 							}, | ||||||
|  | 							"shrink": map[string]interface{}{ | ||||||
|  | 								"number_of_shards": 1, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 					"delete": map[string]interface{}{ | ||||||
|  | 						"min_age": fmt.Sprintf("%dd", retentionDays), | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		policy = map[string]interface{}{ | ||||||
|  | 			"policy": map[string]interface{}{ | ||||||
|  | 				"phases": map[string]interface{}{ | ||||||
|  | 					"hot": map[string]interface{}{ | ||||||
|  | 						"actions": map[string]interface{}{ | ||||||
|  | 							"rollover": dataConfig.NormalRollover, | ||||||
|  | 							"allocate": map[string]interface{}{ | ||||||
|  | 								"include": map[string]string{"_tier_preference": "data_hot"}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 					"warm": map[string]interface{}{ | ||||||
|  | 						"min_age": fmt.Sprintf("%dd", dataConfig.NormalPhases["warm"]), | ||||||
|  | 						"actions": map[string]interface{}{ | ||||||
|  | 							"allocate": map[string]interface{}{ | ||||||
|  | 								"include": map[string]string{"_tier_preference": "data_warm"}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 					"cold": map[string]interface{}{ | ||||||
|  | 						"min_age": fmt.Sprintf("%dd", dataConfig.NormalPhases["cold"]), | ||||||
|  | 						"actions": map[string]interface{}{ | ||||||
|  | 							"allocate": map[string]interface{}{ | ||||||
|  | 								"include": map[string]string{"_tier_preference": "data_cold"}, | ||||||
|  | 							}, | ||||||
|  | 							"shrink": map[string]interface{}{ | ||||||
|  | 								"number_of_shards": 1, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 					"delete": map[string]interface{}{ | ||||||
|  | 						"min_age": fmt.Sprintf("%dd", retentionDays), | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	policyData, _ := json.Marshal(policy) | ||||||
|  | 	req, err := http.NewRequest("PUT", policyURL, bytes.NewBuffer(policyData)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("failed to create ILM policy request: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	req.Header.Set("Content-Type", "application/json") | ||||||
|  | 	req.SetBasicAuth(esConfig.Auth.Username, esConfig.Auth.Password) | ||||||
|  | 
 | ||||||
|  | 	resp, err = client.Do(req) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("failed to send ILM policy request: %v", err) | ||||||
|  | 	} | ||||||
|  | 	defer resp.Body.Close() | ||||||
|  | 
 | ||||||
|  | 	if resp.StatusCode != http.StatusOK { | ||||||
|  | 		body, _ := io.ReadAll(resp.Body) | ||||||
|  | 		return fmt.Errorf("failed to create ILM policy, status: %d, response: %s", resp.StatusCode, string(body)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	fmt.Printf("Successfully created ILM policy: %s\n", policyName) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
| @ -6,8 +6,8 @@ import ( | |||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	cfg "gitea.zjmud.xyz/phyer/tanya/config" | 	cfg "gitea.zjmud.xyz/phyer/tanya/config" | ||||||
|  | 	"gitea.zjmud.xyz/phyer/tanya/elasticilm" | ||||||
| 	"io" | 	"io" | ||||||
| 	"math" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| @ -384,47 +384,7 @@ func calculateCrossPrice(price1, price2 string) (string, error) { | |||||||
| 	return fmt.Sprintf("%.8f", result), nil | 	return fmt.Sprintf("%.8f", result), nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // parsePeriod 将周期字符串转换为time.Duration | // ToElastic 将 Candle 数据发送到 Elasticsearch | ||||||
| func parsePeriod(period string) (time.Duration, error) { |  | ||||||
| 	switch period { |  | ||||||
| 	case "1m": |  | ||||||
| 		return time.Minute, nil |  | ||||||
| 	case "3m": |  | ||||||
| 		return 3 * time.Minute, nil |  | ||||||
| 	case "5m": |  | ||||||
| 		return 5 * time.Minute, nil |  | ||||||
| 	case "15m": |  | ||||||
| 		return 15 * time.Minute, nil |  | ||||||
| 	case "30m": |  | ||||||
| 		return 30 * time.Minute, nil |  | ||||||
| 	case "1H": |  | ||||||
| 		return time.Hour, nil |  | ||||||
| 	case "2H": |  | ||||||
| 		return 2 * time.Hour, nil |  | ||||||
| 	case "4H": |  | ||||||
| 		return 4 * time.Hour, nil |  | ||||||
| 	case "6H": |  | ||||||
| 		return 6 * time.Hour, nil |  | ||||||
| 	case "12H": |  | ||||||
| 		return 12 * time.Hour, nil |  | ||||||
| 	case "1D": |  | ||||||
| 		return 24 * time.Hour, nil |  | ||||||
| 	case "D": |  | ||||||
| 		return 24 * time.Hour, nil |  | ||||||
| 	case "2D": |  | ||||||
| 		return 48 * time.Hour, nil |  | ||||||
| 	case "5D": |  | ||||||
| 		return 120 * time.Hour, nil |  | ||||||
| 	case "7D": |  | ||||||
| 		return 168 * time.Hour, nil |  | ||||||
| 	default: |  | ||||||
| 		return 0, fmt.Errorf("unsupported period: %s", period) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // ToElastic 将Candle数据发送到Elasticsearch |  | ||||||
| 
 |  | ||||||
| // ToElastic 将 Candle 数据发送到 Elasticsearch,并动态配置 ILM 策略 |  | ||||||
| func (cl *CandleList) ToElastic() error { | func (cl *CandleList) ToElastic() error { | ||||||
| 	client := &http.Client{Timeout: 30 * time.Second} | 	client := &http.Client{Timeout: 30 * time.Second} | ||||||
| 
 | 
 | ||||||
| @ -445,17 +405,35 @@ func (cl *CandleList) ToElastic() error { | |||||||
| 		return fmt.Errorf("failed to load config: %v", err) | 		return fmt.Errorf("failed to load config: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// 如果没有数据,直接返回 | ||||||
|  | 	if len(cl.Candles) == 0 { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 使用第一条数据的时间戳配置 ILM | ||||||
|  | 	firstCandle := cl.Candles[0] | ||||||
|  | 	ts, err := strconv.ParseInt(firstCandle.Timestamp, 10, 64) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("invalid timestamp in first candle: %v", err) | ||||||
|  | 	} | ||||||
|  | 	loc, _ := time.LoadLocation("Asia/Shanghai") | ||||||
|  | 	cstTime := time.UnixMilli(ts).In(loc) | ||||||
|  | 
 | ||||||
|  | 	// 配置 ILM(只执行一次) | ||||||
|  | 	err = elasticilm.ConfigureILM(client, config, "candle", strings.ToLower(cl.Period), cstTime) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("failed to configure ILM: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 批量插入数据 | ||||||
| 	for _, candle := range cl.Candles { | 	for _, candle := range cl.Candles { | ||||||
| 		ts, err := strconv.ParseInt(candle.Timestamp, 10, 64) | 		ts, err := strconv.ParseInt(candle.Timestamp, 10, 64) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			fmt.Println("invalid timestamp:", ts, err) | 			fmt.Println("invalid timestamp:", candle.Timestamp, err) | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		loc, _ := time.LoadLocation("Asia/Shanghai") |  | ||||||
| 		cstTime := time.UnixMilli(ts).In(loc) | 		cstTime := time.UnixMilli(ts).In(loc) | ||||||
| 
 |  | ||||||
| 		// 新索引名称:logstash-<period>.candle.<coinpair>.<year>-<month> |  | ||||||
| 		index := fmt.Sprintf("logstash-%s.candle.%s.%d-%02d", | 		index := fmt.Sprintf("logstash-%s.candle.%s.%d-%02d", | ||||||
| 			strings.ToLower(cl.Period), | 			strings.ToLower(cl.Period), | ||||||
| 			strings.ToLower(cl.CoinPair), | 			strings.ToLower(cl.CoinPair), | ||||||
| @ -463,7 +441,7 @@ func (cl *CandleList) ToElastic() error { | |||||||
| 			int(cstTime.Month()), | 			int(cstTime.Month()), | ||||||
| 		) | 		) | ||||||
| 
 | 
 | ||||||
| 		// 验证时间戳对齐 | 		// 检查时间戳对齐 | ||||||
| 		if cl.Period == "1D" { | 		if cl.Period == "1D" { | ||||||
| 			if cstTime.Hour() != 0 || cstTime.Minute() != 0 || cstTime.Second() != 0 { | 			if cstTime.Hour() != 0 || cstTime.Minute() != 0 || cstTime.Second() != 0 { | ||||||
| 				return fmt.Errorf("timestamp %d (%s) is not aligned with period %s", ts, cstTime.Format("2006-01-02 15:04:05"), cl.Period) | 				return fmt.Errorf("timestamp %d (%s) is not aligned with period %s", ts, cstTime.Format("2006-01-02 15:04:05"), cl.Period) | ||||||
| @ -474,13 +452,6 @@ func (cl *CandleList) ToElastic() error { | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		// 动态配置 ILM |  | ||||||
| 		err = configureILM(client, config, index, cstTime, cl.Period) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return fmt.Errorf("failed to configure ILM: %v", err) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// 构造记录 |  | ||||||
| 		record := map[string]interface{}{ | 		record := map[string]interface{}{ | ||||||
| 			"dataTime":  cstTime.Format("2006-01-02 15:04:05"), | 			"dataTime":  cstTime.Format("2006-01-02 15:04:05"), | ||||||
| 			"open":      candle.Open, | 			"open":      candle.Open, | ||||||
| @ -532,246 +503,40 @@ func (cl *CandleList) ToElastic() error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // configureILM 动态配置 ILM 策略 | // parsePeriod 将时间框架字符串转换为 time.Duration | ||||||
| func configureILM(client *http.Client, config *cfg.Config, index string, indexTime time.Time, period string) error { | func parsePeriod(period string) (time.Duration, error) { | ||||||
| 	now := time.Now() |  | ||||||
| 	daysDiff := int(now.Sub(indexTime).Hours() / 24) // 距今天数 |  | ||||||
| 
 |  | ||||||
| 	// 时间框架转换为分钟数 |  | ||||||
| 	minutes := periodToMinutes(period) |  | ||||||
| 
 |  | ||||||
| 	// 计算保留时间(天) |  | ||||||
| 	maxRetention := getMaxRetention(period) |  | ||||||
| 	retentionDays := calculateRetention(daysDiff, minutes, maxRetention) |  | ||||||
| 
 |  | ||||||
| 	// 决定 ILM 策略 |  | ||||||
| 	var ilmPolicy string |  | ||||||
| 	if daysDiff > 730 || retentionDays < 180 { // 超过 2 年或保留时间短,直<EFBC8C><E79BB4><EFBFBD> cold |  | ||||||
| 		ilmPolicy = fmt.Sprintf("logstash_candle_%s_%d_cold", period, indexTime.Year()) |  | ||||||
| 	} else { |  | ||||||
| 		ilmPolicy = fmt.Sprintf("logstash_candle_%s_%d_normal", period, indexTime.Year()) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// 创建 ILM 策略 |  | ||||||
| 	if err := ensureILMPolicy(client, config, ilmPolicy, daysDiff, retentionDays); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// 配置索引模板 |  | ||||||
| 	templateName := fmt.Sprintf("logstash-candle-%s-%d-%02d", period, indexTime.Year(), int(indexTime.Month())) |  | ||||||
| 	template := map[string]interface{}{ |  | ||||||
| 		"index_patterns": []string{fmt.Sprintf("logstash-%s.candle.*.%d-%02d", period, indexTime.Year(), int(indexTime.Month()))}, |  | ||||||
| 		"template": map[string]interface{}{ |  | ||||||
| 			"settings": map[string]interface{}{ |  | ||||||
| 				"number_of_shards":     2, |  | ||||||
| 				"number_of_replicas":   1, |  | ||||||
| 				"index.lifecycle.name": ilmPolicy, |  | ||||||
| 			}, |  | ||||||
| 			"mappings": map[string]interface{}{ |  | ||||||
| 				"properties": map[string]interface{}{ |  | ||||||
| 					"dataTime":  map[string]string{"type": "date", "format": "yyyy-MM-dd HH:mm:ss"}, |  | ||||||
| 					"open":      map[string]string{"type": "float"}, |  | ||||||
| 					"high":      map[string]string{"type": "float"}, |  | ||||||
| 					"low":       map[string]string{"type": "float"}, |  | ||||||
| 					"close":     map[string]string{"type": "float"}, |  | ||||||
| 					"volume":    map[string]string{"type": "float"}, |  | ||||||
| 					"volumeCcy": map[string]string{"type": "float"}, |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	templateURL := fmt.Sprintf("%s/_index_template/%s", strings.TrimRight(config.Elasticsearch.URL, "/"), templateName) |  | ||||||
| 	templateData, _ := json.Marshal(template) |  | ||||||
| 	req, err := http.NewRequest("PUT", templateURL, bytes.NewBuffer(templateData)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("failed to create template request: %v", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	req.Header.Set("Content-Type", "application/json") |  | ||||||
| 	req.SetBasicAuth(config.Elasticsearch.Auth.Username, config.Elasticsearch.Auth.Password) |  | ||||||
| 
 |  | ||||||
| 	resp, err := client.Do(req) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("failed to create index template: %v", err) |  | ||||||
| 	} |  | ||||||
| 	defer resp.Body.Close() |  | ||||||
| 
 |  | ||||||
| 	if resp.StatusCode != http.StatusOK { |  | ||||||
| 		body, _ := io.ReadAll(resp.Body) |  | ||||||
| 		return fmt.Errorf("failed to create template, status: %d, response: %s", resp.StatusCode, string(body)) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	fmt.Printf("Successfully configured ILM template: %s\n", templateName) |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // periodToMinutes 将时间框架转换为分钟数 |  | ||||||
| func periodToMinutes(period string) int { |  | ||||||
| 	switch period { | 	switch period { | ||||||
| 	case "1m": | 	case "1m": | ||||||
| 		return 1 | 		return time.Minute, nil | ||||||
| 	case "3m": | 	case "3m": | ||||||
| 		return 3 | 		return 3 * time.Minute, nil | ||||||
| 	case "5m": | 	case "5m": | ||||||
| 		return 5 | 		return 5 * time.Minute, nil | ||||||
| 	case "15m": | 	case "15m": | ||||||
| 		return 15 | 		return 15 * time.Minute, nil | ||||||
| 	case "30m": | 	case "30m": | ||||||
| 		return 30 | 		return 30 * time.Minute, nil | ||||||
| 	case "1h": | 	case "1h": | ||||||
| 		return 60 | 		return time.Hour, nil | ||||||
| 	case "2h": | 	case "2h": | ||||||
| 		return 120 | 		return 2 * time.Hour, nil | ||||||
| 	case "4h": | 	case "4h": | ||||||
| 		return 240 | 		return 4 * time.Hour, nil | ||||||
| 	case "6h": | 	case "6h": | ||||||
| 		return 360 | 		return 6 * time.Hour, nil | ||||||
| 	case "12h": | 	case "12h": | ||||||
| 		return 720 | 		return 12 * time.Hour, nil | ||||||
| 	case "1d", "1D": | 	case "1D", "1d": | ||||||
| 		return 1440 | 		return 24 * time.Hour, nil | ||||||
| 	case "2d": | 	case "2d": | ||||||
| 		return 2880 | 		return 48 * time.Hour, nil | ||||||
| 	case "5d": | 	case "5d": | ||||||
| 		return 7200 | 		return 120 * time.Hour, nil | ||||||
| 	case "1W": | 	case "1W": | ||||||
| 		return 10080 | 		return 168 * time.Hour, nil | ||||||
| 	case "1M": | 	case "1M": | ||||||
| 		return 43200 // 假设 30 天 | 		return 720 * time.Hour, nil // 假设 30 天 | ||||||
| 	default: | 	default: | ||||||
| 		return 1 // 默认 1 分钟 | 		return 0, fmt.Errorf("unsupported period: %s", period) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 |  | ||||||
| // getMaxRetention 获取时间框架的最大保留时间(天) |  | ||||||
| func getMaxRetention(period string) int { |  | ||||||
| 	switch period { |  | ||||||
| 	case "1m", "3m": |  | ||||||
| 		return 365 // 1 年 |  | ||||||
| 	case "5m", "15m", "30m": |  | ||||||
| 		return 730 // 2 年 |  | ||||||
| 	case "1h", "2h": |  | ||||||
| 		return 1095 // 3 年 |  | ||||||
| 	case "4h", "6h", "12h": |  | ||||||
| 		return 1825 // 5 年 |  | ||||||
| 	case "1d", "1D", "2d", "5d": |  | ||||||
| 		return 2555 // 7 年 |  | ||||||
| 	case "1W", "1M": |  | ||||||
| 		return 3650 // 10 年 |  | ||||||
| 	default: |  | ||||||
| 		return 365 |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // calculateRetention 计算保留时间(天) |  | ||||||
| func calculateRetention(daysDiff, minutes, maxRetention int) int { |  | ||||||
| 	timeFactor := 1 - math.Sqrt(float64(daysDiff))/50 // 距今时间非线性衰减 |  | ||||||
| 	if timeFactor < 0 { |  | ||||||
| 		timeFactor = 0 |  | ||||||
| 	} |  | ||||||
| 	periodFactor := math.Sqrt(float64(minutes)) / math.Sqrt(43200) // 时间框架非线性增长 |  | ||||||
| 	retention := float64(maxRetention) * timeFactor * periodFactor |  | ||||||
| 	if retention < 30 { |  | ||||||
| 		return 30 // 最小保留 30 天 |  | ||||||
| 	} |  | ||||||
| 	return int(retention) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // ensureILMPolicy 确保 ILM 策略存在 |  | ||||||
| func ensureILMPolicy(client *http.Client, config *cfg.Config, policyName string, daysDiff, retentionDays int) error { |  | ||||||
| 	policyURL := fmt.Sprintf("%s/_ilm/policy/%s", strings.TrimRight(config.Elasticsearch.URL, "/"), policyName) |  | ||||||
| 	resp, err := client.Get(policyURL) |  | ||||||
| 	if err == nil && resp.StatusCode == http.StatusOK { |  | ||||||
| 		resp.Body.Close() |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 	resp.Body.Close() |  | ||||||
| 
 |  | ||||||
| 	var policy map[string]interface{} |  | ||||||
| 	if daysDiff > 730 || retentionDays < 180 { // 超过 2 年或保留时间短,直接 cold |  | ||||||
| 		policy = map[string]interface{}{ |  | ||||||
| 			"policy": map[string]interface{}{ |  | ||||||
| 				"phases": map[string]interface{}{ |  | ||||||
| 					"cold": map[string]interface{}{ |  | ||||||
| 						"min_age": "0d", |  | ||||||
| 						"actions": map[string]interface{}{ |  | ||||||
| 							"allocate": map[string]interface{}{ |  | ||||||
| 								"include": map[string]string{"_tier_preference": "data_cold"}, |  | ||||||
| 							}, |  | ||||||
| 							"shrink": map[string]interface{}{ |  | ||||||
| 								"number_of_shards": 1, |  | ||||||
| 							}, |  | ||||||
| 						}, |  | ||||||
| 					}, |  | ||||||
| 					"delete": map[string]interface{}{ |  | ||||||
| 						"min_age": fmt.Sprintf("%dd", retentionDays), |  | ||||||
| 					}, |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 		} |  | ||||||
| 	} else { |  | ||||||
| 		policy = map[string]interface{}{ |  | ||||||
| 			"policy": map[string]interface{}{ |  | ||||||
| 				"phases": map[string]interface{}{ |  | ||||||
| 					"hot": map[string]interface{}{ |  | ||||||
| 						"actions": map[string]interface{}{ |  | ||||||
| 							"rollover": map[string]interface{}{ |  | ||||||
| 								"max_size": "30GB", |  | ||||||
| 								"max_age":  "14d", |  | ||||||
| 							}, |  | ||||||
| 							"allocate": map[string]interface{}{ |  | ||||||
| 								"include": map[string]string{"_tier_preference": "data_hot"}, |  | ||||||
| 							}, |  | ||||||
| 						}, |  | ||||||
| 					}, |  | ||||||
| 					"warm": map[string]interface{}{ |  | ||||||
| 						"min_age": "60d", |  | ||||||
| 						"actions": map[string]interface{}{ |  | ||||||
| 							"allocate": map[string]interface{}{ |  | ||||||
| 								"include": map[string]string{"_tier_preference": "data_warm"}, |  | ||||||
| 							}, |  | ||||||
| 						}, |  | ||||||
| 					}, |  | ||||||
| 					"cold": map[string]interface{}{ |  | ||||||
| 						"min_age": "180d", |  | ||||||
| 						"actions": map[string]interface{}{ |  | ||||||
| 							"allocate": map[string]interface{}{ |  | ||||||
| 								"include": map[string]string{"_tier_preference": "data_cold"}, |  | ||||||
| 							}, |  | ||||||
| 							"shrink": map[string]interface{}{ |  | ||||||
| 								"number_of_shards": 1, |  | ||||||
| 							}, |  | ||||||
| 						}, |  | ||||||
| 					}, |  | ||||||
| 					"delete": map[string]interface{}{ |  | ||||||
| 						"min_age": fmt.Sprintf("%dd", retentionDays), |  | ||||||
| 					}, |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	policyData, _ := json.Marshal(policy) |  | ||||||
| 	req, err := http.NewRequest("PUT", policyURL, bytes.NewBuffer(policyData)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("failed to create ILM policy request: %v", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	req.Header.Set("Content-Type", "application/json") |  | ||||||
| 	req.SetBasicAuth(config.Elasticsearch.Auth.Username, config.Elasticsearch.Auth.Password) |  | ||||||
| 
 |  | ||||||
| 	resp, err = client.Do(req) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("failed to create ILM policy: %v", err) |  | ||||||
| 	} |  | ||||||
| 	defer resp.Body.Close() |  | ||||||
| 
 |  | ||||||
| 	if resp.StatusCode != http.StatusOK { |  | ||||||
| 		body, _ := io.ReadAll(resp.Body) |  | ||||||
| 		return fmt.Errorf("failed to create ILM policy, status: %d, response: %s", resp.StatusCode, string(body)) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	fmt.Printf("Successfully created ILM policy: %s\n", policyName) |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 zhangkun9038@dingtalk.com
						zhangkun9038@dingtalk.com