實現基於 Grafana Loki 的日誌報警

語言: CN / TW / HK

對於生產環境以及一個有追求的運維人員來說,哪怕是毫秒級別的宕機也是不能容忍的。對基礎設施及應用進行適當的日誌記錄和監控非常有助於解決問題,還可以幫助優化成本和資源,以及幫助檢測以後可能會發生的一些問題。前面我們學習使用了 Prometheus 來進行監控報警,但是如果我們使用 Loki 收集日誌是否可以根據採集的日誌來進行報警呢?答案是肯定的,而且有兩種方式可以來實現:Promtail 中的 metrics 階段和 Loki 的 ruler 元件。

測試應用

比如現在我們有一個如下所示的 nginx 應用用於 Loki 日誌報警:

# nginx-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- name: nginx
port: 80
protocol: TCP
selector:
app: nginx
type: NodePort

為方便測試,我們這裡使用 NodePort 型別的服務來暴露應用,直接安裝即可:

$ kubectl apply -f nginx-deploy.yaml
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-5d59d67564-ll9xf 1/1 Running 0 16s
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 91d
nginx NodePort 10.99.153.32 <none> 80:31313/TCP 22s

我們可以通過如下命令來來模擬每隔10s訪問 Nginx 應用:

$ while true; do curl --silent --output /dev/null --write-out '%{http_code}' http://192.168.0.106:31313; sleep 10; echo; done
200
200

metrics 階段

前面我們提到在 Promtail 中通過一系列 Pipeline 來處理日誌,其中就包括一個 metrics 的階段,可以根據我們的需求來增加一個監控指標,這就是我們需要實現的基於日誌的監控報警的核心點,通過結構化日誌,增加監控指標,然後使用 Prometheus 結合 Alertmanager 完成之前我們非常熟悉的監控報警。

首先我們需要安裝 Prometheus 與 Alertmanager,可以手動安裝,也可以使用 Prometheus Operator 的方式,可以參考監控報警章節相關內容,比如這裡我們選擇使用 Prometheus Operator 的方式。

前面我們介紹了幾種 Loki 的部署方式,這裡我們就保留上節微服務模式的 Loki 叢集,接下來我們需要重新配置 Promtail,為其新增一個 metrics 處理階段,使用如下所示的 values 檔案重新安裝。

# ci/metrics-values.yaml
rbac:
pspEnabled: false
config:
clients:
- url: http://loki-loki-distributed-gateway/loki/api/v1/push
snippets:
pipelineStages:
- cri: {}
- match:
selector: '{app="nginx"}'
stages:
- regex:
expression: '.*(?P<hits>GET /.*)'
- metrics:
nginx_hits:
type: Counter
description: "Total nginx requests"
source: hits
config:
action: inc
serviceMonitor:
enabled: true
additionalLabels:
app: prometheus-operator
release: prometheus

上面最重要的部分就是為 Promtail 添加了 pipelineStages 配置,用於對日誌行進行轉換,在這裡我們添加了一個 match 的階段,會去匹配具有 app=nginx 這樣的日誌流資料,然後下一個階段是利用正則表示式過濾出包含 GET 關鍵字的日誌行。

在 metrics 指標階段,我們定義了一個 nginx_hits 的指標,Promtail 通過其 /metrics 端點暴露這個自定義的指標資料。這裡我們定義的是一個 Counter 型別的指標,當從 regex 階段匹配上後,這個計數器就會遞增。

為了在 Prometheus 中能夠這個指標,我們通過 promtail.serviceMonitor.enable=true 開啟了一個 ServiceMonitor。接下來重新更新 Loki 應用,使用如下所示的命令即可:

$ helm upgrade --install loki -n logging -f ci/metrics-values.yaml .

更新完成後會建立一個 ServiceMonitor 物件用於發現 Promtail 的指標資料:

$ kubectl get servicemonitor -n logging
NAME AGE
loki-promtail 10s

如果你使用的 Prometheus-Operator 預設不能發現 logging 名稱空間下面的資料,則需要建立如下所示的一個 Role 許可權:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
labels:
app.kubernetes.io/component: prometheus
app.kubernetes.io/name: prometheus
app.kubernetes.io/part-of: kube-prometheus
app.kubernetes.io/version: 2.26.0
name: prometheus-k8s
namespace: logging
rules:
- apiGroups:
- ""
resources:
- services
- endpoints
- pods
verbs:
- get
- list
- watch
- apiGroups:
- extensions
resources:
- ingresses
verbs:
- get
- list
- watch
- apiGroups:
- networking.k8s.io
resources:
- ingresses
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: prometheus-k8s
namespace: logging
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: prometheus-k8s
subjects:
- kind: ServiceAccount
name: prometheus-k8s
namespace: monitoring

