WASM фильтры в FluentBit
Начиная с версии 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() {}
Фильтр получается максимально простым.
- Сначала мы вычитываем запись лога
record
. (L12–14) - Парсим полученную строку в JSON-объект. (L15–25)
- Читаем значение нужного нам поля L27
- Маппим его на новое значение.
- И записываем обратно в объект L48–50
На что нужно обратить внимание.
- Комментарий
//export level_mapper
на 10 строке важен. Без него компилятор не сохранит имя функции и мы не сможем вызвать ее из FluentBit. - На 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