Painless 是一種高性能、安全的腳本語言,專為 Elasticsearch 設計。你可以使用 Painless 在 Elasticsearch 支持腳本的任何地方安全地編寫內聯和存儲腳本。
Painless 提供眾多功能,這些功能圍繞以下核心原則:
- 安全性:確保集群的安全性至關重要。為此,Painless 使用細粒度的允許列表,粒度細到類的成員。任何不屬于允許列表的內容都會導致編譯錯誤。請參閱 Painless API 參考,了解每個腳本上下文的可用類、方法和字段的完整列表。
- 性能:Painless 直接編譯為 JVM 字節碼,以利用 JVM 提供的所有可能的優化。此外,Painless 通常會避免在運行時需要額外較慢檢查的功能。
- 簡單性:Painless 實現的語法對任何具有一些基本編碼經驗的人來說都很自然熟悉。Painless 使用 Java 語法的子集,并進行了一些額外的改進,以增強可讀性并消除樣板。
本篇文章的是系列文章中的其中一篇:
-
Elasticsearch:Painless scripting 語言(一)
-
Elasticsearch:Painless scripting 語言(二)
開始編寫腳本
準備好開始使用 Painless 編寫腳本了嗎?讓我開始編寫我們的第一個腳本。
只要 Elasticsearch API 支持腳本,語法就會遵循相同的模式;你可以指定腳本的語言、提供腳本邏輯(或源代碼),并添加傳遞到腳本中的參數:
"script": {"lang": "...","source" | "id": "...","params": { ... }}
條目 | 描述 |
---|---|
lang | 指定腳本所用的語言。默認為 painless。 |
source ,?id | 腳本本身,你將其指定為內聯腳本的源或存儲腳本的 id。使用存儲腳本 API 創建和管理存儲腳本。 |
params | 指定作為變量傳遞到腳本中的任何命名參數。使用參數而不是硬編碼值來減少編譯時間。 |
書寫你的第一個腳本
Painless 是 Elasticsearch 的默認腳本語言。它安全、高效,并為任何具有一點編碼經驗的人提供自然的語法。
Painless 腳本的結構為一個或多個語句,并且可以在開頭選擇一個或多個用戶定義的函數。腳本必須始終至少有一個語句。
Painless 執行 API 提供了使用簡單的用戶定義參數測試腳本并接收結果的能力。讓我們從完整的腳本開始并查看其組成部分。
首先,使用單個字段索引文檔,以便我們可以使用一些數據:
PUT my-index-000001/_doc/1
{"my_field": 5
}
然后,我們可以構建一個對該字段進行操作的腳本,并作為查詢的一部分運行評估該腳本。以下查詢使用搜索 API 的 script_fields 參數來檢索腳本評估。這里發生了很多事情,但我們將分解各個組件以分別理解它們。現在,你只需要了解此腳本接受 my_field 并對其進行操作。
GET my-index-000001/_search
{"script_fields": {"my_doubled_field": {"script": { // 1"source": "doc['my_field'].value * params['multiplier']", // 2"params": {"multiplier": 2}}}}
}
- script 對象
- script 代碼
script 是一個標準 JSON 對象,用于定義 Elasticsearch 中大多數 API 下的腳本。此對象需要 source 來定義腳本本身。script 未指定語言,因此默認為 Painless。
運行上面的代碼,我們可以看到如下的結果:
從上面的輸出中,我們可以看到有一個新的字段叫做?my_doubled_field。它的紙是 10。是之前的字段 my_field 的值的兩倍。
請注意,從 Elastic Stack 7.11 發布后,我們可以使用 runtime fields 來實現同樣的功能:
GET my-index-000001/_search
{"runtime_mappings": {"my_doubled_field": {"type": "long","script": {"source": "emit(doc['my_field'].value * 2)"}}},"fields": ["my_doubled_field"]
}
上面運行的結果為:
從 Elastic Stack 8.14 后,我們甚至可以使用 ES|QL 來實現同樣的功能:
POST _query?format=txt
{"query": """FROM my-index-000001| EVAL my_doubled_field = my_field*2| LIMIT 1"""
}
上面代碼執行的結果為:
在腳本中使用參數
Elasticsearch 第一次看到新腳本時,它會編譯該腳本并將編譯后的版本存儲在緩存中。編譯可能是一個繁重的過程。與其在腳本中硬編碼值,不如將它們作為命名參數傳遞。
例如,在上一個腳本中,我們可以只硬編碼值并編寫一個看似不太復雜的腳本。我們可以只檢索 my_field 的第一個值,然后將其乘以 2:
"source": "return doc['my_field'].value * 2"
雖然這種方法有效,但靈活性很差。我們必須修改腳本源代碼才能更改乘數,而且每次乘數更改時,Elasticsearch 都必須重新編譯腳本。
不用對值進行硬編碼,而是使用命名參數來提高腳本的靈活性,同時還可以減少腳本運行時的編譯時間。現在,你可以更改 multiplier 參數,而無需 Elasticsearch 重新編譯腳本。
"source": "doc['my_field'].value * params['multiplier']",
"params": {"multiplier": 2
}
默認情況下,每 5 分鐘最多可以編譯 150 個腳本。對于采集上下文,默認腳本編譯速率不受限制。
script.context.field.max_compilations_rate=100/10m
重要:如果你在短時間內編譯了太多唯一腳本,Elasticsearch 會拒絕新的動態腳本并出現 circuit_breaking_exception 錯誤。
縮短腳本
使用 Painless 固有的語法功能,你可以減少腳本的冗長程度并使其更短。下面是我們可以縮短的簡單腳本:
GET my-index-000001/_search
{"script_fields": {"my_doubled_field": {"script": {"lang": "painless","source": "doc['my_field'].value * params.get('multiplier');","params": {"multiplier": 2}}}}
}
讓我們看一下腳本的縮短版本,看看它與上一次版本相比有哪些改進:
GET my-index-000001/_search
{"script_fields": {"my_doubled_field": {"script": {"source": "field('my_field').get(null) * params['multiplier']","params": {"multiplier": 2}}}}
}
此版本的腳本刪除了幾個組件并顯著簡化了語法:
- lang 聲明。由于 Painless 是默認語言,因此如果你正在編寫 Painless 腳本,則無需指定語言。
- return 關鍵字。Painless 會自動使用腳本中的最后一個語句(如果可能)在需要返回值的腳本上下文中生成返回值。
- get 方法,用括號 [] 替換。Painless 專門為 Map 類型使用了快捷方式,允許我們使用括號而不是較長的 get 方法。
- source 語句末尾的分號。Painless 不需要在塊的最后一個語句中使用分號 ;。但是,在其他情況下確實需要它們來消除歧義。
在 Elasticsearch 支持腳本的任何地方使用此縮寫語法,例如當你創建 runtime fields。
存儲和檢索腳本
你可以使用存儲腳本 API 從集群狀態存儲和檢索腳本。存儲腳本可減少編譯時間并加快搜索速度。
注意:與常規腳本不同,存儲腳本要求你使用 lang 參數指定腳本語言。
要創建腳本,請使用 create stored script API。例如,以下請求將創建一個名為 calculate-score 的存儲腳本。
POST _scripts/calculate-score
{"script": {"lang": "painless","source": "Math.log(_score * 2) + params['my_modifier']"}
}
你可以使用?get stored script API?檢索該腳本。
GET _scripts/calculate-score
要在查詢中使用存儲的腳本,請在腳本聲明中包含腳本 ID:
GET my-index-000001/_search
{"query": {"script_score": {"query": {"match": {"message": "some message"}},"script": {"id": "calculate-score", // 1"params": {"my_modifier": 2}}}}
}
- ?id 是存儲腳本的 id
要刪除存儲的腳本,請提交 delete stored script API?請求。
DELETE _scripts/calculate-score
使用腳本更新文檔
你可以使用?update API 使用指定的腳本更新文檔。腳本可以更新、刪除或跳過修改文檔。更新 API 還支持傳遞部分文檔,該部分文檔將合并到現有文檔中。
首先,讓我們索引一個簡單的文檔:
DELETE my-index-000001PUT my-index-000001/_doc/1
{"counter" : 1,"tags" : ["red"]
}
要增加 counter,你可以使用以下腳本提交更新請求:
POST my-index-000001/_update/1
{"script" : {"source": "ctx._source.counter += params.count","lang": "painless","params" : {"count" : 4}}
}
我們可通過如下的命令來進行查看:
GET my-index-000001/_doc/1
我們可以看見 counter 的值從 1?變為 5。
類似地,你可以使用更新腳本將標簽添加到 tag 列表中。因為這只是一個列表,所以即使標簽存在,也會被添加:
POST my-index-000001/_update/1
{"script": {"source": "ctx._source.tags.add(params['tag'])","lang": "painless","params": {"tag": "blue"}}
}
GET my-index-000001/_doc/1
我們可以看到 blue 已經被添加。
注意:在修改 source 的時候,我們需要使用正確的上下文,也即使用 ctx._source 來進行修改。
你還可以從標簽列表中刪除標簽。Java List 的 remove 方法在 Painless 中可用。它采用要刪除的元素的索引。為避免可能的運行時錯誤,你首先需要確保標簽存在。如果列表包含標簽的重復項,則此腳本只會刪除一個出現項。
POST my-index-000001/_update/1
{"script": {"source": "if (ctx._source.tags.contains(params['tag'])) { ctx._source.tags.remove(ctx._source.tags.indexOf(params['tag'])) }","lang": "painless","params": {"tag": "blue"}}
}
你還可以在文檔中添加和刪除字段。例如,此腳本添加了字段 new_field:
POST my-index-000001/_update/1
{"script" : "ctx._source.new_field = 'value_of_new_field'"
}
相反,此腳本刪除了字段 new_field:
POST my-index-000001/_update/1
{"script" : "ctx._source.remove('new_field')"
}
除了更新文檔之外,你還可以更改從腳本內部執行的操作。例如,如果 tags 字段包含 green,則此請求將刪除文檔。否則它不執行任何操作(noop):
POST my-index-000001/_update/1
{"script": {"source": "if (ctx._source.tags.contains(params['tag'])) { ctx.op = 'delete' } else { ctx.op = 'none' }","lang": "painless","params": {"tag": "green"}}
}
腳本、緩存和搜索速度
Elasticsearch 執行了許多優化,以使使用腳本的速度盡可能快。一個重要的優化是腳本緩存。編譯后的腳本放置在緩存中,以便引用腳本的請求不會產生編譯懲罰。
緩存大小很重要。你的腳本緩存應該足夠大,以容納用戶需要同時訪問的所有腳本。
如果您看到大量腳本緩存驅逐和節點統計信息(node stats)中的編譯數量不斷增加,則你的緩存可能太小。
默認情況下,所有腳本都緩存,因此只需在發生更新時重新編譯它們。默認情況下,腳本沒有基于時間的過期時間。你可以使用 script.cache.expire 設置更改此行為。使用 script.cache.max_size 設置配置緩存的大小。
注意:腳本的大小限制為 65,535 字節。設置 script.max_size_in_bytes 的值以增加該軟限制。如果你的腳本確實很大,那么請考慮使用本機腳本引擎。
提高搜索速度
腳本非常有用,但不能使用 Elasticsearch 的索引結構或相關優化。這種關系有時會導致搜索速度變慢。
如果你經常使用腳本來轉換索引數據,則可以通過在攝取期間轉換數據來加快搜索速度。但是,這通常意味著索引速度變慢。讓我們看一個實際的例子來說明如何提高搜索速度。
運行搜索時,通常按兩個值的總和對結果進行排序。例如,考慮一個名為 my_test_scores 的索引,其中包含測試分數數據。此索引包括兩個 long 類型的字段:
- math_score
- verbal_score
你可以使用將這些值加在一起的腳本運行查詢。這種方法沒有錯,但是查詢會變慢,因為腳本估值是請求的一部分。以下請求返回 grad_year 等于 2099 的文檔,并按腳本的估值對結果進行排序。
GET /my_test_scores/_search
{"query": {"term": {"grad_year": "2099"}},"sort": [{"_script": {"type": "number","script": {"source": "doc['math_score'].value + doc['verbal_score'].value"},"order": "desc"}}]
}
如果你正在搜索一個小型索引,那么將腳本作為搜索查詢的一部分可能是一個很好的解決方案。如果你想加快搜索速度,你可以在提取期間執行此計算,并將總和索引到字段。
首先,我們將向名為 total_score 的索引添加一個新的字段,它將包含 math_score 和 verbal_score 字段值的總和。
PUT my_test_scores/_doc/1
{"math_score": 90,"verbal_score": 80
}PUT /my_test_scores/_mapping
{"properties": {"total_score": {"type": "long"}}
}
接下來,使用包含?script processor 的? ingest pipeline?計算 math_score 和 verbal_score 的總和,并將其索引到 total_score 字段中。
PUT _ingest/pipeline/my_test_scores_pipeline
{"description": "Calculates the total test score","processors": [{"script": {"source": "ctx.total_score = (ctx.math_score + ctx.verbal_score)"}}]
}
要更新現有數據,請使用此管道將 my_test_scores 中的所有文檔 reindex 到名為 my_test_scores_2 的新索引。
POST /_reindex
{"source": {"index": "my_test_scores"},"dest": {"index": "my_test_scores_2","pipeline": "my_test_scores_pipeline"}
}
繼續使用管道將任何新文檔索引到 my_test_scores_2。
POST /my_test_scores_2/_doc/?pipeline=my_test_scores_pipeline
{"student": "kimchy","grad_year": "2099","math_score": 1200,"verbal_score": 800
}
我們可以通過如的代碼來查詢?my_test_scores_2:
GET my_test_scores_2/_search
我們可以看到一個新的字段 total_score,并且它的值是 math_score 和 verbal_score 兩個字段的總和。
這些更改會減慢索引過程,但可以加快搜索速度。你可以使用 total_score 字段對 my_test_scores_2 上的搜索進行排序,而無需使用腳本。響應幾乎是實時的!雖然此過程會減慢采集時間,但它會大大增加搜索時的查詢次數。
GET /my_test_scores_2/_search
{"query": {"term": {"grad_year": "2099"}},"sort": [{"total_score": {"order": "desc"}}]
}
Dissect 數據
Dissect 將單個文本字段與定義的模式進行匹配。剖析模式由要丟棄的字符串部分定義。特別注意字符串的每個部分有助于構建成功的 dissect 模式。
如果你不需要正則表達式的強大功能,請使用剖析模式而不是 grok。Dissect 使用的語法比 grok 簡單得多,而且通常總體上速度更快。dissect 的語法是透明的:告訴 dissect 你想要什么,它就會將這些結果返回給你。
Dissect 模式
Dissect 模式由變量和分隔符組成。任何由百分號和花括號 %{} 定義的內容都被視為變量,例如 %{clientip}。你可以將變量分配給字段中任何部分的數據,然后僅返回所需的部分。分隔符是變量之間的任何值,可以是空格、破折號或其他分隔符。
例如,假設你有一個日志數據,其中的 message 字段如下所示:
"message" : "247.37.0.0 - - [30/Apr/2020:14:31:22 -0500] \"GET /images/hm_nbg.jpg HTTP/1.0\" 304 0"
你將變量分配給數據的每個部分以構建成功的 dissect 模式。請記住,告訴剖析你想要匹配的內容。
數據的第一部分看起來像一個 IP 地址,因此你可以分配一個變量,例如 %{clientip}。接下來的兩個字符是破折號,兩邊各有一個空格。你可以為每個破折號分配一個變量,或者分配一個變量來表示破折號和空格。接下來是一組包含時間戳的括號。括號是分隔符,因此你可以將它們包含在剖析模式中。到目前為止,數據和匹配的剖析模式如下所示:
247.37.0.0 - - [30/Apr/2020:14:31:22 -0500] #1%{clientip} %{ident} %{auth} [%{@timestamp}] #2
- message 字段中的第一個數據塊
- dissect 模式以匹配所選數據塊
使用相同的邏輯,你可以為剩余的數據塊創建變量。雙引號是分隔符,因此請將其包含在你的 dissect 模式中。該模式用 %{verb} 變量替換 GET,但將 HTTP 保留為模式的一部分。
\"GET /images/hm_nbg.jpg HTTP/1.0\" 304 0"%{verb} %{request} HTTP/%{httpversion}" %{response} %{size}
將這兩種模式結合起來會得到如下所示的 dissect 模式:
%{clientip} %{ident} %{auth} [%{@timestamp}] \"%{verb} %{request} HTTP/%{httpversion}\" %{status} %{size}
現在您有了 dissect 模式,如何測試和使用它?
使用 Painless 測試 dissect 模式
你可以將 dissect 模式合并到 Painless 腳本中以提取數據。要測試你的腳本,請使用 Painless execute?API 的?field contexts 或創建包含腳本的運行時字段。運行時字段提供了更大的靈活性并接受多個文檔,但如果你在測試腳本的集群上沒有寫入權限,則 Painless execute API 是一個不錯的選擇。
例如,通過包含 Painless 腳本和與你的數據匹配的單個文檔,使用 Painless execute?API 測試你的剖析模式。首先將消息字段索引為 wildcard 數據類型:
PUT my-index
{"mappings": {"properties": {"message": {"type": "wildcard"}}}
}
如果要檢索 HTTP 響應代碼,請將你的解析模式添加到提取響應值的 Painless 腳本中。要從字段中提取值,請使用此函數:
`.extract(doc["<field_name>"].value)?.<field_value>`
在此示例中,message 是 <field_name>,response 是 <field_value>:
POST /_scripts/painless/_execute
{"script": {"source": """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)); //1"""},"context": "long_field", //2"context_setup": {"index": "my-index","document": { //3"message": """247.37.0.0 - - [30/Apr/2020:14:31:22 -0500] "GET /images/hm_nbg.jpg HTTP/1.0" 304 0"""}}
}
- 運行時字段需要 emit 方法才能返回值。
- 由于響應代碼是整數,因此請使用 long_field 上下文。
- 包含與你的數據匹配的示例文檔。
在運行時字段中使用 dissect 模式和腳本
如果你有一個功能性 dissect 模式,則可以將其添加到運行時字段以操作數據。由于運行時字段不需要你索引字段,因此你可以非常靈活地修改腳本及其功能。如果你已經使用 Painless exectute?API 測試了 dissect 模式,則可以在運行時字段中使用該 Painless 腳本。
首先,像上一節一樣將 message 字段添加為 wildcard 類型,但也要添加 @timestamp 作為日期,以防你想在其他用例中對該字段進行操作:
DELETE my-indexPUT /my-index/
{"mappings": {"properties": {"@timestamp": {"format": "strict_date_optional_time||epoch_second","type": "date"},"message": {"type": "wildcard"}}}
}
如果你想使用 dissect 模式提取 HTTP 響應代碼,你可以創建一個運行時字段,如 http.response:
PUT my-index/_mappings
{"runtime": {"http.response": {"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));"""}}
}
映射要檢索的字段后,將日志數據中的幾條記錄索引到 Elasticsearch 中。以下請求使用 bulk API將原始日志數據索引到 my-index 中:
POST /my-index/_bulk?refresh=true
{"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"}
你可以定義一個簡單的查詢來搜索特定的 HTTP 響應并返回所有相關字段。使用 search API 的 fields 參數來檢索 http.response 運行時字段。
GET my-index/_search
{"query": {"match": {"http.response": "304"}},"fields" : ["http.response"]
}
或者,你可以在搜索請求的上下文中定義相同的運行時字段。運行時定義和腳本與先前在索引映射中定義的完全相同。只需將該定義復制到搜索請求的 “runtime_mappings” 部分下,并包含與運行時字段匹配的查詢即可。此查詢返回的結果與先前在索引映射中為 http.response 運行時字段定義的搜索查詢相同,但僅限于此特定搜索的上下文:
GET my-index/_search
{"runtime_mappings": {"http.response": {"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));"""}},"query": {"match": {"http.response": "304"}},"fields" : ["http.response"]
}
Grokking grok
Grok 是一種支持可重復使用別名表達式的正則表達式方言。Grok 非常適合處理 syslog 日志、Apache 和其他 Web 服務器日志、mysql 日志以及通常為人類而非計算機使用而編寫的任何日志格式。
Grok 位于 Oniguruma 正則表達式庫之上,因此任何正則表達式在 grok 中都是有效的。Grok 使用這種正則表達式語言來命名現有模式并將它們組合成與你的字段匹配的更復雜的模式。
Grok 模式
Elastic Stack 附帶許多預定義的 grok 模式,可簡化 grok 的使用。重復使用 grok 模式的語法采用以下形式之一:
%{SYNTAX}? ? ? %{SYNTAX:ID}? ? ?%{SYNTAX:ID:TYPE}
條目 | 描述 |
---|---|
SYNTAX | 將與你的文本匹配的模式的名稱。例如,NUMBER 和 IP 都是默認模式集中提供的模式。NUMBER 模式匹配 3.44 之類的數據,而 IP 模式匹配 55.3.244.1 之類的數據。 |
ID | 你為匹配的文本片段指定的標識符。例如,3.44 可能是事件的持續時間,因此您可以將其稱為 duration。字符串 55.3.244.1 可能標識發出請求的 client。 |
TYPE | 你想要轉換命名字段的數據類型。支持的類型包括 int、long、double、float 和 boolean。 |
例如,假設你有如下消息數據:
3.44 55.3.244.1
第一個值是一個數字,后面跟著一個看起來像是 IP 地址的東西。你可以使用以下 grok 表達式匹配此文本:
%{NUMBER:duration} %{IP:client}
遷移到 Elastic Common Schema (ECS)
為了簡化遷移到 Elastic Common Schema (ECS) 的過程,除了現有模式之外,還提供一組新的符合 ECS 的模式。新的 ECS 模式定義捕獲符合該模式的事件字段名稱。
ECS 模式集包含舊集的所有模式定義,并且是直接替換。使用 ecs-compatability 設置切換模式。
新功能和增強功能將添加到符合 ECS 的文件中。舊模式可能仍會收到向后兼容的錯誤修復。
在 Painless 腳本中使用 grok 模式
你可以將預定義的 grok 模式合并到 Painless 腳本中以提取數據。要測試你的腳本,請使用 Painless exectute?API 的 field contexts 文或創建包含腳本的運行時字段。運行時字段提供了更大的靈活性并接受多個文檔,但如果你在測試腳本的集群上沒有寫入權限,Painless execute?API 是一個不錯的選擇。
如果你需要幫助構建 grok 模式以匹配你的數據,請使用 Kibana 中的 Grok 調試器工具。有關 Grok 的使用,請閱讀文章 “Logstash:日志解析的 Grok 模式示例”。
例如,如果你正在處理 Apache 日志數據,則可以使用 %{COMMONAPACHELOG} 語法,該語法可以理解 Apache 日志的結構。示例文檔可能如下所示:
"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"
要從消息字段中提取 IP 地址,你可以編寫一個包含 %{COMMONAPACHELOG} 語法的 Painless 腳本。你可以使用 Painless 執行 API 的 ip 字段上下文測試此腳本,但我們改用運行時字段。
根據示例文檔,索引 @timestamp 和 message 字段。為了保持靈活性,請使用通配符作為 message 的字段類型:
DELETE my-indexPUT /my-index/
{"mappings": {"properties": {"@timestamp": {"format": "strict_date_optional_time||epoch_second","type": "date"},"message": {"type": "wildcard"}}}
}
接下來,使用 bulk?API 將一些日志數據索引到 my-index 中。
POST /my-index/_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"}
在運行時字段中整合 grok 模式和腳本
現在,你可以在包含 Painless 腳本和 grok 模式的映射中定義一個運行時字段。如果模式匹配,腳本將發出匹配 IP 地址的值。如果模式不匹配(clientip != null),腳本只會返回字段值而不會崩潰。
PUT my-index/_mappings
{"runtime": {"http.clientip": {"type": "ip","script": """String clientip=grok('%{COMMONAPACHELOG}').extract(doc["message"].value)?.clientip;if (clientip != null) emit(clientip);"""}}
}
或者,你可以在搜索請求的上下文中定義相同的運行時字段。運行時定義和腳本與之前在索引映射中定義的完全相同。只需將該定義復制到搜索請求的 “runtime_mappings” 部分下,并包含與運行時字段匹配的查詢即可。此查詢返回的結果與你在索引映射中為 http.clientip 運行時字段定義搜索查詢的結果相同,但僅限于此特定搜索的上下文:
GET my-index/_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"]
}
返回計算結果
使用 http.clientip 運行時字段,您可以定義一個簡單的查詢來搜索特定的 IP 地址并返回所有相關字段。_search API 上的 fields 參數適用于所有字段,甚至包括那些未作為原始 _source 的一部分發送的字段:
GET my-index/_search
{"query": {"match": {"http.clientip": "40.135.0.0"}},"fields" : ["http.clientip"]
}
響應包含您在搜索查詢中指出的特定 IP 地址。Painless 腳本中的 grok 模式在運行時從 message 字段中提取了此值。
請繼續閱讀文章 “Elasticsearch:Painless scripting 語言(二)”。