[筆記] Loki 基礎使用
log監控工具, 類似prometheus的角色, 但主要是用於log的查詢
與prometheus的角色對應
loki 相關元件 / 語法 | 對應prometheus的角色 |
---|---|
loki | prometheus |
promtail | exporter |
grafana | grafana |
LogQL | PromQL |
loki 相關元件 | url |
---|---|
loki | http://localhost:3100 |
promtail | http://localhost:9080 |
grafana | http://localhost:3000 |
組件 | 說明 |
---|---|
loki | 主要的log監控工具, 支援LogQL的查詢語法 |
promtail | log收集工具, loki依賴promtai獲取目標Logl |
grafana | 可視化工具, 可理解成loki的前端 |
儲存方式
- 會將log本身的metadata作為label (log level, 主機, 時間…) 存為index, 主要目的是可以用來檢索
- log本體會存儲為chunk
Promtail
主要是將存在的log檔內容抽出來,提供給loki
Promtail config
## 對外port
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
sync_period: 60 # default 10s
## api url
clients:
- url: http://loki:3100/loki/api/v1/push
## 需求抽取的log 設定
scrape_configs:
- job_name: system
static_configs:
- targets:
- localhost
labels:
job: varlogs
__path__: /var/log/*log ## 會讀取該目錄下所有匹配的檔案 自帶一個labe filename = <檔案路徑>
loki promtail 注意事項
需注意 雖然 promtail 可以將log的內容抽出來做為label 但實際使用較不建議, 在官方介紹文章表示 更少的label = 更好的性能
As a Loki user or operator, your goal should be to use the fewest labels possible to store your logs. Fewer labels means a smaller index which leads to better performance.
官方舉的範例
ts=2020-08-25T16:55:42.986960888Z caller=spanlogger.go:53 org_id=29 traceID=2612c3ff044b7d02 method=Store.lookupIdsByMetricNameMatcher level=debug matcher="pod=\"loki-canary-25f2k\"" queries=16
直覺上用 logQL, 可能會想預先把 traceID抽出來作為label, 但千萬別這麼做
{cluster="ops-cluster-1",namespace="loki-dev", traceID=”2612c3ff044b7d02”}
推薦做法
{cluster="ops-cluster-1",namespace="loki-dev"} |= “traceID=2612c3ff044b7d02”
compose config
loki/grafana/promtail/alertmanager example
version: "3"
networks:
loki:
services:
loki:
restart: always
image: grafana/loki:2.9.4
ports:
- "3100:3100"
volumes:
- ./loki/local-config.yaml:/etc/loki/local-config.yaml
- ./loki/rules/:/loki/rules/
command: -config.file=/etc/loki/local-config.yaml
networks:
- loki
promtail:
restart: always
image: grafana/promtail:2.9.4
volumes:
- /var/log:/var/log
- ./promtail/config.yml:/etc/promtail/config.yml
- ./logs/:/tmp/logs/
command: -config.file=/etc/promtail/config.yml
networks:
- loki
grafana:
restart: always
environment:
- GF_PATHS_PROVISIONING=/etc/grafana/provisioning
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
entrypoint:
- sh
- -euc
- |
mkdir -p /etc/grafana/provisioning/datasources
cat <<EOF > /etc/grafana/provisioning/datasources/ds.yaml
apiVersion: 1
datasources:
- name: Loki
type: loki
access: proxy
orgId: 1
url: http://loki:3100
basicAuth: false
isDefault: true
version: 1
editable: false
EOF
/run.sh
image: grafana/grafana:latest
ports:
- "3000:3000"
networks:
- loki
alertmanager:
image: prom/alertmanager:v0.26.0
container_name: alertmanager
restart: always
volumes:
- /etc/localtime:/etc/localtime:ro
- ./alertmanager/:/etc/alertmanager/
command:
- '--config.file=/etc/alertmanager/alertmanager.yml'
- '--storage.path=/alertmanager'
networks:
- loki
expose:
- '9093'
ports:
- 9093:9093
補充 docker loki driver
算是docker 官方對loki 的Plugin
可以直接將docker log 直接輸出至loki中, 同promtail腳色, 但應用範圍較窄
只能用於docker / compose / swarm log
install
docker plugin install grafana/loki-docker-driver:2.9.4 --alias loki --grant-all-permissions
$ docker plugin ls
ID NAME DESCRIPTION ENABLED
ac720b8fcfdb loki Loki Logging Driver true
uninstall
docker plugin disable loki --force
docker plugin rm loki
compose config
version: '3.8'
x-logging: ## 自定義 logging
&docker_driver
driver: loki
options:
loki-url: "http://168.20.0.17:3100/loki/api/v1/push"
services:
PGdb:
image: pg
build:
context: .
dockerfile: DockerfilePG
container_name: demo_pg
restart: always
environment:
POSTGRES_PASSWORD: example4thinktron
PGDATA: /var/lib/postgresql/data/pgdata
PGPORT: 5432
POSTGRES_DB: prefect
USER: $(id -u):$(id -g)
volumes:
- ./pg_data/:/var/lib/postgresql/data
profiles:
- main
- all
ports:
- "5432:5432"
logging: *docker_driver ### 設置logging
networks:
demo_net:
ipv4_address: 168.20.0.3
LogQL
可用於較複雜的Log查詢, grafana需選擇code mode,
跟promQL 有點神似, 基礎查詢依賴於label
LogQL的query流程, 大致上為
graph LR
A[All logs] -->|label filter| B[Specific logs] -->|log queries| C[target logs]
依據需求也可重複操作
支援運算符
大致上跟promQL雷同
運算符
運算符 | 說明 |
---|---|
+ |
加法 |
- |
減法 |
* |
乘法 |
/ |
除法 |
% |
取餘數 |
^ |
次方 |
邏輯運算符
運算符 | 說明 |
---|---|
and |
且 |
or |
或 |
unless |
非 |
label filter
用於將當前蒐集到的所有log利用label進行篩選
運算符 | 說明 |
---|---|
= |
等於 |
!= |
不等於 |
=~ |
regular matches |
!~ |
regular does not match |
{job="varlogs"}
## 查詢 label 為 varlogs 的所有log
{job=~"api_log|varlogs"}
## 查詢 label 匹配 "api_log|varlogs" regex 的所有log
{job!="varlogs"}
## 查詢 label 不為 varlogs 的所有log
log queries (log pipeline)
利用label filter 篩選出一群log後, 可以利用log queries進行查詢
該方法其實就是一個log的pipeline, 將log一個個row 依序用log queries的語法進行過濾
這樣就可以得到進一步的結果, 針對查詢手段,有以下幾種方式
- line filter expression: 這邊主要是針對字串進行過濾,
- parse expression: 支援 json,logfmt,pattern,regexp, unpack 等方式進行解析, 並將解析出的field轉成label
- label filter expression: 若在pipeline中已經被parse出field to label, 可以用label進行過濾, 常見的是json格式直接使用
| json
就可以得到各field, 之後就可以使用field數值直接過濾
利用pipeline配合這些方式, 分段過濾出需要的log
line filter expression
- |可理解為肯定 , ! 就與其他語言一樣, 表示否定
- = 代表包含特定字串, ~ 代表包含特定regex匹配
運算符 | 說明 |
---|---|
\|= |
該log line 包含 特定字串 |
!= |
該log line 不包含 特定字串 |
\|~ |
該log line 包含 特定regex匹配 |
!~ |
該log line 不包含 特定regex匹配 |
{job="varlogs"} |= "error"
# 查詢 label 為 varlogs 的所有檔案中 含有error字串的log
{job="varlogs"} !="error"
# 查詢 label 為 varlogs 的所有檔案中 不含有error字串的log
{job="api_log"} |~ "INFO|WARNING"
# 查詢 label 為 api_log 的所有檔案中 含有匹配 regex "INFO|WARNING" 的log
# 實際為 含有 INFO 或 WARNING 字串的log
{job="api_log"} !~ "INFO|WARNING"
# 查詢 label 為 varlogs 的所有檔案中 不含有匹配 regex "info|warning" 的log
# 實際為 不含有 info 或 warning 字串的log
parse expression
利用各種不同的方式過濾出特定field, 並將field轉成label
json
若為json 格式的log, 可直接 | json
進行解析, 就可以直接使用field進行過濾
假設 {job=”api_log”} 的log為json格式
{
"text": "2024-05-13T15:12:33.293568+0800 | SUCCESS | __init__:init_request:168.20.0.7:null:/main/metrics:a92ea9838fcf4c3fac610461ff1ade15:NO AUTH:35 - status_code: 200\n",
"record": {
"elapsed": {
"repr": "9 days, 22:41:52.399077",
"seconds": 859312.399077
},
"exception": null,
"extra": {
"ip": "168.20.0.7",
"request_id": "a92ea9838fcf4c3fac610461ff1ade15",
"user": "NO AUTH",
"path": "/main/metrics",
"method": "null",
"status_code": 200,
"media_type": "GET"
},
"file": {
"name": "__init__.py",
"path": "/src/components/__init__.py"
},
"function": "init_request",
"level": {
"icon": "✅",
"name": "SUCCESS",
"no": 25
},
"line": 35,
"message": "status_code: 200",
"module": "__init__",
"name": "__init__",
"process": {
"id": 25,
"name": "MainProcess"
},
"thread": {
"id": 131334633921408,
"name": "MainThread"
},
"time": {
"repr": "2024-05-13 15:12:33.293568+08:00",
"timestamp": 1715584353.293568
}
可直接
{job="api_log"} | json}
Field | Value | ||
---|---|---|---|
filename | /tmp/logs/logic.2024-05-13_12-00-03_298341.log | ||
job | api_log | ||
log_type | logic | ||
record_elapsed_repr | 9 days, 22:42:52.398255 | ||
record_elapsed_seconds | 859372.398255 | ||
record_extra_ip | 168.20.0.7 | ||
record_extra_media_type | GET | ||
record_extra_method | null | ||
record_extra_path | /main/metrics | ||
record_extra_request_id | 9e96f9a8c2a8460197c24d8d8365a40c | ||
record_extra_status_code | 200 | ||
record_extra_user | NO AUTH | ||
record_file_name | init.py | ||
record_file_path | /src/components/init.py | ||
record_function | init_request | ||
record_level_icon | ✅ | ||
record_level_name | SUCCESS | ||
record_level_no | 25 | ||
record_line | 35 | ||
record_message | status_code: 200 | ||
record_module | init | ||
record_name | init | ||
record_process_id | 23 | ||
record_process_name | MainProcess | ||
record_thread_id | 131334633921408 | ||
record_thread_name | MainThread | ||
record_time_repr | 2024-05-13 15:13:33.292746+08:00 | ||
record_time_timestamp | 1715584413.292746 | ||
text | 2024-05-13T15:13:33.292746+0800 | SUCCESS | init:init_request:168.20.0.7:null:/main/metrics:9e96f9a8c2a8460197c24d8d8365a40c:NO AUTH:35 - status_code: 200 |
logfmt
若為logfmt 則可使用 | logfmt
進行解析
logfmt格式為 key1=value1 key2=value2 …
{app="my-app"}
level=info msg="http request successful" status=200
{app="my-app"} | logfmt | level == "info"
Pattern
用於解析特定格式的log
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>" <_>
Key | Value |
---|---|
ip | 0.191.12.2 |
method | GET |
uri | /api/plugins/versioncheck |
status | 200 |
size | 2 |
agent | Go-http-client/2.0 |
regexp
利用regular expression 的group 進行解析
{job="varlogs"}
May 13 13:34:20 ip-15-0-1-175 sshd[46038]: Connection closed by invalid user ubuntu 61.216.156.141 port 50492 [preauth]
{job="varlogs"} | regexp "invalid user (?P<user>[a-zA-Z0-9]+) (?P<ip>[0-9\\.]+)"
Key | Value |
---|---|
filename | /var/log/auth.log |
ip | 61.216.156.141 |
job | varlogs |
user | ubuntu |
label filter expression
運算符 | 說明 |
---|---|
= or == |
等於 |
!= |
不等於 |
> |
大於 |
>= |
大於等於 |
< |
小於 |
<= |
小於等於 |
假設用regexp parse出field, 可以直接使用field進行過濾
{job="varlogs"} | regexp "invalid user (?P<user>[a-zA-Z0-9]+) (?P<ip>[0-9\\.]+)"
filename
/var/log/auth.log
ip
XXX.XXX.XXX.XXX
job
varlogs
user
OOOO
這邊選出特定 ip 與 user
{job="varlogs"} | regexp "invalid user (?P<user>[a-zA-Z0-9]+) (?P<ip>[0-9\\.]+)" | ip="61.216.156.141" and user="ubuntu"
filename
/var/log/auth.log
ip
61.216.156.141
job
varlogs
user
ubuntu
多條件查詢
label query, log query(log pipeline) 可以組合使用 過濾出特定的log
{job="api_log"} | json |= "dev123 |= "error"
# {job="api_log"} 過濾出label job="api_log", json parse (將field 轉成 label) , 過濾 含有dev123 的結果, 再過濾 "error" 的結果
{job="api_log"} | json | record_extra_user="dev123" |="SUCCESS" | record_extra_ip = "168.20.0.1" | record_extra_method="GET"
function
loki 內建方法
範例基底說明
下面範例都用該log舉例
{job="varlogs"}
## 輸出所有label為varlogs的log
## 該job 還有filename的label , 該filename有四種 若以label groupby 會有四群log
## 這邊群是將 log的所有label來分的 假設這些log都有三個label 有兩個大家都相同 最後一組大家都不同, 這樣就是都不同群
常用函式
range
[] 代表一個單位區間, 用於選取一單位時間內的log為一組,配合其他函數進行統計 這邊區間為 x 方向的區間 , 也就是時間區間
{job="varlogs"}[5m]
# 將log區分為 五分鐘為一組
# ----- (假設為連線黑長線) >>> - - - - - 的概念
count_over_time({job="varlogs"}[5m])
# 以五分鐘為單位資料群進行統計
# - - - - - >>> .....
count_over_time
並統計每群資料的 單位時間內的log數量
count_over_time({job="varlogs"}[5m])
# 每五分鐘產生的總log數量
# 四群資料 各自統計五分鐘為單位的log數量
rate
每群資料 單位時間內 (末log總數量-首log總數量)/時間區間
可以說為平均每秒的變化量
rate({job="varlogs"}[5m])
# 每五分鐘log數量的平均變化率, 簡單說就是每秒平均增加多少條log
bytes_over_time
統計單位時間內產生log的大小 (byte為單位)
bytes_over_time({job="varlogs"}[5m])
# 四群資料 計算每五分鐘 產出的log總大小
bytes_rate
統計單位時間內產生log占用的變化量 (byte為單位)
單位時間內 (末占用byte - 首占用byte) / 單位時間 (秒)
(簡單說就是每秒平均增加多少 byte, 可能為負數, 代表減少多少byte)
bytes_rate({job="varlogs"}[5m])
# 四群資料 計算每五分鐘 該時間內 每秒平均增加多少 byte
aggressive
簡單說就是 aggressive 就是處理 range過的輸出結果(多個單位時間的輸出結果)
但可能range輸出結果是有多個群, 再將多個群的資料進行統計 (同一組單位時間會有多個資料)
( range 為 x 方向區間 , 而aggressive 為 y 方向區間統計, 多群資料 在同一時間區間的統計 )
函數名稱 | 說明 |
---|---|
sum | 總和 |
min | 最小值 |
max | 最大值 |
avg | 平均值 |
stddev | 標準差 |
stdvar | 變異數 |
count | 計數 |
bottomk | 最小的k個值 |
topk | 最大的k個值 |
sum(count_over_time({job="varlogs"}[5m]))
## count 會計算各群資料 各個時間的log數量, sum 會將所有群 的log數量加總
## 因此輸出各時期 只有一條線
topk(3,count_over_time({job="varlogs"}[5m]))
## count 會計算各個label各個時間的log數量, topk 會保留各時期的top 3 數值
## 因此每個時期會有三個點
group aggregate
類似上面, 但上面 Y 方向統計 只能將全部一起統計計算
而這邊可以將資料依照label分組 再進行統計 , 例如有四群資料 但實際上 四群資量 某個label 兩兩相同
用group aggregate 可以將這兩群資料分別進行統計
sum(count_over_time({job="varlogs"}[5m])) by (filename)
# count_over_time 會出書四組資料 各時期以五分鐘為單位的log數量,
# sum 若指定 job, 則會變成所有log總和 因大家job相同
# 若指定filename, 實際上輸出不變 , 因四組資料原本filename都不同
json query 範例
json log 範例
可參考field格式
{
"text": "2024-02-21T14:58:17.829850+0800 | SUCCESS | __init__:init_request:168.20.0.1:GET://api/etc/weather/latest:560e890266c143e4993ddd735995be2a:dev123:34 - status_code: 200\n",
"record": {
"elapsed": {
"repr": "0:21:39.526741",
"seconds": 1299.526741
},
"exception": null,
"extra": {
"ip": "168.20.0.1",
"request_id": "560e890266c143e4993ddd735995be2a",
"user": "dev123",
"path": "//api/etc/weather/latest",
"method": "GET"
},
"file": {
"name": "__init__.py",
"path": "/src/components/__init__.py"
},
"function": "init_request",
"level": {
"icon": "✅",
"name": "SUCCESS",
"no": 25
},
"line": 34,
"message": "status_code: 200",
"module": "__init__",
"name": "__init__",
"process": {
"id": 30,
"name": "MainProcess"
},
"thread": {
"id": 140263807331200,
"name": "MainThread"
},
"time": {
"repr": "2024-02-21 14:58:17.829850+08:00",
"timestamp": 1708498697.82985
}
}
}
log query 可以使用 | json, 可以在查詢時把log解析,且把field變成label |
{job="api_log",host="loki_server"} | json
輸出
| label | Value |
|------------------------|--------------------------------------------------------------------------------------------|
| filename | /tmp/logs/logic.log |
| host | loki_server |
| job | api_log |
| record_elapsed_repr | 0:21:39.526741 |
| record_elapsed_seconds | 1299.526741 |
| record_extra_ip | 168.20.0.1 |
| record_extra_method | GET |
| record_extra_path | //api/etc/weather/latest |
| record_extra_request_id| 560e890266c143e4993ddd735995be2a |
| record_extra_user | dev123 |
| record_file_name | __init__.py |
| record_file_path | /src/components/__init__.py |
| record_function | init_request |
| record_level_icon | ✅ |
| record_level_name | SUCCESS |
| record_level_no | 25 |
| record_line | 34 |
| record_message | status_code: 200 |
| record_module | __init__ |
| record_name | __init__ |
| record_process_id | 30 |
| record_process_name | MainProcess |
| record_thread_id | 140263807331200 |
| record_thread_name | MainThread |
| record_time_repr | 2024-02-21 14:58:17.829850+08:00 |
| record_time_timestamp | 1708498697.82985 |
| text | 2024-02-21T14:58:17.829850+0800 | SUCCESS | __init__:init_request:168.20.0.1:GET://api/etc/weather/latest:560e890266c143e4993ddd735995be2a:dev123:34 - status_code: 200 |
json 解析出的 label , 會為record開頭 + “” field, 若有nested field, 則是 繼續 “” + field_name
如
"exception": null,
"extra": {
"ip": "168.20.0.1",
"request_id": "560e890266c143e4993ddd735995be2a",
"user": "dev123",
"path": "//api/etc/weather/latest",
"method": "GET"
},
user則會被輸出成
record_extra_user: dev123