作者:刘晓国
在很多的设计中,我们所采集的数据来自不同的数据源,从而导致数据字段名称的不一致。如果,我们在一开始就遵循 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 来统计数据方便很多了,而且它不需要有大量的计算了。