Grafana Loki 查詢語言 LogQL 使用

受 PromQL 的啓發,Loki 也有自己的查詢語言,稱爲 LogQL,它就像一個分佈式的 grep,可以聚合查看日誌。和 PromQL 一樣,LogQL 也是使用標籤和運算符進行過濾的,主要有兩種類型的查詢功能:

日誌查詢

一個基本的日誌查詢由兩部分組成。

由於 Loki 的設計,所有 LogQL 查詢必須包含一個日誌流選擇器。一個 Log Stream 代表了具有相同元數據 (Label 集) 的日誌條目。

日誌流選擇器決定了有多少日誌將被搜索到,一個更細粒度的日誌流選擇器將搜索到流的數量減少到一個可管理的數量,通過精細的匹配日誌流,可以大幅減少查詢期間帶來資源消耗。

而日誌流選擇器後面的日誌管道是可選的,用於進一步處理和過濾日誌流信息,它由一組表達式組成,每個表達式都以從左到右的順序爲每個日誌行執行相關過濾,每個表達式都可以過濾、解析和改變日誌行內容以及各自的標籤。

下面的例子顯示了一個完整的日誌查詢的操作:

{container="query-frontend",namespace="loki-dev"} |= "metrics.go" | logfmt | duration > 10s and throughput_mb < 500

該查詢語句由以下幾個部分組成:

爲了避免轉義特色字符,你可以在引用字符串的時候使用單引號,而不是雙引號,比如 \w+1 與 "\w+" 是相同的。

Log Stream Selector

日誌流選擇器決定了哪些日誌流應該被包含在你的查詢結果中,選擇器由一個或多個鍵值對組成,其中每個鍵是一個日誌標籤,每個值是該標籤的值。

日誌流選擇器是通過將鍵值對包裹在一對大括號中編寫的,比如:

{app="mysql", }

上面這個示例表示,所有標籤爲 app 且其值爲 mysql 和標籤爲 name 且其值爲 mysql-backup 的日誌流將被包括在查詢結果中。

其中標籤名後面的 = 運算符是一個標籤匹配運算符,LogQL 中一共支持以下幾種標籤匹配運算符:

例如:

適用於 Prometheus 標籤選擇器的規則同樣適用於 Loki 日誌流選擇器。

Log Pipeline

日誌管道可以附加到日誌流選擇器上,以進一步處理和過濾日誌流。它通常由一個或多個表達式組成,每個表達式針對每個日誌行依次執行。如果一個表達式過濾掉了日誌行,則管道將在此處停止並開始處理下一行。一些表達式可以改變日誌內容和各自的標籤,然後可用於進一步過濾和處理後續表達式或指標查詢。

一個日誌管道可以由以下部分組成。

其中 unwrap 表達式是一個特殊的表達式,只能在度量查詢中使用。

日誌行過濾表達式

日誌行過濾表達式用於對匹配日誌流中的聚合日誌進行分佈式 grep。

編寫入日誌流選擇器後,可以使用一個搜索表達式進一步過濾得到的日誌數據集,搜索表達式可以是文本或正則表達式,比如:

上面示例中的 |=|~!=過濾運算符,支持下面幾種:

過濾運算符可以是鏈式的,並將按順序過濾表達式,產生的日誌行必須滿足每個過濾器。當使用 |~!~ 時,可以使用 Golang 的 RE2 語法的正則表達式,默認情況下,匹配是區分大小寫的,可以用 (?i) 作爲正則表達式的前綴,切換爲不區分大小寫。

雖然日誌行過濾表達式可以放在管道的任何地方,但最好把它們放在開頭,這樣可以提高查詢的性能,當某一行匹配時才做進一步的後續處理。例如,雖然結果是一樣的,但下面的查詢 {job="mysql"} |= "error" |json | line_format "{{.err}}" 會比 {job="mysql"} | json | line_format "{{.message}}" |= "error" 更快,日誌行過濾表達式是繼日誌流選擇器之後過濾日誌的最快方式

解析器表達式

