This series is a general purpose getting started guide for those of us wanting to learn about the Cloud Native Computing Foundation (CNCF) project Fluent Bit.
Each article in this series addresses a single topic by providing insights into what the topic is, why we are interested in exploring that topic, where to get started with the topic, and how to get hands-on with learning about the topic as it relates to the Fluent Bit project.
The idea is that each article can stand on its own, but that they also lead down a path that slowly increases our abilities to implement solutions with Fluent Bit telemetry pipelines.
Let's take a look at the topic of this article, using Fluent Bit routing for developers. In case you missed the previous article, check out the top three telemetry pipeline filters where you explore the most useful filters for manipulating and controlling your telemetry data.
This article will be a hands-on exploration of routing patterns that help you as a developer building sophisticated telemetry pipelines. We'll look at how to direct telemetry data to different destinations based on tags, patterns, and conditions in your Fluent Bit configurations.
All examples in this article have been done on OSX and are assuming the reader is able to convert the actions shown here to their own local machines.
You should have explored the previous articles in this series to install and get started with Fluent Bit on your developer local machine, either using the source code or container images. Links at the end of this article will point you to a free hands-on workshop that lets you explore more of Fluent Bit in detail.
You can verify that you have a functioning installation by testing your Fluent Bit, either using a source installation or a container installation as shown below:
# For source installation.$ fluent-bit -i dummy -o stdout
# For container installation.$ podman run -ti ghcr.io/fluent/fluent-bit:4.2.0 -i dummy -o stdout
...
[0] dummy.0: [[1753105021.031338000, {}], {"message"=>"dummy"}]
[0] dummy.0: [[1753105022.033205000, {}], {"message"=>"dummy"}]
[0] dummy.0: [[1753105023.032600000, {}], {"message"=>"dummy"}]
[0] dummy.0: [[1753105024.033517000, {}], {"message"=>"dummy"}]...Let's explore how routing works in Fluent Bit and why it's essential for building production-ready telemetry pipelines.
Understanding routing in telemetry pipelines
See this article for details about the service section of the configurations used in the rest of this article, but for now we plan to focus on our Fluent Bit pipeline and specifically the routing capabilities that let us direct telemetry data to appropriate destinations.
Below in the figure you see the phases of a telemetry pipeline. Routing happens throughout the pipeline but is most evident in the final output phase, where we decide which processed events go to which destinations.
Routing in Fluent Bit determines which events flow to which outputs, and there are two primary routing approaches available to developers:
Tag-based routing
Every event in Fluent Bit carries a tag, and outputs use the match parameter to subscribe to events with specific tags. Tags are simple strings that act as identifiers for event streams, similar to topics in message queues. This tag-based routing system is the traditional foundation of Fluent Bit's routing capabilities.
Tags typically follow a hierarchical naming convention like app.frontend.logs or system.metrics.cpu. This structure makes it easy to create wildcard patterns for flexible matching. The match parameter in outputs supports wildcards: a single asterisk (*) matches any characters at one level, while a double asterisk (**) matches multiple levels of the hierarchy.
For example, a match pattern of app.* would catch app.logs and app.metrics, but not app.frontend.logs. Meanwhile, app.** would match all three. This wildcard system provides the flexibility needed for complex routing scenarios while keeping configurations manageable.
Conditional routing
Conditional routing is a newer approach available in Fluent Bit 4.2+ that evaluates individual records and routes them to different outputs based on their content. Unlike tag-based routing which operates on entire data chunks, conditional routing provides per-record routing decisions.
This mechanism uses a routes block defined within input configurations. Each route specifies conditions that determine which records match, and the outputs where matching records should be sent. Conditions can examine any field within the record using comparison operators like equals, greater than, regular expressions, and array membership tests.
Conditional routing is particularly powerful when you need to split logs based on severity levels, route records from different services to dedicated outputs, or implement complex multi-condition routing logic. It provides fine-grained control without requiring filters to modify tags, making your routing configuration more explicit and easier to understand.
The key advantage is that routing decisions are made per-record at the input stage, before any filtering or processing occurs. This enables efficient routing that can send subsets of data to different destinations based on content, without processing or forwarding unnecessary records.
Combining routing mechanisms
In production environments, you rarely send all your telemetry data to a single destination. Different types of logs need different handling. Error logs might go to an alerting system, audit logs to long-term storage, and debug logs might be dropped entirely to save costs.
You can combine both routing approaches in a single configuration. Use tag-based routing for broad categorization by source or input type, and conditional routing for fine-grained decisions based on record content like severity level, error codes, or specific field values. Together, these routing approaches give you complete control over your telemetry pipeline.
Understanding these routing patterns is essential for developers who want to build sophisticated telemetry pipelines that efficiently direct data where it needs to go while minimizing costs and maximizing value.
Now let's look at practical implementations of these routing patterns that developers need to master.
Basic tag-based routing with wildcards
The traditional routing mechanism in Fluent Bit is the tag-based system. Every input assigns data a human-readable identifier (tag), and every output uses a match pattern to select which events it receives. Let's create a configuration that demonstrates basic routing patterns.
First, create a configuration file called fluent-bit.yaml as follows:
service: flush: 1
log_level: info
http_server: on
http_listen: 0.0.0.0
http_port: 2020
hot_reload: on
pipeline:
inputs:
- name: dummy tag: app.frontend.requests dummy: '{"service":"frontend","level":"info","message":"Request processed"}'
- name: dummy
tag: app.backend.errors
dummy: '{"service":"backend","level":"error","message":"Database connection failed"}'
- name: dummy
tag: system.metrics
dummy: '{"cpu_usage":75,"memory_usage":60}'
outputs:
- name: stdout
match: app.frontend.*
format: json_lines
- name: stdout
match: app.backend.*
format: json_lines
- name: stdout
match: system.*
format: json_linesOur configuration creates three different dummy inputs, each with its own tag representing different sources of telemetry data. The tag structure uses a hierarchical naming convention, similar to Java package names or DNS domains, which makes it easy to create wildcard patterns.
The outputs section shows three stdout outputs, each matching a different tag pattern. The asterisk (*) is a wildcard that matches any characters at that level. This means app.frontend.* matches any event with a tag starting with app.frontend., regardless of what follows.
Let's run this configuration to see tag-based routing in action:
# For source installation.$ fluent-bit --config fluent-bit.yaml# For container installation after building new image with your# configuration using a Buildfile as follows:## FROM ghcr.io/fluent/fluent-bit:4.2.0# COPY ./fluent-bit.yaml /fluent-bit/etc/fluent-bit.yaml# CMD [ "fluent-bit", "-c", "/fluent-bit/etc/fluent-bit.yaml" ]#$ podman build -t fb -f Buildfile$ podman run --rm fb...{"date":"2025-12-18 10:15:23.456789","service":"frontend","level":"info","message":"Request processed"} {"date":"2025-12-18 10:15:24.567890","service":"backend","level":"error","message":"Database connection failed"} {"date":"2025-12-18 10:15:25.678901","cpu_usage":75,"memory_usage":60}...
Each event is routed to its corresponding output based on the tag match pattern. This basic routing pattern is the foundation for more complex scenarios. Note that we use stdout for most examples, leaving it to the reader to experiment with other output destinations in their own environment for routing to different destinations.
You can also use the double asterisk (**) wildcard to match multiple levels. Let's modify our outputs to demonstrate this:
service: flush: 1
log_level: info
http_server: on
http_listen: 0.0.0.0
http_port: 2020
hot_reload: on
pipeline:
inputs:
- name: dummy tag: app.frontend.requests dummy: '{"service":"frontend","level":"info","message":"Request processed"}'
- name: dummy
tag: app.backend.errors
dummy: '{"service":"backend","level":"error","message":"Database connection failed"}'
- name: dummy
tag: system.metrics
dummy: '{"cpu_usage":75,"memory_usage":60}'
outputs:
- name: stdout
match: app.**
format: json_lines
Running this configuration shows that the single output with app.** matches both app.frontend.requests and app.backend.errors, but not system.metrics. The double asterisk matches any number of tag levels, making it perfect for catching all events under a hierarchical namespace.
Conditional routing
While tag-based routing directs entire chunks of data based on their source, conditional routing evaluates individual records and makes routing decisions based on their content. This feature, available in Fluent Bit 4.2 and greater, uses a routes block within input configurations to define routing rules.
Let's create a configuration that routes logs to different destinations based on their severity level:
service: flush: 1
log_level: info
http_server: on
http_listen: 0.0.0.0
http_port: 2020
hot_reload: on
pipeline:
inputs:
- name: dummy
tag: app.logs
dummy: '{"service":"api","level":"ERROR","message":"Failed to connect"}'
routes:
logs:
- name: critical_errors
condition:
op: or
rules:
- field: "$level"
op: eq
value: "ERROR"
- field: "$level"
op: eq
value: "FATAL"
to:
outputs:
- critical_output
- name: dummy
tag: app.logs
dummy: '{"service":"api","level":"INFO","message":"Request completed"}'
routes:
logs:
- name: normal_logs
condition:
op: and
rules:
- field: "$level"
op: eq
value: "INFO"
to:
outputs:
- info_output
outputs:
- name: stdout
alias: critical_output
format: json_lines
- name: stdout
alias: info_output
format: json_linesThe routes block is where conditional routing configuration happens. Each route has several key components:
- name - A unique identifier for the route
- condition - The logic block that determines which records match
- condition.op - The logical operator (and or or) for combining multiple rules
- condition.rules - An array of rules to evaluate against each record
- to.outputs - The destination outputs (referenced by alias or name) for matching records
Each rule in the condition.rules array specifies:
- field - The field to examine using record accessor syntax ($level, $service, etc.)
- op - The comparison operator (eq, neq, gt, lt, gte, lte, regex, not_regex, in, not_in)
- value - The value to compare against (can be a single value or array for in/not_in operators)
- context - Optional parameter specifying where to look for the field (body, metadata, otel_resource_attributes, etc.)
When a record arrives, Fluent Bit evaluates the conditions for each route in order. Records are sent to outputs whose conditions they match. This allows per-record routing decisions based on the actual content of each log entry.
Let's run this configuration:
# For source installation.$ fluent-bit --config fluent-bit.yaml# For container installation after building new image with your# configuration using a Buildfile as follows:## FROM ghcr.io/fluent/fluent-bit:4.2.0# COPY ./fluent-bit.yaml /fluent-bit/etc/fluent-bit.yaml# CMD [ "fluent-bit", "-c", "/fluent-bit/etc/fluent-bit.yaml" ]#$ podman build -t fb -f Buildfile$ podman run --rm fb...{"date":"2025-12-18 10:30:45.123456","service":"api","level":"ERROR","message":"Failed to connect"} {"date":"2025-12-18 10:30:46.234567","service":"api","level":"INFO","message":"Request completed"}...
The ERROR logs are routed to the critical_output while INFO logs go to info_output, all based on examining the level field in each record.
You can also define a default route to catch records that don't match any other conditions:
service: flush: 1
log_level: info
http_server: on
http_listen: 0.0.0.0
http_port: 2020
hot_reload: on
pipeline:
inputs:
- name: dummy
tag: app.logs
dummy: '{"service":"api","level":"ERROR","message":"Failed to connect"}'
routes:
logs:
- name: error_logs
condition:
op: and
rules:
- field: "$level"
op: eq
value: "ERROR"
to:
outputs:
- error_output
- name: default_logs
condition:
default: true
to:
outputs:
- default_output
outputs:
- name: stdout
alias: error_output
format: json_lines
- name: stdout
alias: default_output
format: json_linesThe route with condition.default: true acts as a catch-all for any records that don't match the earlier routes.
Let's create a more complex example that routes based on multiple conditions:
service: flush: 1
log_level: info
http_server: on
http_listen: 0.0.0.0
http_port: 2020
hot_reload: on
pipeline:
inputs:
- name: dummy
tag: app.logs
dummy: '{"service":"api","level":"ERROR","environment":"production","response_time":6000}'
routes:
logs:
- name: high_priority_errors
condition:
op: and
rules:
- field: "$level"
op: eq
value: "ERROR"
- field: "$environment"
op: eq
value: "production"
- field: "$response_time"
op: gt
value: 5000
to:
outputs:
- critical_output
- audit_output
- name: all_other_logs
condition:
default: true
to:
outputs:
- general_output
outputs:
- name: stdout
alias: critical_output
format: json_lines
- name: stdout
alias: audit_output
format: json_lines
- name: stdout
alias: general_output
format: json_linesThis configuration uses the and operator to combine three conditions. Only records that are ERROR level, from production, AND have a response time greater than 5000ms will be sent to both the critical_output and audit_output. All other records go to general_output.
Let's run this configuration:
# For source installation.$ fluent-bit --config fluent-bit.yaml# For container installation after building new image with your# configuration using a Buildfile as follows:## FROM ghcr.io/fluent/fluent-bit:4.2.0# COPY ./fluent-bit.yaml /fluent-bit/etc/fluent-bit.yaml# CMD [ "fluent-bit", "-c", "/fluent-bit/etc/fluent-bit.yaml" ]#$ podman build -t fb -f Buildfile$ podman run --rm fb...{"date":"2025-12-18 10:45:12.345678","service":"api","level":"ERROR","environment":"production","response_time":6000} {"date":"2025-12-18 10:45:12.345678","service":"api","level":"ERROR","environment":"production","response_time":6000} ...
Notice how the high-priority error appears twice in the output because it's being sent to two different outputs (critical_output and audit_output).
Conditional routing also supports the in and not_in operators for matching against arrays of values:
service: flush: 1
log_level: info
http_server: on
http_listen: 0.0.0.0
http_port: 2020
hot_reload: on
pipeline:
inputs:
- name: dummy
tag: app.logs
dummy: '{"service":"payment","level":"ERROR","message":"Transaction failed"}'
routes:
logs:
- name: critical_services
condition:
op: and
rules:
- field: "$service"
op: in
value: ["payment", "authentication", "database"]
to:
outputs:
- critical_output
- name: standard_logs
condition:
default: true
to:
outputs:
- standard_output
outputs:
- name: stdout
alias: critical_output
format: json_lines
- name: stdout
alias: standard_output
format: json_linesThis configuration routes logs from critical services (payment, authentication, database) to a dedicated output, while all other services go to standard output. The in operator makes it easy to match against multiple values without creating separate rules for each one.
Advanced routing with regular expressions and nested tags
Let's look at advanced routing which we can implement using tag-based fan-out, where a single event is sent to multiple destinations by having multiple outputs with overlapping match patterns:
service: flush: 1
log_level: info
http_server: on
http_listen: 0.0.0.0
http_port: 2020
hot_reload: on
pipeline:
inputs:
- name: dummy
tag: prod.svc.auth.logs
dummy: '{"service":"auth","environment":"production","level":"ERROR","message":"Token validation failed"}'
outputs:
- name: stdout
match: 'prod.**'
format: json_lines
- name: stdout
match_regex: '.*\.auth\..*'
format: json_lines
- name: stdout
match: '**.logs'
format: json_lines
Running this configuration shows that the single input event matches all three output patterns:
# For source installation.$ fluent-bit --config fluent-bit.yaml# For container installation after building new image with your# configuration using a Buildfile as follows:## FROM ghcr.io/fluent/fluent-bit:4.2.0# COPY ./fluent-bit.yaml /fluent-bit/etc/fluent-bit.yaml# CMD [ "fluent-bit", "-c", "/fluent-bit/etc/fluent-bit.yaml" ]#$ podman build -t fb -f Buildfile$ podman run --rm fb...{"date":"2025-12-18 11:15:45.567890","service":"auth","environment":"production","level":"ERROR","message":"Token validation failed"} {"date":"2025-12-18 11:15:45.567890","service":"auth","environment":"production","level":"ERROR","message":"Token validation failed"} {"date":"2025-12-18 11:15:45.567890","service":"auth","environment":"production","level":"ERROR","message":"Token validation failed"}...
The event appears three times because it matches:
- prod.** (production environment)
- .*\.auth\..* (auth service)
- **.logs (logs data type)
This fan-out pattern is essential for implementing multi-destination routing without duplicating configuration or using additional filters.
Important routing considerations
When implementing routing in your Fluent Bit pipelines, keep these important points in mind:
- Design tag structure carefully - Tags are the backbone of your routing strategy. Use hierarchical naming with clear levels that represent environment, service type, service name, and data type. This makes it easy to create flexible match patterns.
- Be aware of ordering - Fluent Bit processes outputs in the order they're defined in the configuration. For most routing scenarios this doesn't matter, but if you're doing advanced processing or using filters that depend on routing results, order can be significant.
- Use match patterns efficiently - While you can create many specific outputs with exact tag matches, it's often more maintainable to use wildcard patterns and fewer outputs. Balance specificity with simplicity.
- Consider performance implications - Every output adds overhead. If you're routing to many destinations, test the performance impact and consider whether you can consolidate outputs or use features like tag-based fan-out more efficiently.
- Test your routing logic - Use the stdout output during development to verify that events are being routed as expected. It's easy to make mistakes with wildcard patterns, and seeing the actual output helps catch routing errors early.
- Document your routing patterns - Complex routing configurations can be difficult to understand months later. Add comments explaining your tag structure and routing logic to help future maintainers.
This covers the essential routing patterns that developers need to master when building Fluent Bit telemetry pipelines for their applications.
More in the series
In this article you learned how to implement simple and more sophisticated routing patterns in Fluent Bit using tags, wildcards, and content-based routing. This article is based on this online free workshop.
There will be more in this series as you continue to learn how to configure, run, manage, and master the use of Fluent Bit in the wild. Next up, exploring Fluent Bit buffering and reliability patterns to ensure your telemetry data is never lost.

No comments:
Post a Comment
Note: Only a member of this blog may post a comment.