使用运行时字段探索数据

编辑

使用运行时字段探索数据编辑

假设您有一组大型日志数据,您想从中提取字段。对数据建立索引非常耗时,并且会占用大量磁盘空间,而您只想探索数据结构,而无需预先提交架构。

您知道您的日志数据包含您要提取的特定字段。在这种情况下,我们希望关注 @timestampmessage 字段。通过使用运行时字段,您可以定义脚本以在搜索时计算这些字段的值。

将索引字段定义为起点编辑

您可以从一个简单的示例开始,方法是将 @timestampmessage 字段作为索引字段添加到 my-index-000001 映射中。为了保持灵活性,请使用 wildcard 作为 message 的字段类型。

response = client.indices.create(
  index: 'my-index-000001',
  body: {
    mappings: {
      properties: {
        "@timestamp": {
          format: 'strict_date_optional_time||epoch_second',
          type: 'date'
        },
        message: {
          type: 'wildcard'
        }
      }
    }
  }
)
puts response
PUT /my-index-000001/
{
  "mappings": {
    "properties": {
      "@timestamp": {
        "format": "strict_date_optional_time||epoch_second",
        "type": "date"
      },
      "message": {
        "type": "wildcard"
      }
    }
  }
}

提取一些数据编辑

映射了要检索的字段后,将日志数据中的一些记录索引到 Elasticsearch 中。以下请求使用 批量 API 将原始日志数据索引到 my-index-000001 中。您可以使用一小部分样本数据来试验运行时字段,而不是索引所有日志数据。

最终文档不是有效的 Apache 日志格式,但我们可以在脚本中考虑到这种情况。

response = client.bulk(
  index: 'my-index-000001',
  refresh: true,
  body: [
    {
      index: {}
    },
    {
      timestamp: '2020-04-30T14:30:17-05:00',
      message: '40.135.0.0 - - [30/Apr/2020:14:30:17 -0500] "GET /images/hm_bg.jpg HTTP/1.0" 200 24736'
    },
    {
      index: {}
    },
    {
      timestamp: '2020-04-30T14:30:53-05:00',
      message: '232.0.0.0 - - [30/Apr/2020:14:30:53 -0500] "GET /images/hm_bg.jpg HTTP/1.0" 200 24736'
    },
    {
      index: {}
    },
    {
      timestamp: '2020-04-30T14:31:12-05:00',
      message: '26.1.0.0 - - [30/Apr/2020:14:31:12 -0500] "GET /images/hm_bg.jpg HTTP/1.0" 200 24736'
    },
    {
      index: {}
    },
    {
      timestamp: '2020-04-30T14:31:19-05:00',
      message: '247.37.0.0 - - [30/Apr/2020:14:31:19 -0500] "GET /french/splash_inet.html HTTP/1.0" 200 3781'
    },
    {
      index: {}
    },
    {
      timestamp: '2020-04-30T14:31:22-05:00',
      message: '247.37.0.0 - - [30/Apr/2020:14:31:22 -0500] "GET /images/hm_nbg.jpg HTTP/1.0" 304 0'
    },
    {
      index: {}
    },
    {
      timestamp: '2020-04-30T14:31:27-05:00',
      message: '252.0.0.0 - - [30/Apr/2020:14:31:27 -0500] "GET /images/hm_bg.jpg HTTP/1.0" 200 24736'
    },
    {
      index: {}
    },
    {
      timestamp: '2020-04-30T14:31:28-05:00',
      message: 'not a valid apache log'
    }
  ]
)
puts response
POST /my-index-000001/_bulk?refresh
{"index":{}}
{"timestamp":"2020-04-30T14:30:17-05:00","message":"40.135.0.0 - - [30/Apr/2020:14:30:17 -0500] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"}
{"index":{}}
{"timestamp":"2020-04-30T14:30:53-05:00","message":"232.0.0.0 - - [30/Apr/2020:14:30:53 -0500] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"}
{"index":{}}
{"timestamp":"2020-04-30T14:31:12-05:00","message":"26.1.0.0 - - [30/Apr/2020:14:31:12 -0500] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"}
{"index":{}}
{"timestamp":"2020-04-30T14:31:19-05:00","message":"247.37.0.0 - - [30/Apr/2020:14:31:19 -0500] \"GET /french/splash_inet.html HTTP/1.0\" 200 3781"}
{"index":{}}
{"timestamp":"2020-04-30T14:31:22-05:00","message":"247.37.0.0 - - [30/Apr/2020:14:31:22 -0500] \"GET /images/hm_nbg.jpg HTTP/1.0\" 304 0"}
{"index":{}}
{"timestamp":"2020-04-30T14:31:27-05:00","message":"252.0.0.0 - - [30/Apr/2020:14:31:27 -0500] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"}
{"index":{}}
{"timestamp":"2020-04-30T14:31:28-05:00","message":"not a valid apache log"}

