系列文章
前言
實際應用中除了基於 Metrics 告警, 往往還有基於日誌的告警需求, 可以作為基於 Metrics 告警之外的一個補充. 典型如基於 NGINX 日誌的錯誤率告警.本文將介紹如何基於 Loki 實現基於日誌的告警.
本文我們基於以下 2 類實際場景進行實戰演練:
- 基於 NGINX 日誌的錯誤率告警
- 基於 Nomad 日誌的心跳異常告警(關於 Nomad 的介紹, 可以參見這篇文章: 《大規模 IoT 邊緣容器叢集管理的幾種架構 -2-HashiCorp 解決方案 Nomad》)
基於日誌告警的應用場景
基於日誌告警的廣泛應用於如下場景:
黑盒監控
對於不是我們開發的元件, 如雲廠商/第三方的負載均衡器和無數其他元件(包括開源元件和封閉第三方元件)支援我們的應用程式,但不會公開我們想要的指標。有些根本不公開任何指標。 Loki 的警報和記錄規則可以生成有關係統狀態的指標和警報,並透過使用日誌將元件帶入我們的可觀察性堆疊中。這是一種將高階可觀察性引入遺留架構的極其強大的方法。
事件告警
有時,您想知道某件事情是否已經發生。根據日誌發出警報可以很好地解決這個問題,例如查詢身份驗證憑據洩露的示例:
- name: credentials_leak
rules:
- alert: http-credentials-leaked
annotations:
message: "{{ $labels.job }} is leaking http basic auth credentials."
expr: 'sum by (cluster, job, pod) (count_over_time({namespace="prod"} |~ "http(s?)://(\\w+):(\\w+)@" [5m]) > 0)'
for: 10m
labels:
severity: critical
關於 Nomad 的就屬於這類場景.
技術儲備
Loki 告警
Grafana Loki 包含一個名為 ruler 的元件。Ruler 負責持續評估一組可配置查詢並根據結果執行操作。其支援兩種規則:alerting 規則和 recording 規則。
Loki Alering 規則
Loki 的告警規則格式幾乎與 Prometheus 一樣. 這裡舉一個完整的例子:
groups:
- name: should_fire
rules:
- alert: HighPercentageError
expr: |
sum(rate({app="foo", env="production"} |= "error" [5m])) by (job)
/
sum(rate({app="foo", env="production"}[5m])) by (job)
> 0.05
for: 10m
labels:
severity: page
annotations:
summary: High request latency
- name: credentials_leak
rules:
- alert: http-credentials-leaked
annotations:
message: "{{ $labels.job }} is leaking http basic auth credentials."
expr: 'sum by (cluster, job, pod) (count_over_time({namespace="prod"} |~ "http(s?)://(\\w+):(\\w+)@" [5m]) > 0)'
for: 10m
labels:
severity: critical
Loki LogQL 查詢
Loki 日誌查詢語言 (LogQL) 是一種查詢語言,用於從 Loki 中檢索日誌。LogQL 與 Prometheus 非常相似,但有一些重要的區別。
LogQL 快速上手
所有 LogQL 查詢都包含日誌流選擇器(log stream selector)。如下圖:
可選擇在日誌流選擇器後新增日誌管道(log pipeline)。日誌管道是一組階段表示式,它們串聯在一起並應用於選定的日誌流。每個表示式都可以過濾、解析或更改日誌行及其各自的標籤。
以下示例顯示了正在執行的完整日誌查詢:
{container="query-frontend",namespace="loki-dev"}
|= "metrics.go"
| logfmt
| duration > 10s
and throughput_mb < 500
該查詢由以下部分組成:
- 日誌流選擇器
{container="query-frontend",namespace="loki-dev"}
,其目標是loki-dev
名稱空間中的query-frontend
容器。 - 日誌管道
|= "metrics.go" | logfmt | duration > 10s and throughput_mb < 500
它將過濾掉包含單詞metrics.go
的日誌,然後解析每個日誌行以提取更多標籤並使用它們進行過濾。
解析器表示式
為了進行告警, 我們往往需要在告警之前對非結構化日誌進行解析, 解析後會獲得更精確的欄位資訊(稱為label
), 這就是為什麼我們需要使用解析器表示式.
解析器表示式可從日誌內容中解析和提取標籤(label)。這些提取的標籤可用於使用標籤過濾表示式進行過濾,或用於 metrics 彙總。
如果原始日誌流中已經存在提取的標籤 key名稱(典型如: level
),提取的標籤 key 將以 _extracted
關鍵字為字尾,以區分兩個標籤。你也可以使用標籤格式表示式強行覆蓋原始標籤。不過,如果提取的鍵出現兩次,則只保留第一個標籤值。
Loki 支援 JSON、logfmt、pattern、regexp 和 unpack 解析器。
今天我們重點介紹下 logfmt, pattern 和 regexp 解析器。
logfmt 解析器
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"
帶引數
與 JSON 解析器類似,在管道中使用 | logfmt label="expression", another="expression"
將導致只提取標籤指定的欄位。
例如, | logfmt host, fwd_ip="fwd"
將從以下日誌行中提取標籤 host
和 fwd
:
at=info method=GET path=/ host=grafana.net fwd="124.133.124.161" service=8ms status=200
並將 fwd
重新命名為 fwd_ip
:
"host" => "grafana.net"
"fwd_ip" => "124.133.124.161"
Pattern 解析器
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"
Pattern 表示式由捕獲(captures )和文字組成。
捕獲是以 <
和 >
字元分隔的欄位名。<example>
定義欄位名 example
。未命名的捕獲顯示為 <_>
。未命名的捕獲會跳過匹配的內容。
Regular Expression 解析器
logfmt 和 json 會隱式提取所有值且不需要引數,而 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"
實戰演練
📝說明:
下面的這 2 個例子只是為了演示 Loki 的實際使用場景. 實際環境中, 如果你透過 Prometheus 已經可以獲取到如:
- NGINX 錯誤率
- Nomad Client 活躍數/Nomad Client 總數
則可以直接使用 Prometheus 進行告警. 不需要多此一舉.
基於 NGINX 日誌的錯誤率告警
我們將使用 | pattern
解析器從 NGINX 日誌中提取 status label,並使用 rate()
函式計算每秒錯誤率。
假設 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"
再根據 status
label 進行計算, status > 500
記為錯誤. 則最終告警語句如下:
sum(rate({job="nginx"} | pattern <ip> - - <_> "<method> <uri> <_>" <status> <size> <_> "<agent>" <_> | status > 500 [5m])) by (instance)
/
sum(rate({job="nginx"} [5m])) by (instance)
> 0.05
詳細說明如下:
- 完整 LogQL 的含義是: NGINX 單個 instance 錯誤率 > 5%
{job="nginx"}
Log Stream, 這裡假設 NGINX 其job
為nginx
. 表明檢索的是 NGINX 的日誌.| pattern <ip> - - <_> "<method> <uri> <_>" <status> <size> <_> "<agent>" <_>
使用 Pattern 解析器解析, 上文詳細說明過了, 這裡不做解釋了| status > 500
解析後得到status
label, 使用 Log Pipeline 篩選出status > 500
的錯誤日誌rate(... [5m])
計算 5m 內的每秒 500 錯誤數sum () by (instance)
按 instance 聚合, 即計算每個 instance 的每秒 500 錯誤數/ sum(rate({job="nginx"} [5m])) by (instance) > 0.05
用 每個 instance 的每秒 500 錯誤數 / 每個 instance 的每秒請求總數得出每秒的錯誤率是否大於 5%
再使用該指標建立告警規則, 具體如下:
alert: NGINXRequestsErrorRate
expr: >-
sum(rate({job="nginx"} | pattern <ip> - - <_> "<method> <uri> <_>" <status> <size> <_> "<agent>" <_> | status > 500 [5m])) by (instance)
/
sum(rate({job="nginx"} [5m])) by (instance)
> 0.05
for: 1m
annotations:
summary: NGINX 例項{{ $labels.instance }}的錯誤率超過 5%.
description: ''
runbook_url: ''
labels:
severity: 'warning'
完成! 🎉🎉🎉
基於 Nomad 日誌的心跳異常告警
Nomad 的日誌的典型格式如下:
2023-12-08T21:39:09.718+0800 [WARN] nomad.heartbeat: node TTL expired: node_id=daf861cc-641d-f0a6-62ee-d954f6edd3a4
2023-12-07T21:39:04.905+0800 [ERROR] nomad.rpc: multiplex_v2 conn accept failed: error="keepalive timeout"
這裡我嘗試先使用 pattern 解析器進行解析, 解析表示式如下:
{unit="nomad.service", transport="stdout"}
| pattern <time> [<level>] <component>: <message>
結果解析異常, 解析後得到:
...
"level" => "WARN"
...
"level" => ERROR] nomad.rpc: multiplex_v2 conn accept failed: error="keepalive timeout"
level
解析明顯不正確, 原因是 level
後面不是空格, 而是 tab 製表符. 導致在 [WARN]
時後面有 2 個空格; [ERROR]
時後面有 1 個空格. pattern 解析器對這種情況支援不好, 我查閱官方資料短期內並沒有找到這種情況的解決辦法.
所以最終只能透過 regexp 解析器進行解析.
最終的解析表示式如下:
{unit="nomad.service", transport="stdout"}
| regexp `(?P<time>\S+)\s+\[(?P<level>\w+)\]\s+(?P<component>\S+): (.+)
詳細說明如下:
(?P<time>\S+)
解析時間. 以 Nomad 的格式, 就是第一批非空格字串. 如:2023-12-08T21:39:09.718+0800
\s+
匹配時間和日誌級別之間的空格\[(?P<level>\w+)\]\
匹配告警級別, 如[WARN]
[ERROR]
, 這裡[]
是特殊字元, 所以前面要加\
作為普通字元處理\s+
匹配日誌級別和元件之間的空白字元. 無論是一個/兩個空格, 還是一個 tab 都能命中(?P<component>\S+):
匹配元件, 這裡的\S+
匹配至少一個非空白字元, 即匹配到元件名. 這一段匹配如:nomad.heartbeat:
和nomad.rpc:
. component匹配到nomad.heartbeat
和nomad.rpc
\s
也行(.+)
匹配日誌最後的內容, 這裡的(.+)
匹配至少一個非空白字元, 即匹配到日誌內容. 如:node TTL expired: node_id=daf861cc-641d-f0a6-62ee-d954f6edd3a4
解析後得到:
"time" => 2023-12-08T21:39:09.718+0800
"level_extracted" => WARN
"component" => nomad.heartbeat
"message" => node TTL expired: node_id=daf861cc-641d-f0a6-62ee-d954f6edd3a4
"time" => 2023-12-07T21:39:04.905+0800
"level_extracted" => ERROR
"component" => nomad.rpc
"message" => multiplex_v2 conn accept failed: error="keepalive timeout"
解析後再以此進行告警, 告警條件暫定為: component = nomad.heartbeat, level_extracted =~ WARN|ERROR
具體 LogQL 為:
count by(job)
(rate(
{unit="nomad.service", transport="stdout"}
| regexp `(?P<time>\S+)\s+\[(?P<level>\w+)\]\s+(?P<component>\S+): (.+)`
| component = `nomad.heartbeat`
| level_extracted =~ `WARN|ERROR` [5m]))
> 3
詳細說明如下:
- Nomad 日誌流為:
{unit="nomad.service", transport="stdout"}
{unit="nomad.service", transport="stdout"}
| regexp `(?P<time>\S+)\s+\[(?P<level>\w+)\]\s+(?P<component>\S+): (.+)`
| component = `nomad.heartbeat`
| level_extracted =~ `WARN|ERROR`
- 篩選出 component 為 nomad.heartbeat, level_extracted 為
WARN|ERROR
的日誌條目 - 每秒心跳錯誤數 > 3 就告警
最終告警規則如下:
alert: Nomad HeartBeat Error
for: 1m
annotations:
summary: Nomad Server和Client之間心跳異常.
description: ''
runbook_url: ''
labels:
severity: 'warning'
expr: >-
count by(job) (rate({unit="nomad.service", transport="stdout"} | regexp
`(?P<time>\S+)\s+\[(?P<level>\w+)\]\s+(?P<component>\S+): (.+)` | component =
`nomad.heartbeat` | level_extracted =~ `WARN|ERROR` [5m])) > 3
完成🎉🎉🎉
善用 Grafana UI 進行 LogQL
Grafana UI 對於 LogQL 的支援比較好, 有完善的提示/幫助和指南, 以及非常適合不瞭解 LogQL 語法的 Builder 模式及 Explain 功能. 讀者上手的時候不要被前面大段大段的 LogQL 和 YAML 嚇到, 可以直接使用 Grafana 構造自己想要的基於日誌的查詢和告警.
Grafana 具體的功能增強有:
-
語法/拼寫驗證(查詢表示式驗證): 為了加快編寫正確 LogQL 查詢的過程,Grafana 9.4 新增了一項新功能:查詢表示式驗證。或者可以直觀地叫做 "紅色斜線 "功能,因為它使用的波浪線與您在文書處理器中輸入錯別字時看到的下劃線文字相同🙂。有了查詢驗證功能,你就不必再執行查詢來檢視它是否正確了。相反,如果查詢無效,你會得到實時反饋。出現這種情況時,紅色斜線會顯示錯誤的具體位置,以及哪些字元不正確。查詢表示式驗證還支援多行查詢。
-
自動補全功能: 如可以根據查詢檢視建議的解析器型別(如
logfmt
、JSON
), 能幫助您為資料編寫更合適的查詢。此外,如果您在查詢中使用解析器,所有標籤(包括解析器提取的標籤)都會在帶分組的範圍聚合(如sum by()
)中得到建議。 -
歷史記錄: Loki 的程式碼編輯器現在直接整合了查詢歷史記錄。一旦您開始編寫新查詢,就會顯示您之前執行的查詢。此功能在 Explore 中特別有用,因為您通常不會從頭開始,而是想利用以前的工作。
-
標籤瀏覽器: 直接瀏覽所有標籤, 並在查詢中使用它們. 這對於快速瀏覽和查詢標籤非常有用.
-
日誌樣本: 我們知道,很多在 Explore 中進行度量查詢的使用者都希望看到促成該度量的日誌行示例。這正是在 Grafana 9.4 中提供的新功能!這將有助於除錯過程,主要是透過基於日誌行內容的行過濾器或標籤過濾器幫助您縮小度量查詢的範圍。
我其實對 LogQL 也剛開始學習, 這次也是主要在 Grafana 的幫助下完成, 具體如下:
👍️👍️👍️
總結
以上就是基於 Loki 實現告警的基本流程. 告警之前往往需要對日誌進行解析和篩選, 具體實現細節可以根據實際情況進行調整.
最後, 一定要結合 Grafana UI 進行 LogQL 的使用, 這樣可以更加方便地進行 LogQL 的編寫和除錯.
希望本文對大家有所幫助.
📚️參考文件
- Log queries | Grafana Loki documentation --- 日誌查詢 | Grafana Loki 文件
- Loki 官方文件 - Alerting
- Write Loki queries easier with Grafana 9.4: Query validation, improved autocomplete, and more | Grafana Labs --- 使用 Grafana 9.4 更輕鬆地編寫 Loki 查詢:查詢驗證、改進的自動完成等等 |格拉法納實驗室
三人行, 必有我師; 知識共享, 天下為公. 本文由東風微鳴技術部落格 EWhisper.cn 編寫.