Fluent Bit’s extensibility is a major draw, and while C plugins are the most common way to extend it, WebAssembly (Wasm) offers a compelling, safer, and language-agnostic alternative for custom filtering.

Here’s Fluent Bit processing logs through a Wasm filter:

[
  {
    "log": "{\"message\":\"hello world\",\"level\":\"info\"}",
    "time": "2023-10-27T10:00:00.123Z",
    "host": "my-host",
    "tag": "app.log"
  }
]

Let’s say we have a Wasm plugin that adds a processed_by_wasm: true field to every record. After the Wasm filter runs, the output would look like this:

[
  {
    "log": "{\"message\":\"hello world\",\"level\":\"info\"}",
    "time": "2023-10-27T10:00:00.123Z",
    "host": "my-host",
    "tag": "app.log",
    "processed_by_wasm": true
  }
]

This is made possible by Fluent Bit’s Wasm runtime, which embeds a Wasm engine (like Wasmtime) and exposes an API for Wasm modules to interact with Fluent Bit’s record processing pipeline.

The Problem Wasm Solves

Traditionally, extending Fluent Bit meant writing C code, compiling it, and linking it directly into the Fluent Bit binary or as a shared library. This has several drawbacks:

  • Memory Safety: C’s manual memory management can lead to buffer overflows, segmentation faults, and other memory-related bugs that can crash Fluent Bit itself.
  • Language Lock-in: You’re largely confined to C or C++, limiting the pool of developers and the tools available.
  • Compilation Complexity: Managing build environments and dependencies for C plugins can be cumbersome.

WebAssembly, on the other hand, provides a sandboxed execution environment. Wasm modules are compiled from various source languages (Rust, Go, C++, etc.) into a portable bytecode. This bytecode is then executed by a Wasm runtime, which enforces strict memory isolation and security boundaries. For Fluent Bit, this means custom logic can run without the risk of corrupting the main process’s memory or introducing hard-to-debug crashes.

How It Works Internally

Fluent Bit’s Wasm integration involves a few key components:

  1. Wasm Runtime: Fluent Bit embeds a WebAssembly runtime (e.g., Wasmtime). This runtime is responsible for loading, validating, and executing Wasm modules.
  2. Plugin API (WASI/Custom): The Wasm module needs to interact with Fluent Bit. This is achieved through an interface. Fluent Bit provides a custom Wasm API that exposes functions for reading input records, modifying them, and writing them back. This API is often implemented using WebAssembly System Interface (WASI) concepts or a direct FFI (Foreign Function Interface) layer.
  3. Wasm Module: This is your custom filter logic, compiled into a .wasm file. It contains the code that manipulates log records.
  4. Fluent Bit Configuration: You configure Fluent Bit to load your Wasm plugin, specifying the path to the .wasm file and any necessary parameters.

When Fluent Bit encounters a record that needs to be processed by a Wasm filter, it calls into the embedded Wasm runtime. The runtime then invokes the appropriate functions within your Wasm module. Your module reads the record data, applies its transformations, and returns the modified record back to Fluent Bit via the API.

Building a Custom Wasm Plugin (Example with Rust)

Let’s outline the process using Rust, a popular choice for Wasm development due to its memory safety and performance.

1. Project Setup (Rust):

You’ll need a Rust toolchain and the wasm-pack tool.

# Install wasm-pack if you haven't already
cargo install wasm-pack

# Create a new Rust library project
cargo new --lib my_wasm_filter
cd my_wasm_filter

2. Dependencies:

Add necessary dependencies to your Cargo.toml:

[package]
name = "my_wasm_filter"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"] # Compile as a dynamic library for Wasm

[dependencies]
fluent_bit_wasm_sdk = "0.1.0" # Hypothetical SDK for Fluent Bit Wasm interaction
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

Note: fluent_bit_wasm_sdk is a placeholder. In a real scenario, you’d use the actual SDK provided by Fluent Bit or a community project.

3. Plugin Logic:

In src/lib.rs, implement your filter logic. This example adds a simple key-value pair.

use fluent_bit_wasm_sdk::{
    record::{Record, Value},
    plugin::{Filter, Plugin},
};
use serde_json::Value as JsonValue;

#[no_mangle]
pub extern "C" fn plugin_init() -> u32 {
    // Initialize plugin, return FF_OK on success
    Plugin::register("my_wasm_filter", || Box::new(MyFilter))
}

struct MyFilter;

impl Filter for MyFilter {
    fn filter(&mut self, record: &mut Record) -> Result<(), ()> {
        // Example: Add a boolean field
        record.insert("processed_by_wasm".to_string(), Value::Boolean(true));

        // Example: Parse the 'log' field if it's JSON and add a timestamp
        if let Some(log_value) = record.get("log") {
            if let Value::String(log_str) = log_value {
                match serde_json::from_str::<JsonValue>(log_str) {
                    Ok(mut json_log) => {
                        if let Some(obj) = json_log.as_object_mut() {
                            obj.insert("wasm_added_ts".to_string(), JsonValue::String(chrono::Utc::now().to_rfc3339()));
                            record.insert("log".to_string(), Value::String(serde_json::to_string(&json_log).unwrap()));
                        }
                    }
                    Err(_) => {
                        // Log an error or handle non-JSON log field
                    }
                }
            }
        }

        Ok(())
    }
}

4. Build the Wasm Module:

Use wasm-pack to compile your Rust code into a Wasm module.

wasm-pack build --target wasm32-unknown-unknown --out-dir ../../wasm_modules

This command creates a pkg directory containing my_wasm_filter_bg.wasm. You’ll want to copy this .wasm file to a location accessible by Fluent Bit.

5. Fluent Bit Configuration:

Configure Fluent Bit to use your Wasm plugin.

[SERVICE]
    # ... other service configurations

[INPUT]
    Name        dummy
    Dummy       { "message": "test log", "level": "debug" }
    Interval    5
    Alias       my_input

[FILTER]
    Name        wasm
    Alias       my_wasm_filter
    Wasm.File   /path/to/your/wasm_modules/my_wasm_filter_bg.wasm
    Wasm.Entry  plugin_init # The initialization function in your Wasm module

[OUTPUT]
    Name        stdout
    Match       *
    Format      json

In this configuration:

  • Name wasm tells Fluent Bit to use its built-in Wasm filter plugin.
  • Wasm.File points to the compiled .wasm file.
  • Wasm.Entry specifies the function in the Wasm module that Fluent Bit should call to initialize the plugin.

When Fluent Bit starts, it will load the Wasm runtime, load your .wasm module, call plugin_init, and then pass records through your filter function.

The Unseen Cost of Wasm Initialization

While Wasm offers isolation, the initial loading and instantiation of a Wasm module by the runtime isn’t free. Every time Fluent Bit loads a Wasm plugin, it needs to:

  1. Read the .wasm file from disk.
  2. Validate the Wasm bytecode for correctness and safety.
  3. Instantiate the Wasm module, allocating its linear memory.
  4. Call the module’s initialization function (plugin_init in our example).

If you have many Wasm filters or frequently reload configurations that involve Wasm plugins, this instantiation overhead can become noticeable. For performance-critical paths with many Wasm filters, consider if they can be combined into a single Wasm module or if a native C plugin might be more appropriate for that specific, high-throughput stage.

The next step is often integrating Wasm filters with more complex data structures and external dependencies, which can involve careful management of the Wasm host API and the capabilities exposed by the Wasm runtime.

Want structured learning?

Take the full Fluentbit course →