Promtail pipeline_stagesを完全理解したい

promtailのmultilineの設定がよくわからなかったので調べてたら、pipeline_stagesからちゃんと理解する必要がありました
ので、一応最後のmultilineがメインです

# ドキュメント

https://grafana.com/docs/loki/latest/clients/promtail/pipelines/

https://grafana.com/docs/loki/latest/clients/promtail/stages/

https://github.com/grafana/loki

# 処理順序

設定した順番で処理されます
stageの種類で処理の優先度、とかはないです

# ログエントリのstructure

pipeline処理時に内部で保持している構造物
なんとなく頭に入ってると色々理解しやすいです

- `Line`
    - 流れてきた生ログ
    - 最終的なアウトプットになるもの
    - string
- `Timestamp`
    - Lokiで読み取る際に使用するtimestamp（のはず）
    - デフォルトはログエントリが作られた時刻が入る？（未確認）
    - time.Time
- `Labels`
    - アウトプットするログに付与するlabel
    - keyとvalueのmap
- `Extracted`
    - 各stageでparseしたものを保持するところ
    - keyとvalueのmap

# stages

（個人的に）よく使うものを掘っていきます

## json

https://grafana.com/docs/loki/latest/clients/promtail/stages/json/

文字列をjsonにparseして、任意のフィールドを`Extracted`に格納する
parseしたjsonのkeyと`Extracted`に格納するkeyを同じにする場合は省略できる
これは他のstageでも共通

```yaml
- json:
    expressions:
      extracted_key: json_key
      common_key:
```

parseする文字列はデフォルトで`Line`で、`source`を指定すればExtractedの内容をparseできるので、入れ子のjsonをparseできる

例えば、以下のjson文字列を
`{"one": {"two": {"three":"goal"}}}`

```yaml
pipeline_stages:
- json: 
    expressions:
      one:
- json:
    expressions:
      two:
    source: one
- json:
    expressions
      three:
    source: two
- output:
    source: three
```

こんな流れで完全にparseできる

## regex

https://grafana.com/docs/loki/latest/clients/promtail/stages/regex/

parseしてExtractedに格納する流れはjson stageと同じで、parseにjsonでなく正規表現を使用する
処理対象はデフォルトで`Line`、sourceで指定可能
`expression: '(?P<keyname>.*)'`という形で、Extractedにkeyにkeyname、valueに正規表現でキャプチャした値（この場合は全文）が入る

## labels

https://grafana.com/docs/loki/latest/clients/promtail/stages/labels/

`Extracted`の内容をlabelで使用する
指定したkeyが`Extracted`に存在しない場合は何も起きない（エラーにはならない）

## timestamp

https://grafana.com/docs/loki/latest/clients/promtail/stages/timestamp/

ログエントリの`Timestamp`を設定する
設定する値は、sourceで`Extracted`内のフィールドを指定するので、事前に何かしらのparseをしておく必要がある
フォーマットはプリセットから選ぶ必要があって、自由記述できない

## output

https://grafana.com/docs/loki/latest/clients/promtail/stages/output/

`Extracted`の中から、ログとして出力するフィールドを決める
内部では`Line`を置換している
sourceで指定したものが`Extracted`に存在しない場合は何もしない
なので、outputステージを設定しなかったり、指定してもそれが`Extracted`に存在しなかったら、生ログをそのまま出力する

## docker

https://grafana.com/docs/loki/latest/clients/promtail/stages/docker/

以下のstagesのwrapperになっている

```yaml
- json:
    output: log
    stream: stream
    timestamp: time
- labels:
    stream:
- timestamp:
    source: timestamp
    format: RFC3339Nano
- output:
    source: output
```

## cri

https://grafana.com/docs/loki/latest/clients/promtail/stages/cri/

以下のstagesのwrapperになっている

```yaml
- regex:
    expression: "^(?s)(?P<time>\\S+?) (?P<stream>stdout|stderr) (?P<flags>\\S+?) (?P<content>.*)$",
- labels:
    stream:
- timestamp:
    source: time
    format: RFC3339Nano
- output:
    source: content
```

## multiline

https://grafana.com/docs/loki/latest/clients/promtail/stages/multiline/

ちゃんと理解するためにだいぶ長くなる
複数行のログの集合体`block`を作り、最後に繋げてひとつのログにする

### 設定項目

- firstline
    - 最初の行であるという判定を行う正規表現
- max_wait_times
    - 後続のログを待つ時間
    - あまり長く設定すると、最新のログが長時間ここで待ちになるのでその間の閲覧ができない
    - 最初の行が出てからではなく、前の行が出てからの間隔
    - デフォルトは3秒
- max_lines
    - ひとつのblockの最大行数
    - デフォルトは128

### 内部ステータス

multiline専用の内部ステータスがある

- buffer
    - 現在のblockで保持してるログのバッファ
- startLineEntry
    - firstlineに該当したログのログエントリstructure
- currentLines
    - 現在のblockで保持しているログの行数

### 処理の流れ

- ログが来る
- firstlineに該当するか判定
    - 判定の対象になるのは`Line`
        - よって、output stageの前か後かで挙動が変わる
    - 該当する場合、flush処理（後述）をして現在のログエントリをまるっとstartLineEntryに入れる
- bufferに Line を追加して、currentLinesを+1する
    - ログは\n（改行）で繋ぐ
    - firstlineに該当したエントリもこの処理は行う
- currentLinesがmax_linesに達している場合、flush処理
- 後続のログを待ち、max_wait_timesを超えるとflush処理
- 先頭へ

### flush処理とは

- bufferが空の場合は何もしない
- ログエントリを作成
    - `Timestamp` `Labels` `Extracted` はstartLineEntryのものを使用
    - Lineはbufferを文字列にしたもの
- 作成したログエントリを後続のstageに渡す
- bufferとcurrentLinesをリセットする
- __startLineEntryはリセットしない__
    - おそらくmax_linesやmax_wait_timesでflushされた際に次のblockでまだ使うため

### ユニーク性
上記の処理は、ログエントリのlabelごとに独立して行われる

### ポイント
- `Line`に対して先頭行の判定を行ったり繋げたり
- `Line`以外の情報は先頭行のものを使用
- k8sの場合、dockerやcriで1回parseしてからoutputでLineを置き換えてからmultilineで処理する必要がある
- 複数のログを改行で繋いでる
    - これをなくしたいならこの後replaceする必要があり、元のログに改行がある場合は一緒に消えてしまう

# おまけ

そもそも今回multilneでやりたかったのは、dockerやcontainerdのログが16kbで分割されるやつの再結合だったんですが
これは現状のmultiline stageでは不可能なようです

### dockerの場合
ログ分割が起きてるかどうかを見分ける方法は、本文の終端が`\n`で終わっているかどうか（だったはず）
終端が`\n`で終わっていない場合は、ログが分割されていてまだ続きがある（3分割以上の可能性もあるので、先頭とは限らない）
終わっている場合は、分割されたログの最後
-> multilineで判定できるのは先頭行のみなので不可能（最終行で判定できればよかった）

### containerdの場合
containerdのログ分割が起きている場合はタグに入るP/Fで判定
-> ログ本文以外での判定はできないので不可能