在很多的设计中,我们所采集的数据来自不同的数据源,从而导致数据字段名称的不一致。如果,我们在一开始就遵循 Elastic Common Schema,那么我们就不会有任何的问题。但是在实际的生产环境中,有可能在一开始我们就没有这么做,那我们该如何解决这个问题呢?比如我们有如下的两个数据:
POST logs_server1/_doc/
{
"level": "info"
}
POST logs_server2/_doc/
{
"log_level": "info"
}
在上面的两个数据是来自两个不同的服务器,在当时设计的时候,表示 log 的级别分别用了不同的字段:level 及 log_level。显然这两个不同的字段不便于我们统计数据。安装 Elastic Common Schema 的要求,正确的字段应该是 log.level。那么我们在不改变原有的 log 的设计基础之上,该如何实现符号 ECS 规范的 mapping 呢?
在之前的文章 “Elasticsearch : alias 数据类型”,我已经讲述了 alias 的数据类型。在今天的文章中,我来详细描述如何使用 alias 来解决这个问题。
准备数据
我们安装上面所显示的那样,把两个数据导入到 Elasticsearch 中:
POST logs_server1/_doc/
{
"level": "info"
}
POST logs_server2/_doc/
{
"log_level": "info"
}
我们可以通过如下的命令来检查一下这两个索引的 mapping:
GET logs_server1/_mapping
{
"logs_server1" : {
"mappings" : {
"properties" : {
"level" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
}
GET logs_server2/_mapping
{
"logs_server2" : {
"mappings" : {
"properties" : {
"log_level" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
}
显然上面的两个索引的 mapping 都是不一样的。
如果我们想统计一下 logs 按照级别 level 进行统计的话,我们只能按照如下的方法来进行:
GET logs_server*/_search
{
"size": 0,
"aggs": {
"levels": {
"terms": {
"script": {
"source": """
if (doc.containsKey('level.keyword')) {
return doc['level.keyword'].value
} else {
return doc['log_level.keyword'].value
}
"""
}
}
}
}
}
在上面,我使用了 script 来进行统计。在上面脚本中的 doc,其实就是 doc_values。如果大家对这个 doc 的方法不是很熟悉的话,请参阅我之前的文章 “Elasticsearch:Painless 编程调试”。我们可以使用如下的方法:
GET logs_server*/_search
{
"size": 0,
"aggs": {
"levels": {
"terms": {
"script": {
"source": """
Debug.explain(doc)
"""
}
}
}
}
}
上面的查询会导致如下的错误信息:
{
"error" : {
"root_cause" : [
{
"type" : "script_exception",
"reason" : "runtime error",
"to_string" : "org.elasticsearch.search.lookup.LeafDocLookup@6814c133",
"java_class" : "org.elasticsearch.search.lookup.LeafDocLookup",
"script_stack" : [
"Debug.explain(doc)\n ",
" ^---- HERE"
],
"script" : "\n Debug.explain(doc)\n ",
"lang" : "painless",
"position" : {
"offset" : 27,
"start" : 13,
"end" : 42
}
},
{
"type" : "script_exception",
"reason" : "runtime error",
"to_string" : "org.elasticsearch.search.lookup.LeafDocLookup@6454e4d",
"java_class" : "org.elasticsearch.search.lookup.LeafDocLookup",
"script_stack" : [
"Debug.explain(doc)\n ",
" ^---- HERE"
],
"script" : "\n Debug.explain(doc)\n ",
"lang" : "painless",
"position" : {
"offset" : 27,
"start" : 13,
"end" : 42
}
}
],
"type" : "search_phase_execution_exception",
"reason" : "all shards failed",
"phase" : "query",
"grouped" : true,
"failed_shards" : [
{
"shard" : 0,
"index" : "logs_server1",
"node" : "2bFyWe-OSpeW98xsZMrjng",
"reason" : {
"type" : "script_exception",
"reason" : "runtime error",
"to_string" : "org.elasticsearch.search.lookup.LeafDocLookup@6814c133",
"java_class" : "org.elasticsearch.search.lookup.LeafDocLookup",
"script_stack" : [
"Debug.explain(doc)\n ",
" ^---- HERE"
],
"script" : "\n Debug.explain(doc)\n ",
"lang" : "painless",
"position" : {
"offset" : 27,
"start" : 13,
"end" : 42
},
"caused_by" : {
"type" : "painless_explain_error",
"reason" : null
}
}
},
{
"shard" : 0,
"index" : "logs_server2",
"node" : "2bFyWe-OSpeW98xsZMrjng",
"reason" : {
"type" : "script_exception",
"reason" : "runtime error",
"to_string" : "org.elasticsearch.search.lookup.LeafDocLookup@6454e4d",
"java_class" : "org.elasticsearch.search.lookup.LeafDocLookup",
"script_stack" : [
"Debug.explain(doc)\n ",
" ^---- HERE"
],
"script" : "\n Debug.explain(doc)\n ",
"lang" : "painless",
"position" : {
"offset" : 27,
"start" : 13,
"end" : 42
},
"caused_by" : {
"type" : "painless_explain_error",
"reason" : null
}
}
}
]
},
"status" : 400
}
从上面我们可以看出来 doc 是一个 org.elasticsearch.search.lookup.LeafDocLookup 类型的数据。我们可以通过谷歌搜索来找到这个数据类型的所有方法。其中 containsKey 的描述在链接 https://www.javadoc.io/doc/org.elasticsearch/elasticsearch/6.0.1/org/elasticsearch/search/lookup/LeafDocLookup.html
上面按照脚本的方法来进行统计,有一个很大的缺点:每次在统计的时候都需要进行计算,如果有大量的数据的话,这样的计算量会很大。那么有没有一种比较简单的方法呢?
使用 alias 数据类型把数据归一化
我们参照之前的文章 “Elasticsearch : alias 数据类型”,我们可以把 level 都按照 ECS 的要求,对应于 log.level。对上面的两个索引做如下的操作:
PUT logs_server1/_mapping
{
"properties": {
"log": {
"properties": {
"level": {
"type": "alias",
"path": "level.keyword"
}
}
}
}
}
经过上面的操作后,logs_sever1 的 mapping 如下:
GET logs_server1/_mapping
{
"logs_server1" : {
"mappings" : {
"properties" : {
"level" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"log" : {
"properties" : {
"level" : {
"type" : "alias",
"path" : "level.keyword"
}
}
}
}
}
}
}
同样地,我们对 logs_server2 也进行同样的操作:
PUT logs_server2/_mapping
{
"properties": {
"log": {
"properties": {
"level": {
"type": "alias",
"path": "log_level.keyword"
}
}
}
}
}
那么 logs_server2 的 mapping 变为:
{
"logs_server2" : {
"mappings" : {
"properties" : {
"log" : {
"properties" : {
"level" : {
"type" : "alias",
"path" : "log_level.keyword"
}
}
},
"log_level" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
}
经过上面的改造之后,我们可以看出来,这两个索引的 mapping 都有一个共同的字段 log.level,尽管它们是 alias 数据类型。
我们很容易使用如下的方法来对 level 进行统计了:
GET logs_server*/_search
{
"size": 0,
"aggs": {
"levels": {
"terms": {
"field": "log.level",
"size": 10
}
}
}
}
现在显然比之前的 script 来统计数据方便很多了,而且它不需要有大量的计算了。