此时,您可以查看 Elasticsearch 如何存储您的原始数据。

resp = client.indices.get(
    index="my-index-000001",
)
print(resp)
response = client.indices.get(
  index: 'my-index-000001'
)
puts response
GET /my-index-000001

映射包含两个字段:@timestampmessage

{
  "my-index-000001" : {
    "aliases" : { },
    "mappings" : {
      "properties" : {
        "@timestamp" : {
          "type" : "date",
          "format" : "strict_date_optional_time||epoch_second"
        },
        "message" : {
          "type" : "wildcard"
        },
        "timestamp" : {
          "type" : "date"
        }
      }
    },
    ...
  }
}

使用 grok 模式定义运行时字段编辑

如果要检索包含 clientip 的结果,可以将该字段作为运行时字段添加到映射中。以下运行时脚本定义了一个 grok 模式,用于从文档中的单个文本字段中提取结构化字段。grok 模式类似于正则表达式,它支持可以重复使用的别名表达式。

该脚本匹配 %{COMMONAPACHELOG} 日志模式,该模式了解 Apache 日志的结构。如果模式匹配(clientip != null),则脚本会发出匹配 IP 地址的值。如果模式不匹配,则脚本仅返回字段值而不崩溃。

PUT my-index-000001/_mappings
{
  "runtime": {
    "http.client_ip": {
      "type": "ip",
      "script": """
        String clientip=grok('%{COMMONAPACHELOG}').extract(doc["message"].value)?.clientip;
        if (clientip != null) emit(clientip); 
      """
    }
  }
}

此条件可确保即使消息的模式不匹配,脚本也不会崩溃。

或者,您可以在搜索请求的上下文中定义相同的运行时字段。运行时定义和脚本与先前在索引映射中定义的完全相同。只需将该定义复制到 runtime_mappings 部分下的搜索请求中,并包含与运行时字段匹配的查询即可。此查询返回的结果与在索引映射中为 http.clientip 运行时字段定义搜索查询的结果相同,但仅在此特定搜索的上下文中有效。

GET my-index-000001/_search
{
  "runtime_mappings": {
    "http.clientip": {
      "type": "ip",
      "script": """
        String clientip=grok('%{COMMONAPACHELOG}').extract(doc["message"].value)?.clientip;
        if (clientip != null) emit(clientip);
      """
    }
  },
  "query": {
    "match": {
      "http.clientip": "40.135.0.0"
    }
  },
  "fields" : ["http.clientip"]
}

定义复合运行时字段编辑

您还可以定义一个*复合*运行时字段,以从单个脚本发出多个字段。您可以定义一组类型化的子字段并发出一个值映射。在搜索时,每个子字段都会检索与其名称关联的值。这意味着您只需要指定一次 grok 模式,并且可以返回多个值。

response = client.indices.put_mapping(
  index: 'my-index-000001',
  body: {
    runtime: {
      http: {
        type: 'composite',
        script: 'emit(grok("%<COMMONAPACHELOG>s").extract(doc["message"].value))',
        fields: {
          clientip: {
            type: 'ip'
          },
          verb: {
            type: 'keyword'
          },
          response: {
            type: 'long'
          }
        }
      }
    }
  }
)
puts response
PUT my-index-000001/_mappings
{
  "runtime": {
    "http": {
      "type": "composite",
      "script": "emit(grok(\"%{COMMONAPACHELOG}\").extract(doc[\"message\"].value))",
      "fields": {
        "clientip": {
          "type": "ip"
        },
        "verb": {
          "type": "keyword"
        },
        "response": {
          "type": "long"
        }
      }
    }
  }
}

搜索特定的 IP 地址编辑

使用 http.clientip 运行时字段,您可以定义一个简单的查询来运行对特定 IP 地址的搜索并返回所有相关字段。

GET my-index-000001/_search
{
  "query": {
    "match": {
      "http.clientip": "40.135.0.0"
    }
  },
  "fields" : ["*"]
}

API 返回以下结果。因为 http 是一个 composite 运行时字段,所以响应包含 fields 下的每个子字段,包括与查询匹配的任何关联值。无需预先构建数据结构,您就可以以有意义的方式搜索和探索数据,以进行试验并确定要索引哪些字段。