解析器表達式可以解析和提取日誌內容中的標籤,這些提取的標籤可以用於標籤過濾表達式進行過濾,或者用於指標聚合。

提取的標籤鍵將由解析器進行自動格式化,以遵循 Prometheus 指標名稱的約定(它們只能包含 ASCII 字母和數字,以及下劃線和冒號,不能以數字開頭)。

例如下面的日誌經過管道 | json 將產生以下 Map 數據:

{ "a.b"{ "c""d" }"e""f" }

->

{a_b_c="d", e="f"}

在出現錯誤的情況下,例如,如果該行不是預期的格式,該日誌行不會被過濾,而是會被添加一個新的 __error__ 標籤。

需要注意的是如果一個提取的標籤鍵名已經存在於原始日誌流中,那麼提取的標籤鍵將以 _extracted 作爲後綴,以區分兩個標籤,你可以使用一個標籤格式化表達式來強行覆蓋原始標籤,但是如果一個提取的鍵出現了兩次,那麼只有最新的標籤值會被保留。

目前支持 jsonlogfmtpatternregexpunpack 這幾種解析器。

我們應該儘可能使用 jsonlogfmt 等預定義的解析器,這會更加容易,而當日志行結構異常時,可以使用 regexp,可以在同一日誌管道中使用多個解析器,這在你解析複雜日誌時很有用。

JSON

json 解析器有兩種模式運行。

  1. 沒有參數。
  1. 帶參數的

logfmt

logfmt 解析器可以通過使用 | logfmt 來添加,它將從 logfmt 格式的日誌行中提前所有的鍵和值。

例如,下面的日誌行數據:

at=info method=GET path=/ host=grafana.net fwd="124.133.124.161" service=8ms status=200

將提取得到如下所示的標籤:

"at" => "info"
"method" => "GET"
"path" => "/"
"host" => "grafana.net"
"fwd" => "124.133.124.161"
"service" => "8ms"
"status" => "200"

regexp

logfmtjson(它們隱式提取所有值且不需要參數)不同,regexp 解析器採用單個參數 | regexp "<re>" 的格式,其參數是使用 Golang RE2 語法的正則表達式。

正則表達式必須包含至少一個命名的子匹配(例如(?P<name>re)),每個子匹配項都會提取一個不同的標籤。

例如,解析器 | regexp "(?P<method>\\w+) (?P<path>[\\w|/]+) \\((?P<status>\\d+?)\\) (?P<duration>.*)" 將從以下行中提取標籤:

POST /api/prom/api/v1/query_range (200) 1.5s

提取的標籤爲:

"method" => "POST"
"path" => "/api/prom/api/v1/query_range"
"status" => "200"
"duration" => "1.5s"

pattern

模式解析器允許通過定義模式表達式(| pattern "<pattern-expression>")從日誌行中顯式提取字段,該表達式與日誌行的結構相匹配。

比如我們來考慮下面的 NGINX 日誌行數據:

0.191.12.2 - - [10/Jun/2021:09:14:29 +0000] "GET /api/plugins/versioncheck HTTP/1.1" 200 2 "-" "Go-http-client/2.0" "13.76.247.102, 34.120.177.193" "TLSv1.2" "US" ""

該日誌行可以用下面的表達式來解析:

<ip> - - <_> "<method> <uri> <_>" <status> <size> <_> "<agent>" <_>

解析後可以提取出下面的這些屬性:

"ip" => "0.191.12.2"
"method" => "GET"
"uri" => "/api/plugins/versioncheck"
"status" => "200"
"size" => "2"
"agent" => "Go-http-client/2.0"

模式表達式的捕獲是由 <> 字符分隔的字段名稱,比如 <example> 定義了字段名稱爲 example,未命名的 capture 顯示爲 <_>,未命名的 capture 會跳過匹配的內容。默認情況下,模式表達式錨定在日誌行的開頭,可以在表達式的開頭使用 <_> 將表達式錨定在開頭。

比如我們查看下面的日誌行數據:

