据说索引都要有alias,alias不能跟别的索引重复,加上了相关逻辑,有待后续确认
This commit is contained in:
parent
e4f05bb543
commit
dcb41e3266
@ -1,5 +0,0 @@
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
RUN fluent-gem install fluent-plugin-elasticsearch fluent-plugin-rewrite-tag-filter
|
||||
>>>>>>> Snippet
|
@ -35,6 +35,13 @@ type ILMConfig struct {
|
||||
}
|
||||
|
||||
// DataTypeConfig 每种数据类型的 ILM 配置
|
||||
type TimeParameters struct {
|
||||
TimestampFormat string `json:"timestamp_format"`
|
||||
DefaultAfter string `json:"default_after"`
|
||||
DefaultBefore string `json:"default_before"`
|
||||
MaxRangeDays int `json:"max_range_days"`
|
||||
}
|
||||
|
||||
type DataTypeConfig struct {
|
||||
IndexPattern string `json:"index_pattern"`
|
||||
MaxRetention map[string]int `json:"max_retention"`
|
||||
@ -43,6 +50,7 @@ type DataTypeConfig struct {
|
||||
Replicas int `json:"replicas"`
|
||||
NormalRollover map[string]string `json:"normal_rollover"`
|
||||
NormalPhases map[string]int `json:"normal_phases"`
|
||||
TimeParameters TimeParameters `json:"time_parameters"`
|
||||
}
|
||||
|
||||
// LoadConfig 加载配置文件
|
||||
|
@ -35,12 +35,14 @@
|
||||
"replicas": 1,
|
||||
"normal_rollover": {
|
||||
"max_size": "30GB",
|
||||
"max_age": "14d"
|
||||
"max_age": "14d",
|
||||
"max_docs": "1000000"
|
||||
},
|
||||
"normal_phases": {
|
||||
"warm": 60,
|
||||
"cold": 180
|
||||
}
|
||||
"normal_phases": {
|
||||
"warm": 60,
|
||||
"cold": 180,
|
||||
"delete": 365
|
||||
}
|
||||
},
|
||||
"ma": {
|
||||
"index_pattern": "logstash-%s.ma.*.%d-%02d",
|
||||
@ -55,10 +57,16 @@
|
||||
"max_size": "50GB",
|
||||
"max_age": "30d"
|
||||
},
|
||||
"normal_phases": {
|
||||
"warm": 90,
|
||||
"cold": 270
|
||||
}
|
||||
"normal_phases": {
|
||||
"warm": 90,
|
||||
"cold": 270
|
||||
},
|
||||
"time_parameters": {
|
||||
"timestamp_format": "unix_millis",
|
||||
"default_after": "now-7d",
|
||||
"default_before": "now",
|
||||
"max_range_days": 30
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
49
config/default_policy.json
Normal file
49
config/default_policy.json
Normal file
@ -0,0 +1,49 @@
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
{
|
||||
"policy": {
|
||||
"phases": {
|
||||
"hot": {
|
||||
"actions": {
|
||||
"rollover": {
|
||||
"max_size": "50GB",
|
||||
"max_age": "1d"
|
||||
},
|
||||
"allocate": {
|
||||
"include": {
|
||||
"_tier_preference": "data_hot"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"warm": {
|
||||
"min_age": "1d",
|
||||
"actions": {
|
||||
"allocate": {
|
||||
"include": {
|
||||
"_tier_preference": "data_warm"
|
||||
}
|
||||
},
|
||||
"shrink": {
|
||||
"number_of_shards": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"cold": {
|
||||
"min_age": "7d",
|
||||
"actions": {
|
||||
"allocate": {
|
||||
"include": {
|
||||
"_tier_preference": "data_cold"
|
||||
}
|
||||
},
|
||||
"shrink": {
|
||||
"number_of_shards": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
>>>>>>> Snippet
|
||||
|
9
config/fluentd.yaml
Normal file
9
config/fluentd.yaml
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
host ${ENV['ELASTICSEARCH_HOST'] || "elasticsearch"}
|
||||
port ${ENV['ELASTICSEARCH_PORT'] || 9200}
|
||||
scheme ${ENV['ELASTICSEARCH_SCHEME'] || "http"}
|
||||
user ${ENV['ELASTICSEARCH_USER'] || "fluentd_user"}
|
||||
password ${ENV['ELASTICSEARCH_PASSWORD'] || "fluentd_password"}
|
||||
>>>>>>> Snippet
|
@ -7,6 +7,8 @@ import (
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cfg "gitea.zjmud.xyz/phyer/tanya/config" // 导入你的 config 包
|
||||
@ -82,190 +84,171 @@ func periodToMinutes(period string) int {
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
// getPhase 根据时间差和保留时间决定初始阶段
|
||||
func getPhase(daysDiff, retentionDays int) string {
|
||||
if daysDiff > 730 || retentionDays < 180 { // 超过 2 年或保留时间少于 180 天
|
||||
fmt.Printf("[ILM Phase] 数据已超过2年或保留时间少于180天,判定为cold阶段。daysDiff: %d, retentionDays: %d\n", daysDiff, retentionDays)
|
||||
return "cold"
|
||||
}
|
||||
|
||||
// 创建保留时间计算器
|
||||
calc := DefaultRetentionCalculator{
|
||||
MaxRetention: dataConfig.MaxRetention,
|
||||
MinRetention: dataConfig.MinRetention,
|
||||
if daysDiff <= 7 { // 7天内的数据为hot阶段
|
||||
fmt.Printf("[ILM Phase] 数据在7天内,判定为hot阶段。daysDiff: %d\n", daysDiff)
|
||||
return "hot"
|
||||
}
|
||||
fmt.Printf("[ILM Phase] 数据在7天到2年之间,判定为warm阶段。daysDiff: %d\n", daysDiff)
|
||||
return "warm"
|
||||
}
|
||||
|
||||
// 计算距今时间差和保留时间
|
||||
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))
|
||||
func ensureAlias(client *http.Client, esConfig cfg.ElasticsearchConfig, alias, period string) error {
|
||||
// 获取当前别名关联的索引
|
||||
getAliasURL := fmt.Sprintf("%s/_alias/%s", esConfig.URL, alias)
|
||||
req, err := http.NewRequest("GET", getAliasURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create template request: %v", err)
|
||||
return 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)
|
||||
return 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))
|
||||
}
|
||||
// 准备别名更新的操作
|
||||
var actions []interface{}
|
||||
var aliasInfo map[string]map[string]interface{}
|
||||
|
||||
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),
|
||||
"actions": map[string]interface{}{
|
||||
"delete": map[string]interface{}{}, // 添加 delete 动作
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 如果别名存在,解析当前关联的索引
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
if err := json.NewDecoder(resp.Body).Decode(&aliasInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
} 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"},
|
||||
|
||||
// 移除所有现有索引的写入索引标记
|
||||
for indexName, info := range aliasInfo {
|
||||
if aliases, ok := info["aliases"].(map[string]interface{}); ok {
|
||||
for aliasName, aliasDetails := range aliases {
|
||||
if aliasName == alias && aliasDetails.(map[string]interface{})["is_write_index"] == true {
|
||||
actions = append(actions, map[string]interface{}{
|
||||
"remove": map[string]interface{}{
|
||||
"index": indexName,
|
||||
"alias": alias,
|
||||
},
|
||||
},
|
||||
},
|
||||
"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),
|
||||
"actions": map[string]interface{}{
|
||||
"delete": map[string]interface{}{}, // 添加 delete 动作
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
policyData, _ := json.Marshal(policy)
|
||||
req, err := http.NewRequest("PUT", policyURL, bytes.NewBuffer(policyData))
|
||||
// 确定最新的索引作为写入索引
|
||||
var latestIndex string
|
||||
var latestTime time.Time
|
||||
for indexName := range aliasInfo {
|
||||
parts := strings.Split(indexName, ".")
|
||||
if len(parts) < 3 {
|
||||
continue
|
||||
}
|
||||
datePart := parts[len(parts)-1] // 例如 2025-03
|
||||
indexTime, err := time.Parse("2006-01", datePart)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if latestIndex == "" || indexTime.After(latestTime) {
|
||||
latestIndex = indexName
|
||||
latestTime = indexTime
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到合适的索引,创建一个新的索引模式
|
||||
if latestIndex == "" {
|
||||
latestIndex = fmt.Sprintf("logstash-%s.candle.okb-eth.%s", period, time.Now().Format("2006-01"))
|
||||
}
|
||||
|
||||
// 检查索引是否存在,如果不存在则创建
|
||||
indexURL := fmt.Sprintf("%s/%s", esConfig.URL, latestIndex)
|
||||
req, err = http.NewRequest("HEAD", indexURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create ILM policy request: %v", err)
|
||||
return err
|
||||
}
|
||||
req.SetBasicAuth(esConfig.Auth.Username, esConfig.Auth.Password)
|
||||
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
// 索引不存在,创建索引
|
||||
fmt.Printf("Index %s does not exist, creating it...\n", latestIndex)
|
||||
createIndexURL := fmt.Sprintf("%s/%s", esConfig.URL, latestIndex)
|
||||
// 简单的索引创建请求,可以根据需要添加更多设置
|
||||
indexData := map[string]interface{}{
|
||||
"settings": map[string]interface{}{
|
||||
"number_of_shards": 2, // 根据你的配置调整
|
||||
"number_of_replicas": 1, // 根据你的配置调整
|
||||
},
|
||||
}
|
||||
data, err := json.Marshal(indexData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal index creation data: %v", err)
|
||||
}
|
||||
|
||||
req, err = http.NewRequest("PUT", createIndexURL, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return 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 err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("failed to create index %s, status: %d, response: %s", latestIndex, resp.StatusCode, string(body))
|
||||
}
|
||||
fmt.Printf("Successfully created index: %s\n", latestIndex)
|
||||
}
|
||||
|
||||
// 添加最新的索引作为写入索引
|
||||
actions = append(actions, map[string]interface{}{
|
||||
"add": map[string]interface{}{
|
||||
"index": latestIndex,
|
||||
"alias": alias,
|
||||
"is_write_index": true,
|
||||
},
|
||||
})
|
||||
|
||||
// 确保其他索引的 is_write_index 为 false
|
||||
for indexName := range aliasInfo {
|
||||
if indexName != latestIndex {
|
||||
actions = append(actions, map[string]interface{}{
|
||||
"add": map[string]interface{}{
|
||||
"index": indexName,
|
||||
"alias": alias,
|
||||
"is_write_index": false,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 执行别名更新操作
|
||||
aliasURL := fmt.Sprintf("%s/_aliases", esConfig.URL)
|
||||
aliasData := map[string]interface{}{
|
||||
"actions": actions,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(aliasData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err = http.NewRequest("POST", aliasURL, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
@ -273,15 +256,325 @@ func ensureILMPolicy(client *http.Client, esConfig cfg.ElasticsearchConfig, poli
|
||||
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send ILM policy request: %v", err)
|
||||
return 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))
|
||||
return fmt.Errorf("failed to create alias, status: %d, response: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully created ILM policy: %s\n", policyName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func ConfigureILM(client *http.Client, config *cfg.Config, dataType, period string, indexTime time.Time) error {
|
||||
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)
|
||||
|
||||
// 确保保留时间不低于冷阶段的最小值
|
||||
minDeleteAge := dataConfig.NormalPhases["cold"]
|
||||
if retentionDays < minDeleteAge {
|
||||
retentionDays = minDeleteAge
|
||||
}
|
||||
|
||||
// 格式化策略名称、模板名称、索引模式和别名
|
||||
year, month := indexTime.Year(), int(indexTime.Month())
|
||||
policyName := fmt.Sprintf("logstash_%s_%s_%d_%02d", dataType, period, year, month)
|
||||
templateName := fmt.Sprintf("log_stash_%s_%s_%d_%02d", dataType, period, year, month)
|
||||
indexPattern := fmt.Sprintf(dataConfig.IndexPattern, period, year, month)
|
||||
aliasName := fmt.Sprintf("%s_alias", policyName)
|
||||
|
||||
fmt.Printf("[ConfigureILM] Configuring ILM with policyName: %s, templateName: %s, aliasName: %s\n", policyName, templateName, aliasName)
|
||||
|
||||
// 创建索引模板
|
||||
template := map[string]interface{}{
|
||||
"index_patterns": []string{indexPattern},
|
||||
"priority": 100,
|
||||
"template": map[string]interface{}{
|
||||
"settings": map[string]interface{}{
|
||||
"number_of_shards": dataConfig.Shards,
|
||||
"number_of_replicas": dataConfig.Replicas,
|
||||
"index.lifecycle.name": policyName,
|
||||
"index.lifecycle.rollover_alias": aliasName, // 确保与 aliasName 一致
|
||||
},
|
||||
"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", esConfig.URL, templateName)
|
||||
templateData, err := json.Marshal(template)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal index template data: %v", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("PUT", templateURL, bytes.NewBuffer(templateData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create index 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 index template request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("failed to create index template, status: %d, response: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
fmt.Printf("[ConfigureILM] Successfully configured ILM template: %s\n", templateName)
|
||||
|
||||
// 创建或更新 ILM 策略
|
||||
if err := ensureILMPolicy(client, esConfig, policyName, dataType, period, daysDiff, retentionDays, dataConfig); err != nil {
|
||||
return fmt.Errorf("failed to ensure ILM policy: %v", err)
|
||||
}
|
||||
|
||||
// 设置别名
|
||||
if err := ensureAlias(client, esConfig, aliasName, period); err != nil {
|
||||
return fmt.Errorf("failed to ensure alias: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("[ConfigureILM] Successfully completed ILM configuration for policy: %s\n", policyName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureILMPolicy(client *http.Client, esConfig cfg.ElasticsearchConfig, policyName, dataType, period string, daysDiff, retentionDays int, dataConfig cfg.DataTypeConfig) error {
|
||||
// 检查 default_policy 是否存在
|
||||
defaultPolicyURL := fmt.Sprintf("%s/_ilm/policy/default_policy", esConfig.URL)
|
||||
resp, err := client.Get(defaultPolicyURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check default_policy: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 如果 default_policy 不存在,则创建
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Println("[ILM Policy] Default policy does not exist, creating it...")
|
||||
defaultPolicy := map[string]interface{}{
|
||||
"policy": map[string]interface{}{
|
||||
"phases": map[string]interface{}{
|
||||
"hot": map[string]interface{}{
|
||||
"actions": map[string]interface{}{
|
||||
"set_priority": map[string]interface{}{
|
||||
"priority": 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
"delete": map[string]interface{}{
|
||||
"min_age": "365d",
|
||||
"actions": map[string]interface{}{
|
||||
"delete": map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
defaultPolicyData, err := json.Marshal(defaultPolicy)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal default_policy: %v", err)
|
||||
}
|
||||
req, err := http.NewRequest("PUT", defaultPolicyURL, bytes.NewBuffer(defaultPolicyData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create default_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 create default_policy: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("failed to create default_policy, status: %d, response: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
fmt.Println("[ILM Policy] Successfully created default_policy")
|
||||
}
|
||||
|
||||
// 检查目标策略是否存在
|
||||
policyURL := fmt.Sprintf("%s/_ilm/policy/%s", esConfig.URL, policyName)
|
||||
resp, err = client.Get(policyURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check ILM policy: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 如果策略已存在,更新索引设置并删除旧策略
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
fmt.Printf("[ILM Policy] Policy %s already exists, updating index settings...\n", policyName)
|
||||
|
||||
// 更新索引设置
|
||||
updateIndexURL := fmt.Sprintf("%s/logstash-%s.%s.*/_settings", esConfig.URL, period, dataType)
|
||||
updateData := map[string]interface{}{
|
||||
"index.lifecycle.name": "default_policy",
|
||||
"index.lifecycle.rollover_alias": fmt.Sprintf("logs-%s-%s-candle", dataType, period),
|
||||
}
|
||||
updateDataBytes, err := json.Marshal(updateData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal update index data: %v", err)
|
||||
}
|
||||
req, err := http.NewRequest("PUT", updateIndexURL, bytes.NewBuffer(updateDataBytes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create update index request: %v", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.SetBasicAuth(esConfig.Auth.Username, esConfig.Auth.Password)
|
||||
updateResp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update index settings: %v", err)
|
||||
}
|
||||
defer updateResp.Body.Close()
|
||||
if updateResp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(updateResp.Body)
|
||||
return fmt.Errorf("failed to update index settings, status: %d, response: %s", updateResp.StatusCode, string(body))
|
||||
}
|
||||
fmt.Printf("[ILM Policy] Successfully updated index settings for policy: %s\n", policyName)
|
||||
|
||||
// 删除旧策略
|
||||
req, err = http.NewRequest("DELETE", policyURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create delete policy request: %v", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.SetBasicAuth(esConfig.Auth.Username, esConfig.Auth.Password)
|
||||
deleteResp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete old policy: %v", err)
|
||||
}
|
||||
defer deleteResp.Body.Close()
|
||||
if deleteResp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(deleteResp.Body)
|
||||
return fmt.Errorf("failed to delete old policy, status: %d, response: %s", deleteResp.StatusCode, string(body))
|
||||
}
|
||||
fmt.Printf("[ILM Policy] Successfully deleted old policy: %s\n", policyName)
|
||||
} else {
|
||||
fmt.Printf("[ILM Policy] Policy %s does not exist, creating a new one...\n", policyName)
|
||||
}
|
||||
|
||||
// 创建新的 ILM 策略
|
||||
initialPhase := getPhase(daysDiff, retentionDays)
|
||||
fmt.Printf("[ILM Policy] Initial phase for policy %s is: %s\n", policyName, initialPhase)
|
||||
|
||||
rolloverActions := map[string]interface{}{
|
||||
"max_age": dataConfig.NormalRollover["max_age"],
|
||||
"max_size": dataConfig.NormalRollover["max_size"],
|
||||
}
|
||||
if maxDocsStr, ok := dataConfig.NormalRollover["max_docs"]; ok && maxDocsStr != "" {
|
||||
maxDocs, err := strconv.Atoi(maxDocsStr)
|
||||
if err != nil || maxDocs <= 0 {
|
||||
fmt.Printf("[ILM Policy] Invalid max_docs value: %s, skipping max_docs in rollover\n", maxDocsStr)
|
||||
} else {
|
||||
rolloverActions["max_docs"] = maxDocs
|
||||
}
|
||||
} else {
|
||||
fmt.Println("[ILM Policy] max_docs not set in NormalRollover, skipping max_docs in rollover")
|
||||
}
|
||||
|
||||
policy := map[string]interface{}{
|
||||
"policy": map[string]interface{}{
|
||||
"phases": map[string]interface{}{
|
||||
initialPhase: map[string]interface{}{
|
||||
"actions": map[string]interface{}{
|
||||
"rollover": rolloverActions,
|
||||
"set_priority": map[string]interface{}{
|
||||
"priority": 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
"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"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"delete": map[string]interface{}{
|
||||
"min_age": fmt.Sprintf("%dd", retentionDays),
|
||||
"actions": map[string]interface{}{
|
||||
"delete": map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 如果数据超过 730 天(2 年),直接进入冷阶段
|
||||
if daysDiff > 730 {
|
||||
phases, ok := policy["policy"].(map[string]interface{})["phases"].(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to get phases from policy")
|
||||
}
|
||||
delete(phases, initialPhase)
|
||||
if _, exists := phases["cold"]; !exists {
|
||||
phases["cold"] = map[string]interface{}{
|
||||
"min_age": "0d",
|
||||
"actions": map[string]interface{}{
|
||||
"allocate": map[string]interface{}{
|
||||
"include": map[string]string{"_tier_preference": "data_cold"},
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
coldPhase, ok := phases["cold"].(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to get cold phase from phases")
|
||||
}
|
||||
coldPhase["min_age"] = "0d"
|
||||
}
|
||||
}
|
||||
|
||||
// 创建或更新策略
|
||||
policyData, err := json.Marshal(policy)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal ILM policy: %v", err)
|
||||
}
|
||||
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("[ILM Policy] Successfully created or updated ILM policy: %s\n", policyName)
|
||||
return nil
|
||||
}
|
||||
|
11
fluentd-deployment.yaml
Normal file
11
fluentd-deployment.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
initContainers:
|
||||
- name: install-rewrite-tag-filter
|
||||
image: your-fluentd-image:version
|
||||
command: ["fluent-gem", "install", "fluent-plugin-rewrite-tag-filter"]
|
||||
containers:
|
||||
- name: fluentd
|
||||
image: your-fluentd-image:version
|
||||
>>>>>>> Snippet
|
29
fluentd.conf
Normal file
29
fluentd.conf
Normal file
@ -0,0 +1,29 @@
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
<match tanya.**>
|
||||
@type copy
|
||||
<store>
|
||||
@type elasticsearch
|
||||
@id output_elasticsearch_tanya
|
||||
host elasticsearch
|
||||
port 9200
|
||||
scheme http
|
||||
user fluentd_user
|
||||
password fluentd_password
|
||||
logstash_format true
|
||||
logstash_prefix tanya
|
||||
logstash_dateformat %Y.%m.%d
|
||||
flush_interval 5s
|
||||
@log_level debug
|
||||
id_key _id
|
||||
remove_keys _id
|
||||
include_tag_key true
|
||||
tag_key @log_name
|
||||
</store>
|
||||
<store>
|
||||
@type stdout
|
||||
@id output_stdout_tanya
|
||||
</store>
|
||||
</match>
|
||||
>>>>>>> Snippet
|
@ -388,7 +388,7 @@ func calculateCrossPrice(price1, price2 string) (string, error) {
|
||||
func (cl *CandleList) ToElastic() error {
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
|
||||
duration, err := parsePeriod(cl.Period)
|
||||
_, err := parsePeriod(cl.Period)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse period: %v", err)
|
||||
}
|
||||
@ -462,11 +462,62 @@ func (cl *CandleList) ToElastic() error {
|
||||
cstTime := time.UnixMilli(ts).In(loc)
|
||||
|
||||
// 时间戳对齐检查
|
||||
if cl.Period == "1D" {
|
||||
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)
|
||||
duration, err := parsePeriod(cl.Period)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse period: %v", err)
|
||||
}
|
||||
|
||||
// 对于日线及以上周期,检查是否为周期的整数倍
|
||||
if duration >= 24*time.Hour {
|
||||
// 特殊处理1D周期
|
||||
if cl.Period == "1D" {
|
||||
// 检查是否为当天的00:00:00
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
// 对于其他日线及以上周期,使用原来的检查逻辑
|
||||
totalHours := cstTime.Unix() / 3600
|
||||
periodHours := int64(duration.Hours())
|
||||
|
||||
if totalHours%periodHours != 0 {
|
||||
return fmt.Errorf("timestamp %d (%s) is not aligned with period %s", ts, cstTime.Format("2006-01-02 15:04:05"), cl.Period)
|
||||
}
|
||||
|
||||
if cstTime.Minute() != 0 || cstTime.Second() != 0 {
|
||||
return fmt.Errorf("timestamp %d (%s) has non-zero minutes/seconds for period %s", ts, cstTime.Format("2006-01-02 15:04:05"), cl.Period)
|
||||
}
|
||||
}
|
||||
} else if duration >= time.Hour {
|
||||
// 对于小时级周期,检查小时、分钟、秒是否对齐
|
||||
switch cl.Period {
|
||||
case "1H":
|
||||
if cstTime.Minute() != 0 || cstTime.Second() != 0 {
|
||||
return fmt.Errorf("timestamp %d (%s) has non-zero minutes/seconds for period %s", ts, cstTime.Format("2006-01-02 15:04:05"), cl.Period)
|
||||
}
|
||||
case "2H":
|
||||
if cstTime.Hour()%2 != 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)
|
||||
}
|
||||
case "4H":
|
||||
if cstTime.Hour()%4 != 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)
|
||||
}
|
||||
case "6H":
|
||||
if (cstTime.Hour()%6 != 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)
|
||||
}
|
||||
case "12H":
|
||||
if (cstTime.Hour() != 0 && cstTime.Hour() != 12) || 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)
|
||||
}
|
||||
default:
|
||||
if cstTime.UnixMilli()%duration.Milliseconds() != 0 {
|
||||
return fmt.Errorf("timestamp %d is not aligned with period %s", ts, cl.Period)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 对于分钟级周期,使用毫秒取模检查
|
||||
if cstTime.UnixMilli()%duration.Milliseconds() != 0 {
|
||||
return fmt.Errorf("timestamp %d is not aligned with period %s", ts, cl.Period)
|
||||
}
|
||||
|
@ -8,9 +8,48 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// getPeriodDuration 根据时间周期字符串返回对应的 duration
|
||||
func getPeriodDuration(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 "2D":
|
||||
return 2 * 24 * time.Hour, nil
|
||||
case "3D":
|
||||
return 3 * 24 * time.Hour, nil
|
||||
case "5D":
|
||||
return 5 * 24 * time.Hour, nil
|
||||
case "1W":
|
||||
return 7 * 24 * time.Hour, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported bar period: %s", period)
|
||||
}
|
||||
}
|
||||
|
||||
type OkxPublicDataService struct {
|
||||
BaseURL string
|
||||
client *http.Client
|
||||
@ -79,7 +118,46 @@ func (s *OkxPublicDataService) GetInstruments(params InstrumentsRequest) ([]Inst
|
||||
|
||||
// GetCandles 获取K线数据
|
||||
func (s *OkxPublicDataService) GetCandles(params CandlesRequest) ([]*Candle, error) {
|
||||
u, err := url.Parse(s.BaseURL + "/market/candles")
|
||||
// 根据时间范围选择不同的API端点
|
||||
endpoint := "/market/candles"
|
||||
|
||||
// 计算时间范围
|
||||
now := time.Now()
|
||||
var startTime time.Time
|
||||
var err error
|
||||
|
||||
// 优先使用 After 参数,如果都提供了
|
||||
if params.After != "" {
|
||||
// 将毫秒时间戳转换为 time.Time
|
||||
timestamp, err := strconv.ParseInt(params.After, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid After timestamp: %v", err)
|
||||
}
|
||||
startTime = time.Unix(0, timestamp*int64(time.Millisecond))
|
||||
} else if params.Before != "" {
|
||||
// 将毫秒时间戳转换为 time.Time
|
||||
timestamp, err := strconv.ParseInt(params.Before, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid Before timestamp: %v", err)
|
||||
}
|
||||
startTime = time.Unix(0, timestamp*int64(time.Millisecond))
|
||||
} else {
|
||||
return nil, fmt.Errorf("either After or Before parameter is required")
|
||||
}
|
||||
|
||||
// 根据时间维度计算20个周期
|
||||
unitDuration, err := getPeriodDuration(params.Bar)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
periodDuration := 20 * unitDuration
|
||||
|
||||
// 如果数据超过20个周期,使用历史数据接口
|
||||
if now.Sub(startTime) > periodDuration {
|
||||
endpoint = "/market/history-candles"
|
||||
}
|
||||
|
||||
u, err := url.Parse(s.BaseURL + endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -75,29 +75,39 @@ import (
|
||||
// if err != nil {
|
||||
// t.Fatalf("ToEs failed: %v", err)
|
||||
// }
|
||||
// }
|
||||
// }:q
|
||||
|
||||
func TestCandleListI_CalculateCrossPair(t *testing.T) {
|
||||
startTime := time.Date(2025, 2, 1, 0, 0, 0, 0, time.UTC)
|
||||
endTime := time.Date(2025, 3, 28, 0, 0, 0, 0, time.UTC)
|
||||
okbUsdt, err := MakeCandleList("OKB-USDT", "4H", startTime, endTime, 50)
|
||||
// 使用更早的时间范围来触发不同的phase
|
||||
startTime := time.Date(2022, 2, 1, 0, 0, 0, 0, time.UTC)
|
||||
endTime := time.Date(2022, 2, 28, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// 打印测试时间范围
|
||||
t.Logf("Test time range: %s to %s", startTime, endTime)
|
||||
okbUsdt, err := MakeCandleList("OKB-USDT", "30m", startTime, endTime, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("ToEs failed: %v", err)
|
||||
t.Fatalf("MakeCandleList failed: %v", err)
|
||||
}
|
||||
ethUsdt, err := MakeCandleList("ETH-USDT", "4H", startTime, endTime, 50)
|
||||
ethUsdt, err := MakeCandleList("ETH-USDT", "30m", startTime, endTime, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("ToEs failed: %v", err)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("ToEs failed: %v", err)
|
||||
t.Fatalf("MakeCandleList failed: %v", err)
|
||||
}
|
||||
|
||||
okbEth, err := okbUsdt.CalculateCrossPair(ethUsdt)
|
||||
if err != nil {
|
||||
t.Fatalf("ToEs failed: %v", err)
|
||||
t.Fatalf("CalculateCrossPair failed: %v", err)
|
||||
}
|
||||
|
||||
// 打印交叉对信息
|
||||
t.Logf("Cross pair: %s, period: %s, candle count: %d",
|
||||
okbEth.CoinPair, okbEth.Period, len(okbEth.Candles))
|
||||
|
||||
// 添加详细的日志输出
|
||||
err = okbEth.ToElastic()
|
||||
if err != nil {
|
||||
t.Fatalf("ToEs failed: %v", err)
|
||||
t.Fatalf("ToElastic failed: %v", err)
|
||||
}
|
||||
|
||||
// 打印成功信息
|
||||
t.Log("Test completed successfully")
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user