正常在 Prometheus 裡面就可以看到 Promtail 的抓取目標了:

如果你使用的是 Prometheus Operator 自帶的 Grafana,則需要手動新增上 Loki 的資料來源,前面微服務模式中我們已經在 Grafana 中配置了 Loki 的資料來源,現在當我們訪問測試應用的時候,在 Loki 中是可以檢視到日誌資料的:

而且現在在 Prometheus 中還可以查詢到我們在  Promtail 中新增的 metrics 指標資料:

因為現在已經有監控指標了,所以我們就可以根據需求來建立報警規則了,我們這裡使用的 Prometheus Operator,所以可以直接建立一個 PrometheusRule 資源物件即可:

# nginx-prometheus-rule.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
labels:
prometheus: k8s
role: alert-rules
name: promtail-nginx-hits
namespace: logging
spec:
groups:
- name: nginx-hits
rules:
- alert: LokiNginxHits
annotations:
summary: nginx hits counter
description: 'nginx_hits total insufficient count ({{ $value }}).'
expr: |
sum(increase(promtail_custom_nginx_hits[1m])) > 2
for: 2m
labels:
severity: critical

這裡我們配置了名為 nginx_hits 的報警規則,這些規則在同一個分組中,每隔一定的時間間隔依次執行。觸發報警的閾值通過 expr 表示式進行配置。我們這裡表示的是1分鐘之內新增的總和是否大於2,當 expor 表示式的條件持續了2分鐘時間後,報警就會真正被觸發,報警真正被觸發之前會保持為 Pending 狀態。

然後具體想要把報警傳送到什麼地方去,可以根據標籤去配置 receiver,比如可以通過 WebHook 來接收。我們在 AlertManager 中也是可以看到接收到的報警事件的。

Ruler 元件

上面的方式雖然可以實現我們的日誌報警功能,但是還是不夠直接,需要通過 Promtail 去進行處理,那麼我們能否直接通過 Loki 來實現報警功能呢?其實在 Loki2.0 版本就提供了報警功能,其中有一個 Ruler 元件可以持續查詢一個 rules 規則,並將超過閾值的事件推送給 AlertManager 或者其他 Webhook 服務,這也就是 Loki 自帶的報警功能了,而且是相容 AlertManager 的。

首先我們需要開啟 Loki Ruler 元件,更新 loki-distributed 安裝的 Values 檔案,在前面微服務模式的基礎上增加 ruler 元件配置:

# ci/alert-values.yaml
loki:
structuredConfig:
ingester:
max_transfer_retries: 0
chunk_idle_period: 1h
chunk_target_size: 1536000
max_chunk_age: 1h
storage_config: # 儲存的配置,定義其他元件可能用到的儲存
aws: # s3 / s3 相容的物件儲存
endpoint: minio.logging.svc.cluster.local:9000
insecure: true
bucketnames: loki-data
access_key_id: myaccessKey
secret_access_key: mysecretKey
s3forcepathstyle: true
boltdb_shipper:
shared_store: s3
schema_config:
configs:
- from: 2022-06-21
store: boltdb-shipper # index
object_store: s3 # chunks
schema: v12
index:
prefix: loki_index_
period: 24h
ruler:
storage:
type: local
local:
directory: /etc/loki/rules
ring:
kvstore:
store: memberlist
rule_path: /tmp/loki/scratch
alertmanager_url: http://alertmanager-main.monitoring.svc.cluster.local:9093
external_url: http:/192.168.0.106:31918

distributor:
replicas: 2

ingester: # WAL(replay)
replicas: 2
persistence:
enabled: true
size: 1Gi
storageClass: local-path

querier:
replicas: 2
persistence:
enabled: true
size: 1Gi
storageClass: local-path

queryFrontend:
replicas: 2

gateway: # nginx容器 -> 路由日誌寫/讀的請求
nginxConfig:
httpSnippet: |-
client_max_body_size 100M;
serverSnippet: |-
client_max_body_size 100M;