level=debug ts=2021-06-10T09:24:13.472094048Z caller=logging.go:66 traceID=0568b66ad2d9294c msg="POST /loki/api/v1/push (204) 16.652862ms"

我們如果只希望去匹配 msg=" 的內容,我們可以使用下面的表達式來進行匹配:

<_> msg="<method> <path> (<status>) <latency>"

前面大部分日誌數據我們不需要,只需要使用 <_> 進行佔位即可,明顯可以看出這種方式比正則表達式要簡單得多。

unpack

unpack 解析器將解析 json 日誌行,並通過打包階段解開所有嵌入的標籤,一個特殊的屬性 _entry 也將被用來替換原來的日誌行。

例如,使用 | unpack 解析器,可以得到如下所示的標籤:

{
  "container""myapp",
  "pod""pod-3223f",
  "_entry""original log message"
}

允許提取 containerpod 標籤以及原始日誌信息作爲新的日誌行。

如果原始嵌入的日誌行是特定的格式,你可以將 unpack 與 json 解析器(或其他解析器)相結合使用。

標籤過濾表達式

標籤過濾表達式允許使用其原始和提取的標籤來過濾日誌行,它可以包含多個謂詞。

一個謂詞包含一個標籤標識符、操作符和用於比較標籤的值。

例如 cluster="namespace" 其中的 cluster 是標籤標識符,操作符是 =,值是"namespace"

LogQL 支持從查詢輸入中自動推斷出的多種值類型:

字符串類型的工作方式與 Prometheus 標籤匹配器在日誌流選擇器中使用的方式完全一樣,這意味着你可以使用同樣的操作符(=!==~!~)。

使用 Duration、Number 和 Bytes 將在比較前轉換標籤值,並支持以下比較器。

例如 logfmt | duration > 1m and bytes_consumed > 20MB 過濾表達式。

如果標籤值的轉換失敗,日誌行就不會被過濾,而會添加一個 __error__ 標籤。你可以使用 andor 來連接多個謂詞,它們分別表示的二進制操作,and 可以用逗號、空格或其他管道來表示,標籤過濾器可以放在日誌管道的任何地方。

以下所有的表達式都是等價的:

| duration >= 20ms or size == 20kb and method!~"2.."
| duration >= 20ms or size == 20kb | method!~"2.."
| duration >= 20ms or size == 20kb,method!~"2.."
| duration >= 20ms or size == 20kb method!~"2.."

默認情況下,多個謂詞的優先級是從右到左,你可以用圓括號包裝謂詞,強制使用從左到右的不同優先級。

例如,以下內容是等價的:

| duration >= 20ms or method="GET" and size <= 20KB
| ((duration >= 20ms or method="GET") and size <= 20KB)

它將首先評估 duration>=20ms or method="GET",要首先評估 method="GET" and size<=20KB,請確保使用適當的括號,如下所示。

| duration >= 20ms or (method="GET" and size <= 20KB)

日誌行格式表達式

日誌行格式化表達式可以通過使用 Golang 的 text/template 模板格式重寫日誌行的內容,它需要一個字符串參數 | line_format "{{.label_name}}" 作爲模板格式,所有的標籤都是注入模板的變量,可以用 {{.label_name}} 的符號來使用。

例如,下面的表達式:

{container="frontend"} | logfmt | line_format "{{.query}} {{.duration}}"

將提取並重寫日誌行,只包含 query 和請求的 duration。你可以爲模板使用雙引號字符串或反引號 {{.label_name}} 來避免轉義特殊字符。

此外 line_format 也支持數學函數,例如:

如果我們有以下標籤 ip=1.1.1.1, status=200duration=3000(ms), 我們可以用 duration 除以 1000 得到以秒爲單位的值:

{container="frontend"} | logfmt | line_format "{{.ip}} {{.status}} {{div .duration 1000}}"

上面的查詢將得到的日誌行內容爲1.1.1.1 200 3

標籤格式表達式

| label_format 表達式可以重命名、修改或添加標籤,它以逗號分隔的操作列表作爲參數,可以同時進行多個操作。

