Перейти к основному содержимому

WASM фильтры в FluentBit

· 3 мин. чтения

Начиная с версии 2.0 FluentBit поддерживает фильтры скомпилированные в WASM. При этом вы можете писать код на любом языке. У них в документации есть примеры на C, TinyGo и Rust.

Зачем вам может понадобиться свой фильтр?

Ну, например, вы можете захотеть описать маппинг полей. Да, вы можете описать его цепочкой фильтров modify, но это будет длинный и не самый читаемый конфиг.

Еще одна альтернатива — Lua. Но это не так прикольно. Если честно, я решил использовать, WASM именно потому, что мне давно хотелось его попробовать применить где-нибудь, а не потому что я провел глубокие сравнительные исследования WASM и Lua.

Установка инструментов.

Для начала нам понадобится поставить TinyGo. Он как зависимость хочет go. Ставить я буду через brew. Другие способы установки вы можете найти здесь.

brew install tinygo

У меня установилась версия tinygo version 0.27.0 darwin/amd64 (using go version go1.20.2 and LLVM version 15.0.0).

Код фильтра

package main

import (
"fmt"
"unsafe"

"github.com/valyala/fastjson"
)

//export level_mapper
func level_mapper(tag *uint8, tag_len uint, time_sec uint, time_nsec uint, record *uint8, record_len uint) *uint8 {
brecord := unsafe.Slice(record, record_len)

br := string(brecord)
var p fastjson.Parser
value, err := p.Parse(br)
if err != nil {
fmt.Println(err)
return nil
}
obj, err := value.Object()
if err != nil {
fmt.Println(err)
return nil
}
// Get the level value and convert it to the string
level := string(obj.Get("level").GetStringBytes())

// Syslog severity levels https://datatracker.ietf.org/doc/html/rfc3164#autoid-8
var mappedLevel string
switch level {
case "0":
mappedLevel = "FATAL"
case "1", "2", "3":
mappedLevel = "ERROR"
case "4", "5":
mappedLevel = "WARN"
case "6":
mappedLevel = "INFO"
case "7":
mappedLevel = "DEBUG"
default:
mappedLevel = "LEVEL_UNSPECIFIED"
}

var arena fastjson.Arena
// Set new key if you want to save original value
obj.Set("mappedLevel", arena.NewString(mappedLevel))
// or replace it
obj.Set("level", arena.NewString(mappedLevel))
s := obj.String()
s += string(rune(0)) // Note: explicit null terminator.
rv := []byte(s)

return &rv[0]
}

func main() {}

Фильтр получается максимально простым.

  1. Сначала мы вычитываем запись лога record. (L12–14)
  2. Парсим полученную строку в JSON-объект. (L15–25)
  3. Читаем значение нужного нам поля L27
  4. Маппим его на новое значение.
  5. И записываем обратно в объект L48–50

На что нужно обратить внимание.

  1. Комментарий //export level_mapper на 10 строке важен. Без него компилятор не сохранит имя функции и мы не сможем вызвать ее из FluentBit.
  2. На 52 строке мы дописываем в конец строки null byte. Он просигнализирует FluentBit об окончании возвращаемой из фильтра записи лога.

Теперь мы можем скомпилировать наш фильтр.

tinygo build --no-debug -scheduler=none -target=wasi -o filter.wasm ./filter.go

В итоге получается файл filter.wasm размером 123Kb. Не сказать, что мало, но и не много по сравнению с обычными go бинарными файлами. При помощи wasm-opt можно оптимизировать его еще немного, в итоге получив 118Kb. Оптимизированная версия фильтра, написанная на Rust и скомпилированная под WASM весит 161Kb.

И проверить его работоспособность запустив FluentBit со следующим конфигом.

[SERVICE]
Flush 1
Daemon Off
Log_Level info


[INPUT]
Name dummy
Tag dummy.local
Dummy {"level": "4", "message": "warn"}

[FILTER]
Name wasm
Match dummy.*
WASM_Path filter.wasm
Function_Name level_mapper
Accessible_Paths .

[OUTPUT]
Name stdout
Match *

Полный код https://github.com/nikolaymatrosov/fb-wasm-filter