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

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