當兩邊都是標籤標識符時,例如 dst=src,該操作將把 src 標籤重命名爲 dst

左邊也可以是一個模板字符串,例如 dst="{{.status}} {{.query}}",在這種情況下,dst 標籤值會被 Golang 模板執行結果所取代,這與 | line_format 表達式是同一個模板引擎,這意味着標籤可以作爲變量使用,也可以使用同樣的函數列表。

在上面兩種情況下,如果目標標籤不存在,那麼就會創建一個新的標籤。

重命名形式 dst=src 會在將 src 標籤重新映射到 dst 標籤後將其刪除,然而,模板形式將保留引用的標籤,例如 dst="{{.src}}" 的結果是 dstsrc 都有相同的值。

一個標籤名稱在每個表達式中只能出現一次,這意味着 | label_format foo=bar,foo="new" 是不允許的,但你可以使用兩個表達式來達到預期效果,比如 | label_format foo=bar | label_format foo="new"

日誌度量

LogQL 同樣支持通過函數方式將日誌流進行度量,通常我們可以用它來計算消息的錯誤率或者排序一段時間內的應用日誌輸出 Top N。

區間向量

LogQL 同樣也支持有限的區間向量度量語句,使用方式和 PromQL 類似,常用函數主要是如下 4 個:

比如計算 nginx 的 qps:

rate({file}[5m]))

計算 kernel 過去 5 分鐘發生 oom 的次數:

count_over_time({file} |~ "oom_kill_process" [5m]))

聚合函數

LogQL 也支持聚合運算,我們可用它來聚合單個向量內的元素,從而產生一個具有較少元素的新向量,當前支持的聚合函數如下:

聚合函數我們可以用如下表達式描述:

<aggr-op>([parameter,] <vector expression>) [without|by (<label list>)]

對於需要對標籤進行分組時,我們可以用 without 或者 by 來區分。比如計算 nginx 的 qps,並按照 pod 來分組:

sum(rate({file}[5m])) by (pod)

只有在使用 bottomktopk 函數時,我們可以對函數輸入相關的參數。比如計算 nginx 的 qps 最大的前 5 個,並按照 pod 來分組:

topk(5,sum(rate({file}[5m])) by (pod)))

二元運算

數學計算

Loki 存的是日誌,都是文本,怎麼計算呢?顯然 LogQL 中的數學運算是面向區間向量操作的,LogQL 中的支持的二進制運算符如下:

比如我們要找到某個業務日誌裏面的錯誤率,就可以按照如下方式計算:

sum(rate({app="foo", level="error"}[1m])) / sum(rate({app="foo"}[1m]))

邏輯運算

集合運算僅在區間向量範圍內有效,當前支持

比如:

rate({app=~"foo|bar"}[1m]) and rate({app="bar"}[1m])

比較運算

LogQL 支持的比較運算符和 PromQL 一樣,包括:

通常我們使用區間向量計算後會做一個閾值的比較,這對應告警是非常有用的,比如統計 5 分鐘內 error 級別日誌條目大於 10 的情況:

count_over_time({app="foo", level="error"}[5m]) > 10

我們也可以通過布爾計算來表達,比如統計 5 分鐘內 error 級別日誌條目大於 10 爲真,反正則爲假:

count_over_time({app="foo", level="error"}[5m]) > bool 10

註釋

LogQL 查詢可以使用 # 字符進行註釋,例如:

{app="foo"} # anything that comes after will not be interpreted in your query

對於多行 LogQL 查詢,可以使用 # 排除整個或部分行:

{app="foo"}
    | json
    # this line will be ignored
    | bar="baz" # this checks if bar = "baz"

查詢示例

這裏我們部署一個示例應用,該應用程序是一個僞造的記錄器,它的日誌具有 debug、info 和 warning 輸出到 stdout。error 級別的日誌將被寫入 stderr,實際的日誌消息以 JSON 格式生成,每 500 毫秒將創建一條新的日誌消息。日誌消息格式如下所示:

