在 elasticsearch/kibana 脚本字段中将 IP(字符串)转换为 long

Convert IP (string) into long in elasticsearch/kibana scripted fields

我在文档中有一个字段是 ipv4 ("1.2.3.4") 的字符串表示,该字段的名称是 "originating_ip"。 我正在尝试使用无痛语言的脚本字段来添加一个新字段 (originating_ip_calc) 以具有所述 IPv4 的 int (long) 表示形式。

以下脚本在 groovy 中有效(根据我的理解,这基本上应该几乎相同),但在这种特定情况下似乎几乎没有。

​String[] ipAddressInArray = "1.2.3.4".split("\.");

long result = 0;
for (int i = 0; i < ipAddressInArray.length; i++) {
    int power = 3 - i;
    int ip = Integer.parseInt(ipAddressInArray[i]);
    long longIP = (ip * Math.pow(256, power)).toLong();
    result = result + longIP;
}
return result;

我也在查看 ,正如您从上面的代码中看到的那样,它基于那里的一个答案。

也尝试过使用 InetAddress 但没有成功。

借助 Elasticsearch 无痛脚本,您可以使用如下代码:

POST ip_search/doc/_search
{
  "query": {
    "match_all": {}
  },
  "script_fields": {
    "originating_ip_calc": {
      "script": {
        "source": """
String ip_addr = params['_source']['originating_ip'];
def ip_chars = ip_addr.toCharArray();
int chars_len = ip_chars.length;
long result = 0;
int cur_power = 0;
int last_dot = chars_len;
for(int i = chars_len -1; i>=-1; i--) {
  if (i == -1 || ip_chars[i] == (char) '.' ){
    result += (Integer.parseInt(ip_addr.substring(i+ 1, last_dot)) * Math.pow(256, cur_power));
    last_dot = i;
    cur_power += 1;
  }
}         
return result
""",
        "lang": "painless"
      }
    }
  },
  "_source": ["originating_ip"]
}

(请注意,我使用 Kibana console 将请求发送到 ES,它会在发送前进行一些转义以使其成为有效的 JSON。)

这将给出如下响应:

"hits": [
  {
    "_index": "ip_search",
    "_type": "doc",
    "_id": "2",
    "_score": 1,
    "_source": {
      "originating_ip": "10.0.0.1"
    },
    "fields": {
      "originating_ip_calc": [
        167772161
      ]
    }
  },
  {
    "_index": "ip_search",
    "_type": "doc",
    "_id": "1",
    "_score": 1,
    "_source": {
      "originating_ip": "1.2.3.4"
    },
    "fields": {
      "originating_ip_calc": [
        16909060
      ]
    }
  }
]

但为什么一定要这样呢?

为什么 .split 的方法不起作用?

如果您将问题中的代码发送给 ES,它会回复如下错误:

      "script": "String[] ipAddressInArray = \"1.2.3.4\".split(\"\\.\");\n\nlong result = 0;\nfor (int i = 0; i < ipAddressInArray.length; i++) {\n    int power = 3 - i;\n    int ip = Integer.parseInt(ipAddressInArray[i]);\n    long longIP = (ip * Math.pow(256, power)).toLong();\n    result = result + longIP;\n}\nreturn result;",
      "lang": "painless",
      "caused_by": {
        "type": "illegal_argument_exception",
        "reason": "Unknown call [split] with [1] arguments on type [String]."

这主要是因为 Java 的 String.split() is not considered safe to use (because it creates regex Pattern implicitly). They suggest to use Pattern#split 但要这样做,您应该在索引中启用正则表达式。

默认情况下,它们是禁用的:

      "script": "String[] ipAddressInArray = /\./.split(\"1.2.3.4\");...
      "lang": "painless",
      "caused_by": {
        "type": "illegal_state_exception",
        "reason": "Regexes are disabled. Set [script.painless.regex.enabled] to [true] in elasticsearch.yaml to allow them. Be careful though, regexes break out of Painless's protection against deep recursion and long loops."

为什么我们必须进行显式转换 (char) '.'

因此,我们必须手动将字符串拆分为点。直接的方法是将字符串的每个字符与 '.' 进行比较(在 Java 中表示 char 文字,而不是 String)。

但是对于painless,它意味着String。所以我们必须显式转换为 char(因为我们正在迭代一个字符数组)。

为什么我们必须直接使用 char 数组?

因为显然 painless 也不允许 String.length 方法:

    "reason": {
      "type": "script_exception",
      "reason": "compile error",
      "script_stack": [
        "\"1.2.3.4\".length",
        "         ^---- HERE"
      ],
      "script": "\"1.2.3.4\".length",
      "lang": "painless",
      "caused_by": {
        "type": "illegal_argument_exception",
        "reason": "Unknown field [length] for type [String]."
      }
    }

那为什么叫painless呢?

虽然我在快速谷歌搜索后找不到任何关于命名的历史记录,但从 documentation page 和一些经验(如上面的答案)我可以推断它被设计为无痛 在生产中使用.

它的前身,Groovy, was a ticking bomb due to resources usage and security vulnerabilities。因此,Elasticsearch 团队创建了一个非常有限的 Java/Groovy 脚本子集,它具有可预测的性能并且不会包含那些安全漏洞,并将其称为 painless.

如果说 painless 脚本语言有什么是真的,那就是它是 limitedsandboxed.