{
  ...
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my-index-000001",
        "_id" : "sRVHBnwBB-qjgFni7h_O",
        "_score" : 1.0,
        "_source" : {
          "timestamp" : "2020-04-30T14:30:17-05:00",
          "message" : "40.135.0.0 - - [30/Apr/2020:14:30:17 -0500] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"
        },
        "fields" : {
          "http.verb" : [
            "GET"
          ],
          "http.clientip" : [
            "40.135.0.0"
          ],
          "http.response" : [
            200
          ],
          "message" : [
            "40.135.0.0 - - [30/Apr/2020:14:30:17 -0500] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"
          ],
          "http.client_ip" : [
            "40.135.0.0"
          ],
          "timestamp" : [
            "2020-04-30T19:30:17.000Z"
          ]
        }
      }
    ]
  }
}

另外,还记得脚本中的 if 语句吗?

if (clientip != null) emit(clientip);

如果脚本中没有包含此条件,则查询将在任何与模式不匹配的分片上失败。通过包含此条件,查询将跳过与 grok 模式不匹配的数据。

搜索特定范围内的文档编辑

您还可以运行对 timestamp 字段进行操作的 范围查询。以下查询返回 timestamp 大于或等于 2020-04-30T14:31:27-05:00 的任何文档。

GET my-index-000001/_search
{
  "query": {
    "range": {
      "timestamp": {
        "gte": "2020-04-30T14:31:27-05:00"
      }
    }
  }
}

响应包括日志格式不匹配但时间戳在定义范围内的文档。

{
  ...
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my-index-000001",
        "_id" : "hdEhyncBRSB6iD-PoBqe",
        "_score" : 1.0,
        "_source" : {
          "timestamp" : "2020-04-30T14:31:27-05:00",
          "message" : "252.0.0.0 - - [30/Apr/2020:14:31:27 -0500] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"
        }
      },
      {
        "_index" : "my-index-000001",
        "_id" : "htEhyncBRSB6iD-PoBqe",
        "_score" : 1.0,
        "_source" : {
          "timestamp" : "2020-04-30T14:31:28-05:00",
          "message" : "not a valid apache log"
        }
      }
    ]
  }
}

使用 dissect 模式定义运行时字段编辑

如果不需要正则表达式的强大功能,则可以使用 dissect 模式 而不是 grok 模式。Dissect 模式匹配固定的分隔符,但通常比 grok 快。

您可以使用 dissect 来获得与使用 grok 模式 解析 Apache 日志相同的结果。您需要包含要丢弃的字符串部分,而不是匹配日志模式。特别注意要丢弃的字符串部分将有助于构建成功的 dissect 模式。

PUT my-index-000001/_mappings
{
  "runtime": {
    "http.client.ip": {
      "type": "ip",
      "script": """
        String clientip=dissect('%{clientip} %{ident} %{auth} [%{@timestamp}] "%{verb} %{request} HTTP/%{httpversion}" %{status} %{size}').extract(doc["message"].value)?.clientip;
        if (clientip != null) emit(clientip);
      """
    }
  }
}

类似地,您可以定义一个 dissect 模式来提取 HTTP 响应代码

PUT my-index-000001/_mappings
{
  "runtime": {
    "http.responses": {
      "type": "long",
      "script": """
        String response=dissect('%{clientip} %{ident} %{auth} [%{@timestamp}] "%{verb} %{request} HTTP/%{httpversion}" %{response} %{size}').extract(doc["message"].value)?.response;
        if (response != null) emit(Integer.parseInt(response));
      """
    }
  }
}

然后,您可以运行查询以使用 http.responses 运行时字段检索特定的 HTTP 响应。使用 _search 请求的 fields 参数来指示要检索哪些字段。

response = client.search(
  index: 'my-index-000001',
  body: {
    query: {
      match: {
        'http.responses' => '304'
      }
    },
    fields: [
      'http.client_ip',
      'timestamp',
      'http.verb'
    ]
  }
)
puts response
GET my-index-000001/_search
{
  "query": {
    "match": {
      "http.responses": "304"
    }
  },
  "fields" : ["http.client_ip","timestamp","http.verb"]
}

响应包括 HTTP 响应为 304 的单个文档。

{
  ...
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my-index-000001",
        "_id" : "A2qDy3cBWRMvVAuI7F8M",
        "_score" : 1.0,
        "_source" : {
          "timestamp" : "2020-04-30T14:31:22-05:00",
          "message" : "247.37.0.0 - - [30/Apr/2020:14:31:22 -0500] \"GET /images/hm_nbg.jpg HTTP/1.0\" 304 0"
        },
        "fields" : {
          "http.verb" : [
            "GET"
          ],
          "http.client_ip" : [
            "247.37.0.0"
          ],
          "timestamp" : [
            "2020-04-30T19:31:22.000Z"
          ]
        }
      }
    ]
  }
}