{
    "app":"The fanciest app of mankind",
    "executable":"fake-logger",
    "is_even": true,
    "level":"debug",
    "msg":"This is a debug message. Hope you'll catch the bug",
    "time":"2022-04-04T13:41:50+02:00",
    "version":"1.0.0"
}

使用下面的命令來創建示例應用:

cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
 labels:
  app: fake-logger
  environment: development
 name: fake-logger
spec:
 selector:
  matchLabels:
   app: fake-logger
   environment: development
 template:
  metadata:
   labels:
    app: fake-logger
    environment: development
  spec:
   containers:
   - image: thorstenhans/fake-logger:0.0.2
     name: fake-logger
     resources:
      requests:
       cpu: 10m
       memory: 32Mi
      limits:
       cpu: 10m
       memory: 32Mi
EOF

我們可以使用 {app="fake-logger"} 在 Grafana 中查詢到該應用的日誌流數據。

由於我們該示例應用的日誌是 JSON 形式的,我們可以採用 JSON 解析器來解析日誌,表達式爲 {app="fake-logger"} | json,如下所示。

使用 JSON 解析器解析日誌後可以看到 Grafana 提供的面板會根據 level 的值使用不同的顏色進行區分,而且現在我們日誌的屬性也被添加到了 Log 的標籤中去了。

現在 JSON 中的數據變成了日誌標籤我們自然就可以使用這些標籤來過濾日誌數據了,比如我們要過濾 level=error 的日誌,只使用表達式 {app="fake-logger"} | json | level="error" 即可實現。

此外我們還可以根據我們的需求去格式化輸出日誌,使用 line_format 即可實現,比如我們這裏使用查詢語句 {app="fake-logger"} | json |is_even="true" | line_format "在 {{.time}} 於 {{.level}}@{{.pod}} Pod中產生了日誌 {{.msg}}" 來格式化日誌輸出

監控大盤

這裏我們以監控 Kubernetes 的事件爲例進行說明。首先需要安裝 [kubernetes-event-exporter],地址  https://github.com/opsgenie/kubernetes-event-exporter/tree/master/deploy,kubernetes-event-exporter 日誌會打印到 stdout,然後我們的 promtail 會將日誌上傳到 Loki。

然後導入 https://grafana.com/grafana/dashboards/14003 這個 Dashboard 即可,不過需要注意修改每個圖表中的過濾標籤爲 job="monitoring/event-exporter"

修改後正常就可以在 Dashboard 中看到集羣中的相關事件信息了,不過建議用記錄規則去替換面板中的查詢語句。

建議

  1. 儘量使用靜態標籤,開銷更小,通常日誌在發送到 Loki 之前注入 label,推薦的靜態標籤包含:
  1. 謹慎使用動態標籤。過多的標籤組合會造成大量的流,它會讓 Loki 存儲大量的索引和小塊的對象文件。這些都會顯著消耗 Loki 的查詢性能。爲避免這些問題,在你知道需要之前不要添加標籤。Loki 的優勢在於並行查詢,使用過濾器表達式(label="text", |~ "regex", ...)來查詢日誌會更有效,並且速度也很快。

  2. 有界的標籤值範圍,作爲 Loki 的用戶或操作員,我們的目標應該是使用盡可能少的標籤來存儲你的日誌。這意味着,更少的標籤帶來更小的索引,從而導致更好的性能,所以我們在添加標籤之前一定要三思而行。

  3. 配置緩存,Loki 可以爲多個組件配置緩存, 可以選擇 redis 或者 memcached,這可以顯著提高性能。

  4. 合理使用 LogQL 語法,可大幅提高查詢效率。Label matchers(標籤匹配器)是你的第一道防線,是大幅減少你搜索的日誌數量 (例如,從 100TB 到 1TB) 的最好方法。當然,這意味着你需要在日誌採集端上有良好的標籤定義規範。

k8s 技術圈 專注容器、專注 kubernetes 技術......

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/-5vS_xVxKDfrVW_Tg0PMzQ