Elasticsearch:使用 alias 数据类型来遵循 ECS (Elastic Common Schema)

在很多的设计中,我们所采集的数据来自不同的数据源,从而导致数据字段名称的不一致。如果,我们在一开始就遵循 Elastic Common Schema,那么我们就不会有任何的问题。但是在实际的生产环境中,有可能在一开始我们就没有这么做,那我们该如何解决这个问题呢?比如我们有如下的两个数据:

POST logs_server1/_doc/
{
  "level": "info"
}

POST logs_server2/_doc/
{
  "log_level": "info"
}

在上面的两个数据是来自两个不同的服务器,在当时设计的时候,表示 log 的级别分别用了不同的字段:levellog_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 来统计数据方便很多了,而且它不需要有大量的计算了。