7 minute read

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.

src

官方舉的範例

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

Tags:

Categories:

Updated: