APM数据写入ES必须使用elastic/go-elasticsearch/v9客户端,因ES 8.0+移除_type、强制HTTPS、变更/_search路径、要求Content-Type: application/json,而olivere/elastic/v7未适配;需显式禁用健康检查、提前创建索引并固定mapping(如service.name设为keyword、@timestamp设为date)、Body必须用bytes.NewReader封装、批量写入必须用Bulk。

APM数据写入ES必须用elastic/go-elasticsearch/v9客户端
ES 8.0+(包括当前主流的 8.15、9.0)已移除_type、强制 HTTPS、变更/_search路径、要求Content-Type: application/json,而olivere/elastic/v7完全未适配这些变更。现象是:APM Server 能连上,但 Go 服务上报的 trace 数据在 ES 中查不到,或esapi.SearchRequest返回空 hits / 406 Not Acceptable,日志里却无明确错误提示——本质是协议断层,不是配置错。
必须统一使用elastic/go-elasticsearch/v9,且初始化时显式禁用健康检查:elasticsearch.Config{Addresses: []string{"https://es:9200"}, Transport: http.DefaultTransport, ...};若用 HTTP(仅限开发),需设SetScheme("http")并配自定义http.Transport超时,否则 goroutine 可能悬停。
APM索引名和mapping不能靠自动创建
APM Server 默认会向apm-*系列索引写数据,但 Go 服务若直接调用 ES API 上报自定义 metric 或 span,必须提前建好索引并固定 mapping。常见错误是只写不建,导致 ES 拒绝文档或字段类型被自动推断为text而非keyword,后续聚合查询失败。
-
CreateIndex时指定index_patterns: ["myapp-apm-span-*"],并绑定 ILM 策略 - mapping 中关键字段如
service.name、transaction.name必须设为keyword,否则terms aggregation查不出分组 - 时间字段
@timestamp必须用date类型,并配"format": "strict_date_optional_time||epoch_millis"
写入span/metric文档时Body必须是*bytes.Reader
esapi.IndexRequest对 Body 类型极其严格:传json.Marshal()结果本身([]byte)会静默失败,ES 返回400 Bad Request;必须包一层bytes.NewReader(body)。漏这步,请求体为空,APM 数据就根本进不去索引。
立即学习“go语言免费学习笔记(深入)”;
示例片段:
body, _ := json.Marshal(span)
req := esapi.IndexRequest{
Index: "myapp-apm-span-2026.07",
DocumentID: span.ID,
Body: bytes.NewReader(body), // 关键!不能是 body 或 strings.NewReader(string(body))
Refresh: "false", // 生产环境禁用 true
}
批量写入务必用Bulk,别循环Index:100 条 span 单发耗时可能超 2s,Bulk 合并后通常http.max_content_length限制(默认 100MB)。
解析搜索结果时hit.Source是*json.RawMessage
APM 场景下常需从 ES 拉取 span 做链路还原或统计,但直接json.Unmarshal(res.Body, &Span{})会失败——因为响应体是顶层{"hits": {...}}结构,而每个hit的原始 JSON 在hit._source下,且hit.Source是*json.RawMessage指针。
正确做法:
for _, hit := range res.Hits.Hits {
var span Span
if err := json.Unmarshal(*hit.Source, &span); err != nil {
log.Printf("failed to unmarshal span: %v", err)
continue
}
// 处理 span
}
这个细节极容易被忽略,尤其在调试 trace 查询为空时,第一反应总以为是 query 写错,其实只是反序列化失败导致字段全空。


