# Configuration for the ruler
ruler:
enabled: true
kind: Deployment
replicas: 1
persistence:
enabled: true
size: 1Gi
storageClass: local-path
# -- Directories containing rules files
directories:
tenant_no:
rules1.txt: |
groups:
- name: nginx-rate
rules:
- alert: LokiNginxRate
expr: sum(rate({app="nginx"} |= "error" [1m])) by (job)
/
sum(rate({app="nginx"}[1m])) by (job)
> 0.01
for: 1m
labels:
severity: critical
annotations:
summary: loki nginx rate
description: high request latency

我們首先通過 loki.structuredConfig.ruler 對 Ruler 元件進行配置,比如指定 Alertmanager 的地址,規則儲存方式等,然後通過 ruler 屬性配置了元件的相關資訊以及報警規則,重新使用上面的 values 檔案安裝 Loki:

$ helm upgrade --install loki -n logging -f ci/alert-values.yaml .
$ kubectl get pods -n logging
NAME READY STATUS RESTARTS AGE
grafana-55d8779dc6-gkgpf 1/1 Running 2 (66m ago) 3d21h
loki-loki-distributed-distributor-56959cc548-xpv6d 1/1 Running 0 3m36s
loki-loki-distributed-distributor-56959cc548-zjfsb 1/1 Running 0 2m52s
loki-loki-distributed-gateway-6f4cfd898c-p9xxf 1/1 Running 0 21m
loki-loki-distributed-ingester-0 1/1 Running 0 2m32s
loki-loki-distributed-ingester-1 1/1 Running 0 3m34s
loki-loki-distributed-querier-0 1/1 Running 0 2m48s
loki-loki-distributed-querier-1 1/1 Running 0 3m29s
loki-loki-distributed-query-frontend-5bcc7949d-brzg6 1/1 Running 0 3m30s
loki-loki-distributed-query-frontend-5bcc7949d-g2wwd 1/1 Running 0 3m35s
loki-loki-distributed-ruler-5d4b8cd889-m2vbd 1/1 Running 0 3m35s
minio-548656f786-mjd4c 1/1 Running 2 (66m ago) 3d21h
promtail-ddz27 1/1 Running 0 19m
promtail-lzr6v 1/1 Running 0 20m
promtail-nldqx 1/1 Running 0 20m

Loki 的 rulers 規則和結構與 Prometheus 是完全相容,唯一的區別在於查詢語句(LogQL)不同,在 Loki 中我們用 LogQL 來查詢日誌,一個典型的 rules 配置檔案如下所示:

groups:
# 組名稱
- name: xxxx
rules:
# Alert名稱
- alert: xxxx
# logQL查詢語句
expr: xxxx
# 產生告警的持續時間 pending.
[ for: | default = 0s ]
# 自定義告警事件的label
labels:
[ : ]
# 告警時間的註釋
annotations:
[ : ]

比如我們這裡配置的規則 sum(rate({app="nginx"} |= "error" [1m])) by (job) / sum(rate({app="nginx"}[1m])) by (job) > 0.01 表示通過日誌查到 nginx 日誌的錯誤率大於1%就觸發告警,同樣重新使用上面的 values 檔案更新 Loki:

更新完成後我們檢視 Ruler 元件的日誌可以看到一些關於上面我們配置的報警規則的資訊:

$ kubectl logs -f loki-loki-distributed-ruler-5d4b8cd889-m2vbd -n logging
......
level=info ts=2022-06-25T10:10:07.445554993Z caller=metrics.go:122 component=ruler org_id=tenant_no latency=fast query="((sum by(job)(rate({app=\"nginx\"} |= \"error\"[1m])) / sum by(job)(rate({app=\"nginx\"}[1m]))) > 0.01)" query_type=metric range_type=instant length=0s step=0s duration=25.306079ms status=200 limit=0 returned_lines=0 throughput=0B total_bytes=0B queue_time=0s subqueries=1
level=info ts=2022-06-25T10:11:03.196836972Z caller=pool.go:171 msg="removing stale client" addr=10.244.2.165:9095
level=info ts=2022-06-25T10:11:07.423644116Z caller=metrics.go:122 component=ruler org_id=tenant_no latency=fast query="((sum by(job)(rate({app=\"nginx\"} |= \"error\"[1m])) / sum by(job)(rate({app=\"nginx\"}[1m]))) > 0.01)" query_type=metric range_type=instant length=0s step=0s duration=3.234499ms status=200 limit=0 returned_lines=0 throughput=0B total_bytes=0B queue_time=0s subqueries=1

同樣在 1m 之內如果持續超過閾值,則會真正觸發報警規則,觸發後我們在 Alertmanager 也可以看到對應的報警資訊了:

到這裡我們就完成了使用 Loki 基於日誌的監控報警。