You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In the input-level conditional router, when two or more routes target the same output instance, Fluent Bit cannot deliver correctly, and the current data model appears to make a fully-correct outcome impossible without a redesign. I'd like maintainer input on the intended semantics and on a proposed fix before sending a large change.
Example config intent (one input, two named conditional routes, same destinationes):
flb_router_apply_config() deduplicates direct routes by output instance only (input_has_direct_route() in src/flb_router_config.c). Because routes_direct ends up with a single path for es, split_and_append_route_payloads() (src/flb_input_log.c) builds a per-route payload only for the first route — the second route is silently dropped. A record matching checkout_logs but not error_logs never reaches es.
Why the naive fix does not work
Making the dedupe key (route, output) instead of output stops the silent drop, but the conditional-routing data model assumes at most one route per output:
split_and_append_route_payloads() builds one chunk per route (payload->route, tag = route name) and build_payload_for_route() re-evaluates each route's condition independently with no cross-route dedup. A record matching both routes is encoded into both payload chunks, so the shared output es receives it twice (two separate chunks/tasks — this cannot be deduped at the task layer).
routes_mask and fs_chunks_size accounting are keyed by output id. With two routes on one output, route_payload_apply_outputs() subtracts the shared output's fs_chunks_size for the "other" route even though the payload does route there (and never adds it back while fs_counted stays true), undercounting storage and bypassing total_limit_size pressure.
So the choice today is between under-delivery (silent drop, current) and over-delivery + broken storage accounting (naive fix). The only correct semantic is: each output receives each record at most once, if any route targeting it matches (A OR B). A per-route payload split with a uniform per-chunk routes_mask cannot express that when routes have multiple, overlapping output sets.
Expected behavior
Each output should receive each record exactly once when one or more routes targeting it match; no route should be silently dropped.
Proposed design (seeking feedback)
Group records by their computed output set instead of by route:
For each record, compute S = union of outputs(R) over all matching routes R (falling back to default-route outputs when no non-default route matches).
Emit one chunk per distinct non-empty S, with routes_mask = S.
This preserves the output-keyed mask/storage invariant (so the accounting bug disappears), drops no route, and duplicates no delivery.
Open question — chunk tag semantics
Today the delivered chunk tag is the route name, which is user-visible (out_file naming, ES index, downstream Match). In the merged model a record in S = {es, s3} has no single route, so the tag must be redefined. Candidates:
T1: original input tag (simplest; loses route-name tagging even for single-route records).
T2: deterministic "primary" route = first matching route in config order (identical to today whenever a record matches exactly one route; documented tie-break for overlaps).
T3: synthetic tag from route/output names.
T2 seems least disruptive, but this is a semantics change we don't want to decide unilaterally.
Questions for maintainers
Is "multiple routes → same output" a configuration you intend to support, or should it be explicitly rejected/warned at config load?
If supported, is the per-output-set grouping the direction you'd want, and which tag semantics (T1/T2/T3) is acceptable?
Is there existing intent/design for this case we should align with?
Bug Report / Design question
Describe the bug
In the input-level conditional router, when two or more routes target the same output instance, Fluent Bit cannot deliver correctly, and the current data model appears to make a fully-correct outcome impossible without a redesign. I'd like maintainer input on the intended semantics and on a proposed fix before sending a large change.
Example config intent (one input, two named conditional routes, same destination
es):Current behavior
flb_router_apply_config()deduplicates direct routes by output instance only (input_has_direct_route()insrc/flb_router_config.c). Becauseroutes_directends up with a single path fores,split_and_append_route_payloads()(src/flb_input_log.c) builds a per-route payload only for the first route — the second route is silently dropped. A record matchingcheckout_logsbut noterror_logsnever reacheses.Why the naive fix does not work
Making the dedupe key
(route, output)instead ofoutputstops the silent drop, but the conditional-routing data model assumes at most one route per output:split_and_append_route_payloads()builds one chunk per route (payload->route, tag = route name) andbuild_payload_for_route()re-evaluates each route's condition independently with no cross-route dedup. A record matching both routes is encoded into both payload chunks, so the shared outputesreceives it twice (two separate chunks/tasks — this cannot be deduped at the task layer).routes_maskandfs_chunks_sizeaccounting are keyed by output id. With two routes on one output,route_payload_apply_outputs()subtracts the shared output'sfs_chunks_sizefor the "other" route even though the payload does route there (and never adds it back whilefs_countedstays true), undercounting storage and bypassingtotal_limit_sizepressure.So the choice today is between under-delivery (silent drop, current) and over-delivery + broken storage accounting (naive fix). The only correct semantic is: each output receives each record at most once, if any route targeting it matches (A OR B). A per-route payload split with a uniform per-chunk
routes_maskcannot express that when routes have multiple, overlapping output sets.Expected behavior
Each output should receive each record exactly once when one or more routes targeting it match; no route should be silently dropped.
Proposed design (seeking feedback)
Group records by their computed output set instead of by route:
S = union of outputs(R)over all matching routesR(falling back to default-route outputs when no non-default route matches).S, withroutes_mask = S.This preserves the output-keyed mask/storage invariant (so the accounting bug disappears), drops no route, and duplicates no delivery.
Open question — chunk tag semantics
Today the delivered chunk tag is the route name, which is user-visible (out_file naming, ES index, downstream
Match). In the merged model a record inS = {es, s3}has no single route, so the tag must be redefined. Candidates:T2 seems least disruptive, but this is a semantics change we don't want to decide unilaterally.
Questions for maintainers
Additional context
(route, output)dedupe that surfaced the P1/P2 issues above): router_config: wire every route merging into a shared output #12142src/flb_router_config.c(flb_router_apply_config,input_has_direct_route),src/flb_input_log.c(split_and_append_route_payloads,build_payload_for_route,route_payload_apply_outputs),src/flb_task.c(flb_task_create).Your Environment